@endora-commerce/mod-customers 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 +60 -0
- package/dist/admin/index.d.ts +26 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +68 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/CustomerDetail.d.ts +9 -0
- package/dist/admin/pages/CustomerDetail.d.ts.map +1 -0
- package/dist/admin/pages/CustomerDetail.js +72 -0
- package/dist/admin/pages/CustomerDetail.js.map +1 -0
- package/dist/admin/pages/CustomersList.d.ts +9 -0
- package/dist/admin/pages/CustomersList.d.ts.map +1 -0
- package/dist/admin/pages/CustomersList.js +115 -0
- package/dist/admin/pages/CustomersList.js.map +1 -0
- package/dist/admin/pages/OnlineCustomers.d.ts +9 -0
- package/dist/admin/pages/OnlineCustomers.d.ts.map +1 -0
- package/dist/admin/pages/OnlineCustomers.js +77 -0
- package/dist/admin/pages/OnlineCustomers.js.map +1 -0
- package/dist/admin/panels/HistoryPanels.d.ts +11 -0
- package/dist/admin/panels/HistoryPanels.d.ts.map +1 -0
- package/dist/admin/panels/HistoryPanels.js +64 -0
- package/dist/admin/panels/HistoryPanels.js.map +1 -0
- package/dist/admin/panels/ManagementPanels.d.ts +14 -0
- package/dist/admin/panels/ManagementPanels.d.ts.map +1 -0
- package/dist/admin/panels/ManagementPanels.js +93 -0
- package/dist/admin/panels/ManagementPanels.js.map +1 -0
- package/dist/backend/entities/customer-address.entity.d.ts +25 -0
- package/dist/backend/entities/customer-address.entity.d.ts.map +1 -0
- package/dist/backend/entities/customer-address.entity.js +95 -0
- package/dist/backend/entities/customer-address.entity.js.map +1 -0
- package/dist/backend/index.d.ts +91 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +201 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/plugin.d.ts +168 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +83 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +70 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +313 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.register.d.ts +12 -0
- package/dist/backend/routes.register.d.ts.map +1 -0
- package/dist/backend/routes.register.js +40 -0
- package/dist/backend/routes.register.js.map +1 -0
- package/dist/backend/routes.self.d.ts +46 -0
- package/dist/backend/routes.self.d.ts.map +1 -0
- package/dist/backend/routes.self.js +145 -0
- package/dist/backend/routes.self.js.map +1 -0
- package/dist/backend/serializers.d.ts +13 -0
- package/dist/backend/serializers.d.ts.map +1 -0
- package/dist/backend/serializers.js +39 -0
- package/dist/backend/serializers.js.map +1 -0
- package/dist/backend/services/customer-address-read-port.d.ts +29 -0
- package/dist/backend/services/customer-address-read-port.d.ts.map +1 -0
- package/dist/backend/services/customer-address-read-port.js +53 -0
- package/dist/backend/services/customer-address-read-port.js.map +1 -0
- package/dist/backend/services/customer-address-service.d.ts +94 -0
- package/dist/backend/services/customer-address-service.d.ts.map +1 -0
- package/dist/backend/services/customer-address-service.js +176 -0
- package/dist/backend/services/customer-address-service.js.map +1 -0
- package/dist/backend/services/customer-admin-query-service.d.ts +57 -0
- package/dist/backend/services/customer-admin-query-service.d.ts.map +1 -0
- package/dist/backend/services/customer-admin-query-service.js +97 -0
- package/dist/backend/services/customer-admin-query-service.js.map +1 -0
- package/dist/backend/services/customer-authority-service.d.ts +46 -0
- package/dist/backend/services/customer-authority-service.d.ts.map +1 -0
- package/dist/backend/services/customer-authority-service.js +42 -0
- package/dist/backend/services/customer-authority-service.js.map +1 -0
- package/dist/backend/services/customer-defaults-service.d.ts +48 -0
- package/dist/backend/services/customer-defaults-service.d.ts.map +1 -0
- package/dist/backend/services/customer-defaults-service.js +71 -0
- package/dist/backend/services/customer-defaults-service.js.map +1 -0
- package/dist/backend/services/customer-deletion-service.d.ts +39 -0
- package/dist/backend/services/customer-deletion-service.d.ts.map +1 -0
- package/dist/backend/services/customer-deletion-service.js +94 -0
- package/dist/backend/services/customer-deletion-service.js.map +1 -0
- package/dist/backend/services/customer-moderation-service.d.ts +58 -0
- package/dist/backend/services/customer-moderation-service.d.ts.map +1 -0
- package/dist/backend/services/customer-moderation-service.js +70 -0
- package/dist/backend/services/customer-moderation-service.js.map +1 -0
- package/dist/backend/services/customer-org-assignment-service.d.ts +52 -0
- package/dist/backend/services/customer-org-assignment-service.d.ts.map +1 -0
- package/dist/backend/services/customer-org-assignment-service.js +106 -0
- package/dist/backend/services/customer-org-assignment-service.js.map +1 -0
- package/dist/backend/services/customer-presence-service.d.ts +44 -0
- package/dist/backend/services/customer-presence-service.d.ts.map +1 -0
- package/dist/backend/services/customer-presence-service.js +47 -0
- package/dist/backend/services/customer-presence-service.js.map +1 -0
- package/dist/backend/services/customer-registration-service.d.ts +56 -0
- package/dist/backend/services/customer-registration-service.d.ts.map +1 -0
- package/dist/backend/services/customer-registration-service.js +43 -0
- package/dist/backend/services/customer-registration-service.js.map +1 -0
- package/dist/backend/workers/anonymization-sweep-worker.d.ts +15 -0
- package/dist/backend/workers/anonymization-sweep-worker.d.ts.map +1 -0
- package/dist/backend/workers/anonymization-sweep-worker.js +20 -0
- package/dist/backend/workers/anonymization-sweep-worker.js.map +1 -0
- package/dist/manifest.d.ts +181 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +192 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260611T140404_customers_customer_addresses_init.d.ts +14 -0
- package/dist/migrations/20260611T140404_customers_customer_addresses_init.d.ts.map +1 -0
- package/dist/migrations/20260611T140404_customers_customer_addresses_init.js +40 -0
- package/dist/migrations/20260611T140404_customers_customer_addresses_init.js.map +1 -0
- package/dist/migrations/index.d.ts +27 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +29 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/customers.md +108 -0
- package/i18n/en.json +94 -0
- package/i18n/pl.json +94 -0
- package/package.json +101 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CustomerAuthorityService — encodes WHICH staff member may act on a given
|
|
3
|
+
* Customer (block / unblock / delete / org-assign), per the clarified rule
|
|
4
|
+
* (spec FR-037, research §R2):
|
|
5
|
+
*
|
|
6
|
+
* - A Platform Administrator may act on any customer. Whether the actor IS
|
|
7
|
+
* a platform admin is decided by the caller (route layer) and passed in —
|
|
8
|
+
* mirroring `SalesRepAssignmentService.canSeeOrganization`, which checks
|
|
9
|
+
* the assignment table only.
|
|
10
|
+
* - Only the Salesperson responsible for that
|
|
11
|
+
* customer's Organization may act — "responsible" = assigned via
|
|
12
|
+
* `OrganizationSalesRepAssignment`, including the unassigned-org fallback
|
|
13
|
+
* (an org with zero assigned reps is visible to every Salesperson).
|
|
14
|
+
*
|
|
15
|
+
* **D-178 removed a third rule** — *"for a standalone (org-less) customer, any
|
|
16
|
+
* Salesperson may act"* — because there is no such customer:
|
|
17
|
+
* `customer_accounts.organization_id` is `NOT NULL` and an individual is backed
|
|
18
|
+
* by a personal organisation. The outcome is unchanged rather than merely
|
|
19
|
+
* unreachable: `SalesRepAssignmentService` refuses to assign a rep to a personal
|
|
20
|
+
* organisation, so such an organisation always has zero assigned reps and the
|
|
21
|
+
* unassigned-org fallback answers `true` for every Salesperson — which is what
|
|
22
|
+
* the deleted rule said, arrived at through the assignment table instead of
|
|
23
|
+
* through the absence of a tenant.
|
|
24
|
+
*
|
|
25
|
+
* The route layer is responsible for the coarse permission gate
|
|
26
|
+
* (`requireAdmin('customers:manage')`) before this finer check runs.
|
|
27
|
+
*/
|
|
28
|
+
export class CustomerAuthorityService {
|
|
29
|
+
salesReps;
|
|
30
|
+
constructor(salesReps) {
|
|
31
|
+
this.salesReps = salesReps;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* True when the acting staff member is authorized to manage the customer.
|
|
35
|
+
*/
|
|
36
|
+
async canManageCustomer(input) {
|
|
37
|
+
if (input.isPlatformAdmin)
|
|
38
|
+
return true;
|
|
39
|
+
return this.salesReps.canSeeOrganization(input.adminUserId, input.customerOrganizationId);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=customer-authority-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-authority-service.js","sourceRoot":"","sources":["../../../src/backend/services/customer-authority-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAcH,MAAM,OAAO,wBAAwB;IACN;IAA7B,YAA6B,SAA6B;QAA7B,cAAS,GAAT,SAAS,CAAoB;IAAG,CAAC;IAE9D;;OAEG;IACH,KAAK,CAAC,iBAAiB,CAAC,KAA6B;QACnD,IAAI,KAAK,CAAC,eAAe;YAAE,OAAO,IAAI,CAAC;QACvC,OAAO,IAAI,CAAC,SAAS,CAAC,kBAAkB,CACtC,KAAK,CAAC,WAAW,EACjB,KAAK,CAAC,sBAAsB,CAC7B,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { CustomerDefaults, DefaultPreferencePort, PreferenceAuditContext } from '@endora-commerce/contracts';
|
|
3
|
+
import type { CustomerAddressService } from './customer-address-service.js';
|
|
4
|
+
/**
|
|
5
|
+
* CustomerDefaultsService — the customer's checkout defaults (feature 040, US2).
|
|
6
|
+
*
|
|
7
|
+
* Split source of truth (research §R5, reconciled with the existing schema):
|
|
8
|
+
* - default **payment / delivery method** → the quick_order
|
|
9
|
+
* `QuickOrderDefaultPreference` (scope `customer`), which checkout already
|
|
10
|
+
* consumes. Reached through `defaultPreferencePort` (issue #216). This
|
|
11
|
+
* module used to build a **second instance** of `quick_order`'s
|
|
12
|
+
* `DefaultPreferenceService` out of an import of that module's directory,
|
|
13
|
+
* which is what kept the port — published for exactly this consumer —
|
|
14
|
+
* unreached, and what blocked converting that service's own entity reads
|
|
15
|
+
* to ports.
|
|
16
|
+
* - default **billing / shipping address** → the `isDefault` flag on the
|
|
17
|
+
* customer's personal `customer_addresses` rows. (The preference table's
|
|
18
|
+
* address columns FK to the org `addresses` table, so personal addresses
|
|
19
|
+
* cannot be stored there.)
|
|
20
|
+
*/
|
|
21
|
+
export interface CustomerDefaultsPatch {
|
|
22
|
+
paymentMethodId?: string | null | undefined;
|
|
23
|
+
deliveryMethodId?: string | null | undefined;
|
|
24
|
+
billingAddressId?: string | null | undefined;
|
|
25
|
+
shippingAddressId?: string | null | undefined;
|
|
26
|
+
}
|
|
27
|
+
export declare class CustomerDefaultsService {
|
|
28
|
+
private readonly emFactory;
|
|
29
|
+
private readonly preferences;
|
|
30
|
+
private readonly addresses;
|
|
31
|
+
constructor(emFactory: () => EntityManager, preferences: DefaultPreferencePort, addresses: CustomerAddressService);
|
|
32
|
+
getForCustomer(customerAccountId: string): Promise<CustomerDefaults>;
|
|
33
|
+
/** Self-service update — the customer manages their own customer-scoped defaults. */
|
|
34
|
+
setForCustomer(customerAccountId: string, patch: CustomerDefaultsPatch, audit: PreferenceAuditContext): Promise<CustomerDefaults>;
|
|
35
|
+
/**
|
|
36
|
+
* The widening is `CustomerAddressService`'s, for the same reason and on the
|
|
37
|
+
* same terms — see `#forAuthorisedCustomer` there. This method has two
|
|
38
|
+
* callers and both establish the authority before reaching it: the self route
|
|
39
|
+
* passes the caller's **own** account id, and the admin detail screen
|
|
40
|
+
* (`CustomerAdminQueryService.getDetail`) has already resolved the account
|
|
41
|
+
* through `customer_accounts`' `@OrgScoped` read model and returned `null` if
|
|
42
|
+
* it was out of scope. The statement pins `customerAccountId`, so widening
|
|
43
|
+
* the filter widens the authority by nothing. It goes when feature 087 gives
|
|
44
|
+
* `customer_addresses` an `organization_id` of its own.
|
|
45
|
+
*/
|
|
46
|
+
private defaultAddressId;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=customer-defaults-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-defaults-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/customer-defaults-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EACV,gBAAgB,EAChB,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAG5E;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,qBAAqB;IACpC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC5C,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC7C,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC7C,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CAC/C;AAED,qBAAa,uBAAuB;IAEhC,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAFT,SAAS,EAAE,MAAM,aAAa,EAC9B,WAAW,EAAE,qBAAqB,EAClC,SAAS,EAAE,sBAAsB;IAG9C,cAAc,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAc1E,qFAAqF;IAC/E,cAAc,CAClB,iBAAiB,EAAE,MAAM,EACzB,KAAK,EAAE,qBAAqB,EAC5B,KAAK,EAAE,sBAAsB,GAC5B,OAAO,CAAC,gBAAgB,CAAC;IA4B5B;;;;;;;;;;OAUG;YACW,gBAAgB;CAiB/B"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { withSystemScope } from '@endora-commerce/platform/tenancy';
|
|
2
|
+
import { CustomerAddress } from '../entities/customer-address.entity.js';
|
|
3
|
+
export class CustomerDefaultsService {
|
|
4
|
+
emFactory;
|
|
5
|
+
preferences;
|
|
6
|
+
addresses;
|
|
7
|
+
constructor(emFactory, preferences, addresses) {
|
|
8
|
+
this.emFactory = emFactory;
|
|
9
|
+
this.preferences = preferences;
|
|
10
|
+
this.addresses = addresses;
|
|
11
|
+
}
|
|
12
|
+
async getForCustomer(customerAccountId) {
|
|
13
|
+
const methods = await this.preferences.resolveForCustomer(customerAccountId);
|
|
14
|
+
const [billingAddressId, shippingAddressId] = await Promise.all([
|
|
15
|
+
this.defaultAddressId(customerAccountId, 'billing'),
|
|
16
|
+
this.defaultAddressId(customerAccountId, 'delivery'),
|
|
17
|
+
]);
|
|
18
|
+
return {
|
|
19
|
+
paymentMethodId: methods.paymentMethodId,
|
|
20
|
+
deliveryMethodId: methods.deliveryMethodId,
|
|
21
|
+
billingAddressId,
|
|
22
|
+
shippingAddressId,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Self-service update — the customer manages their own customer-scoped defaults. */
|
|
26
|
+
async setForCustomer(customerAccountId, patch, audit) {
|
|
27
|
+
// Default payment / delivery method → preference store.
|
|
28
|
+
if (patch.paymentMethodId !== undefined || patch.deliveryMethodId !== undefined) {
|
|
29
|
+
await this.preferences.upsert({ kind: 'customer', customerAccountId }, {
|
|
30
|
+
scope: 'customer',
|
|
31
|
+
scopeId: customerAccountId,
|
|
32
|
+
...(patch.paymentMethodId !== undefined
|
|
33
|
+
? { defaultPaymentMethodId: patch.paymentMethodId }
|
|
34
|
+
: {}),
|
|
35
|
+
...(patch.deliveryMethodId !== undefined
|
|
36
|
+
? { defaultDeliveryMethodId: patch.deliveryMethodId }
|
|
37
|
+
: {}),
|
|
38
|
+
}, audit);
|
|
39
|
+
}
|
|
40
|
+
// Default billing / shipping address → the personal address book.
|
|
41
|
+
if (patch.billingAddressId != null) {
|
|
42
|
+
await this.addresses.setDefault(customerAccountId, patch.billingAddressId);
|
|
43
|
+
}
|
|
44
|
+
if (patch.shippingAddressId != null) {
|
|
45
|
+
await this.addresses.setDefault(customerAccountId, patch.shippingAddressId);
|
|
46
|
+
}
|
|
47
|
+
return this.getForCustomer(customerAccountId);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The widening is `CustomerAddressService`'s, for the same reason and on the
|
|
51
|
+
* same terms — see `#forAuthorisedCustomer` there. This method has two
|
|
52
|
+
* callers and both establish the authority before reaching it: the self route
|
|
53
|
+
* passes the caller's **own** account id, and the admin detail screen
|
|
54
|
+
* (`CustomerAdminQueryService.getDetail`) has already resolved the account
|
|
55
|
+
* through `customer_accounts`' `@OrgScoped` read model and returned `null` if
|
|
56
|
+
* it was out of scope. The statement pins `customerAccountId`, so widening
|
|
57
|
+
* the filter widens the authority by nothing. It goes when feature 087 gives
|
|
58
|
+
* `customer_addresses` an `organization_id` of its own.
|
|
59
|
+
*/
|
|
60
|
+
async defaultAddressId(customerAccountId, kind) {
|
|
61
|
+
const em = this.emFactory();
|
|
62
|
+
const row = await withSystemScope(`customers: default ${kind} address of customer account ${customerAccountId}, authorised by its own @OrgScoped account read`, () => em.findOne(CustomerAddress, {
|
|
63
|
+
customerAccountId,
|
|
64
|
+
kind,
|
|
65
|
+
isDefault: true,
|
|
66
|
+
deletedAt: null,
|
|
67
|
+
}));
|
|
68
|
+
return row?.id ?? null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=customer-defaults-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-defaults-service.js","sourceRoot":"","sources":["../../../src/backend/services/customer-defaults-service.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AA0BzE,MAAM,OAAO,uBAAuB;IAEf;IACA;IACA;IAHnB,YACmB,SAA8B,EAC9B,WAAkC,EAClC,SAAiC;QAFjC,cAAS,GAAT,SAAS,CAAqB;QAC9B,gBAAW,GAAX,WAAW,CAAuB;QAClC,cAAS,GAAT,SAAS,CAAwB;IACjD,CAAC;IAEJ,KAAK,CAAC,cAAc,CAAC,iBAAyB;QAC5C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,kBAAkB,CAAC,iBAAiB,CAAC,CAAC;QAC7E,MAAM,CAAC,gBAAgB,EAAE,iBAAiB,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YAC9D,IAAI,CAAC,gBAAgB,CAAC,iBAAiB,EAAE,SAAS,CAAC;YACnD,IAAI,CAAC,gBAAgB,CAAC,iBAAiB,EAAE,UAAU,CAAC;SACrD,CAAC,CAAC;QACH,OAAO;YACL,eAAe,EAAE,OAAO,CAAC,eAAe;YACxC,gBAAgB,EAAE,OAAO,CAAC,gBAAgB;YAC1C,gBAAgB;YAChB,iBAAiB;SAClB,CAAC;IACJ,CAAC;IAED,qFAAqF;IACrF,KAAK,CAAC,cAAc,CAClB,iBAAyB,EACzB,KAA4B,EAC5B,KAA6B;QAE7B,wDAAwD;QACxD,IAAI,KAAK,CAAC,eAAe,KAAK,SAAS,IAAI,KAAK,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;YAChF,MAAM,IAAI,CAAC,WAAW,CAAC,MAAM,CAC3B,EAAE,IAAI,EAAE,UAAU,EAAE,iBAAiB,EAAE,EACvC;gBACE,KAAK,EAAE,UAAU;gBACjB,OAAO,EAAE,iBAAiB;gBAC1B,GAAG,CAAC,KAAK,CAAC,eAAe,KAAK,SAAS;oBACrC,CAAC,CAAC,EAAE,sBAAsB,EAAE,KAAK,CAAC,eAAe,EAAE;oBACnD,CAAC,CAAC,EAAE,CAAC;gBACP,GAAG,CAAC,KAAK,CAAC,gBAAgB,KAAK,SAAS;oBACtC,CAAC,CAAC,EAAE,uBAAuB,EAAE,KAAK,CAAC,gBAAgB,EAAE;oBACrD,CAAC,CAAC,EAAE,CAAC;aACR,EACD,KAAK,CACN,CAAC;QACJ,CAAC;QACD,kEAAkE;QAClE,IAAI,KAAK,CAAC,gBAAgB,IAAI,IAAI,EAAE,CAAC;YACnC,MAAM,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,iBAAiB,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAC;QAC7E,CAAC;QACD,IAAI,KAAK,CAAC,iBAAiB,IAAI,IAAI,EAAE,CAAC;YACpC,MAAM,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,CAAC,CAAC;QAC9E,CAAC;QACD,OAAO,IAAI,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC;IAChD,CAAC;IAED;;;;;;;;;;OAUG;IACK,KAAK,CAAC,gBAAgB,CAC5B,iBAAyB,EACzB,IAA4B;QAE5B,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,MAAM,eAAe,CAC/B,sBAAsB,IAAI,gCAAgC,iBAAiB,iDAAiD,EAC5H,GAAG,EAAE,CACH,EAAE,CAAC,OAAO,CAAC,eAAe,EAAE;YAC1B,iBAAiB;YACjB,IAAI;YACJ,SAAS,EAAE,IAAI;YACf,SAAS,EAAE,IAAI;SAChB,CAAC,CACL,CAAC;QACF,OAAO,GAAG,EAAE,EAAE,IAAI,IAAI,CAAC;IACzB,CAAC;CACF"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type CustomerAccountLifecycleWritePort, type CustomerAccountReadPort, type CustomerAccountRecord, type PersonalOrganizationPort } from '@endora-commerce/contracts';
|
|
2
|
+
import type { CustomerAuthorityService } from './customer-authority-service.js';
|
|
3
|
+
import type { ModerationActor, SessionRevoker } from './customer-moderation-service.js';
|
|
4
|
+
/**
|
|
5
|
+
* CustomerDeletionService — soft-delete, restore, and permanent anonymization
|
|
6
|
+
* (feature 040, US7 / FR-039/FR-040).
|
|
7
|
+
*
|
|
8
|
+
* Deletion is a soft-disable: login is denied, the account is hidden, and
|
|
9
|
+
* sessions are revoked; history (orders, RFQs, audit) is retained. Within the
|
|
10
|
+
* retention window an authorized actor can restore. The anonymization sweep
|
|
11
|
+
* irreversibly scrubs PII after the window, after which restore is impossible.
|
|
12
|
+
*
|
|
13
|
+
* **Feature 075, Phase C — this service holds no other module's entity.** The
|
|
14
|
+
* account writes and the scrub are `customer_accounts`', through
|
|
15
|
+
* `customerAccountLifecycleWritePort`; the cascade onto a member-less personal
|
|
16
|
+
* organisation is `organizations`', through `personalOrganizationPort`. Each
|
|
17
|
+
* owner records the one audit row that describes its own write, with the same
|
|
18
|
+
* action strings as before. What is left here is the *order* of the sweep and
|
|
19
|
+
* the authority checks, which are this module's.
|
|
20
|
+
*/
|
|
21
|
+
export declare class CustomerDeletionService {
|
|
22
|
+
private readonly accounts;
|
|
23
|
+
private readonly accountWrites;
|
|
24
|
+
private readonly personalOrganizations;
|
|
25
|
+
private readonly authority;
|
|
26
|
+
private readonly sessions;
|
|
27
|
+
constructor(accounts: CustomerAccountReadPort, accountWrites: CustomerAccountLifecycleWritePort, personalOrganizations: PersonalOrganizationPort, authority: CustomerAuthorityService, sessions: SessionRevoker);
|
|
28
|
+
softDelete(customerAccountId: string, actor: ModerationActor): Promise<CustomerAccountRecord>;
|
|
29
|
+
restore(customerAccountId: string, actor: ModerationActor): Promise<CustomerAccountRecord>;
|
|
30
|
+
/**
|
|
31
|
+
* Permanently anonymizes every soft-deleted account whose retention window
|
|
32
|
+
* has elapsed (`deletedAt + retentionDays < now`). Returns the number
|
|
33
|
+
* anonymized. Called by the repeatable sweep worker.
|
|
34
|
+
*/
|
|
35
|
+
sweep(retentionDays: number, now: Date): Promise<number>;
|
|
36
|
+
private load;
|
|
37
|
+
private assertAuthorized;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=customer-deletion-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-deletion-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/customer-deletion-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,iCAAiC,EACtC,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAC9B,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAChF,OAAO,KAAK,EACV,eAAe,EACf,cAAc,EACf,MAAM,kCAAkC,CAAC;AAE1C;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,uBAAuB;IAEhC,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,qBAAqB;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAJR,QAAQ,EAAE,uBAAuB,EACjC,aAAa,EAAE,iCAAiC,EAChD,qBAAqB,EAAE,wBAAwB,EAC/C,SAAS,EAAE,wBAAwB,EACnC,QAAQ,EAAE,cAAc;IAGrC,UAAU,CACd,iBAAiB,EAAE,MAAM,EACzB,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,qBAAqB,CAAC;IA8B3B,OAAO,CACX,iBAAiB,EAAE,MAAM,EACzB,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,qBAAqB,CAAC;IAMjC;;;;OAIG;IACG,KAAK,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC;YAehD,IAAI;YAQJ,gBAAgB;CAa/B"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { ERROR_CODES, } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
/**
|
|
4
|
+
* CustomerDeletionService — soft-delete, restore, and permanent anonymization
|
|
5
|
+
* (feature 040, US7 / FR-039/FR-040).
|
|
6
|
+
*
|
|
7
|
+
* Deletion is a soft-disable: login is denied, the account is hidden, and
|
|
8
|
+
* sessions are revoked; history (orders, RFQs, audit) is retained. Within the
|
|
9
|
+
* retention window an authorized actor can restore. The anonymization sweep
|
|
10
|
+
* irreversibly scrubs PII after the window, after which restore is impossible.
|
|
11
|
+
*
|
|
12
|
+
* **Feature 075, Phase C — this service holds no other module's entity.** The
|
|
13
|
+
* account writes and the scrub are `customer_accounts`', through
|
|
14
|
+
* `customerAccountLifecycleWritePort`; the cascade onto a member-less personal
|
|
15
|
+
* organisation is `organizations`', through `personalOrganizationPort`. Each
|
|
16
|
+
* owner records the one audit row that describes its own write, with the same
|
|
17
|
+
* action strings as before. What is left here is the *order* of the sweep and
|
|
18
|
+
* the authority checks, which are this module's.
|
|
19
|
+
*/
|
|
20
|
+
export class CustomerDeletionService {
|
|
21
|
+
accounts;
|
|
22
|
+
accountWrites;
|
|
23
|
+
personalOrganizations;
|
|
24
|
+
authority;
|
|
25
|
+
sessions;
|
|
26
|
+
constructor(accounts, accountWrites, personalOrganizations, authority, sessions) {
|
|
27
|
+
this.accounts = accounts;
|
|
28
|
+
this.accountWrites = accountWrites;
|
|
29
|
+
this.personalOrganizations = personalOrganizations;
|
|
30
|
+
this.authority = authority;
|
|
31
|
+
this.sessions = sessions;
|
|
32
|
+
}
|
|
33
|
+
async softDelete(customerAccountId, actor) {
|
|
34
|
+
const customer = await this.load(customerAccountId);
|
|
35
|
+
await this.assertAuthorized(actor, customer.organizationId);
|
|
36
|
+
if (customer.deletedAt) {
|
|
37
|
+
throw new HttpError(409, ERROR_CODES.CUSTOMER_ALREADY_DELETED, 'Customer is already deleted.');
|
|
38
|
+
}
|
|
39
|
+
// Refuse to strand an org without an admin.
|
|
40
|
+
if (customer.organizationId && customer.role === 'organization_admin') {
|
|
41
|
+
const remaining = await this.accounts.countByOrganizationRole(customer.organizationId, 'organization_admin', { excludeCustomerAccountId: customer.id, activeOnly: true });
|
|
42
|
+
if (remaining === 0) {
|
|
43
|
+
throw new HttpError(409, ERROR_CODES.ORG_OWNER_DEPLETION, 'Cannot delete the last organization administrator.');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const deleted = await this.accountWrites.softDelete(customer.id, {
|
|
47
|
+
actorAdminUserId: actor.adminUserId,
|
|
48
|
+
});
|
|
49
|
+
await this.sessions.destroyAllForCustomer(customer.id);
|
|
50
|
+
return deleted;
|
|
51
|
+
}
|
|
52
|
+
async restore(customerAccountId, actor) {
|
|
53
|
+
const customer = await this.load(customerAccountId);
|
|
54
|
+
await this.assertAuthorized(actor, customer.organizationId);
|
|
55
|
+
return this.accountWrites.restore(customer.id, { actorAdminUserId: actor.adminUserId });
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Permanently anonymizes every soft-deleted account whose retention window
|
|
59
|
+
* has elapsed (`deletedAt + retentionDays < now`). Returns the number
|
|
60
|
+
* anonymized. Called by the repeatable sweep worker.
|
|
61
|
+
*/
|
|
62
|
+
async sweep(retentionDays, now) {
|
|
63
|
+
const cutoff = new Date(now.getTime() - retentionDays * 24 * 60 * 60 * 1000);
|
|
64
|
+
const due = await this.accountWrites.listDueForAnonymization(cutoff);
|
|
65
|
+
for (const customer of due) {
|
|
66
|
+
await this.accountWrites.anonymize(customer.id);
|
|
67
|
+
// Feature 051 — a personal (B2C) organization backs exactly one customer.
|
|
68
|
+
// Once that customer is anonymized the org is member-less, so the scrub
|
|
69
|
+
// cascades: the owner renames it, blanks its address and soft-deletes it.
|
|
70
|
+
// Company (multi-member) orgs are never touched, and the owner decides
|
|
71
|
+
// that — this sweep only says which customer it just scrubbed.
|
|
72
|
+
await this.personalOrganizations.anonymizeIfOrphaned(customer.id);
|
|
73
|
+
}
|
|
74
|
+
return due.length;
|
|
75
|
+
}
|
|
76
|
+
async load(id) {
|
|
77
|
+
const customer = await this.accounts.findById(id);
|
|
78
|
+
if (!customer) {
|
|
79
|
+
throw new HttpError(404, ERROR_CODES.CUSTOMER_NOT_FOUND, 'Customer not found.');
|
|
80
|
+
}
|
|
81
|
+
return customer;
|
|
82
|
+
}
|
|
83
|
+
async assertAuthorized(actor, organizationId) {
|
|
84
|
+
const allowed = await this.authority.canManageCustomer({
|
|
85
|
+
isPlatformAdmin: actor.isPlatformAdmin,
|
|
86
|
+
adminUserId: actor.adminUserId,
|
|
87
|
+
customerOrganizationId: organizationId,
|
|
88
|
+
});
|
|
89
|
+
if (!allowed) {
|
|
90
|
+
throw new HttpError(403, ERROR_CODES.FORBIDDEN, 'You are not allowed to manage this customer.');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=customer-deletion-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-deletion-service.js","sourceRoot":"","sources":["../../../src/backend/services/customer-deletion-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type CustomerAccountLifecycleWritePort, type CustomerAccountReadPort, type CustomerAccountRecord } from '@endora-commerce/contracts';
|
|
2
|
+
import type { CustomerAuthorityService } from './customer-authority-service.js';
|
|
3
|
+
/**
|
|
4
|
+
* CustomerModerationService — block / unblock a Customer account (feature 040,
|
|
5
|
+
* US3). Enforces the staff-authority rules (FR-013/FR-015 via
|
|
6
|
+
* CustomerAuthorityService), revokes sessions on block, and guards against
|
|
7
|
+
* stranding an Organization with zero usable admins.
|
|
8
|
+
*
|
|
9
|
+
* **Feature 075, Phase C — the row is `customer_accounts`'.** This service used
|
|
10
|
+
* to load and mutate that module's entity and write the audit row for it; both
|
|
11
|
+
* moved behind `customerAccountLifecycleWritePort`, which records exactly the
|
|
12
|
+
* same `customer_account.blocked` / `.unblocked` entry (FR-017). What stays is
|
|
13
|
+
* everything that is a *decision*: whether this staff member may act on this
|
|
14
|
+
* customer, and whether the block would leave an Organization without an
|
|
15
|
+
* administrator. The session revocation stays too — it is `auth`'s, and it must
|
|
16
|
+
* happen after the block is durable rather than inside it.
|
|
17
|
+
*/
|
|
18
|
+
export interface SessionRevoker {
|
|
19
|
+
destroyAllForCustomer(customerAccountId: string): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export interface ModerationActor {
|
|
22
|
+
adminUserId: string;
|
|
23
|
+
isPlatformAdmin: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface ModerationAuditMeta {
|
|
26
|
+
ipAddress?: string | null;
|
|
27
|
+
userAgent?: string | null;
|
|
28
|
+
requestId?: string | null;
|
|
29
|
+
}
|
|
30
|
+
export interface BlockCustomerInput {
|
|
31
|
+
targetCustomerAccountId: string;
|
|
32
|
+
actor: ModerationActor;
|
|
33
|
+
reason?: string | null;
|
|
34
|
+
audit?: ModerationAuditMeta;
|
|
35
|
+
}
|
|
36
|
+
export interface UnblockCustomerInput {
|
|
37
|
+
targetCustomerAccountId: string;
|
|
38
|
+
actor: ModerationActor;
|
|
39
|
+
audit?: ModerationAuditMeta;
|
|
40
|
+
}
|
|
41
|
+
export declare class CustomerModerationService {
|
|
42
|
+
private readonly accounts;
|
|
43
|
+
private readonly accountWrites;
|
|
44
|
+
private readonly authority;
|
|
45
|
+
private readonly sessions;
|
|
46
|
+
constructor(accounts: CustomerAccountReadPort, accountWrites: CustomerAccountLifecycleWritePort, authority: CustomerAuthorityService, sessions: SessionRevoker);
|
|
47
|
+
block(input: BlockCustomerInput): Promise<CustomerAccountRecord>;
|
|
48
|
+
unblock(input: UnblockCustomerInput): Promise<CustomerAccountRecord>;
|
|
49
|
+
private loadActive;
|
|
50
|
+
private assertAuthorized;
|
|
51
|
+
/**
|
|
52
|
+
* Refuses to block the last usable (active, non-blocked) organization admin
|
|
53
|
+
* of an Organization — that would strand the org with nobody able to manage
|
|
54
|
+
* it (spec edge case "org-owner depletion").
|
|
55
|
+
*/
|
|
56
|
+
private assertWouldNotStrandOrg;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=customer-moderation-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-moderation-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/customer-moderation-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,iCAAiC,EACtC,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC3B,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAEhF;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,cAAc;IAC7B,qBAAqB,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjE;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,kBAAkB;IACjC,uBAAuB,EAAE,MAAM,CAAC;IAChC,KAAK,EAAE,eAAe,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC7B;AAED,MAAM,WAAW,oBAAoB;IACnC,uBAAuB,EAAE,MAAM,CAAC;IAChC,KAAK,EAAE,eAAe,CAAC;IACvB,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC7B;AAED,qBAAa,yBAAyB;IAElC,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAHR,QAAQ,EAAE,uBAAuB,EACjC,aAAa,EAAE,iCAAiC,EAChD,SAAS,EAAE,wBAAwB,EACnC,QAAQ,EAAE,cAAc;IAGrC,KAAK,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAmBhE,OAAO,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC;YAY5D,UAAU;YAQV,gBAAgB;IAkB9B;;;;OAIG;YACW,uBAAuB;CAetC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { ERROR_CODES, } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
export class CustomerModerationService {
|
|
4
|
+
accounts;
|
|
5
|
+
accountWrites;
|
|
6
|
+
authority;
|
|
7
|
+
sessions;
|
|
8
|
+
constructor(accounts, accountWrites, authority, sessions) {
|
|
9
|
+
this.accounts = accounts;
|
|
10
|
+
this.accountWrites = accountWrites;
|
|
11
|
+
this.authority = authority;
|
|
12
|
+
this.sessions = sessions;
|
|
13
|
+
}
|
|
14
|
+
async block(input) {
|
|
15
|
+
const customer = await this.loadActive(input.targetCustomerAccountId);
|
|
16
|
+
await this.assertAuthorized(input.actor, customer);
|
|
17
|
+
if (customer.blockedAt)
|
|
18
|
+
return customer; // idempotent
|
|
19
|
+
await this.assertWouldNotStrandOrg(customer);
|
|
20
|
+
const blocked = await this.accountWrites.block(customer.id, {
|
|
21
|
+
actorAdminUserId: input.actor.adminUserId,
|
|
22
|
+
reason: input.reason ?? null,
|
|
23
|
+
...(input.audit ? { audit: input.audit } : {}),
|
|
24
|
+
});
|
|
25
|
+
// Revoke active sessions so the block takes effect promptly (SC-002).
|
|
26
|
+
await this.sessions.destroyAllForCustomer(customer.id);
|
|
27
|
+
return blocked;
|
|
28
|
+
}
|
|
29
|
+
async unblock(input) {
|
|
30
|
+
const customer = await this.loadActive(input.targetCustomerAccountId);
|
|
31
|
+
await this.assertAuthorized(input.actor, customer);
|
|
32
|
+
if (!customer.blockedAt)
|
|
33
|
+
return customer; // idempotent
|
|
34
|
+
return this.accountWrites.unblock(customer.id, {
|
|
35
|
+
actorAdminUserId: input.actor.adminUserId,
|
|
36
|
+
...(input.audit ? { audit: input.audit } : {}),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async loadActive(id) {
|
|
40
|
+
const customer = await this.accounts.findById(id, { activeOnly: true });
|
|
41
|
+
if (!customer) {
|
|
42
|
+
throw new HttpError(404, ERROR_CODES.CUSTOMER_NOT_FOUND, 'Customer not found.');
|
|
43
|
+
}
|
|
44
|
+
return customer;
|
|
45
|
+
}
|
|
46
|
+
async assertAuthorized(actor, customer) {
|
|
47
|
+
const allowed = await this.authority.canManageCustomer({
|
|
48
|
+
isPlatformAdmin: actor.isPlatformAdmin,
|
|
49
|
+
adminUserId: actor.adminUserId,
|
|
50
|
+
customerOrganizationId: customer.organizationId,
|
|
51
|
+
});
|
|
52
|
+
if (!allowed) {
|
|
53
|
+
throw new HttpError(403, ERROR_CODES.FORBIDDEN, 'You are not allowed to manage this customer.');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Refuses to block the last usable (active, non-blocked) organization admin
|
|
58
|
+
* of an Organization — that would strand the org with nobody able to manage
|
|
59
|
+
* it (spec edge case "org-owner depletion").
|
|
60
|
+
*/
|
|
61
|
+
async assertWouldNotStrandOrg(customer) {
|
|
62
|
+
if (!customer.organizationId || customer.role !== 'organization_admin')
|
|
63
|
+
return;
|
|
64
|
+
const remaining = await this.accounts.countByOrganizationRole(customer.organizationId, 'organization_admin', { excludeCustomerAccountId: customer.id, activeOnly: true, notBlocked: true });
|
|
65
|
+
if (remaining === 0) {
|
|
66
|
+
throw new HttpError(409, ERROR_CODES.ORG_OWNER_DEPLETION, 'Cannot block the last organization administrator; the organization would be left without an admin.');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=customer-moderation-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-moderation-service.js","sourceRoot":"","sources":["../../../src/backend/services/customer-moderation-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type CustomerAccountLifecycleWritePort, type CustomerAccountReadPort, type CustomerAccountRecord, type OrganizationDetailsPort, type PersonalOrganizationPort } from '@endora-commerce/contracts';
|
|
2
|
+
import type { CustomerAuthorityService } from './customer-authority-service.js';
|
|
3
|
+
import type { ModerationActor } from './customer-moderation-service.js';
|
|
4
|
+
/**
|
|
5
|
+
* CustomerOrgAssignmentService — assign a Customer to an Organization, or
|
|
6
|
+
* detach one from the company it belongs to (feature 040, US5 / FR-026). Both
|
|
7
|
+
* actions are authority-checked and audited; detaching the last usable org
|
|
8
|
+
* admin is refused (depletion guard, mirroring moderation).
|
|
9
|
+
*
|
|
10
|
+
* **D-178 redefined the second action.** `unassign` wrote
|
|
11
|
+
* `organization_id = NULL` and produced an account with no tenant: it could not
|
|
12
|
+
* place an order, submit an RFQ or read an address, it shared one filter bucket
|
|
13
|
+
* with every other tenant-less account, and nothing ever gave it an
|
|
14
|
+
* organisation back. The operator's intent — *this person no longer belongs to
|
|
15
|
+
* that company* — is served exactly by moving them to their own single-member
|
|
16
|
+
* personal organisation, which is what the operation does now, and what the
|
|
17
|
+
* platform would have given them had they registered on their own.
|
|
18
|
+
*
|
|
19
|
+
* **Feature 075, Phase C — neither row is this module's.** The membership
|
|
20
|
+
* column belongs to `customer_accounts` and the organisation it points at
|
|
21
|
+
* belongs to `organizations`; both are reached through their published ports
|
|
22
|
+
* now, and the audit row goes with the write. The authority checks and the
|
|
23
|
+
* depletion guard stay here, because they are this module's policy.
|
|
24
|
+
*/
|
|
25
|
+
export declare class CustomerOrgAssignmentService {
|
|
26
|
+
private readonly accounts;
|
|
27
|
+
private readonly accountWrites;
|
|
28
|
+
private readonly organizations;
|
|
29
|
+
private readonly authority;
|
|
30
|
+
/** D-178 — where a detached member goes. */
|
|
31
|
+
private readonly personalOrganizations;
|
|
32
|
+
constructor(accounts: CustomerAccountReadPort, accountWrites: CustomerAccountLifecycleWritePort, organizations: OrganizationDetailsPort, authority: CustomerAuthorityService,
|
|
33
|
+
/** D-178 — where a detached member goes. */
|
|
34
|
+
personalOrganizations: PersonalOrganizationPort);
|
|
35
|
+
assign(customerAccountId: string, organizationId: string, actor: ModerationActor): Promise<CustomerAccountRecord>;
|
|
36
|
+
/**
|
|
37
|
+
* Detach the customer from the company they belong to, moving them to their
|
|
38
|
+
* own personal organisation (D-178).
|
|
39
|
+
*
|
|
40
|
+
* The destination is resolved before the depletion guard runs, so the refusal
|
|
41
|
+
* an operator sees is still the depletion one and not a provisioning failure
|
|
42
|
+
* underneath it. It is idempotent: a customer already in their own personal
|
|
43
|
+
* organisation is returned unchanged, and no audit row is written for a move
|
|
44
|
+
* that did not happen.
|
|
45
|
+
*/
|
|
46
|
+
unassign(customerAccountId: string, actor: ModerationActor): Promise<CustomerAccountRecord>;
|
|
47
|
+
/** Sets (or clears) the customer's direct customer-group (feature 040, US6). */
|
|
48
|
+
setCustomerGroup(customerAccountId: string, customerGroupId: string | null, actor: ModerationActor): Promise<CustomerAccountRecord>;
|
|
49
|
+
private loadActive;
|
|
50
|
+
private assertAuthorized;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=customer-org-assignment-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-org-assignment-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/customer-org-assignment-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,iCAAiC,EACtC,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC1B,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC9B,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAChF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AAExE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,4BAA4B;IAErC,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,4CAA4C;IAC5C,OAAO,CAAC,QAAQ,CAAC,qBAAqB;gBALrB,QAAQ,EAAE,uBAAuB,EACjC,aAAa,EAAE,iCAAiC,EAChD,aAAa,EAAE,uBAAuB,EACtC,SAAS,EAAE,wBAAwB;IACpD,4CAA4C;IAC3B,qBAAqB,EAAE,wBAAwB;IAG5D,MAAM,CACV,iBAAiB,EAAE,MAAM,EACzB,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,qBAAqB,CAAC;IAkBjC;;;;;;;;;OASG;IACG,QAAQ,CACZ,iBAAiB,EAAE,MAAM,EACzB,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,qBAAqB,CAAC;IA4BjC,gFAAgF;IAC1E,gBAAgB,CACpB,iBAAiB,EAAE,MAAM,EACzB,eAAe,EAAE,MAAM,GAAG,IAAI,EAC9B,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,qBAAqB,CAAC;YASnB,UAAU;YAQV,gBAAgB;CAa/B"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { ERROR_CODES, } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
/**
|
|
4
|
+
* CustomerOrgAssignmentService — assign a Customer to an Organization, or
|
|
5
|
+
* detach one from the company it belongs to (feature 040, US5 / FR-026). Both
|
|
6
|
+
* actions are authority-checked and audited; detaching the last usable org
|
|
7
|
+
* admin is refused (depletion guard, mirroring moderation).
|
|
8
|
+
*
|
|
9
|
+
* **D-178 redefined the second action.** `unassign` wrote
|
|
10
|
+
* `organization_id = NULL` and produced an account with no tenant: it could not
|
|
11
|
+
* place an order, submit an RFQ or read an address, it shared one filter bucket
|
|
12
|
+
* with every other tenant-less account, and nothing ever gave it an
|
|
13
|
+
* organisation back. The operator's intent — *this person no longer belongs to
|
|
14
|
+
* that company* — is served exactly by moving them to their own single-member
|
|
15
|
+
* personal organisation, which is what the operation does now, and what the
|
|
16
|
+
* platform would have given them had they registered on their own.
|
|
17
|
+
*
|
|
18
|
+
* **Feature 075, Phase C — neither row is this module's.** The membership
|
|
19
|
+
* column belongs to `customer_accounts` and the organisation it points at
|
|
20
|
+
* belongs to `organizations`; both are reached through their published ports
|
|
21
|
+
* now, and the audit row goes with the write. The authority checks and the
|
|
22
|
+
* depletion guard stay here, because they are this module's policy.
|
|
23
|
+
*/
|
|
24
|
+
export class CustomerOrgAssignmentService {
|
|
25
|
+
accounts;
|
|
26
|
+
accountWrites;
|
|
27
|
+
organizations;
|
|
28
|
+
authority;
|
|
29
|
+
personalOrganizations;
|
|
30
|
+
constructor(accounts, accountWrites, organizations, authority,
|
|
31
|
+
/** D-178 — where a detached member goes. */
|
|
32
|
+
personalOrganizations) {
|
|
33
|
+
this.accounts = accounts;
|
|
34
|
+
this.accountWrites = accountWrites;
|
|
35
|
+
this.organizations = organizations;
|
|
36
|
+
this.authority = authority;
|
|
37
|
+
this.personalOrganizations = personalOrganizations;
|
|
38
|
+
}
|
|
39
|
+
async assign(customerAccountId, organizationId, actor) {
|
|
40
|
+
const customer = await this.loadActive(customerAccountId);
|
|
41
|
+
// Actor must be able to manage the customer in its CURRENT state and in
|
|
42
|
+
// the TARGET organization.
|
|
43
|
+
await this.assertAuthorized(actor, customer.organizationId);
|
|
44
|
+
await this.assertAuthorized(actor, organizationId);
|
|
45
|
+
const org = await this.organizations.findById(organizationId);
|
|
46
|
+
if (!org) {
|
|
47
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Organization not found.');
|
|
48
|
+
}
|
|
49
|
+
return this.accountWrites.setOrganization(customer.id, organizationId, {
|
|
50
|
+
actorAdminUserId: actor.adminUserId,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Detach the customer from the company they belong to, moving them to their
|
|
55
|
+
* own personal organisation (D-178).
|
|
56
|
+
*
|
|
57
|
+
* The destination is resolved before the depletion guard runs, so the refusal
|
|
58
|
+
* an operator sees is still the depletion one and not a provisioning failure
|
|
59
|
+
* underneath it. It is idempotent: a customer already in their own personal
|
|
60
|
+
* organisation is returned unchanged, and no audit row is written for a move
|
|
61
|
+
* that did not happen.
|
|
62
|
+
*/
|
|
63
|
+
async unassign(customerAccountId, actor) {
|
|
64
|
+
const customer = await this.loadActive(customerAccountId);
|
|
65
|
+
await this.assertAuthorized(actor, customer.organizationId);
|
|
66
|
+
const personal = await this.personalOrganizations.provisionPersonalOrganization(customer.id);
|
|
67
|
+
if (customer.organizationId === personal.id)
|
|
68
|
+
return customer; // already their own
|
|
69
|
+
// Refuse to strand the org without an admin.
|
|
70
|
+
if (customer.role === 'organization_admin') {
|
|
71
|
+
const remaining = await this.accounts.countByOrganizationRole(customer.organizationId, 'organization_admin', { excludeCustomerAccountId: customer.id, activeOnly: true });
|
|
72
|
+
if (remaining === 0) {
|
|
73
|
+
throw new HttpError(409, ERROR_CODES.ORG_OWNER_DEPLETION, 'Cannot unassign the last organization administrator.');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return this.accountWrites.detachToPersonalOrganization(customer.id, personal.id, {
|
|
77
|
+
actorAdminUserId: actor.adminUserId,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** Sets (or clears) the customer's direct customer-group (feature 040, US6). */
|
|
81
|
+
async setCustomerGroup(customerAccountId, customerGroupId, actor) {
|
|
82
|
+
const customer = await this.loadActive(customerAccountId);
|
|
83
|
+
await this.assertAuthorized(actor, customer.organizationId);
|
|
84
|
+
return this.accountWrites.setCustomerGroup(customer.id, customerGroupId, {
|
|
85
|
+
actorAdminUserId: actor.adminUserId,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async loadActive(id) {
|
|
89
|
+
const customer = await this.accounts.findById(id, { activeOnly: true });
|
|
90
|
+
if (!customer) {
|
|
91
|
+
throw new HttpError(404, ERROR_CODES.CUSTOMER_NOT_FOUND, 'Customer not found.');
|
|
92
|
+
}
|
|
93
|
+
return customer;
|
|
94
|
+
}
|
|
95
|
+
async assertAuthorized(actor, organizationId) {
|
|
96
|
+
const allowed = await this.authority.canManageCustomer({
|
|
97
|
+
isPlatformAdmin: actor.isPlatformAdmin,
|
|
98
|
+
adminUserId: actor.adminUserId,
|
|
99
|
+
customerOrganizationId: organizationId,
|
|
100
|
+
});
|
|
101
|
+
if (!allowed) {
|
|
102
|
+
throw new HttpError(403, ERROR_CODES.FORBIDDEN, 'Not allowed for this organization.');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=customer-org-assignment-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-org-assignment-service.js","sourceRoot":"","sources":["../../../src/backend/services/customer-org-assignment-service.ts"],"names":[],"mappings":"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"}
|