@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,39 @@
|
|
|
1
|
+
/** Maps a persisted CustomerAddress to its API contract shape. */
|
|
2
|
+
export function serializeCustomerAddress(a) {
|
|
3
|
+
return {
|
|
4
|
+
id: a.id,
|
|
5
|
+
customerAccountId: a.customerAccountId,
|
|
6
|
+
kind: a.kind,
|
|
7
|
+
recipientName: a.recipientName,
|
|
8
|
+
street: a.street,
|
|
9
|
+
city: a.city,
|
|
10
|
+
postalCode: a.postalCode,
|
|
11
|
+
country: a.country,
|
|
12
|
+
phone: a.phone ?? null,
|
|
13
|
+
isDefault: a.isDefault,
|
|
14
|
+
createdAt: a.createdAt.toISOString(),
|
|
15
|
+
updatedAt: a.updatedAt.toISOString(),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Maps an org-shared address to the read-only reference shape.
|
|
20
|
+
*
|
|
21
|
+
* Feature 075 — the argument is `addresses`' published {@link AddressRecord},
|
|
22
|
+
* not its `Address` entity: this module only ever read the columns the record
|
|
23
|
+
* carries, and an entity crossing a boundary is what the port exists to stop.
|
|
24
|
+
*/
|
|
25
|
+
export function serializeOrganizationAddress(a) {
|
|
26
|
+
return {
|
|
27
|
+
id: a.id,
|
|
28
|
+
organizationId: a.organizationId,
|
|
29
|
+
kind: a.kind,
|
|
30
|
+
recipientName: a.recipientName,
|
|
31
|
+
street: a.street,
|
|
32
|
+
city: a.city,
|
|
33
|
+
postalCode: a.postalCode,
|
|
34
|
+
country: a.country,
|
|
35
|
+
phone: a.phone ?? null,
|
|
36
|
+
isDefault: a.isDefault,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=serializers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serializers.js","sourceRoot":"","sources":["../../src/backend/serializers.ts"],"names":[],"mappings":"AAOA,kEAAkE;AAClE,MAAM,UAAU,wBAAwB,CAAC,CAAkB;IACzD,OAAO;QACL,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,iBAAiB,EAAE,CAAC,CAAC,iBAAiB;QACtC,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,aAAa,EAAE,CAAC,CAAC,aAAa;QAC9B,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,UAAU,EAAE,CAAC,CAAC,UAAU;QACxB,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI;QACtB,SAAS,EAAE,CAAC,CAAC,SAAS;QACtB,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,WAAW,EAAE;QACpC,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,WAAW,EAAE;KACrC,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,4BAA4B,CAAC,CAAgB;IAC3D,OAAO;QACL,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,cAAc,EAAE,CAAC,CAAC,cAAc;QAChC,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,aAAa,EAAE,CAAC,CAAC,aAAa;QAC9B,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,UAAU,EAAE,CAAC,CAAC,UAAU;QACxB,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI;QACtB,SAAS,EAAE,CAAC,CAAC,SAAS;KACvB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { CustomerAddressReadPort, CustomerAddressRecord } from '@endora-commerce/contracts';
|
|
3
|
+
import { CustomerAddress } from '../entities/customer-address.entity.js';
|
|
4
|
+
/**
|
|
5
|
+
* The customer-address read model `customers` publishes (feature 075, Phase P).
|
|
6
|
+
*
|
|
7
|
+
* One consumer: `quick_order` resolves a buyer's saved personal address when
|
|
8
|
+
* it fills in their one-click defaults.
|
|
9
|
+
*
|
|
10
|
+
* The table is distinct from `addresses`' on purpose and the port keeps them
|
|
11
|
+
* distinct: this one hangs off a **customer account**, that one off an
|
|
12
|
+
* **organisation**. A B2C buyer keeps addresses that are theirs rather than
|
|
13
|
+
* their personal organisation's, and a merged shape would hide which of the
|
|
14
|
+
* two a caller is holding.
|
|
15
|
+
*
|
|
16
|
+
* `findById` is scoped by customer account for the reason its organisation
|
|
17
|
+
* twin gives: the caller already knows whose address it wants, and one query
|
|
18
|
+
* is what stops the ownership check being forgotten.
|
|
19
|
+
*/
|
|
20
|
+
export declare class CustomerAddressReadService implements CustomerAddressReadPort {
|
|
21
|
+
private readonly emFactory;
|
|
22
|
+
constructor(emFactory: () => EntityManager);
|
|
23
|
+
findById(customerAccountId: string, addressId: string, options?: {
|
|
24
|
+
liveOnly?: boolean;
|
|
25
|
+
}): Promise<CustomerAddressRecord | null>;
|
|
26
|
+
listForCustomer(customerAccountId: string, kind?: 'delivery' | 'billing'): Promise<CustomerAddressRecord[]>;
|
|
27
|
+
}
|
|
28
|
+
export declare function toCustomerAddressRecord(address: CustomerAddress): CustomerAddressRecord;
|
|
29
|
+
//# sourceMappingURL=customer-address-read-port.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-address-read-port.d.ts","sourceRoot":"","sources":["../../../src/backend/services/customer-address-read-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACjG,OAAO,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AAEzE;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,0BAA2B,YAAW,uBAAuB;IAC5D,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAErD,QAAQ,CACZ,iBAAiB,EAAE,MAAM,EACzB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAC/B,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC;IASlC,eAAe,CACnB,iBAAiB,EAAE,MAAM,EACzB,IAAI,CAAC,EAAE,UAAU,GAAG,SAAS,GAC5B,OAAO,CAAC,qBAAqB,EAAE,CAAC;CAQpC;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,eAAe,GAAG,qBAAqB,CAgBvF"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { CustomerAddress } from '../entities/customer-address.entity.js';
|
|
2
|
+
/**
|
|
3
|
+
* The customer-address read model `customers` publishes (feature 075, Phase P).
|
|
4
|
+
*
|
|
5
|
+
* One consumer: `quick_order` resolves a buyer's saved personal address when
|
|
6
|
+
* it fills in their one-click defaults.
|
|
7
|
+
*
|
|
8
|
+
* The table is distinct from `addresses`' on purpose and the port keeps them
|
|
9
|
+
* distinct: this one hangs off a **customer account**, that one off an
|
|
10
|
+
* **organisation**. A B2C buyer keeps addresses that are theirs rather than
|
|
11
|
+
* their personal organisation's, and a merged shape would hide which of the
|
|
12
|
+
* two a caller is holding.
|
|
13
|
+
*
|
|
14
|
+
* `findById` is scoped by customer account for the reason its organisation
|
|
15
|
+
* twin gives: the caller already knows whose address it wants, and one query
|
|
16
|
+
* is what stops the ownership check being forgotten.
|
|
17
|
+
*/
|
|
18
|
+
export class CustomerAddressReadService {
|
|
19
|
+
emFactory;
|
|
20
|
+
constructor(emFactory) {
|
|
21
|
+
this.emFactory = emFactory;
|
|
22
|
+
}
|
|
23
|
+
async findById(customerAccountId, addressId, options) {
|
|
24
|
+
const address = await this.emFactory().findOne(CustomerAddress, {
|
|
25
|
+
id: addressId,
|
|
26
|
+
customerAccountId,
|
|
27
|
+
...(options?.liveOnly ? { deletedAt: null } : {}),
|
|
28
|
+
});
|
|
29
|
+
return address ? toCustomerAddressRecord(address) : null;
|
|
30
|
+
}
|
|
31
|
+
async listForCustomer(customerAccountId, kind) {
|
|
32
|
+
const addresses = await this.emFactory().find(CustomerAddress, { customerAccountId, deletedAt: null, ...(kind === undefined ? {} : { kind }) }, { orderBy: { isDefault: 'desc', createdAt: 'asc' } });
|
|
33
|
+
return addresses.map(toCustomerAddressRecord);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function toCustomerAddressRecord(address) {
|
|
37
|
+
return {
|
|
38
|
+
id: address.id,
|
|
39
|
+
customerAccountId: address.customerAccountId,
|
|
40
|
+
kind: address.kind,
|
|
41
|
+
recipientName: address.recipientName,
|
|
42
|
+
street: address.street,
|
|
43
|
+
city: address.city,
|
|
44
|
+
postalCode: address.postalCode,
|
|
45
|
+
country: address.country,
|
|
46
|
+
phone: address.phone ?? null,
|
|
47
|
+
isDefault: address.isDefault,
|
|
48
|
+
createdAt: address.createdAt,
|
|
49
|
+
updatedAt: address.updatedAt,
|
|
50
|
+
deletedAt: address.deletedAt ?? null,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=customer-address-read-port.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-address-read-port.js","sourceRoot":"","sources":["../../../src/backend/services/customer-address-read-port.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AAEzE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,0BAA0B;IACR;IAA7B,YAA6B,SAA8B;QAA9B,cAAS,GAAT,SAAS,CAAqB;IAAG,CAAC;IAE/D,KAAK,CAAC,QAAQ,CACZ,iBAAyB,EACzB,SAAiB,EACjB,OAAgC;QAEhC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,eAAe,EAAE;YAC9D,EAAE,EAAE,SAAS;YACb,iBAAiB;YACjB,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAClD,CAAC,CAAC;QACH,OAAO,OAAO,CAAC,CAAC,CAAC,uBAAuB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC3D,CAAC;IAED,KAAK,CAAC,eAAe,CACnB,iBAAyB,EACzB,IAA6B;QAE7B,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,IAAI,CAC3C,eAAe,EACf,EAAE,iBAAiB,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAC/E,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,CACrD,CAAC;QACF,OAAO,SAAS,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;IAChD,CAAC;CACF;AAED,MAAM,UAAU,uBAAuB,CAAC,OAAwB;IAC9D,OAAO;QACL,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,iBAAiB,EAAE,OAAO,CAAC,iBAAiB;QAC5C,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,aAAa,EAAE,OAAO,CAAC,aAAa;QACpC,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,IAAI;QAC5B,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,IAAI;KACrC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { type AddressReadPort, type AddressRecord, type CustomerAccountReadPort } from '@endora-commerce/contracts';
|
|
3
|
+
import { CustomerAddress } from '../entities/customer-address.entity.js';
|
|
4
|
+
import type { AuditPort } from '@endora-commerce/platform/kernel';
|
|
5
|
+
/**
|
|
6
|
+
* CustomerAddressService — the personal address book (feature 040, US2).
|
|
7
|
+
*
|
|
8
|
+
* Clones the org `AddressService` default-demotion pattern (transactional +
|
|
9
|
+
* partial unique index) but keyed by `customerAccountId`. For org-bound
|
|
10
|
+
* customers the read endpoints also surface the Organization's shared
|
|
11
|
+
* addresses (read-only) so they can be selected (FR-038).
|
|
12
|
+
*/
|
|
13
|
+
export interface CustomerAddressInputView {
|
|
14
|
+
kind: 'delivery' | 'billing';
|
|
15
|
+
recipientName: string;
|
|
16
|
+
street: string;
|
|
17
|
+
city: string;
|
|
18
|
+
postalCode: string;
|
|
19
|
+
country: string;
|
|
20
|
+
phone?: string | undefined;
|
|
21
|
+
isDefault?: boolean | undefined;
|
|
22
|
+
}
|
|
23
|
+
/** Mutable fields on update — `kind` is immutable once created. */
|
|
24
|
+
export interface CustomerAddressPatch {
|
|
25
|
+
recipientName?: string | undefined;
|
|
26
|
+
street?: string | undefined;
|
|
27
|
+
city?: string | undefined;
|
|
28
|
+
postalCode?: string | undefined;
|
|
29
|
+
country?: string | undefined;
|
|
30
|
+
phone?: string | undefined;
|
|
31
|
+
isDefault?: boolean | undefined;
|
|
32
|
+
}
|
|
33
|
+
export declare class CustomerAddressService {
|
|
34
|
+
#private;
|
|
35
|
+
private readonly emFactory;
|
|
36
|
+
/**
|
|
37
|
+
* Feature 075 — `addresses`' published read port, where this service used
|
|
38
|
+
* to run `em.find(Address, …)` over that module's table. The org-shared
|
|
39
|
+
* list is the only thing it ever asked for, and the port already scopes
|
|
40
|
+
* every read by organisation.
|
|
41
|
+
*/
|
|
42
|
+
private readonly organizationAddresses;
|
|
43
|
+
/**
|
|
44
|
+
* `customer_accounts`' published read model — the tenant boundary of every
|
|
45
|
+
* method on this service, and not a convenience.
|
|
46
|
+
*
|
|
47
|
+
* `CustomerAddress` is `@CustomerScoped`, and `customerFilterCond`
|
|
48
|
+
* contributes **no predicate at all** under `allowed-set`, the mode an
|
|
49
|
+
* assignment-scoped administrator resolves to. This table carries no
|
|
50
|
+
* organization column of its own, so nothing here can express the
|
|
51
|
+
* restriction — but its owner `CustomerAccount` is `@OrgScoped`, and this
|
|
52
|
+
* port reads it through a filtered EntityManager. Asking it whether the
|
|
53
|
+
* account exists *is* asking whether the caller may reach that customer.
|
|
54
|
+
*
|
|
55
|
+
* For the buyer's own surface it is a no-op: their own account is in their
|
|
56
|
+
* own scope. For a platform administrator it is a no-op too.
|
|
57
|
+
*/
|
|
58
|
+
private readonly customerAccounts;
|
|
59
|
+
private readonly auditLog?;
|
|
60
|
+
constructor(emFactory: () => EntityManager,
|
|
61
|
+
/**
|
|
62
|
+
* Feature 075 — `addresses`' published read port, where this service used
|
|
63
|
+
* to run `em.find(Address, …)` over that module's table. The org-shared
|
|
64
|
+
* list is the only thing it ever asked for, and the port already scopes
|
|
65
|
+
* every read by organisation.
|
|
66
|
+
*/
|
|
67
|
+
organizationAddresses: AddressReadPort,
|
|
68
|
+
/**
|
|
69
|
+
* `customer_accounts`' published read model — the tenant boundary of every
|
|
70
|
+
* method on this service, and not a convenience.
|
|
71
|
+
*
|
|
72
|
+
* `CustomerAddress` is `@CustomerScoped`, and `customerFilterCond`
|
|
73
|
+
* contributes **no predicate at all** under `allowed-set`, the mode an
|
|
74
|
+
* assignment-scoped administrator resolves to. This table carries no
|
|
75
|
+
* organization column of its own, so nothing here can express the
|
|
76
|
+
* restriction — but its owner `CustomerAccount` is `@OrgScoped`, and this
|
|
77
|
+
* port reads it through a filtered EntityManager. Asking it whether the
|
|
78
|
+
* account exists *is* asking whether the caller may reach that customer.
|
|
79
|
+
*
|
|
80
|
+
* For the buyer's own surface it is a no-op: their own account is in their
|
|
81
|
+
* own scope. For a platform administrator it is a no-op too.
|
|
82
|
+
*/
|
|
83
|
+
customerAccounts: CustomerAccountReadPort, auditLog?: AuditPort | undefined);
|
|
84
|
+
listPersonal(customerAccountId: string, kind?: 'delivery' | 'billing'): Promise<CustomerAddress[]>;
|
|
85
|
+
/** Org-shared addresses, read-only, for org-bound customers (FR-038). */
|
|
86
|
+
listOrganizationAddresses(organizationId: string, kind?: 'delivery' | 'billing'): Promise<AddressRecord[]>;
|
|
87
|
+
create(customerAccountId: string, input: CustomerAddressInputView): Promise<CustomerAddress>;
|
|
88
|
+
update(customerAccountId: string, addressId: string, patch: CustomerAddressPatch): Promise<CustomerAddress>;
|
|
89
|
+
/** Marks the address the default for its kind (demoting the previous one). */
|
|
90
|
+
setDefault(customerAccountId: string, addressId: string): Promise<CustomerAddress>;
|
|
91
|
+
delete(customerAccountId: string, addressId: string): Promise<void>;
|
|
92
|
+
private findOwned;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=customer-address-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-address-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/customer-address-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,uBAAuB,EAC7B,MAAM,4BAA4B,CAAC;AAGpC,OAAO,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AAEzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAElE;;;;;;;GAOG;AACH,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,UAAU,GAAG,SAAS,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACjC;AAED,mEAAmE;AACnE,MAAM,WAAW,oBAAoB;IACnC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACjC;AAED,qBAAa,sBAAsB;;IAE/B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,qBAAqB;IACtC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAxBT,SAAS,EAAE,MAAM,aAAa;IAC/C;;;;;OAKG;IACc,qBAAqB,EAAE,eAAe;IACvD;;;;;;;;;;;;;;OAcG;IACc,gBAAgB,EAAE,uBAAuB,EACzC,QAAQ,CAAC,EAAE,SAAS,YAAA;IAmDjC,YAAY,CAChB,iBAAiB,EAAE,MAAM,EACzB,IAAI,CAAC,EAAE,UAAU,GAAG,SAAS,GAC5B,OAAO,CAAC,eAAe,EAAE,CAAC;IAY7B,yEAAyE;IACnE,yBAAyB,CAC7B,cAAc,EAAE,MAAM,EACtB,IAAI,CAAC,EAAE,UAAU,GAAG,SAAS,GAC5B,OAAO,CAAC,aAAa,EAAE,CAAC;IAIrB,MAAM,CACV,iBAAiB,EAAE,MAAM,EACzB,KAAK,EAAE,wBAAwB,GAC9B,OAAO,CAAC,eAAe,CAAC;IA+BrB,MAAM,CACV,iBAAiB,EAAE,MAAM,EACzB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,oBAAoB,GAC1B,OAAO,CAAC,eAAe,CAAC;IA2B3B,8EAA8E;IACxE,UAAU,CACd,iBAAiB,EAAE,MAAM,EACzB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC;IAIrB,MAAM,CAAC,iBAAiB,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAc3D,SAAS;CAqBxB"}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { ERROR_CODES, } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
import { withSystemScope } from '@endora-commerce/platform/tenancy';
|
|
4
|
+
import { CustomerAddress } from '../entities/customer-address.entity.js';
|
|
5
|
+
import { recordAuditFromContext } from '@endora-commerce/platform/commands';
|
|
6
|
+
export class CustomerAddressService {
|
|
7
|
+
emFactory;
|
|
8
|
+
organizationAddresses;
|
|
9
|
+
customerAccounts;
|
|
10
|
+
auditLog;
|
|
11
|
+
constructor(emFactory,
|
|
12
|
+
/**
|
|
13
|
+
* Feature 075 — `addresses`' published read port, where this service used
|
|
14
|
+
* to run `em.find(Address, …)` over that module's table. The org-shared
|
|
15
|
+
* list is the only thing it ever asked for, and the port already scopes
|
|
16
|
+
* every read by organisation.
|
|
17
|
+
*/
|
|
18
|
+
organizationAddresses,
|
|
19
|
+
/**
|
|
20
|
+
* `customer_accounts`' published read model — the tenant boundary of every
|
|
21
|
+
* method on this service, and not a convenience.
|
|
22
|
+
*
|
|
23
|
+
* `CustomerAddress` is `@CustomerScoped`, and `customerFilterCond`
|
|
24
|
+
* contributes **no predicate at all** under `allowed-set`, the mode an
|
|
25
|
+
* assignment-scoped administrator resolves to. This table carries no
|
|
26
|
+
* organization column of its own, so nothing here can express the
|
|
27
|
+
* restriction — but its owner `CustomerAccount` is `@OrgScoped`, and this
|
|
28
|
+
* port reads it through a filtered EntityManager. Asking it whether the
|
|
29
|
+
* account exists *is* asking whether the caller may reach that customer.
|
|
30
|
+
*
|
|
31
|
+
* For the buyer's own surface it is a no-op: their own account is in their
|
|
32
|
+
* own scope. For a platform administrator it is a no-op too.
|
|
33
|
+
*/
|
|
34
|
+
customerAccounts, auditLog) {
|
|
35
|
+
this.emFactory = emFactory;
|
|
36
|
+
this.organizationAddresses = organizationAddresses;
|
|
37
|
+
this.customerAccounts = customerAccounts;
|
|
38
|
+
this.auditLog = auditLog;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Refuse a customer the caller's organizations do not reach.
|
|
42
|
+
*
|
|
43
|
+
* 404 rather than 403: an out-of-scope customer must read the same as one
|
|
44
|
+
* that is not there, which is what the account read already answers.
|
|
45
|
+
*/
|
|
46
|
+
async #assertCustomerInScope(customerAccountId) {
|
|
47
|
+
const account = await this.customerAccounts.findById(customerAccountId);
|
|
48
|
+
if (!account) {
|
|
49
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Customer account not found.');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Run a `customer_addresses` statement for the account
|
|
54
|
+
* {@link CustomerAddressService.#assertCustomerInScope} has just authorised.
|
|
55
|
+
*
|
|
56
|
+
* Feature 087 gave `customerFilterCond` a real `allowed-set` arm, and this
|
|
57
|
+
* table is one of the fifteen that carry no `organization_id` yet — so the
|
|
58
|
+
* arm matches **nothing** here, which is the right floor for a guard that
|
|
59
|
+
* cannot determine the allowed set and the wrong answer for a caller who has
|
|
60
|
+
* already been shown to be inside it. Without this widening a sales
|
|
61
|
+
* representative's own customer's address book reads as empty, and the
|
|
62
|
+
* default-demotion `nativeUpdate` below silently demotes nothing, which
|
|
63
|
+
* leaves two defaults.
|
|
64
|
+
*
|
|
65
|
+
* The widening is the sanctioned crossing (feature 050, FR-005) and not a
|
|
66
|
+
* `catch`: every call is preceded, in the same method, by the
|
|
67
|
+
* `customer_accounts` read that decides whether this caller may reach this
|
|
68
|
+
* account at all — and `customer_accounts` is `@OrgScoped`, so that read is
|
|
69
|
+
* the organization check. It is also narrow by construction: every statement
|
|
70
|
+
* it covers names `customerAccountId` in its own `where`, so widening the
|
|
71
|
+
* filter widens the authority by exactly nothing. It stops being needed when
|
|
72
|
+
* feature 087 gives this table its own column.
|
|
73
|
+
*/
|
|
74
|
+
#forAuthorisedCustomer(customerAccountId, fn) {
|
|
75
|
+
return withSystemScope(`customers: personal addresses of customer account ${customerAccountId}, authorised by its own @OrgScoped account read`, fn);
|
|
76
|
+
}
|
|
77
|
+
#audit(em, action, objectId, stateBefore, stateAfter) {
|
|
78
|
+
if (this.auditLog) {
|
|
79
|
+
recordAuditFromContext(this.auditLog, em, { action, objectType: 'customer_address', objectId, stateBefore, stateAfter });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async listPersonal(customerAccountId, kind) {
|
|
83
|
+
await this.#assertCustomerInScope(customerAccountId);
|
|
84
|
+
const em = this.emFactory();
|
|
85
|
+
const where = { customerAccountId, deletedAt: null };
|
|
86
|
+
if (kind)
|
|
87
|
+
where.kind = kind;
|
|
88
|
+
return this.#forAuthorisedCustomer(customerAccountId, () => em.find(CustomerAddress, where, {
|
|
89
|
+
orderBy: { isDefault: 'desc', createdAt: 'asc' },
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
/** Org-shared addresses, read-only, for org-bound customers (FR-038). */
|
|
93
|
+
async listOrganizationAddresses(organizationId, kind) {
|
|
94
|
+
return this.organizationAddresses.listForOrganization(organizationId, kind);
|
|
95
|
+
}
|
|
96
|
+
async create(customerAccountId, input) {
|
|
97
|
+
await this.#assertCustomerInScope(customerAccountId);
|
|
98
|
+
const em = this.emFactory();
|
|
99
|
+
return em.transactional(async (txEm) => {
|
|
100
|
+
if (input.isDefault) {
|
|
101
|
+
await this.#forAuthorisedCustomer(customerAccountId, () => txEm.nativeUpdate(CustomerAddress, { customerAccountId, kind: input.kind, isDefault: true, deletedAt: null }, { isDefault: false }));
|
|
102
|
+
}
|
|
103
|
+
const address = txEm.create(CustomerAddress, {
|
|
104
|
+
customerAccountId,
|
|
105
|
+
kind: input.kind,
|
|
106
|
+
recipientName: input.recipientName,
|
|
107
|
+
street: input.street,
|
|
108
|
+
city: input.city,
|
|
109
|
+
postalCode: input.postalCode,
|
|
110
|
+
country: input.country,
|
|
111
|
+
phone: input.phone ?? null,
|
|
112
|
+
isDefault: input.isDefault ?? false,
|
|
113
|
+
});
|
|
114
|
+
txEm.persist(address);
|
|
115
|
+
this.#audit(txEm, 'customer_address.create', address.id, null, { customerAccountId, kind: address.kind });
|
|
116
|
+
await txEm.flush();
|
|
117
|
+
return address;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async update(customerAccountId, addressId, patch) {
|
|
121
|
+
await this.#assertCustomerInScope(customerAccountId);
|
|
122
|
+
const em = this.emFactory();
|
|
123
|
+
return em.transactional(async (txEm) => {
|
|
124
|
+
const address = await this.findOwned(txEm, customerAccountId, addressId);
|
|
125
|
+
if (patch.isDefault === true && !address.isDefault) {
|
|
126
|
+
await this.#forAuthorisedCustomer(customerAccountId, () => txEm.nativeUpdate(CustomerAddress, { customerAccountId, kind: address.kind, isDefault: true, deletedAt: null }, { isDefault: false }));
|
|
127
|
+
}
|
|
128
|
+
if (patch.recipientName !== undefined)
|
|
129
|
+
address.recipientName = patch.recipientName;
|
|
130
|
+
if (patch.street !== undefined)
|
|
131
|
+
address.street = patch.street;
|
|
132
|
+
if (patch.city !== undefined)
|
|
133
|
+
address.city = patch.city;
|
|
134
|
+
if (patch.postalCode !== undefined)
|
|
135
|
+
address.postalCode = patch.postalCode;
|
|
136
|
+
if (patch.country !== undefined)
|
|
137
|
+
address.country = patch.country;
|
|
138
|
+
if (patch.phone !== undefined)
|
|
139
|
+
address.phone = patch.phone ?? null;
|
|
140
|
+
if (patch.isDefault !== undefined)
|
|
141
|
+
address.isDefault = patch.isDefault;
|
|
142
|
+
this.#audit(txEm, 'customer_address.update', address.id, null, { customerAccountId, kind: address.kind });
|
|
143
|
+
await txEm.persistAndFlush(address);
|
|
144
|
+
return address;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** Marks the address the default for its kind (demoting the previous one). */
|
|
148
|
+
async setDefault(customerAccountId, addressId) {
|
|
149
|
+
return this.update(customerAccountId, addressId, { isDefault: true });
|
|
150
|
+
}
|
|
151
|
+
async delete(customerAccountId, addressId) {
|
|
152
|
+
await this.#assertCustomerInScope(customerAccountId);
|
|
153
|
+
const em = this.emFactory();
|
|
154
|
+
await em.transactional(async (txEm) => {
|
|
155
|
+
const address = await this.findOwned(txEm, customerAccountId, addressId);
|
|
156
|
+
address.deletedAt = new Date();
|
|
157
|
+
// Deleting a default leaves no default of that kind (FR-009 graceful
|
|
158
|
+
// degrade); the storefront prompts re-selection.
|
|
159
|
+
address.isDefault = false;
|
|
160
|
+
this.#audit(txEm, 'customer_address.delete', address.id, { customerAccountId, kind: address.kind }, null);
|
|
161
|
+
await txEm.persistAndFlush(address);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
async findOwned(em, customerAccountId, addressId) {
|
|
165
|
+
const address = await this.#forAuthorisedCustomer(customerAccountId, () => em.findOne(CustomerAddress, {
|
|
166
|
+
id: addressId,
|
|
167
|
+
customerAccountId,
|
|
168
|
+
deletedAt: null,
|
|
169
|
+
}));
|
|
170
|
+
if (!address) {
|
|
171
|
+
throw new HttpError(404, ERROR_CODES.CUSTOMER_ADDRESS_NOT_FOUND, 'Address not found.');
|
|
172
|
+
}
|
|
173
|
+
return address;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=customer-address-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-address-service.js","sourceRoot":"","sources":["../../../src/backend/services/customer-address-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { AdminCustomerDetail, AdminCustomerListItem, CustomerAccountAdminSearchPort, CustomerAccountReadPort, CustomerGroupReadPort, OrderReadPort, OrganizationDetailsPort } from '@endora-commerce/contracts';
|
|
2
|
+
import type { CustomerDefaultsService } from './customer-defaults-service.js';
|
|
3
|
+
/**
|
|
4
|
+
* CustomerAdminQueryService — the admin customer list + detail (feature 040,
|
|
5
|
+
* US5). Applies the same visibility scope as moderation: a Platform
|
|
6
|
+
* Administrator sees everyone; a Salesperson sees standalone customers plus
|
|
7
|
+
* customers of the Organizations they are assigned to.
|
|
8
|
+
*
|
|
9
|
+
* **Feature 075, Phase C — the three foreign tables are three ports now.** The
|
|
10
|
+
* account page, the organisation names beside it and the customer-group names
|
|
11
|
+
* were all `em.find` calls on other modules' entities. The scope decision stays
|
|
12
|
+
* here, because it is this module's: the port takes the organisations the actor
|
|
13
|
+
* may see and does not decide who the actor is.
|
|
14
|
+
*
|
|
15
|
+
* **Feature 080, T048 (D-169) — and so is the fourth.** The detail header's
|
|
16
|
+
* sales-channel list was the last `orders` entity this module named:
|
|
17
|
+
* `em.find(Order, { placedByCustomerAccountId }, { fields: ['salesChannelId'] })`.
|
|
18
|
+
* It is a plain read, so it took a read-port method
|
|
19
|
+
* (`OrderReadPort.salesChannelIdsForCustomer`) and not an
|
|
20
|
+
* `EntityManager`-taking apply port — handing a read a transaction handle
|
|
21
|
+
* re-opens a write seam to serve it. With that gone this service holds no
|
|
22
|
+
* `EntityManager` at all: it owns no table, and every question it answers is
|
|
23
|
+
* somebody's published read.
|
|
24
|
+
*/
|
|
25
|
+
export interface AdminCustomerListScope {
|
|
26
|
+
isPlatformAdmin: boolean;
|
|
27
|
+
allowedOrganizationIds: string[];
|
|
28
|
+
}
|
|
29
|
+
export interface AdminCustomerListQuery {
|
|
30
|
+
q?: string | undefined;
|
|
31
|
+
status?: 'active' | 'blocked' | 'deleted' | undefined;
|
|
32
|
+
organizationId?: string | undefined;
|
|
33
|
+
customerGroupId?: string | undefined;
|
|
34
|
+
page: number;
|
|
35
|
+
pageSize: number;
|
|
36
|
+
}
|
|
37
|
+
export interface AdminCustomerListResult {
|
|
38
|
+
rows: AdminCustomerListItem[];
|
|
39
|
+
total: number;
|
|
40
|
+
}
|
|
41
|
+
export interface CustomerAdminQueryPorts {
|
|
42
|
+
accounts: CustomerAccountReadPort;
|
|
43
|
+
accountSearch: CustomerAccountAdminSearchPort;
|
|
44
|
+
organizations: OrganizationDetailsPort;
|
|
45
|
+
customerGroups: CustomerGroupReadPort;
|
|
46
|
+
orders: OrderReadPort;
|
|
47
|
+
}
|
|
48
|
+
export declare class CustomerAdminQueryService {
|
|
49
|
+
private readonly defaults;
|
|
50
|
+
private readonly ports;
|
|
51
|
+
constructor(defaults: CustomerDefaultsService, ports: CustomerAdminQueryPorts);
|
|
52
|
+
list(query: AdminCustomerListQuery, scope: AdminCustomerListScope): Promise<AdminCustomerListResult>;
|
|
53
|
+
getDetail(id: string): Promise<AdminCustomerDetail | null>;
|
|
54
|
+
private resolveNames;
|
|
55
|
+
private toListItem;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=customer-admin-query-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-admin-query-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/customer-admin-query-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EACnB,qBAAqB,EACrB,8BAA8B,EAC9B,uBAAuB,EAEvB,qBAAqB,EACrB,aAAa,EACb,uBAAuB,EACxB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAE9E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,sBAAsB;IACrC,eAAe,EAAE,OAAO,CAAC;IACzB,sBAAsB,EAAE,MAAM,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,sBAAsB;IACrC,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvB,MAAM,CAAC,EAAE,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;IACtD,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,qBAAqB,EAAE,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,uBAAuB,CAAC;IAClC,aAAa,EAAE,8BAA8B,CAAC;IAC9C,aAAa,EAAE,uBAAuB,CAAC;IACvC,cAAc,EAAE,qBAAqB,CAAC;IACtC,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,qBAAa,yBAAyB;IAElC,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADL,QAAQ,EAAE,uBAAuB,EACjC,KAAK,EAAE,uBAAuB;IAG3C,IAAI,CACR,KAAK,EAAE,sBAAsB,EAC7B,KAAK,EAAE,sBAAsB,GAC5B,OAAO,CAAC,uBAAuB,CAAC;IAoB7B,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;YAuClD,YAAY;IAsB1B,OAAO,CAAC,UAAU;CAoBnB"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export class CustomerAdminQueryService {
|
|
2
|
+
defaults;
|
|
3
|
+
ports;
|
|
4
|
+
constructor(defaults, ports) {
|
|
5
|
+
this.defaults = defaults;
|
|
6
|
+
this.ports = ports;
|
|
7
|
+
}
|
|
8
|
+
async list(query, scope) {
|
|
9
|
+
const { rows, total } = await this.ports.accountSearch.search({
|
|
10
|
+
q: query.q,
|
|
11
|
+
status: query.status,
|
|
12
|
+
organizationId: query.organizationId,
|
|
13
|
+
customerGroupId: query.customerGroupId,
|
|
14
|
+
// `null` is the unscoped read. A salesperson gets their territory, and
|
|
15
|
+
// org-less customers are visible to every salesperson by construction.
|
|
16
|
+
allowedOrganizationIds: scope.isPlatformAdmin ? null : scope.allowedOrganizationIds,
|
|
17
|
+
page: query.page,
|
|
18
|
+
pageSize: query.pageSize,
|
|
19
|
+
});
|
|
20
|
+
const { orgNames, groupNames } = await this.resolveNames(rows);
|
|
21
|
+
return {
|
|
22
|
+
rows: rows.map((c) => this.toListItem(c, orgNames, groupNames)),
|
|
23
|
+
total,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async getDetail(id) {
|
|
27
|
+
const customer = await this.ports.accounts.findById(id);
|
|
28
|
+
if (!customer)
|
|
29
|
+
return null;
|
|
30
|
+
const { orgNames, groupNames } = await this.resolveNames([customer]);
|
|
31
|
+
const base = this.toListItem(customer, orgNames, groupNames);
|
|
32
|
+
// Already distinct, newest-ordered-on first — the owner does both, because
|
|
33
|
+
// the deduplication is what makes the ordering mean anything.
|
|
34
|
+
const salesChannelIds = await this.ports.orders.salesChannelIdsForCustomer(customer.id);
|
|
35
|
+
const defaults = await this.defaults.getForCustomer(customer.id);
|
|
36
|
+
return {
|
|
37
|
+
...base,
|
|
38
|
+
block: customer.blockedAt
|
|
39
|
+
? {
|
|
40
|
+
blockedAt: customer.blockedAt.toISOString(),
|
|
41
|
+
blockSource: customer.blockSource ?? 'staff',
|
|
42
|
+
blockReason: customer.blockReason ?? null,
|
|
43
|
+
blockedByAdminUserId: customer.blockedByAdminUserId ?? null,
|
|
44
|
+
blockedByCustomerAccountId: customer.blockedByCustomerAccountId ?? null,
|
|
45
|
+
}
|
|
46
|
+
: null,
|
|
47
|
+
deletion: customer.deletedAt
|
|
48
|
+
? {
|
|
49
|
+
deletedAt: customer.deletedAt.toISOString(),
|
|
50
|
+
anonymizedAt: customer.anonymizedAt ? customer.anonymizedAt.toISOString() : null,
|
|
51
|
+
restorableUntil: null,
|
|
52
|
+
}
|
|
53
|
+
: null,
|
|
54
|
+
emailVerifiedAt: customer.emailVerifiedAt ? customer.emailVerifiedAt.toISOString() : null,
|
|
55
|
+
twoFactorEnabled: customer.twoFactorEnabled,
|
|
56
|
+
salesChannelIds,
|
|
57
|
+
defaults,
|
|
58
|
+
customFieldValues: customer.customFieldValues ?? {},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async resolveNames(rows) {
|
|
62
|
+
const orgIds = [...new Set(rows.map((r) => r.organizationId).filter((x) => !!x))];
|
|
63
|
+
const groupIds = [
|
|
64
|
+
...new Set(rows.map((r) => r.customerGroupId).filter((x) => !!x)),
|
|
65
|
+
];
|
|
66
|
+
const orgNames = new Map();
|
|
67
|
+
const groupNames = new Map();
|
|
68
|
+
if (orgIds.length) {
|
|
69
|
+
for (const o of await this.ports.organizations.findByIds(orgIds)) {
|
|
70
|
+
orgNames.set(o.id, o.name);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (groupIds.length) {
|
|
74
|
+
for (const g of await this.ports.customerGroups.findByIds(groupIds)) {
|
|
75
|
+
groupNames.set(g.id, g.name);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { orgNames, groupNames };
|
|
79
|
+
}
|
|
80
|
+
toListItem(c, orgNames, groupNames) {
|
|
81
|
+
return {
|
|
82
|
+
id: c.id,
|
|
83
|
+
email: c.email,
|
|
84
|
+
firstName: c.firstName,
|
|
85
|
+
lastName: c.lastName,
|
|
86
|
+
organizationId: c.organizationId ?? null,
|
|
87
|
+
organizationName: c.organizationId ? orgNames.get(c.organizationId) ?? null : null,
|
|
88
|
+
customerGroupId: c.customerGroupId ?? null,
|
|
89
|
+
customerGroupName: c.customerGroupId ? groupNames.get(c.customerGroupId) ?? null : null,
|
|
90
|
+
blocked: c.blockedAt != null,
|
|
91
|
+
deleted: c.deletedAt != null,
|
|
92
|
+
createdAt: c.createdAt.toISOString(),
|
|
93
|
+
lastLoginAt: c.lastLoginAt ? c.lastLoginAt.toISOString() : null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=customer-admin-query-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-admin-query-service.js","sourceRoot":"","sources":["../../../src/backend/services/customer-admin-query-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
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 interface SalesRepVisibility {
|
|
29
|
+
canSeeOrganization(adminUserId: string, organizationId: string): Promise<boolean>;
|
|
30
|
+
}
|
|
31
|
+
export interface CustomerAuthorityInput {
|
|
32
|
+
/** True when the acting admin is a Platform Administrator (caller-decided). */
|
|
33
|
+
isPlatformAdmin: boolean;
|
|
34
|
+
adminUserId: string;
|
|
35
|
+
/** The target customer's Organization. Never null since D-178. */
|
|
36
|
+
customerOrganizationId: string;
|
|
37
|
+
}
|
|
38
|
+
export declare class CustomerAuthorityService {
|
|
39
|
+
private readonly salesReps;
|
|
40
|
+
constructor(salesReps: SalesRepVisibility);
|
|
41
|
+
/**
|
|
42
|
+
* True when the acting staff member is authorized to manage the customer.
|
|
43
|
+
*/
|
|
44
|
+
canManageCustomer(input: CustomerAuthorityInput): Promise<boolean>;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=customer-authority-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customer-authority-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/customer-authority-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,WAAW,kBAAkB;IACjC,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnF;AAED,MAAM,WAAW,sBAAsB;IACrC,+EAA+E;IAC/E,eAAe,EAAE,OAAO,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,sBAAsB,EAAE,MAAM,CAAC;CAChC;AAED,qBAAa,wBAAwB;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,kBAAkB;IAE1D;;OAEG;IACG,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,OAAO,CAAC;CAOzE"}
|