@endora-commerce/contracts 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 +34 -0
- package/dist/actor.d.ts +79 -0
- package/dist/actor.d.ts.map +1 -0
- package/dist/actor.js +41 -0
- package/dist/actor.js.map +1 -0
- package/dist/addresses.d.ts +134 -0
- package/dist/addresses.d.ts.map +1 -0
- package/dist/addresses.js +16 -0
- package/dist/addresses.js.map +1 -0
- package/dist/admin-actions.d.ts +367 -0
- package/dist/admin-actions.d.ts.map +1 -0
- package/dist/admin-actions.js +287 -0
- package/dist/admin-actions.js.map +1 -0
- package/dist/admin-contributions.d.ts +518 -0
- package/dist/admin-contributions.d.ts.map +1 -0
- package/dist/admin-contributions.js +495 -0
- package/dist/admin-contributions.js.map +1 -0
- package/dist/admin-i18n.d.ts +135 -0
- package/dist/admin-i18n.d.ts.map +1 -0
- package/dist/admin-i18n.js +72 -0
- package/dist/admin-i18n.js.map +1 -0
- package/dist/admin-notifications.d.ts +55 -0
- package/dist/admin-notifications.d.ts.map +1 -0
- package/dist/admin-notifications.js +16 -0
- package/dist/admin-notifications.js.map +1 -0
- package/dist/admin-roles.d.ts +125 -0
- package/dist/admin-roles.d.ts.map +1 -0
- package/dist/admin-roles.js +2 -0
- package/dist/admin-roles.js.map +1 -0
- package/dist/admin-users.d.ts +178 -0
- package/dist/admin-users.d.ts.map +1 -0
- package/dist/admin-users.js +14 -0
- package/dist/admin-users.js.map +1 -0
- package/dist/admin.d.ts +243 -0
- package/dist/admin.d.ts.map +1 -0
- package/dist/admin.js +246 -0
- package/dist/admin.js.map +1 -0
- package/dist/analytics.d.ts +123 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +68 -0
- package/dist/analytics.js.map +1 -0
- package/dist/api-keys.d.ts +97 -0
- package/dist/api-keys.d.ts.map +1 -0
- package/dist/api-keys.js +64 -0
- package/dist/api-keys.js.map +1 -0
- package/dist/assets-library.d.ts +684 -0
- package/dist/assets-library.d.ts.map +1 -0
- package/dist/assets-library.js +181 -0
- package/dist/assets-library.js.map +1 -0
- package/dist/audit-logs.d.ts +141 -0
- package/dist/audit-logs.d.ts.map +1 -0
- package/dist/audit-logs.js +31 -0
- package/dist/audit-logs.js.map +1 -0
- package/dist/auth.d.ts +174 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +27 -0
- package/dist/auth.js.map +1 -0
- package/dist/blog.d.ts +669 -0
- package/dist/blog.d.ts.map +1 -0
- package/dist/blog.js +360 -0
- package/dist/blog.js.map +1 -0
- package/dist/capabilities.d.ts +40 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +38 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/carts.d.ts +1367 -0
- package/dist/carts.d.ts.map +1 -0
- package/dist/carts.js +405 -0
- package/dist/carts.js.map +1 -0
- package/dist/catalog.d.ts +2855 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +1543 -0
- package/dist/catalog.js.map +1 -0
- package/dist/cms.d.ts +872 -0
- package/dist/cms.d.ts.map +1 -0
- package/dist/cms.js +468 -0
- package/dist/cms.js.map +1 -0
- package/dist/common.d.ts +82 -0
- package/dist/common.d.ts.map +1 -0
- package/dist/common.js +72 -0
- package/dist/common.js.map +1 -0
- package/dist/comparisons.d.ts +487 -0
- package/dist/comparisons.d.ts.map +1 -0
- package/dist/comparisons.js +221 -0
- package/dist/comparisons.js.map +1 -0
- package/dist/credentials.d.ts +292 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +142 -0
- package/dist/credentials.js.map +1 -0
- package/dist/credit-limits.d.ts +111 -0
- package/dist/credit-limits.d.ts.map +1 -0
- package/dist/credit-limits.js +35 -0
- package/dist/credit-limits.js.map +1 -0
- package/dist/currencies.d.ts +127 -0
- package/dist/currencies.d.ts.map +1 -0
- package/dist/currencies.js +20 -0
- package/dist/currencies.js.map +1 -0
- package/dist/custom-fields.d.ts +345 -0
- package/dist/custom-fields.d.ts.map +1 -0
- package/dist/custom-fields.js +185 -0
- package/dist/custom-fields.js.map +1 -0
- package/dist/customer-accounts.d.ts +690 -0
- package/dist/customer-accounts.d.ts.map +1 -0
- package/dist/customer-accounts.js +41 -0
- package/dist/customer-accounts.js.map +1 -0
- package/dist/customers.d.ts +305 -0
- package/dist/customers.d.ts.map +1 -0
- package/dist/customers.js +158 -0
- package/dist/customers.js.map +1 -0
- package/dist/dictionary.d.ts +580 -0
- package/dist/dictionary.d.ts.map +1 -0
- package/dist/dictionary.js +297 -0
- package/dist/dictionary.js.map +1 -0
- package/dist/email-address.d.ts +62 -0
- package/dist/email-address.d.ts.map +1 -0
- package/dist/email-address.js +64 -0
- package/dist/email-address.js.map +1 -0
- package/dist/email.d.ts +175 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +45 -0
- package/dist/email.js.map +1 -0
- package/dist/envelopes.d.ts +15 -0
- package/dist/envelopes.d.ts.map +1 -0
- package/dist/envelopes.js +16 -0
- package/dist/envelopes.js.map +1 -0
- package/dist/environment-inputs.d.ts +306 -0
- package/dist/environment-inputs.d.ts.map +1 -0
- package/dist/environment-inputs.js +277 -0
- package/dist/environment-inputs.js.map +1 -0
- package/dist/erp-connector.d.ts +52 -0
- package/dist/erp-connector.d.ts.map +1 -0
- package/dist/erp-connector.js +34 -0
- package/dist/erp-connector.js.map +1 -0
- package/dist/errors.d.ts +455 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +532 -0
- package/dist/errors.js.map +1 -0
- package/dist/google-analytics.d.ts +181 -0
- package/dist/google-analytics.d.ts.map +1 -0
- package/dist/google-analytics.js +176 -0
- package/dist/google-analytics.js.map +1 -0
- package/dist/google-tag-manager.d.ts +111 -0
- package/dist/google-tag-manager.d.ts.map +1 -0
- package/dist/google-tag-manager.js +129 -0
- package/dist/google-tag-manager.js.map +1 -0
- package/dist/i18n.d.ts +69 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +59 -0
- package/dist/i18n.js.map +1 -0
- package/dist/import-export.d.ts +63 -0
- package/dist/import-export.d.ts.map +1 -0
- package/dist/import-export.js +37 -0
- package/dist/import-export.js.map +1 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +126 -0
- package/dist/index.js.map +1 -0
- package/dist/inventory.d.ts +673 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/inventory.js +412 -0
- package/dist/inventory.js.map +1 -0
- package/dist/invoice-ledger.d.ts +366 -0
- package/dist/invoice-ledger.d.ts.map +1 -0
- package/dist/invoice-ledger.js +114 -0
- package/dist/invoice-ledger.js.map +1 -0
- package/dist/invoices.d.ts +845 -0
- package/dist/invoices.d.ts.map +1 -0
- package/dist/invoices.js +314 -0
- package/dist/invoices.js.map +1 -0
- package/dist/kernel.d.ts +49 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +19 -0
- package/dist/kernel.js.map +1 -0
- package/dist/languages.d.ts +122 -0
- package/dist/languages.d.ts.map +1 -0
- package/dist/languages.js +24 -0
- package/dist/languages.js.map +1 -0
- package/dist/linkedin-ads.d.ts +167 -0
- package/dist/linkedin-ads.d.ts.map +1 -0
- package/dist/linkedin-ads.js +156 -0
- package/dist/linkedin-ads.js.map +1 -0
- package/dist/megamenu.d.ts +556 -0
- package/dist/megamenu.d.ts.map +1 -0
- package/dist/megamenu.js +186 -0
- package/dist/megamenu.js.map +1 -0
- package/dist/meta-ads.d.ts +126 -0
- package/dist/meta-ads.d.ts.map +1 -0
- package/dist/meta-ads.js +112 -0
- package/dist/meta-ads.js.map +1 -0
- package/dist/mfa.d.ts +274 -0
- package/dist/mfa.d.ts.map +1 -0
- package/dist/mfa.js +187 -0
- package/dist/mfa.js.map +1 -0
- package/dist/modules.d.ts +1706 -0
- package/dist/modules.d.ts.map +1 -0
- package/dist/modules.js +1390 -0
- package/dist/modules.js.map +1 -0
- package/dist/newsletter.d.ts +611 -0
- package/dist/newsletter.d.ts.map +1 -0
- package/dist/newsletter.js +345 -0
- package/dist/newsletter.js.map +1 -0
- package/dist/orders.d.ts +1175 -0
- package/dist/orders.d.ts.map +1 -0
- package/dist/orders.js +630 -0
- package/dist/orders.js.map +1 -0
- package/dist/organizations.d.ts +938 -0
- package/dist/organizations.d.ts.map +1 -0
- package/dist/organizations.js +418 -0
- package/dist/organizations.js.map +1 -0
- package/dist/pagination.d.ts +21 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +22 -0
- package/dist/pagination.js.map +1 -0
- package/dist/payment-methods.d.ts +472 -0
- package/dist/payment-methods.d.ts.map +1 -0
- package/dist/payment-methods.js +175 -0
- package/dist/payment-methods.js.map +1 -0
- package/dist/payment-return-url.d.ts +53 -0
- package/dist/payment-return-url.d.ts.map +1 -0
- package/dist/payment-return-url.js +35 -0
- package/dist/payment-return-url.js.map +1 -0
- package/dist/payments.d.ts +386 -0
- package/dist/payments.d.ts.map +1 -0
- package/dist/payments.js +84 -0
- package/dist/payments.js.map +1 -0
- package/dist/pim-connector.d.ts +60 -0
- package/dist/pim-connector.d.ts.map +1 -0
- package/dist/pim-connector.js +43 -0
- package/dist/pim-connector.js.map +1 -0
- package/dist/pim-field-path.d.ts +6 -0
- package/dist/pim-field-path.d.ts.map +1 -0
- package/dist/pim-field-path.js +101 -0
- package/dist/pim-field-path.js.map +1 -0
- package/dist/platform-language.d.ts +20 -0
- package/dist/platform-language.d.ts.map +1 -0
- package/dist/platform-language.js +22 -0
- package/dist/platform-language.js.map +1 -0
- package/dist/price-lists.d.ts +685 -0
- package/dist/price-lists.d.ts.map +1 -0
- package/dist/price-lists.js +330 -0
- package/dist/price-lists.js.map +1 -0
- package/dist/product-feeds.d.ts +2837 -0
- package/dist/product-feeds.d.ts.map +1 -0
- package/dist/product-feeds.js +1504 -0
- package/dist/product-feeds.js.map +1 -0
- package/dist/product-scope-overrides.d.ts +134 -0
- package/dist/product-scope-overrides.d.ts.map +1 -0
- package/dist/product-scope-overrides.js +82 -0
- package/dist/product-scope-overrides.js.map +1 -0
- package/dist/product-value-resolver.d.ts +88 -0
- package/dist/product-value-resolver.d.ts.map +1 -0
- package/dist/product-value-resolver.js +128 -0
- package/dist/product-value-resolver.js.map +1 -0
- package/dist/promotions.d.ts +678 -0
- package/dist/promotions.d.ts.map +1 -0
- package/dist/promotions.js +479 -0
- package/dist/promotions.js.map +1 -0
- package/dist/prompt-actions.d.ts +582 -0
- package/dist/prompt-actions.d.ts.map +1 -0
- package/dist/prompt-actions.js +221 -0
- package/dist/prompt-actions.js.map +1 -0
- package/dist/pwa.d.ts +293 -0
- package/dist/pwa.d.ts.map +1 -0
- package/dist/pwa.js +204 -0
- package/dist/pwa.js.map +1 -0
- package/dist/quick-order.d.ts +340 -0
- package/dist/quick-order.d.ts.map +1 -0
- package/dist/quick-order.js +177 -0
- package/dist/quick-order.js.map +1 -0
- package/dist/quote-requests.d.ts +538 -0
- package/dist/quote-requests.d.ts.map +1 -0
- package/dist/quote-requests.js +308 -0
- package/dist/quote-requests.js.map +1 -0
- package/dist/returns.d.ts +774 -0
- package/dist/returns.d.ts.map +1 -0
- package/dist/returns.js +389 -0
- package/dist/returns.js.map +1 -0
- package/dist/sales-channels.d.ts +392 -0
- package/dist/sales-channels.d.ts.map +1 -0
- package/dist/sales-channels.js +285 -0
- package/dist/sales-channels.js.map +1 -0
- package/dist/scope-notice.d.ts +60 -0
- package/dist/scope-notice.d.ts.map +1 -0
- package/dist/scope-notice.js +56 -0
- package/dist/scope-notice.js.map +1 -0
- package/dist/search.d.ts +321 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +160 -0
- package/dist/search.js.map +1 -0
- package/dist/seo.d.ts +113 -0
- package/dist/seo.d.ts.map +1 -0
- package/dist/seo.js +63 -0
- package/dist/seo.js.map +1 -0
- package/dist/settings.d.ts +453 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +337 -0
- package/dist/settings.js.map +1 -0
- package/dist/shipments.d.ts +140 -0
- package/dist/shipments.d.ts.map +1 -0
- package/dist/shipments.js +14 -0
- package/dist/shipments.js.map +1 -0
- package/dist/shipping-methods.d.ts +350 -0
- package/dist/shipping-methods.d.ts.map +1 -0
- package/dist/shipping-methods.js +99 -0
- package/dist/shipping-methods.js.map +1 -0
- package/dist/shopping-lists.d.ts +122 -0
- package/dist/shopping-lists.d.ts.map +1 -0
- package/dist/shopping-lists.js +92 -0
- package/dist/shopping-lists.js.map +1 -0
- package/dist/taxes.d.ts +106 -0
- package/dist/taxes.d.ts.map +1 -0
- package/dist/taxes.js +80 -0
- package/dist/taxes.js.map +1 -0
- package/dist/text-normalization.d.ts +199 -0
- package/dist/text-normalization.d.ts.map +1 -0
- package/dist/text-normalization.js +205 -0
- package/dist/text-normalization.js.map +1 -0
- package/dist/transactional-emails.d.ts +459 -0
- package/dist/transactional-emails.d.ts.map +1 -0
- package/dist/transactional-emails.js +212 -0
- package/dist/transactional-emails.js.map +1 -0
- package/dist/webhooks.d.ts +69 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +53 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `customer_accounts` module contracts — the in-process port surface
|
|
3
|
+
* (feature 075, Phase P).
|
|
4
|
+
*
|
|
5
|
+
* `customer_accounts` is the second-heaviest provider in the tree: 65 import
|
|
6
|
+
* sites across 19 modules, 51 of them on the `CustomerAccount` entity alone.
|
|
7
|
+
* That concentration is what this file answers — one record shape and one read
|
|
8
|
+
* port, so nineteen modules stop each writing their own `em.findOne`.
|
|
9
|
+
*
|
|
10
|
+
* Plain TypeScript rather than Zod: these describe in-process calls, not API
|
|
11
|
+
* boundaries. The module's HTTP shapes live in `customers.ts`, which is a
|
|
12
|
+
* different module's surface over the same rows and stays where it is — with
|
|
13
|
+
* one exception, the customer-group admin surface, whose Zod schemas arrived
|
|
14
|
+
* here with the entity in feature 076 (D-79) because this module serves those
|
|
15
|
+
* three routes itself.
|
|
16
|
+
*
|
|
17
|
+
* Nothing here imports from `backend/src/` (FR-034).
|
|
18
|
+
*/
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
export declare const customerGroupSchema: z.ZodObject<{
|
|
21
|
+
id: z.ZodString;
|
|
22
|
+
code: z.ZodString;
|
|
23
|
+
name: z.ZodString;
|
|
24
|
+
description: z.ZodNullable<z.ZodString>;
|
|
25
|
+
createdAt: z.ZodString;
|
|
26
|
+
updatedAt: z.ZodString;
|
|
27
|
+
}, z.core.$strip>;
|
|
28
|
+
export type CustomerGroup = z.infer<typeof customerGroupSchema>;
|
|
29
|
+
export declare const upsertCustomerGroupRequestSchema: z.ZodObject<{
|
|
30
|
+
code: z.ZodString;
|
|
31
|
+
name: z.ZodString;
|
|
32
|
+
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
33
|
+
}, z.core.$strip>;
|
|
34
|
+
/** A segmentation bucket as a module outside `customer_accounts` sees it. */
|
|
35
|
+
export interface CustomerGroupRecord {
|
|
36
|
+
id: string;
|
|
37
|
+
code: string;
|
|
38
|
+
name: string;
|
|
39
|
+
description: string | null;
|
|
40
|
+
createdAt: Date;
|
|
41
|
+
updatedAt: Date;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Container name: `customerGroupReadPort`. Owner: `customer_accounts`.
|
|
45
|
+
*
|
|
46
|
+
* The container name is unchanged by the relocation (D-81): it is what
|
|
47
|
+
* `check-port-dependencies.ts` compares, and keeping it means `pwa` did not
|
|
48
|
+
* have to be edited at all. `price_lists` reads it for the pricing rule-target
|
|
49
|
+
* picker and the rule-target validation; `pwa` for the push audience builder;
|
|
50
|
+
* `customers` for the group name on the admin customer list.
|
|
51
|
+
*
|
|
52
|
+
* This module is non-deactivatable, so the gate the port registration applies
|
|
53
|
+
* cannot be reached — as it could not under the previous owner, which is also
|
|
54
|
+
* non-deactivatable. The registration is a `providePort` anyway, for the reason
|
|
55
|
+
* its siblings give.
|
|
56
|
+
*/
|
|
57
|
+
export interface CustomerGroupReadPort {
|
|
58
|
+
findById(id: string): Promise<CustomerGroupRecord | null>;
|
|
59
|
+
findByIds(ids: readonly string[]): Promise<CustomerGroupRecord[]>;
|
|
60
|
+
/** Every group, ordered by code. */
|
|
61
|
+
listAll(): Promise<CustomerGroupRecord[]>;
|
|
62
|
+
}
|
|
63
|
+
export type CustomerAccountRole = 'organization_admin' | 'regular_user';
|
|
64
|
+
export type CustomerAccountBlockSource = 'staff' | 'org_owner';
|
|
65
|
+
/**
|
|
66
|
+
* A customer account as it crosses a module boundary — a plain shape, never
|
|
67
|
+
* the ORM entity (FR-011).
|
|
68
|
+
*
|
|
69
|
+
* `passwordHash` is deliberately absent. It is read by exactly one module —
|
|
70
|
+
* the owner — and has no business travelling: a record that carries it turns
|
|
71
|
+
* every consumer into a place a credential can leak from.
|
|
72
|
+
*/
|
|
73
|
+
export interface CustomerAccountRecord {
|
|
74
|
+
id: string;
|
|
75
|
+
/**
|
|
76
|
+
* The tenant that scopes this account. Never `null` since D-178:
|
|
77
|
+
* `customer_accounts.organization_id` is `NOT NULL`, an individual is backed
|
|
78
|
+
* by a single-member personal organisation, and there is no
|
|
79
|
+
* "no-organization" scoping path (Principle XI).
|
|
80
|
+
*/
|
|
81
|
+
organizationId: string;
|
|
82
|
+
email: string;
|
|
83
|
+
firstName: string;
|
|
84
|
+
lastName: string;
|
|
85
|
+
role: CustomerAccountRole;
|
|
86
|
+
emailVerifiedAt: Date | null;
|
|
87
|
+
/** Whether a confirmed TOTP enrolment exists. The secret itself never travels. */
|
|
88
|
+
twoFactorEnabled: boolean;
|
|
89
|
+
lastLoginAt: Date | null;
|
|
90
|
+
createdAt: Date;
|
|
91
|
+
updatedAt: Date;
|
|
92
|
+
customFieldValues: Record<string, unknown>;
|
|
93
|
+
deletedAt: Date | null;
|
|
94
|
+
customerGroupId: string | null;
|
|
95
|
+
subtreeRollupEnabled: boolean;
|
|
96
|
+
blockedAt: Date | null;
|
|
97
|
+
blockReason: string | null;
|
|
98
|
+
blockSource: CustomerAccountBlockSource | null;
|
|
99
|
+
blockedByAdminUserId: string | null;
|
|
100
|
+
blockedByCustomerAccountId: string | null;
|
|
101
|
+
deletionRequestedByAdminUserId: string | null;
|
|
102
|
+
anonymizedAt: Date | null;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Which accounts a lookup should consider. Soft-deleted accounts still resolve
|
|
106
|
+
* from historical Orders, Invoices and RFQs, so "include them" is a real and
|
|
107
|
+
* frequently correct answer — sixteen of the measured call sites pass
|
|
108
|
+
* `deletedAt: null` and the rest deliberately do not.
|
|
109
|
+
*/
|
|
110
|
+
export interface CustomerAccountLookupOptions {
|
|
111
|
+
/** Exclude soft-deleted accounts. Defaults to `false` — the wider read. */
|
|
112
|
+
activeOnly?: boolean;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Container name: `customerAccountReadPort`. Owner: `customer_accounts`.
|
|
116
|
+
*
|
|
117
|
+
* The union of what the nineteen consuming modules measurably ask for, and no
|
|
118
|
+
* more: identity lookups by id, ids, email and organisation, plus the two
|
|
119
|
+
* counting questions the org-admin invariant is enforced with.
|
|
120
|
+
*
|
|
121
|
+
* When `customer_accounts` is off every method throws `ModuleDisabledError`
|
|
122
|
+
* (503 `MODULE_DISABLED`) at the resolution seam. That is the right answer for
|
|
123
|
+
* a read whose absence would otherwise be indistinguishable from "no such
|
|
124
|
+
* account": a cart approval that cannot identify its buyer must refuse, not
|
|
125
|
+
* proceed anonymously.
|
|
126
|
+
*
|
|
127
|
+
* Whether `customer_accounts` has an off state at all is its manifest's `activation` to
|
|
128
|
+
* say, not this line's: a module declaring `nonDeactivatable` never enters one.
|
|
129
|
+
*/
|
|
130
|
+
export interface CustomerAccountReadPort {
|
|
131
|
+
findById(id: string, options?: CustomerAccountLookupOptions): Promise<CustomerAccountRecord | null>;
|
|
132
|
+
findByIds(ids: readonly string[], options?: CustomerAccountLookupOptions): Promise<CustomerAccountRecord[]>;
|
|
133
|
+
findByEmail(email: string, options?: CustomerAccountLookupOptions): Promise<CustomerAccountRecord | null>;
|
|
134
|
+
/**
|
|
135
|
+
* The account, but only if it belongs to that organisation. The two-argument
|
|
136
|
+
* form exists because every admin path that touches a member checks
|
|
137
|
+
* membership first, and doing it in one query is what stops the check being
|
|
138
|
+
* forgotten.
|
|
139
|
+
*/
|
|
140
|
+
findInOrganization(id: string, organizationId: string, options?: CustomerAccountLookupOptions): Promise<CustomerAccountRecord | null>;
|
|
141
|
+
/** Members of an organisation, ordered by role then creation date. */
|
|
142
|
+
listByOrganization(organizationId: string, options?: CustomerAccountLookupOptions): Promise<CustomerAccountRecord[]>;
|
|
143
|
+
/**
|
|
144
|
+
* How many accounts in the organisation hold the role, optionally ignoring
|
|
145
|
+
* one id and optionally counting only unblocked accounts. This is the shape
|
|
146
|
+
* the "an organisation may not lose its last admin" rule is spelled with, in
|
|
147
|
+
* three modules, three slightly different ways.
|
|
148
|
+
*/
|
|
149
|
+
countByOrganizationRole(organizationId: string, role: CustomerAccountRole, options?: {
|
|
150
|
+
excludeCustomerAccountId?: string;
|
|
151
|
+
activeOnly?: boolean;
|
|
152
|
+
notBlocked?: boolean;
|
|
153
|
+
}): Promise<number>;
|
|
154
|
+
/**
|
|
155
|
+
* Substring search over the e-mail address, ordered by e-mail, capped at
|
|
156
|
+
* `limit`. An empty `query` returns the first `limit` accounts.
|
|
157
|
+
*
|
|
158
|
+
* Published after Phase P because `pwa`'s push Rule Builder is the one
|
|
159
|
+
* measured consumer that asks this question, and `listAll` is not the same
|
|
160
|
+
* answer: a picker that loads every account to keep 200 of them is a
|
|
161
|
+
* full-table read wearing a port.
|
|
162
|
+
*/
|
|
163
|
+
searchByEmail(query: string, limit: number): Promise<CustomerAccountRecord[]>;
|
|
164
|
+
/**
|
|
165
|
+
* Ids of accounts whose e-mail, first name or last name contains `query`,
|
|
166
|
+
* case-insensitively. An empty `query` returns no ids.
|
|
167
|
+
*
|
|
168
|
+
* Published after Phase P, as the twin of
|
|
169
|
+
* `OrganizationDetailsPort.searchIdsByName` and for the same consumer: the
|
|
170
|
+
* admin orders list resolves the people a search term names, then constrains
|
|
171
|
+
* orders to them. It is deliberately **not** {@link searchByEmail}, which
|
|
172
|
+
* matches the address alone — an operator typing a surname into the orders
|
|
173
|
+
* search expects the surname to match, and it does today.
|
|
174
|
+
*
|
|
175
|
+
* Ids only, and uncapped, because the caller feeds them straight into an
|
|
176
|
+
* `$in` over its own table and a cap would silently drop orders rather than
|
|
177
|
+
* accounts.
|
|
178
|
+
*/
|
|
179
|
+
searchIdsByName(query: string): Promise<string[]>;
|
|
180
|
+
/** Every account, for the bulk export adapter. Ordered by email. */
|
|
181
|
+
listAll(): Promise<CustomerAccountRecord[]>;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* A new account, as the module that owns the membership asks for one.
|
|
185
|
+
*
|
|
186
|
+
* The **plain** password crosses, not a hash: `passwordHash` is deliberately
|
|
187
|
+
* absent from {@link CustomerAccountRecord} for the same reason, and a caller
|
|
188
|
+
* that hashes is a caller that has to be told which algorithm the owner uses
|
|
189
|
+
* and be trusted to keep using it. Hashing belongs on the owner's side of the
|
|
190
|
+
* port, and moving it there removed the last three `hashPassword` imports from
|
|
191
|
+
* `organizations`.
|
|
192
|
+
*/
|
|
193
|
+
export interface CustomerAccountCreateInput {
|
|
194
|
+
organizationId: string;
|
|
195
|
+
email: string;
|
|
196
|
+
password: string;
|
|
197
|
+
firstName: string;
|
|
198
|
+
lastName: string;
|
|
199
|
+
role: CustomerAccountRole;
|
|
200
|
+
/**
|
|
201
|
+
* Whether the address is already confirmed. Accepting an invitation implies
|
|
202
|
+
* it — the invitation was delivered to that address; self-registration and
|
|
203
|
+
* the admin direct-create do not.
|
|
204
|
+
*/
|
|
205
|
+
emailVerified?: boolean;
|
|
206
|
+
}
|
|
207
|
+
/** Absent fields are left unchanged. */
|
|
208
|
+
export interface CustomerAccountProfilePatch {
|
|
209
|
+
/**
|
|
210
|
+
* Lower-cased by the owner. Changing it clears `emailVerifiedAt`: the new
|
|
211
|
+
* address has not been confirmed, and leaving the old confirmation standing
|
|
212
|
+
* would let an admin verify an address by editing it.
|
|
213
|
+
*/
|
|
214
|
+
email?: string;
|
|
215
|
+
firstName?: string;
|
|
216
|
+
lastName?: string;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Container name: `customerAccountMemberWritePort`. Owner: `customer_accounts`.
|
|
220
|
+
*
|
|
221
|
+
* The member lifecycle `organizations` runs over accounts in its
|
|
222
|
+
* organisations, published because that module ran it by creating and mutating
|
|
223
|
+
* this module's entity directly (feature 075, Phase C). It is the union of
|
|
224
|
+
* what that module measurably does and no more: three creation paths
|
|
225
|
+
* (self-registration, invitation accept, admin direct-create) collapse into
|
|
226
|
+
* one `create`, and the four field writes are one method each.
|
|
227
|
+
*
|
|
228
|
+
* **Every method is one unit of work on this module's table only** (D-78 rule
|
|
229
|
+
* 1). No `EntityManager` crosses, and none needs to: each caller's remaining
|
|
230
|
+
* writes are its own tables, flushed on its own side. Where that splits a
|
|
231
|
+
* flush the caller used to share — the invitation accept wrote the account and
|
|
232
|
+
* consumed the invitation together — the caller orders the two so the
|
|
233
|
+
* recoverable half fails first, and says so at the call site.
|
|
234
|
+
*
|
|
235
|
+
* **Each method audits its own write**, in the same unit of work, exactly as
|
|
236
|
+
* `roleService` and `addresses`' `addressService` do. The caller's own audit
|
|
237
|
+
* row is a different fact — "an operator edited this member on the
|
|
238
|
+
* organisation panel" rather than "this account's e-mail changed" — and both
|
|
239
|
+
* are kept, which is what the role endpoint has always recorded.
|
|
240
|
+
*
|
|
241
|
+
* `changeRole` is **not** here: {@link CustomerRolePort} already owns it, with
|
|
242
|
+
* the "an organisation keeps at least one admin" guard.
|
|
243
|
+
* {@link CustomerAccountMemberWritePort.promoteToOrganizationAdmin} is a
|
|
244
|
+
* different question — the break-glass path an operator reaches *because* an
|
|
245
|
+
* organisation has no admin left — and it is named rather than expressed as an
|
|
246
|
+
* unguarded `setRole`, which is a footgun beside a guarded one.
|
|
247
|
+
*
|
|
248
|
+
* When `customer_accounts` is off every method fails closed. The module is
|
|
249
|
+
* non-deactivatable, so that gate cannot be reached today; it is registered
|
|
250
|
+
* through `providePort` anyway, for the reason its seven siblings give.
|
|
251
|
+
*/
|
|
252
|
+
export interface CustomerAccountMemberWritePort {
|
|
253
|
+
/**
|
|
254
|
+
* Creates the account. Throws HTTP 409 `EMAIL_ALREADY_REGISTERED` when the
|
|
255
|
+
* address is taken — the check is inside the write, so a caller that races
|
|
256
|
+
* its own pre-check still gets the right code rather than a constraint
|
|
257
|
+
* violation.
|
|
258
|
+
*/
|
|
259
|
+
create(input: CustomerAccountCreateInput): Promise<CustomerAccountRecord>;
|
|
260
|
+
/**
|
|
261
|
+
* Throws HTTP 404 `NOT_FOUND` when no live account has that id, and HTTP 409
|
|
262
|
+
* `EMAIL_ALREADY_REGISTERED` when the new address belongs to another one.
|
|
263
|
+
*/
|
|
264
|
+
updateProfile(customerAccountId: string, patch: CustomerAccountProfilePatch): Promise<CustomerAccountRecord>;
|
|
265
|
+
/** Feature 056 — the customer-side subtree roll-up capability. */
|
|
266
|
+
setSubtreeRollup(customerAccountId: string, enabled: boolean): Promise<CustomerAccountRecord>;
|
|
267
|
+
/** The break-glass promotion. Idempotent on an account that already holds it. */
|
|
268
|
+
promoteToOrganizationAdmin(customerAccountId: string): Promise<CustomerAccountRecord>;
|
|
269
|
+
/**
|
|
270
|
+
* Stamps `emailVerifiedAt`, idempotently — a second call keeps the first
|
|
271
|
+
* timestamp, so a retried verification does not move it.
|
|
272
|
+
*/
|
|
273
|
+
markEmailVerified(customerAccountId: string, verifiedAt: Date): Promise<CustomerAccountRecord>;
|
|
274
|
+
/**
|
|
275
|
+
* Feature 051 — binds an org-less account to the organisation just
|
|
276
|
+
* provisioned for it. Throws HTTP 404 `NOT_FOUND` when no account has that
|
|
277
|
+
* id.
|
|
278
|
+
*/
|
|
279
|
+
attachToOrganization(customerAccountId: string, organizationId: string): Promise<CustomerAccountRecord>;
|
|
280
|
+
}
|
|
281
|
+
/** What a caller sets the session cookie from after a successful first factor. */
|
|
282
|
+
export interface CustomerLoginResult {
|
|
283
|
+
customerAccount: CustomerAccountRecord;
|
|
284
|
+
/** Value to put into the Set-Cookie header. */
|
|
285
|
+
sessionCookieValue: string;
|
|
286
|
+
sessionExpiresAt: Date;
|
|
287
|
+
}
|
|
288
|
+
/** Discriminated outcome of the first login step (feature 042). */
|
|
289
|
+
export type CustomerLoginOutcome = ({
|
|
290
|
+
status: 'authenticated';
|
|
291
|
+
} & CustomerLoginResult) | {
|
|
292
|
+
status: 'mfaRequired';
|
|
293
|
+
challengeId: string;
|
|
294
|
+
} | {
|
|
295
|
+
status: 'mfaSetupRequired';
|
|
296
|
+
setupTicket: string;
|
|
297
|
+
};
|
|
298
|
+
export interface CustomerLoginInput {
|
|
299
|
+
email: string;
|
|
300
|
+
password: string;
|
|
301
|
+
ip?: string;
|
|
302
|
+
userAgent?: string;
|
|
303
|
+
salesChannelId?: string | null;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Container name: `customerAuthPort`. Owner: `customer_accounts`.
|
|
307
|
+
*
|
|
308
|
+
* (It said `customerAuthService` until issue #192. That name is registered too,
|
|
309
|
+
* for the `CustomerAuthService` **class**, which returns the `CustomerAccount`
|
|
310
|
+
* entity; it stays registered because this adapter is built over it. `customers/backend.ts` was the last consumer outside the module
|
|
311
|
+
* and resolves `customerAuthPort` since issue #195 — which was **not** the
|
|
312
|
+
* record-over-a-class trap {@link AddressServicePort} describes: it typed that
|
|
313
|
+
* resolution against a direct class-type import, an ordinary FR-011 edge that
|
|
314
|
+
* happened to sit on the same container name.)
|
|
315
|
+
*
|
|
316
|
+
* Consumed by `customers` and `organizations`, which own the storefront login,
|
|
317
|
+
* registration and self-service routes over these accounts.
|
|
318
|
+
*
|
|
319
|
+
* **Owner off:** the seam fails closed — resolving this port throws
|
|
320
|
+
* `ModuleDisabledError` and the call answers 503 `MODULE_DISABLED`, so nothing
|
|
321
|
+
* half-executes. Whether `customer_accounts` has an off state at all is its manifest's
|
|
322
|
+
* `activation` to say, not this line's: a module declaring
|
|
323
|
+
* `nonDeactivatable` never enters one.
|
|
324
|
+
*/
|
|
325
|
+
export interface CustomerAuthPort {
|
|
326
|
+
login(input: CustomerLoginInput): Promise<CustomerLoginOutcome>;
|
|
327
|
+
changePassword(customerAccountId: string, currentPassword: string, newPassword: string): Promise<void>;
|
|
328
|
+
logout(sessionId: string): Promise<void>;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Container name: `passwordResetService`. Owner: `customer_accounts`.
|
|
332
|
+
*
|
|
333
|
+
* `requestReset` returns `{ rawToken: null }` for an unknown address on
|
|
334
|
+
* purpose — the caller must not be able to tell an unknown e-mail from a known
|
|
335
|
+
* one.
|
|
336
|
+
*
|
|
337
|
+
* **Owner off:** the seam fails closed — resolving this port throws
|
|
338
|
+
* `ModuleDisabledError` and the call answers 503 `MODULE_DISABLED`, so nothing
|
|
339
|
+
* half-executes. Whether `customer_accounts` has an off state at all is its manifest's
|
|
340
|
+
* `activation` to say, not this line's: a module declaring
|
|
341
|
+
* `nonDeactivatable` never enters one.
|
|
342
|
+
*/
|
|
343
|
+
export interface CustomerPasswordResetPort {
|
|
344
|
+
requestReset(email: string): Promise<{
|
|
345
|
+
rawToken: string | null;
|
|
346
|
+
}>;
|
|
347
|
+
confirmReset(rawToken: string, newPassword: string): Promise<void>;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Container name: `customerPasswordStatePort`. Owner: `customer_accounts`.
|
|
351
|
+
*
|
|
352
|
+
* Whether the account has a password its holder can actually use — issue #222.
|
|
353
|
+
*
|
|
354
|
+
* `passwordHash` cannot answer that and never travels anyway: it is NOT NULL
|
|
355
|
+
* for every account, because federated auto-create mints a random one to keep
|
|
356
|
+
* the column satisfied. So a consumer asking "is there another way into this
|
|
357
|
+
* account" over `password_hash is not null` gets `true` for exactly the
|
|
358
|
+
* accounts where it is false.
|
|
359
|
+
*
|
|
360
|
+
* Deliberately not a field on {@link CustomerAccountRecord}. That record is
|
|
361
|
+
* read by nineteen modules; the state of a credential is a question one module
|
|
362
|
+
* asks — `mfa`, before severing an account's last federated identity — and a
|
|
363
|
+
* targeted port is what keeps it that way.
|
|
364
|
+
*
|
|
365
|
+
* The date rather than a boolean, because the one is derivable from the other
|
|
366
|
+
* and an account surface that wants to show *when* a password was set should
|
|
367
|
+
* not need a second method for it. `null` means no such password is on record:
|
|
368
|
+
* either none was ever set, or the row predates the column.
|
|
369
|
+
*
|
|
370
|
+
* **Owner off:** the seam fails closed — resolving this port throws
|
|
371
|
+
* `ModuleDisabledError` and the call answers 503 `MODULE_DISABLED`, so nothing
|
|
372
|
+
* half-executes. Whether `customer_accounts` has an off state at all is its
|
|
373
|
+
* manifest's `activation` to say, not this line's: a module declaring
|
|
374
|
+
* `nonDeactivatable` never enters one.
|
|
375
|
+
*/
|
|
376
|
+
export interface CustomerPasswordStatePort {
|
|
377
|
+
/** `null` for an unknown id as well — an account nobody can find has no password on record. */
|
|
378
|
+
passwordSetAt(customerAccountId: string): Promise<Date | null>;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Container name: `customerPasswordVerificationPort`. Owner: `customer_accounts`.
|
|
382
|
+
*
|
|
383
|
+
* Does the stored credential of this account match the password presented?
|
|
384
|
+
* The customer-side twin of {@link AdminPasswordVerificationPort}, asked by
|
|
385
|
+
* the same consumer for the same reason: `mfa`'s step-up re-verification,
|
|
386
|
+
* before it disables a second factor.
|
|
387
|
+
*
|
|
388
|
+
* Deliberately **not** {@link CustomerAuthPort.login}, which is a different
|
|
389
|
+
* operation wearing similar arguments — it takes an e-mail, mints a session,
|
|
390
|
+
* stamps `lastLoginAt` and runs the login side effects. Step-up already knows
|
|
391
|
+
* who is asking and wants none of that.
|
|
392
|
+
*
|
|
393
|
+
* Deliberately **not** {@link CustomerPasswordStatePort} either, though both
|
|
394
|
+
* are credential questions one module asks: `passwordSetAt` answers "is there
|
|
395
|
+
* another way into this account" for an account-severing decision, and
|
|
396
|
+
* conflating the two would put a comparison against a caller-supplied secret
|
|
397
|
+
* on a port whose method takes no secret.
|
|
398
|
+
*
|
|
399
|
+
* `false` for an unknown id as well — an account nobody can find has no
|
|
400
|
+
* password to match. It is a lookup by id and nothing else: whether the caller
|
|
401
|
+
* may act as that account at all is the session layer's question, asked before
|
|
402
|
+
* this one.
|
|
403
|
+
*
|
|
404
|
+
* **Owner off:** the seam fails closed — resolving this port throws
|
|
405
|
+
* `ModuleDisabledError` and the call answers 503 `MODULE_DISABLED`, so nothing
|
|
406
|
+
* half-executes. Whether `customer_accounts` has an off state at all is its
|
|
407
|
+
* manifest's `activation` to say, not this line's: a module declaring
|
|
408
|
+
* `nonDeactivatable` never enters one.
|
|
409
|
+
*/
|
|
410
|
+
export interface CustomerPasswordVerificationPort {
|
|
411
|
+
verifyPassword(customerAccountId: string, password: string): Promise<boolean>;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Container name: `customerRollupScopePort`. Owner: `customer_accounts`.
|
|
415
|
+
*
|
|
416
|
+
* Feature 056 (T032) — whether a customer login widens from single-org to its
|
|
417
|
+
* organization's subtree, and the widened id set when it does. Derived from the
|
|
418
|
+
* authenticated actor and never from request inputs (Principle XI).
|
|
419
|
+
*
|
|
420
|
+
* **Its consumer is a composition root, which is why the shape is unusual.**
|
|
421
|
+
* The per-request tenant-context builder is where the answer is needed, and the
|
|
422
|
+
* question has two halves owned by two modules: the capability flag on the
|
|
423
|
+
* account, which is this module's column, and the tree traversal, which is
|
|
424
|
+
* `organizations`'. The traversal therefore arrives as `subtreeIds` rather than
|
|
425
|
+
* being resolved here — the root already holds that module's tree service, and
|
|
426
|
+
* resolving it from this side would be a cross-module reach into a container
|
|
427
|
+
* name no contract publishes.
|
|
428
|
+
*
|
|
429
|
+
* `undefined` means "stay single-org", for both of the reasons it can: the
|
|
430
|
+
* account has no organisation, or it does not hold the capability. A caller
|
|
431
|
+
* that receives it must not widen.
|
|
432
|
+
*
|
|
433
|
+
* The capability read runs under a system scope on the owner's side, because
|
|
434
|
+
* the tenant context is being *established* by the caller and does not exist
|
|
435
|
+
* yet.
|
|
436
|
+
*
|
|
437
|
+
* **Owner off:** the seam fails closed — resolving this port throws
|
|
438
|
+
* `ModuleDisabledError` and the call answers 503 `MODULE_DISABLED`, so nothing
|
|
439
|
+
* half-executes. Whether `customer_accounts` has an off state at all is its
|
|
440
|
+
* manifest's `activation` to say, not this line's: a module declaring
|
|
441
|
+
* `nonDeactivatable` never enters one.
|
|
442
|
+
*/
|
|
443
|
+
export interface CustomerRollupScopePort {
|
|
444
|
+
resolveSubtreeIds(customerAccountId: string, organizationId: string | null, subtreeIds: (organizationId: string) => Promise<string[]>): Promise<string[] | undefined>;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Container name: `customerRolePort`. Owner: `customer_accounts`.
|
|
448
|
+
*
|
|
449
|
+
* (It said `roleService` until issue #192. Nothing registers that name; the
|
|
450
|
+
* module's own class is `customerRoleService` and the gated port is this one.
|
|
451
|
+
* A name-keyed sweep had already miscounted this port as unreached because of
|
|
452
|
+
* it — see the Phase-P unreached-port audit, A5.)
|
|
453
|
+
*
|
|
454
|
+
* Consumed by `organizations`, which owns the member-management surface. The
|
|
455
|
+
* "an organisation keeps at least one admin" rule lives on this side of the
|
|
456
|
+
* port, not in the caller.
|
|
457
|
+
*
|
|
458
|
+
* **Owner off:** the seam fails closed — resolving this port throws
|
|
459
|
+
* `ModuleDisabledError` and the call answers 503 `MODULE_DISABLED`, so nothing
|
|
460
|
+
* half-executes. Whether `customer_accounts` has an off state at all is its manifest's
|
|
461
|
+
* `activation` to say, not this line's: a module declaring
|
|
462
|
+
* `nonDeactivatable` never enters one.
|
|
463
|
+
*/
|
|
464
|
+
export interface CustomerRolePort {
|
|
465
|
+
listMembers(organizationId: string): Promise<CustomerAccountRecord[]>;
|
|
466
|
+
changeRole(organizationId: string, targetCustomerAccountId: string, newRole: CustomerAccountRole): Promise<CustomerAccountRecord>;
|
|
467
|
+
removeMember(organizationId: string, targetCustomerAccountId: string): Promise<void>;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* A standalone (org-less) self-registration, as `customers` asks for one.
|
|
471
|
+
*
|
|
472
|
+
* The **plain** password crosses, for the reason
|
|
473
|
+
* {@link CustomerAccountCreateInput} gives: hashing belongs on the owner's
|
|
474
|
+
* side of the port, and a caller that hashes is a caller that has to be told
|
|
475
|
+
* which algorithm the owner uses and be trusted to keep using it.
|
|
476
|
+
*
|
|
477
|
+
* There is no `organizationId`, and since D-178 that is not because the account
|
|
478
|
+
* is created without one. The owner provisions the individual's personal
|
|
479
|
+
* organisation and writes the account **in one transaction**, so the caller has
|
|
480
|
+
* no organisation to supply and no window in which to supply it: the account
|
|
481
|
+
* row and its tenant either both exist or neither does. Before D-178 the two
|
|
482
|
+
* were separate units of work and a failure between them left a committed
|
|
483
|
+
* account with `organization_id = NULL` that nothing retried.
|
|
484
|
+
*/
|
|
485
|
+
export interface CustomerAccountStandaloneCreateInput {
|
|
486
|
+
email: string;
|
|
487
|
+
password: string;
|
|
488
|
+
firstName: string;
|
|
489
|
+
lastName: string;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* The request the write was made from, stamped onto its audit row.
|
|
493
|
+
*
|
|
494
|
+
* It travels because the admin moderation surface has always recorded it and
|
|
495
|
+
* the row is written on the owner's side of the port now: dropping it would
|
|
496
|
+
* quietly narrow what an operator can reconstruct from the audit log, which is
|
|
497
|
+
* the one thing a boundary cut must not do.
|
|
498
|
+
*/
|
|
499
|
+
export interface CustomerAccountWriteRequestMeta {
|
|
500
|
+
ipAddress?: string | null;
|
|
501
|
+
userAgent?: string | null;
|
|
502
|
+
requestId?: string | null;
|
|
503
|
+
}
|
|
504
|
+
/** Which lifecycle bucket the admin customer list is asking for. */
|
|
505
|
+
export type CustomerAccountLifecycleStatus = 'active' | 'blocked' | 'deleted';
|
|
506
|
+
/**
|
|
507
|
+
* The admin customer list's filter, as plain data.
|
|
508
|
+
*
|
|
509
|
+
* `allowedOrganizationIds` is `null` for a platform administrator — the
|
|
510
|
+
* unscoped read — and otherwise the organisations the acting staff member may
|
|
511
|
+
* see, in which case org-less accounts are visible too (a standalone customer
|
|
512
|
+
* belongs to nobody's territory, so it belongs to everybody's). Expressing the
|
|
513
|
+
* scope as a nullable list rather than an `isPlatformAdmin` flag keeps the
|
|
514
|
+
* authority decision in `customers`, which is where the sales-rep scope is
|
|
515
|
+
* resolved.
|
|
516
|
+
*/
|
|
517
|
+
export interface CustomerAccountAdminSearchCriteria {
|
|
518
|
+
/** Substring match over e-mail, first name and last name. */
|
|
519
|
+
q?: string | undefined;
|
|
520
|
+
status?: CustomerAccountLifecycleStatus | undefined;
|
|
521
|
+
organizationId?: string | undefined;
|
|
522
|
+
customerGroupId?: string | undefined;
|
|
523
|
+
allowedOrganizationIds: readonly string[] | null;
|
|
524
|
+
page: number;
|
|
525
|
+
pageSize: number;
|
|
526
|
+
}
|
|
527
|
+
export interface CustomerAccountAdminSearchResult {
|
|
528
|
+
rows: CustomerAccountRecord[];
|
|
529
|
+
total: number;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Container name: `customerAccountAdminSearchPort`. Owner: `customer_accounts`.
|
|
533
|
+
*
|
|
534
|
+
* The paginated, filtered read behind the admin customer list (feature 040,
|
|
535
|
+
* US5). Deliberately **not** a method on {@link CustomerAccountReadPort}: that
|
|
536
|
+
* port is what nineteen modules resolve and it is the union of what they all
|
|
537
|
+
* ask, whereas this is one screen's query — the same argument
|
|
538
|
+
* {@link CustomerPasswordStatePort} is separate for.
|
|
539
|
+
*
|
|
540
|
+
* It is one method rather than a set of primitives because the filter, the
|
|
541
|
+
* ordering and the page have to be one SQL statement: a caller that narrows a
|
|
542
|
+
* page after the fact returns fewer rows than it asked for, and one that pages
|
|
543
|
+
* after narrowing reads the whole table.
|
|
544
|
+
*
|
|
545
|
+
* The policy stays with the caller. This port takes the organisations the
|
|
546
|
+
* actor may see; it does not decide who that is.
|
|
547
|
+
*
|
|
548
|
+
* **Owner off:** the seam fails closed — resolving this port throws
|
|
549
|
+
* `ModuleDisabledError` and the call answers 503 `MODULE_DISABLED`, so nothing
|
|
550
|
+
* half-executes. Whether `customer_accounts` has an off state at all is its
|
|
551
|
+
* manifest's `activation` to say, not this line's: a module declaring
|
|
552
|
+
* `nonDeactivatable` never enters one.
|
|
553
|
+
*/
|
|
554
|
+
export interface CustomerAccountAdminSearchPort {
|
|
555
|
+
search(criteria: CustomerAccountAdminSearchCriteria): Promise<CustomerAccountAdminSearchResult>;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Container name: `customerAccountLifecycleWritePort`. Owner: `customer_accounts`.
|
|
559
|
+
*
|
|
560
|
+
* The account lifecycle `customers` runs over this module's table — the
|
|
561
|
+
* counterpart to {@link CustomerAccountMemberWritePort}, which is the member
|
|
562
|
+
* lifecycle `organizations` runs. `customers` ran it by loading and mutating
|
|
563
|
+
* the `CustomerAccount` entity in five services and two route files; these are
|
|
564
|
+
* those writes, one method each, each a single unit of work on this table
|
|
565
|
+
* alone (D-78 rule 1). No `EntityManager` crosses.
|
|
566
|
+
*
|
|
567
|
+
* **The policy stays with the caller and the audit row comes here.** Whether a
|
|
568
|
+
* staff member may act on this customer, and whether blocking them would
|
|
569
|
+
* strand an organisation without an administrator, are `customers`' questions
|
|
570
|
+
* and stay there — the counting half of the second one is
|
|
571
|
+
* {@link CustomerAccountReadPort.countByOrganizationRole}, which already
|
|
572
|
+
* exists. What moves is the write and the one audit row that describes it, so
|
|
573
|
+
* that a row on this table is never written by a module that does not own it
|
|
574
|
+
* and never written without being recorded. Unlike
|
|
575
|
+
* {@link CustomerAccountMemberWritePort} the caller keeps **no** second row:
|
|
576
|
+
* there is no separate host fact here — "an operator blocked this customer" is
|
|
577
|
+
* the write.
|
|
578
|
+
*
|
|
579
|
+
* `anonymize` is the one method that is not an operator action. It is the
|
|
580
|
+
* retention sweep, it records itself with a null actor, and it is idempotent:
|
|
581
|
+
* an account already anonymised is returned unchanged, so a re-run after a
|
|
582
|
+
* partial sweep scrubs nothing twice.
|
|
583
|
+
*
|
|
584
|
+
* **Owner off:** the seam fails closed — resolving this port throws
|
|
585
|
+
* `ModuleDisabledError` and the call answers 503 `MODULE_DISABLED`, so nothing
|
|
586
|
+
* half-executes. Whether `customer_accounts` has an off state at all is its
|
|
587
|
+
* manifest's `activation` to say, not this line's: a module declaring
|
|
588
|
+
* `nonDeactivatable` never enters one.
|
|
589
|
+
*/
|
|
590
|
+
export interface CustomerAccountLifecycleWritePort {
|
|
591
|
+
/**
|
|
592
|
+
* Creates an org-less account. Throws HTTP 409 `EMAIL_ALREADY_REGISTERED`
|
|
593
|
+
* when the address is taken — the check is inside the write, so a caller
|
|
594
|
+
* that races its own pre-check still gets the right code rather than a
|
|
595
|
+
* constraint violation.
|
|
596
|
+
*/
|
|
597
|
+
createStandalone(input: CustomerAccountStandaloneCreateInput): Promise<CustomerAccountRecord>;
|
|
598
|
+
/**
|
|
599
|
+
* Idempotent: an account that is already blocked is returned unchanged and
|
|
600
|
+
* nothing is recorded. Throws HTTP 404 `CUSTOMER_NOT_FOUND` for an id no
|
|
601
|
+
* live account has.
|
|
602
|
+
*/
|
|
603
|
+
block(customerAccountId: string, input: {
|
|
604
|
+
actorAdminUserId: string;
|
|
605
|
+
reason?: string | null;
|
|
606
|
+
audit?: CustomerAccountWriteRequestMeta;
|
|
607
|
+
}): Promise<CustomerAccountRecord>;
|
|
608
|
+
/** Idempotent, the same way round. */
|
|
609
|
+
unblock(customerAccountId: string, input: {
|
|
610
|
+
actorAdminUserId: string;
|
|
611
|
+
audit?: CustomerAccountWriteRequestMeta;
|
|
612
|
+
}): Promise<CustomerAccountRecord>;
|
|
613
|
+
/**
|
|
614
|
+
* Soft-delete. Throws HTTP 409 `CUSTOMER_ALREADY_DELETED` when the account
|
|
615
|
+
* already carries one — the caller's own refusal, kept here because the
|
|
616
|
+
* check and the write have to see the same row.
|
|
617
|
+
*/
|
|
618
|
+
softDelete(customerAccountId: string, input: {
|
|
619
|
+
actorAdminUserId: string;
|
|
620
|
+
}): Promise<CustomerAccountRecord>;
|
|
621
|
+
/**
|
|
622
|
+
* Throws HTTP 409 `CUSTOMER_NOT_DELETED` when there is nothing to restore,
|
|
623
|
+
* and HTTP 409 `CUSTOMER_RESTORE_WINDOW_ELAPSED` once the account has been
|
|
624
|
+
* anonymised, which is irreversible.
|
|
625
|
+
*/
|
|
626
|
+
restore(customerAccountId: string, input: {
|
|
627
|
+
actorAdminUserId: string;
|
|
628
|
+
}): Promise<CustomerAccountRecord>;
|
|
629
|
+
/**
|
|
630
|
+
* Moves the account into `organizationId`, recording
|
|
631
|
+
* `customer_account.organization_assigned`.
|
|
632
|
+
*
|
|
633
|
+
* The parameter was `string | null` until D-178, and the `null` meant "detach
|
|
634
|
+
* this account, leaving it standalone" — a durable row with no tenant, which
|
|
635
|
+
* Principle XI forbids and `customer_accounts.organization_id NOT NULL` now
|
|
636
|
+
* refuses at the column. What an operator detaching a member actually wants
|
|
637
|
+
* is {@link detachToPersonalOrganization} below.
|
|
638
|
+
*/
|
|
639
|
+
setOrganization(customerAccountId: string, organizationId: string, input: {
|
|
640
|
+
actorAdminUserId: string;
|
|
641
|
+
}): Promise<CustomerAccountRecord>;
|
|
642
|
+
/**
|
|
643
|
+
* D-178 — the operator's "this person no longer belongs to that company",
|
|
644
|
+
* expressed as the move it has to be rather than as the detach it used to be.
|
|
645
|
+
*
|
|
646
|
+
* `personalOrganizationId` is the account's own personal organisation, which
|
|
647
|
+
* the caller obtains from
|
|
648
|
+
* `PersonalOrganizationPort.provisionPersonalOrganization`; passing anything
|
|
649
|
+
* else is a plain assignment and belongs in {@link setOrganization}. It is a
|
|
650
|
+
* separate method rather than a flag because each write on this port owns one
|
|
651
|
+
* audit verb, and this one keeps `customer_account.organization_unassigned` —
|
|
652
|
+
* the verb an operator's history already reads, now describing what really
|
|
653
|
+
* happened.
|
|
654
|
+
*/
|
|
655
|
+
detachToPersonalOrganization(customerAccountId: string, personalOrganizationId: string, input: {
|
|
656
|
+
actorAdminUserId: string;
|
|
657
|
+
}): Promise<CustomerAccountRecord>;
|
|
658
|
+
/** `null` clears the direct group; the organisation's own group still applies. */
|
|
659
|
+
setCustomerGroup(customerAccountId: string, customerGroupId: string | null, input: {
|
|
660
|
+
actorAdminUserId: string;
|
|
661
|
+
}): Promise<CustomerAccountRecord>;
|
|
662
|
+
/**
|
|
663
|
+
* Feature 055 — the admin custom-field patch, as a read-modify-write the
|
|
664
|
+
* owner runs inside one Command.
|
|
665
|
+
*
|
|
666
|
+
* The **merge is the caller's**, and it is a callback rather than a
|
|
667
|
+
* pre-merged bag for one reason: the bag has to be read, validated and
|
|
668
|
+
* written inside a single transaction or a concurrent patch is silently
|
|
669
|
+
* lost, and the validator is {@link CustomFieldValuePort}, which the admin
|
|
670
|
+
* surface owning this screen resolves. So the caller supplies the function
|
|
671
|
+
* over the current bag and the owner runs it between its own load and its
|
|
672
|
+
* own write — one transaction, one audit row, and the entity never leaves.
|
|
673
|
+
*
|
|
674
|
+
* `merge` may throw; `custom_fields`' validation failure propagates to the
|
|
675
|
+
* caller unchanged, which is what the admin surface turns into a 422.
|
|
676
|
+
*/
|
|
677
|
+
setCustomFieldValues(customerAccountId: string, merge: (current: Record<string, unknown>) => Promise<Record<string, unknown>>): Promise<CustomerAccountRecord>;
|
|
678
|
+
/**
|
|
679
|
+
* Soft-deleted accounts whose retention window elapsed before `cutoff` and
|
|
680
|
+
* that have not been anonymised yet — the sweep's work list.
|
|
681
|
+
*/
|
|
682
|
+
listDueForAnonymization(cutoff: Date): Promise<CustomerAccountRecord[]>;
|
|
683
|
+
/**
|
|
684
|
+
* Irreversibly scrubs the account's personal data. Returns the account
|
|
685
|
+
* unchanged when it was already anonymised, and throws HTTP 404
|
|
686
|
+
* `CUSTOMER_NOT_FOUND` for an id nothing matches.
|
|
687
|
+
*/
|
|
688
|
+
anonymize(customerAccountId: string): Promise<CustomerAccountRecord>;
|
|
689
|
+
}
|
|
690
|
+
//# sourceMappingURL=customer-accounts.d.ts.map
|