@endora-commerce/mod-quick-order 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 +58 -0
- package/dist/admin/api/quick-order-client.d.ts +76 -0
- package/dist/admin/api/quick-order-client.d.ts.map +1 -0
- package/dist/admin/api/quick-order-client.js +18 -0
- package/dist/admin/api/quick-order-client.js.map +1 -0
- package/dist/admin/components/DefaultPreferencesPanel.d.ts +44 -0
- package/dist/admin/components/DefaultPreferencesPanel.d.ts.map +1 -0
- package/dist/admin/components/DefaultPreferencesPanel.js +107 -0
- package/dist/admin/components/DefaultPreferencesPanel.js.map +1 -0
- package/dist/admin/index.d.ts +48 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +120 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/QuickOrderOnBehalfPage.d.ts +16 -0
- package/dist/admin/pages/QuickOrderOnBehalfPage.d.ts.map +1 -0
- package/dist/admin/pages/QuickOrderOnBehalfPage.js +85 -0
- package/dist/admin/pages/QuickOrderOnBehalfPage.js.map +1 -0
- package/dist/admin/zones/CustomerDefaults.d.ts +27 -0
- package/dist/admin/zones/CustomerDefaults.d.ts.map +1 -0
- package/dist/admin/zones/CustomerDefaults.js +11 -0
- package/dist/admin/zones/CustomerDefaults.js.map +1 -0
- package/dist/admin/zones/OrderEntryQuickTab.d.ts +21 -0
- package/dist/admin/zones/OrderEntryQuickTab.d.ts.map +1 -0
- package/dist/admin/zones/OrderEntryQuickTab.js +26 -0
- package/dist/admin/zones/OrderEntryQuickTab.js.map +1 -0
- package/dist/admin/zones/OrganizationDefaults.d.ts +30 -0
- package/dist/admin/zones/OrganizationDefaults.d.ts.map +1 -0
- package/dist/admin/zones/OrganizationDefaults.js +11 -0
- package/dist/admin/zones/OrganizationDefaults.js.map +1 -0
- package/dist/backend/entities/quick-order-default-preference.entity.d.ts +22 -0
- package/dist/backend/entities/quick-order-default-preference.entity.d.ts.map +1 -0
- package/dist/backend/entities/quick-order-default-preference.entity.js +76 -0
- package/dist/backend/entities/quick-order-default-preference.entity.js.map +1 -0
- package/dist/backend/index.d.ts +88 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +235 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +35 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +64 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.d.ts +35 -0
- package/dist/backend/routes.d.ts.map +1 -0
- package/dist/backend/routes.js +77 -0
- package/dist/backend/routes.js.map +1 -0
- package/dist/backend/routes.one-click.d.ts +22 -0
- package/dist/backend/routes.one-click.d.ts.map +1 -0
- package/dist/backend/routes.one-click.js +35 -0
- package/dist/backend/routes.one-click.js.map +1 -0
- package/dist/backend/routes.preferences.admin.d.ts +23 -0
- package/dist/backend/routes.preferences.admin.d.ts.map +1 -0
- package/dist/backend/routes.preferences.admin.js +40 -0
- package/dist/backend/routes.preferences.admin.js.map +1 -0
- package/dist/backend/routes.preferences.d.ts +21 -0
- package/dist/backend/routes.preferences.d.ts.map +1 -0
- package/dist/backend/routes.preferences.js +42 -0
- package/dist/backend/routes.preferences.js.map +1 -0
- package/dist/backend/services/catalog-lookup.d.ts +39 -0
- package/dist/backend/services/catalog-lookup.d.ts.map +1 -0
- package/dist/backend/services/catalog-lookup.js +104 -0
- package/dist/backend/services/catalog-lookup.js.map +1 -0
- package/dist/backend/services/default-preference-authz.d.ts +31 -0
- package/dist/backend/services/default-preference-authz.d.ts.map +1 -0
- package/dist/backend/services/default-preference-authz.js +26 -0
- package/dist/backend/services/default-preference-authz.js.map +1 -0
- package/dist/backend/services/default-preference-resolver.d.ts +28 -0
- package/dist/backend/services/default-preference-resolver.d.ts.map +1 -0
- package/dist/backend/services/default-preference-resolver.js +27 -0
- package/dist/backend/services/default-preference-resolver.js.map +1 -0
- package/dist/backend/services/default-preference-service.d.ts +63 -0
- package/dist/backend/services/default-preference-service.d.ts.map +1 -0
- package/dist/backend/services/default-preference-service.js +176 -0
- package/dist/backend/services/default-preference-service.js.map +1 -0
- package/dist/backend/services/excel-importer.d.ts +11 -0
- package/dist/backend/services/excel-importer.d.ts.map +1 -0
- package/dist/backend/services/excel-importer.js +47 -0
- package/dist/backend/services/excel-importer.js.map +1 -0
- package/dist/backend/services/import-from-request.d.ts +9 -0
- package/dist/backend/services/import-from-request.d.ts.map +1 -0
- package/dist/backend/services/import-from-request.js +17 -0
- package/dist/backend/services/import-from-request.js.map +1 -0
- package/dist/backend/services/import-pipeline.d.ts +89 -0
- package/dist/backend/services/import-pipeline.d.ts.map +1 -0
- package/dist/backend/services/import-pipeline.js +188 -0
- package/dist/backend/services/import-pipeline.js.map +1 -0
- package/dist/backend/services/import-rows.d.ts +43 -0
- package/dist/backend/services/import-rows.d.ts.map +1 -0
- package/dist/backend/services/import-rows.js +102 -0
- package/dist/backend/services/import-rows.js.map +1 -0
- package/dist/backend/services/one-click-service.d.ts +50 -0
- package/dist/backend/services/one-click-service.d.ts.map +1 -0
- package/dist/backend/services/one-click-service.js +83 -0
- package/dist/backend/services/one-click-service.js.map +1 -0
- package/dist/backend/services/quick-order-build-service.d.ts +35 -0
- package/dist/backend/services/quick-order-build-service.d.ts.map +1 -0
- package/dist/backend/services/quick-order-build-service.js +61 -0
- package/dist/backend/services/quick-order-build-service.js.map +1 -0
- package/dist/backend/services/variant-resolver.d.ts +40 -0
- package/dist/backend/services/variant-resolver.d.ts.map +1 -0
- package/dist/backend/services/variant-resolver.js +47 -0
- package/dist/backend/services/variant-resolver.js.map +1 -0
- package/dist/manifest.d.ts +190 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +129 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260611T140359_quick_order_default_preferences.d.ts +15 -0
- package/dist/migrations/20260611T140359_quick_order_default_preferences.d.ts.map +1 -0
- package/dist/migrations/20260611T140359_quick_order_default_preferences.js +53 -0
- package/dist/migrations/20260611T140359_quick_order_default_preferences.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/quick_order.md +98 -0
- package/i18n/en.json +47 -0
- package/i18n/pl.json +47 -0
- package/package.json +91 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { lazyPort } from '@endora-commerce/platform/kernel';
|
|
3
|
+
import { effectiveState } from '@endora-commerce/platform/kernel';
|
|
4
|
+
import { SettingNotRegistered, SettingOutOfScopeForChannel } from '@endora-commerce/platform/kernel';
|
|
5
|
+
import { QUICK_ORDER_SETTING_CODES, DEFAULT_IMPORT_MAX_ROWS, DEFAULT_ONE_CLICK_BUY_ENABLED, } from '../manifest.js';
|
|
6
|
+
import { CatalogPortLookup } from './services/catalog-lookup.js';
|
|
7
|
+
import { QuickOrderImportPipeline } from './services/import-pipeline.js';
|
|
8
|
+
import { QuickOrderBuildService } from './services/quick-order-build-service.js';
|
|
9
|
+
import { DefaultPreferenceService } from './services/default-preference-service.js';
|
|
10
|
+
import { OneClickService } from './services/one-click-service.js';
|
|
11
|
+
import { registerQuickOrderRoutes } from './routes.js';
|
|
12
|
+
import { registerQuickOrderAdminRoutes } from './routes.admin.js';
|
|
13
|
+
import { registerQuickOrderPreferenceRoutes } from './routes.preferences.js';
|
|
14
|
+
import { registerQuickOrderPreferenceAdminRoutes } from './routes.preferences.admin.js';
|
|
15
|
+
import { registerQuickOrderOneClickRoutes } from './routes.one-click.js';
|
|
16
|
+
import { QuickOrderDefaultPreference } from './entities/quick-order-default-preference.entity.js';
|
|
17
|
+
/**
|
|
18
|
+
* Conditions this module has already reported (D-43's warn-once). Module scope
|
|
19
|
+
* and never reset: the import cap is read on every upload, admin and customer.
|
|
20
|
+
*/
|
|
21
|
+
const warnedConditions = new Set();
|
|
22
|
+
function warnOnce(condition, message) {
|
|
23
|
+
if (warnedConditions.has(condition))
|
|
24
|
+
return;
|
|
25
|
+
warnedConditions.add(condition);
|
|
26
|
+
console.warn(message);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `customers` absent: the buyer has no personal addresses (issue #216).
|
|
30
|
+
*
|
|
31
|
+
* D-44 `degrades-without`, and the degrade is not invented for the occasion.
|
|
32
|
+
* Principle XVII says a switched-off module behaves as if never installed, and
|
|
33
|
+
* a platform on which `customers` was never installed has no
|
|
34
|
+
* `customer_addresses`: only the organisation's shared addresses are eligible
|
|
35
|
+
* defaults. That is exactly what `resolveForCustomer` already does with a
|
|
36
|
+
* default it finds ineligible (FR-020) — it drops the field to `null`, and
|
|
37
|
+
* one-click buy hides itself because not all four defaults resolve. A buyer
|
|
38
|
+
* whose defaults are org addresses is unaffected.
|
|
39
|
+
*
|
|
40
|
+
* Failing closed here instead would 503 the whole preference resolution — and
|
|
41
|
+
* with it one-click buy for *every* buyer — because the personal-address branch
|
|
42
|
+
* is probed on every eligibility check. Switching off a CRM surface must not do
|
|
43
|
+
* that.
|
|
44
|
+
*
|
|
45
|
+
* `customerAddressReadPort` is a **gated** port: a closed gate throws rather
|
|
46
|
+
* than answering `null`, so the presence probe comes *before* the resolution,
|
|
47
|
+
* and it is per call rather than per composition — the factory below is a
|
|
48
|
+
* singleton, and an operator flipping `customers` on must not need a restart.
|
|
49
|
+
*/
|
|
50
|
+
const ABSENT_CUSTOMER_ADDRESSES = {
|
|
51
|
+
findById: async () => null,
|
|
52
|
+
listForCustomer: async () => [],
|
|
53
|
+
};
|
|
54
|
+
export function customerAddressesOrAbsent(port) {
|
|
55
|
+
const pick = () => effectiveState.isPresent('customers') ? port : ABSENT_CUSTOMER_ADDRESSES;
|
|
56
|
+
return {
|
|
57
|
+
findById: (customerAccountId, addressId, options) => pick().findById(customerAccountId, addressId, options),
|
|
58
|
+
listForCustomer: (customerAccountId, kind) => pick().listForCustomer(customerAccountId, kind),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function registerModule(ctx) {
|
|
62
|
+
const cradle = () => ctx.cradle();
|
|
63
|
+
/**
|
|
64
|
+
* Feature 075, Phase P — the buyer's one-click defaults.
|
|
65
|
+
*
|
|
66
|
+
* `customers` renders and edits them from its own customer-detail screen, so
|
|
67
|
+
* the surface and the table sit in different modules by design.
|
|
68
|
+
* `resolveForCustomer` answers with the defaults **already resolved** —
|
|
69
|
+
* through the buyer's saved addresses and the platform's methods — rather
|
|
70
|
+
* than the stored row, because the fallback chain is this module's and a
|
|
71
|
+
* caller reproducing it would reproduce it differently.
|
|
72
|
+
*/
|
|
73
|
+
ctx.di.providePort('defaultPreferencePort', ctx
|
|
74
|
+
.asFunction(() => ({
|
|
75
|
+
resolveForCustomer: (customerAccountId) => cradle().quickOrderPreferenceService.resolveForCustomer(customerAccountId),
|
|
76
|
+
upsert: (actor, input, audit) => cradle().quickOrderPreferenceService.upsert(actor, input, audit),
|
|
77
|
+
}))
|
|
78
|
+
.singleton());
|
|
79
|
+
ctx.di.register({
|
|
80
|
+
quickOrderPipeline: ctx
|
|
81
|
+
.asFunction(() => new QuickOrderImportPipeline(new CatalogPortLookup(lazyPort(ctx, 'catalogProductReadPort'),
|
|
82
|
+
// Issue #259 — the channel assortment gate on the pasted-SKU
|
|
83
|
+
// seam. A kernel registration, so no module edge to declare.
|
|
84
|
+
lazyPort(ctx, 'salesChannelMembershipPort'))))
|
|
85
|
+
.singleton(),
|
|
86
|
+
quickOrderBuildService: ctx
|
|
87
|
+
.asFunction(() => new QuickOrderBuildService(
|
|
88
|
+
// Both are other modules' gated ports and both are stored by the
|
|
89
|
+
// constructor, so a singleton may not hold them directly — Awilix's
|
|
90
|
+
// strict mode refuses a transient inside a longer-lived object.
|
|
91
|
+
lazyPort(ctx, 'cartWritePort'), lazyPort(ctx, 'rfqService')))
|
|
92
|
+
.singleton(),
|
|
93
|
+
quickOrderPreferenceService: ctx
|
|
94
|
+
.asFunction(({ emFactory, auditLogService }) => new DefaultPreferenceService(emFactory, auditLogService, {
|
|
95
|
+
// Five gated ports and one degrade. Every one of these was an
|
|
96
|
+
// `em.findOne` against another module's entity until issue #216:
|
|
97
|
+
// deactivation drops no tables, so a stored default kept resolving
|
|
98
|
+
// out of a switched-off module's rows and one-click buy went on
|
|
99
|
+
// offering a payment method the platform had stopped serving.
|
|
100
|
+
//
|
|
101
|
+
// `payment_methods`, `delivery_methods`, `addresses`,
|
|
102
|
+
// `customer_accounts` and `organizations` are all binding
|
|
103
|
+
// dependencies of this module, so their gates fail closed: the
|
|
104
|
+
// resolution throws `ModuleDisabledError` and the call answers 503
|
|
105
|
+
// `MODULE_DISABLED` rather than resolving half a set of defaults.
|
|
106
|
+
customerAccounts: lazyPort(ctx, 'customerAccountReadPort'),
|
|
107
|
+
paymentMethods: lazyPort(ctx, 'paymentMethodReadPort'),
|
|
108
|
+
deliveryMethods: lazyPort(ctx, 'deliveryMethodReadPort'),
|
|
109
|
+
addresses: lazyPort(ctx, 'addressReadPort'),
|
|
110
|
+
customerAddresses: customerAddressesOrAbsent(lazyPort(ctx, 'customerAddressReadPort')),
|
|
111
|
+
restriction: lazyPort(ctx, 'organizationRestrictionPort'),
|
|
112
|
+
}))
|
|
113
|
+
.singleton(),
|
|
114
|
+
quickOrderOneClickService: ctx
|
|
115
|
+
.asFunction(({ quickOrderPreferenceService }) => new OneClickService(quickOrderPreferenceService, lazyPort(ctx, 'cartWritePort'), lazyPort(ctx, 'orderPlacementPort'),
|
|
116
|
+
// This module's own setting, read here rather than through a
|
|
117
|
+
// resolver a host passes down. The channel comes from the request
|
|
118
|
+
// (`routes.one-click.ts`), so `null` means there was none and the
|
|
119
|
+
// per-storefront value is read platform-wide (D-41 case c).
|
|
120
|
+
//
|
|
121
|
+
// The bare `catch { return false }` this replaces is what made
|
|
122
|
+
// issue #99 invisible: it swallowed `SettingsChannelIdInvalid` from
|
|
123
|
+
// the `'default'` code the service used to compile in, and reported
|
|
124
|
+
// the result as `setting_disabled`. It would swallow
|
|
125
|
+
// `ModuleDisabledError` just as happily (composition rule 7). Only
|
|
126
|
+
// the two conditions with a defined degrade are absorbed now, and
|
|
127
|
+
// both degrade to the manifest default rather than to a second
|
|
128
|
+
// literal beside it (D-43).
|
|
129
|
+
async (salesChannelId) => {
|
|
130
|
+
try {
|
|
131
|
+
return await cradle().settingsReadPort.get(QUICK_ORDER_SETTING_CODES.ONE_CLICK_BUY_ENABLED, salesChannelId, z.boolean());
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error instanceof SettingNotRegistered)
|
|
135
|
+
return DEFAULT_ONE_CLICK_BUY_ENABLED;
|
|
136
|
+
if (error instanceof SettingOutOfScopeForChannel) {
|
|
137
|
+
warnOnce(`out-of-scope:${QUICK_ORDER_SETTING_CODES.ONE_CLICK_BUY_ENABLED}`, `[quick_order] setting "${QUICK_ORDER_SETTING_CODES.ONE_CLICK_BUY_ENABLED}" ` +
|
|
138
|
+
`is scoped to specific sales channels, so it has no value for this read — ` +
|
|
139
|
+
`falling back to the manifest default (logged once per process).`);
|
|
140
|
+
return DEFAULT_ONE_CLICK_BUY_ENABLED;
|
|
141
|
+
}
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}))
|
|
145
|
+
.singleton(),
|
|
146
|
+
});
|
|
147
|
+
ctx.routes(async (app) => {
|
|
148
|
+
const { requireAdmin, quickOrderPipeline: pipeline, quickOrderBuildService: buildService, quickOrderPreferenceService: preferenceService, quickOrderOneClickService: oneClickService, } = cradle();
|
|
149
|
+
const requireCustomer = (req, reply) => cradle().requireCustomer(req, reply);
|
|
150
|
+
const resolveCustomerContext = (req) => cradle().customerContextResolver(req);
|
|
151
|
+
/**
|
|
152
|
+
* Read **platform-wide** (feature 072, D-41 case c). The manifest has
|
|
153
|
+
* always described this setting as "Global" — it is an upper bound on what
|
|
154
|
+
* one upload may cost the server, not a per-storefront policy — and the
|
|
155
|
+
* same value has to answer on the admin surface, where there is no
|
|
156
|
+
* storefront channel to resolve at all.
|
|
157
|
+
*
|
|
158
|
+
* Before T139 the host read it against a `settingsChannelId` option it
|
|
159
|
+
* defaulted to `'default'`, and fell back to the constant whenever a
|
|
160
|
+
* composition passed no settings service — which the harness did, so every
|
|
161
|
+
* test ran the manifest default while production read the configured value
|
|
162
|
+
* (T133's finding, one layer down). T139 lifted the read here and kept the
|
|
163
|
+
* `'default'` literal, which is a channel **code** against a `uuid` column:
|
|
164
|
+
* every read threw in the driver and the bare `catch` made it look like an
|
|
165
|
+
* unconfigured platform. The cap has therefore been the compiled-in 2000 on
|
|
166
|
+
* every deployment, whatever the operator set.
|
|
167
|
+
*/
|
|
168
|
+
const resolveImportMaxRows = async () => {
|
|
169
|
+
try {
|
|
170
|
+
return await cradle().settingsReadPort.get(QUICK_ORDER_SETTING_CODES.IMPORT_MAX_ROWS, null, z.number().int().positive());
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (error instanceof SettingNotRegistered)
|
|
174
|
+
return DEFAULT_IMPORT_MAX_ROWS;
|
|
175
|
+
if (error instanceof SettingOutOfScopeForChannel) {
|
|
176
|
+
warnOnce(`out-of-scope:${QUICK_ORDER_SETTING_CODES.IMPORT_MAX_ROWS}`, `[quick_order] setting "${QUICK_ORDER_SETTING_CODES.IMPORT_MAX_ROWS}" is ` +
|
|
177
|
+
`scoped to specific sales channels, so it has no platform-wide value — ` +
|
|
178
|
+
`falling back to the manifest default (logged once per process).`);
|
|
179
|
+
return DEFAULT_IMPORT_MAX_ROWS;
|
|
180
|
+
}
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
await registerQuickOrderRoutes(app, {
|
|
185
|
+
pipeline,
|
|
186
|
+
buildService,
|
|
187
|
+
requireCustomer,
|
|
188
|
+
resolveCustomerContext,
|
|
189
|
+
resolveImportMaxRows,
|
|
190
|
+
// Issue #174 — one port where three deps used to be, and the type-ahead
|
|
191
|
+
// is scoped to the request's sales channel because `catalog` scopes it.
|
|
192
|
+
catalogQuickSearch: lazyPort(ctx, 'catalogQuickSearchPort'),
|
|
193
|
+
});
|
|
194
|
+
await registerQuickOrderAdminRoutes(app, {
|
|
195
|
+
pipeline,
|
|
196
|
+
buildService,
|
|
197
|
+
requireAdmin,
|
|
198
|
+
resolveImportMaxRows,
|
|
199
|
+
customerAccounts: lazyPort(ctx, 'customerAccountReadPort'),
|
|
200
|
+
});
|
|
201
|
+
await registerQuickOrderPreferenceRoutes(app, {
|
|
202
|
+
service: preferenceService,
|
|
203
|
+
customerAccounts: lazyPort(ctx, 'customerAccountReadPort'),
|
|
204
|
+
requireCustomer,
|
|
205
|
+
resolveCustomerContext,
|
|
206
|
+
});
|
|
207
|
+
await registerQuickOrderPreferenceAdminRoutes(app, {
|
|
208
|
+
service: preferenceService,
|
|
209
|
+
customerAccounts: lazyPort(ctx, 'customerAccountReadPort'),
|
|
210
|
+
salesRepScope: lazyPort(ctx, 'organizationSalesRepScopePort'),
|
|
211
|
+
requireAdmin,
|
|
212
|
+
resolveAdminContext: (req) => cradle().adminContextResolver(req),
|
|
213
|
+
});
|
|
214
|
+
await registerQuickOrderOneClickRoutes(app, {
|
|
215
|
+
service: oneClickService,
|
|
216
|
+
requireCustomer,
|
|
217
|
+
resolveCustomerContext,
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The module's persisted entity classes, on the `./backend` subpath, as one
|
|
223
|
+
* array and **no named class export** (D-168).
|
|
224
|
+
*
|
|
225
|
+
* This is the shape the platform reads when the package is *installed*: the
|
|
226
|
+
* boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
|
|
227
|
+
* and the static declaration reader (`scripts/lib/package-declarations.ts`),
|
|
228
|
+
* which is the third source of `check:module-boundary`'s `table→owner` map and
|
|
229
|
+
* the package pass of `check-entity-tenant-classification`. A missing array is
|
|
230
|
+
* answered with `[]` — zero entities registered, no error anywhere.
|
|
231
|
+
*/
|
|
232
|
+
export const entities = [
|
|
233
|
+
QuickOrderDefaultPreference,
|
|
234
|
+
];
|
|
235
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import { type CustomerAccountReadPort } from '@endora-commerce/contracts';
|
|
3
|
+
import type { QuickOrderImportPipeline } from './services/import-pipeline.js';
|
|
4
|
+
import type { QuickOrderBuildService } from './services/quick-order-build-service.js';
|
|
5
|
+
import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
|
|
6
|
+
/**
|
|
7
|
+
* Admin quick-order routes (feature 039, FR-008). An operator imports a file
|
|
8
|
+
* and builds a Cart / Quote Request on behalf of a specific customer +
|
|
9
|
+
* organization (`onBehalfOf`), priced under that organization's price list.
|
|
10
|
+
* Guarded by the `orders:write` permission.
|
|
11
|
+
*
|
|
12
|
+
* **A permission is not a tenant gate.** `onBehalfOf` names the customer
|
|
13
|
+
* account and the organization the built Cart or Quote Request is written to,
|
|
14
|
+
* and it arrives in the request body. No filter can reach it: MikroORM applies
|
|
15
|
+
* a global filter to `SELECT`, `UPDATE` and `DELETE` and **not** to `INSERT`,
|
|
16
|
+
* so a route that takes its tenant identity from its own input writes wherever
|
|
17
|
+
* the input says. An assignment-scoped administrator (`allowed-set`) therefore
|
|
18
|
+
* has to be refused here or nowhere — see {@link assertOnBehalfOfInScope}.
|
|
19
|
+
*/
|
|
20
|
+
export interface QuickOrderAdminRoutesDeps {
|
|
21
|
+
pipeline: QuickOrderImportPipeline;
|
|
22
|
+
buildService: QuickOrderBuildService;
|
|
23
|
+
requireAdmin: RequireAdminFactory;
|
|
24
|
+
resolveImportMaxRows: () => Promise<number>;
|
|
25
|
+
/**
|
|
26
|
+
* `customer_accounts`' published read model — the tenant boundary of the
|
|
27
|
+
* build route and not a convenience. The account is `@OrgScoped`, so asking
|
|
28
|
+
* this port whether it exists *is* asking whether this caller may act for
|
|
29
|
+
* that customer. The module already declares the edge for its two preference
|
|
30
|
+
* surfaces (manifest `dependencies`).
|
|
31
|
+
*/
|
|
32
|
+
customerAccounts: CustomerAccountReadPort;
|
|
33
|
+
}
|
|
34
|
+
export declare function registerQuickOrderAdminRoutes(app: FastifyInstance, deps: QuickOrderAdminRoutesDeps): Promise<void>;
|
|
35
|
+
//# sourceMappingURL=routes.admin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.admin.d.ts","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAIL,KAAK,uBAAuB,EAC7B,MAAM,4BAA4B,CAAC;AAGpC,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAC9E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,yCAAyC,CAAC;AAEtF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,wBAAwB,CAAC;IACnC,YAAY,EAAE,sBAAsB,CAAC;IACrC,YAAY,EAAE,mBAAmB,CAAC;IAClC,oBAAoB,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C;;;;;;OAMG;IACH,gBAAgB,EAAE,uBAAuB,CAAC;CAC3C;AAED,wBAAsB,6BAA6B,CACjD,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,yBAAyB,GAC9B,OAAO,CAAC,IAAI,CAAC,CA6Ef"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ERROR_CODES, quickOrderBuildRequestSchema, quickOrderImportRequestSchema, } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
import { isOrgInScope } from '@endora-commerce/platform/tenancy';
|
|
4
|
+
import { parseImportRequest } from './services/import-from-request.js';
|
|
5
|
+
export async function registerQuickOrderAdminRoutes(app, deps) {
|
|
6
|
+
const { pipeline, buildService, requireAdmin, resolveImportMaxRows, customerAccounts } = deps;
|
|
7
|
+
/**
|
|
8
|
+
* Refuse an `onBehalfOf` the caller's organizations do not reach.
|
|
9
|
+
*
|
|
10
|
+
* Two questions, because the body answers two: the organization the row is
|
|
11
|
+
* stamped with must be one the caller may act in, and the customer account
|
|
12
|
+
* the row hangs off must be one the caller may reach. Passing an in-scope
|
|
13
|
+
* organization id beside a foreign customer account would otherwise satisfy
|
|
14
|
+
* the first on its own.
|
|
15
|
+
*
|
|
16
|
+
* 404 rather than 403, and the code an absent customer gets: an out-of-scope
|
|
17
|
+
* customer must not be distinguishable from one that is not there.
|
|
18
|
+
*/
|
|
19
|
+
const assertOnBehalfOfInScope = async (onBehalfOf) => {
|
|
20
|
+
const refuse = () => {
|
|
21
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Customer account not found.');
|
|
22
|
+
};
|
|
23
|
+
if (!isOrgInScope(onBehalfOf.organizationId))
|
|
24
|
+
refuse();
|
|
25
|
+
const account = await customerAccounts.findById(onBehalfOf.customerAccountId);
|
|
26
|
+
if (!account)
|
|
27
|
+
refuse();
|
|
28
|
+
};
|
|
29
|
+
app.post('/api/v1/admin/quick-order/import', { preHandler: requireAdmin('orders:write'), schema: { body: quickOrderImportRequestSchema } }, async (request) => {
|
|
30
|
+
const body = quickOrderImportRequestSchema.parse(request.body);
|
|
31
|
+
const maxRows = await resolveImportMaxRows();
|
|
32
|
+
const parse = await parseImportRequest(body);
|
|
33
|
+
// Issue #227 — `'unrestricted'`, and stated rather than omitted. This
|
|
34
|
+
// route is gated by `orders:write`, and the operator names the
|
|
35
|
+
// organisation on the *build* call one step later, so at import time
|
|
36
|
+
// there is no buyer whose allow-list this run could be scoped by.
|
|
37
|
+
const result = await pipeline.run(parse, { maxRows, audience: 'unrestricted' });
|
|
38
|
+
return { data: result };
|
|
39
|
+
});
|
|
40
|
+
app.post('/api/v1/admin/quick-order/build', { preHandler: requireAdmin('orders:write'), schema: { body: quickOrderBuildRequestSchema } }, async (request) => {
|
|
41
|
+
const body = quickOrderBuildRequestSchema.parse(request.body);
|
|
42
|
+
if (!body.onBehalfOf) {
|
|
43
|
+
throw new HttpError(422, ERROR_CODES.VALIDATION_FAILED, 'onBehalfOf is required for an admin quick-order build.');
|
|
44
|
+
}
|
|
45
|
+
await assertOnBehalfOfInScope({
|
|
46
|
+
customerAccountId: body.onBehalfOf.customerAccountId,
|
|
47
|
+
organizationId: body.onBehalfOf.organizationId,
|
|
48
|
+
});
|
|
49
|
+
const result = await buildService.build({
|
|
50
|
+
customerAccountId: body.onBehalfOf.customerAccountId,
|
|
51
|
+
organizationId: body.onBehalfOf.organizationId,
|
|
52
|
+
isOrgAdmin: false,
|
|
53
|
+
}, {
|
|
54
|
+
target: body.target,
|
|
55
|
+
items: body.items.map((item) => ({
|
|
56
|
+
productId: item.productId,
|
|
57
|
+
variantId: item.variantId ?? null,
|
|
58
|
+
quantity: item.quantity,
|
|
59
|
+
})),
|
|
60
|
+
});
|
|
61
|
+
return { data: result };
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=routes.admin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.admin.js","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
|
2
|
+
import { type CatalogQuickSearchPort } from '@endora-commerce/contracts';
|
|
3
|
+
import type { QuickOrderImportPipeline } from './services/import-pipeline.js';
|
|
4
|
+
import type { QuickOrderBuildService } from './services/quick-order-build-service.js';
|
|
5
|
+
/**
|
|
6
|
+
* Quick-order routes (feature 039).
|
|
7
|
+
* - POST /quick-order/import parses CSV / .xlsx → recognised + rejected rows
|
|
8
|
+
* - POST /quick-order/build recognised rows → Cart or Quote Request
|
|
9
|
+
* - GET /quick-order/search type-ahead over the catalogue, in the channel
|
|
10
|
+
* the request resolved
|
|
11
|
+
*
|
|
12
|
+
* Requires an authenticated customer session: SKU-to-product disclosure and
|
|
13
|
+
* cart / RFQ mutation are gated to logged-in buyers.
|
|
14
|
+
*/
|
|
15
|
+
export interface QuickOrderRoutesDeps {
|
|
16
|
+
pipeline: QuickOrderImportPipeline;
|
|
17
|
+
buildService: QuickOrderBuildService;
|
|
18
|
+
requireCustomer: (req: FastifyRequest, reply: unknown) => Promise<void>;
|
|
19
|
+
resolveCustomerContext: (req: FastifyRequest) => {
|
|
20
|
+
customerAccountId: string;
|
|
21
|
+
organizationId: string;
|
|
22
|
+
};
|
|
23
|
+
/** Reads the `quick_order.import_max_rows` setting (falls back internally). */
|
|
24
|
+
resolveImportMaxRows: () => Promise<number>;
|
|
25
|
+
/**
|
|
26
|
+
* `catalog`'s channel-scoped type-ahead (issue #174). It replaces three
|
|
27
|
+
* things at once: the `emFactory` this file used to open a knex query with,
|
|
28
|
+
* the attribute read it used to source `quickSearchable` keys from, and the
|
|
29
|
+
* product read it used to hydrate the ids that query returned. The predicate
|
|
30
|
+
* and the `matchedOn` labels belong to the module that owns the tables.
|
|
31
|
+
*/
|
|
32
|
+
catalogQuickSearch: CatalogQuickSearchPort;
|
|
33
|
+
}
|
|
34
|
+
export declare function registerQuickOrderRoutes(app: FastifyInstance, deps: QuickOrderRoutesDeps): Promise<void>;
|
|
35
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAIL,KAAK,sBAAsB,EAC5B,MAAM,4BAA4B,CAAC;AAGpC,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAC9E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,yCAAyC,CAAC;AAGtF;;;;;;;;;GASG;AAEH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,wBAAwB,CAAC;IACnC,YAAY,EAAE,sBAAsB,CAAC;IACrC,eAAe,EAAE,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,sBAAsB,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK;QAC/C,iBAAiB,EAAE,MAAM,CAAC;QAC1B,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,+EAA+E;IAC/E,oBAAoB,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C;;;;;;OAMG;IACH,kBAAkB,EAAE,sBAAsB,CAAC;CAC5C;AAED,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,oBAAoB,GACzB,OAAO,CAAC,IAAI,CAAC,CAuFf"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { quickOrderBuildRequestSchema, quickOrderImportRequestSchema, quickOrderSearchQuerySchema, } from '@endora-commerce/contracts';
|
|
2
|
+
import { getResolvedChannel } from '@endora-commerce/platform/kernel';
|
|
3
|
+
import { productAudienceOf } from '@endora-commerce/platform/http';
|
|
4
|
+
import { parseImportRequest } from './services/import-from-request.js';
|
|
5
|
+
export async function registerQuickOrderRoutes(app, deps) {
|
|
6
|
+
const { pipeline, buildService, requireCustomer, resolveCustomerContext } = deps;
|
|
7
|
+
app.post('/api/v1/quick-order/import', { preHandler: requireCustomer, schema: { body: quickOrderImportRequestSchema } }, async (request) => {
|
|
8
|
+
const body = quickOrderImportRequestSchema.parse(request.body);
|
|
9
|
+
const maxRows = await deps.resolveImportMaxRows();
|
|
10
|
+
const parse = await parseImportRequest(body);
|
|
11
|
+
// Issue #227 — the audience is the buyer's own, off the session this
|
|
12
|
+
// route already requires. The type-ahead below has had it since issue
|
|
13
|
+
// #174; the pasted-SKU path next to it had not, and a SKU list is the
|
|
14
|
+
// easier of the two to enumerate.
|
|
15
|
+
const result = await pipeline.run(parse, {
|
|
16
|
+
maxRows,
|
|
17
|
+
audience: productAudienceOf(request),
|
|
18
|
+
});
|
|
19
|
+
return { data: result };
|
|
20
|
+
});
|
|
21
|
+
app.post('/api/v1/quick-order/build', { preHandler: requireCustomer, schema: { body: quickOrderBuildRequestSchema } }, async (request) => {
|
|
22
|
+
const body = quickOrderBuildRequestSchema.parse(request.body);
|
|
23
|
+
const ctx = resolveCustomerContext(request);
|
|
24
|
+
const result = await buildService.build({ customerAccountId: ctx.customerAccountId, organizationId: ctx.organizationId }, {
|
|
25
|
+
target: body.target,
|
|
26
|
+
items: body.items.map((item) => ({
|
|
27
|
+
productId: item.productId,
|
|
28
|
+
variantId: item.variantId ?? null,
|
|
29
|
+
quantity: item.quantity,
|
|
30
|
+
})),
|
|
31
|
+
});
|
|
32
|
+
return { data: result };
|
|
33
|
+
});
|
|
34
|
+
app.get('/api/v1/quick-order/search', { preHandler: requireCustomer }, async (request) => {
|
|
35
|
+
const query = quickOrderSearchQuerySchema.parse(request.query ?? {});
|
|
36
|
+
/**
|
|
37
|
+
* Issue #174 — the type-ahead asks `catalog`, and asks it *in a channel*.
|
|
38
|
+
*
|
|
39
|
+
* This used to be a hand-written knex `select` against `catalog`'s
|
|
40
|
+
* `products` table, reaching into its `attribute_values` JSONB and
|
|
41
|
+
* filtering on `status = 'active'` and nothing else. Two costs. The edge
|
|
42
|
+
* had no import specifier, so `check:module-boundary` could not see it and
|
|
43
|
+
* no ledger shard could key it — trap 6's "strictly worse than an import".
|
|
44
|
+
* And with no channel predicate, a signed-in buyer's type-ahead returned
|
|
45
|
+
* every active product on the platform, whichever channel they were
|
|
46
|
+
* shopping: Constitution XII, in the one place no static check was looking.
|
|
47
|
+
*
|
|
48
|
+
* `catalogQuickSearchPort` owns the predicate now, including which
|
|
49
|
+
* attributes are `quickSearchable` and which `matchedOn` labels a hit
|
|
50
|
+
* carries — both are facts about `catalog`'s own tables. What is left here
|
|
51
|
+
* is the buyer's language pick, which is this surface's business.
|
|
52
|
+
*
|
|
53
|
+
* The channel is the one the canonical resolver already put on the request
|
|
54
|
+
* scope (feature 053 / Constitution XII), never re-resolved from a header
|
|
55
|
+
* here. The organisation comes off the buyer's session for the same reason
|
|
56
|
+
* — it decides which restricted products `catalog` will disclose, and this
|
|
57
|
+
* surface is the one that knows who is asking.
|
|
58
|
+
*/
|
|
59
|
+
const hits = await deps.catalogQuickSearch.quickSearch({
|
|
60
|
+
q: query.q,
|
|
61
|
+
limit: query.limit,
|
|
62
|
+
salesChannelId: getResolvedChannel(request).id,
|
|
63
|
+
organizationId: resolveCustomerContext(request).organizationId,
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
data: hits.map((hit) => ({
|
|
67
|
+
productId: hit.productId,
|
|
68
|
+
sku: hit.sku,
|
|
69
|
+
name: hit.name['en-US'] ?? Object.values(hit.name)[0] ?? hit.sku,
|
|
70
|
+
slug: hit.slug,
|
|
71
|
+
status: hit.status,
|
|
72
|
+
matchedOn: hit.matchedOn,
|
|
73
|
+
})),
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
|
2
|
+
import type { OneClickService } from './services/one-click-service.js';
|
|
3
|
+
/**
|
|
4
|
+
* One-click buy routes (feature 039, US5).
|
|
5
|
+
* - GET /quick-order/one-click/eligibility should the PDP button show?
|
|
6
|
+
* - POST /quick-order/one-click place the order from defaults
|
|
7
|
+
*
|
|
8
|
+
* Both read the channel the canonical resolver already put on the request scope
|
|
9
|
+
* (Constitution XII) and hand it to the service. Before issue #99 the service
|
|
10
|
+
* carried a `'default'` constructor default instead — a channel code where an id
|
|
11
|
+
* was wanted — and the storefront request's real channel was never consulted.
|
|
12
|
+
*/
|
|
13
|
+
export interface QuickOrderOneClickRoutesDeps {
|
|
14
|
+
service: OneClickService;
|
|
15
|
+
requireCustomer: (req: FastifyRequest, reply: unknown) => Promise<void>;
|
|
16
|
+
resolveCustomerContext: (req: FastifyRequest) => {
|
|
17
|
+
customerAccountId: string;
|
|
18
|
+
organizationId: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export declare function registerQuickOrderOneClickRoutes(app: FastifyInstance, deps: QuickOrderOneClickRoutesDeps): Promise<void>;
|
|
22
|
+
//# sourceMappingURL=routes.one-click.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.one-click.d.ts","sourceRoot":"","sources":["../../src/backend/routes.one-click.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAG/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAEvE;;;;;;;;;GASG;AACH,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,eAAe,CAAC;IACzB,eAAe,EAAE,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,sBAAsB,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK;QAC/C,iBAAiB,EAAE,MAAM,CAAC;QAC1B,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAED,wBAAsB,gCAAgC,CACpD,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,4BAA4B,GACjC,OAAO,CAAC,IAAI,CAAC,CA6Cf"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { quickOrderOneClickRequestSchema } from '@endora-commerce/contracts';
|
|
2
|
+
import { currentSalesChannel } from '@endora-commerce/platform/kernel';
|
|
3
|
+
export async function registerQuickOrderOneClickRoutes(app, deps) {
|
|
4
|
+
const { service, requireCustomer, resolveCustomerContext } = deps;
|
|
5
|
+
app.get('/api/v1/quick-order/one-click/eligibility', { preHandler: requireCustomer }, async (request) => {
|
|
6
|
+
const ctx = resolveCustomerContext(request);
|
|
7
|
+
return {
|
|
8
|
+
data: await service.eligibility(ctx.customerAccountId, currentSalesChannel()?.id ?? null),
|
|
9
|
+
};
|
|
10
|
+
});
|
|
11
|
+
app.post('/api/v1/quick-order/one-click', { preHandler: requireCustomer, schema: { body: quickOrderOneClickRequestSchema } }, async (request, reply) => {
|
|
12
|
+
const body = quickOrderOneClickRequestSchema.parse(request.body);
|
|
13
|
+
const ctx = resolveCustomerContext(request);
|
|
14
|
+
const order = await service.place(ctx, {
|
|
15
|
+
productId: body.productId,
|
|
16
|
+
variantId: body.variantId ?? null,
|
|
17
|
+
...(body.quantity ? { quantity: body.quantity } : {}),
|
|
18
|
+
...(body.idempotencyKey ? { idempotencyKey: body.idempotencyKey } : {}),
|
|
19
|
+
}, currentSalesChannel()?.id ?? null);
|
|
20
|
+
reply.status(201);
|
|
21
|
+
return {
|
|
22
|
+
data: {
|
|
23
|
+
order: {
|
|
24
|
+
id: order.id,
|
|
25
|
+
businessId: order.businessId,
|
|
26
|
+
status: order.status,
|
|
27
|
+
total: Number(order.total),
|
|
28
|
+
currency: order.currency,
|
|
29
|
+
},
|
|
30
|
+
nextAction: order.nextAction ?? null,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=routes.one-click.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.one-click.js","sourceRoot":"","sources":["../../src/backend/routes.one-click.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,+BAA+B,EAAE,MAAM,4BAA4B,CAAC;AAC7E,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAsBvE,MAAM,CAAC,KAAK,UAAU,gCAAgC,CACpD,GAAoB,EACpB,IAAkC;IAElC,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,sBAAsB,EAAE,GAAG,IAAI,CAAC;IAElE,GAAG,CAAC,GAAG,CACL,2CAA2C,EAC3C,EAAE,UAAU,EAAE,eAAe,EAAE,EAC/B,KAAK,EAAE,OAAO,EAAE,EAAE;QAChB,MAAM,GAAG,GAAG,sBAAsB,CAAC,OAAO,CAAC,CAAC;QAC5C,OAAO;YACL,IAAI,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,iBAAiB,EAAE,mBAAmB,EAAE,EAAE,EAAE,IAAI,IAAI,CAAC;SAC1F,CAAC;IACJ,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,IAAI,CACN,+BAA+B,EAC/B,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,+BAA+B,EAAE,EAAE,EAClF,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACvB,MAAM,IAAI,GAAG,+BAA+B,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACjE,MAAM,GAAG,GAAG,sBAAsB,CAAC,OAAO,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,KAAK,CAC/B,GAAG,EACH;YACE,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;YACjC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACrD,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACxE,EACD,mBAAmB,EAAE,EAAE,EAAE,IAAI,IAAI,CAClC,CAAC;QACF,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAClB,OAAO;YACL,IAAI,EAAE;gBACJ,KAAK,EAAE;oBACL,EAAE,EAAE,KAAK,CAAC,EAAE;oBACZ,UAAU,EAAE,KAAK,CAAC,UAAU;oBAC5B,MAAM,EAAE,KAAK,CAAC,MAAM;oBACpB,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC;oBAC1B,QAAQ,EAAE,KAAK,CAAC,QAAQ;iBACzB;gBACD,UAAU,EAAE,KAAK,CAAC,UAAU,IAAI,IAAI;aACrC;SACF,CAAC;IACJ,CAAC,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
|
2
|
+
import { type CustomerAccountReadPort, type SalesRepAssignmentPort } from '@endora-commerce/contracts';
|
|
3
|
+
import type { DefaultPreferenceService } from './services/default-preference-service.js';
|
|
4
|
+
import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
|
|
5
|
+
/**
|
|
6
|
+
* Admin default-preferences routes (feature 039, FR-018). A platform admin
|
|
7
|
+
* (no sales-rep assignments) manages any scope; a salesperson manages only
|
|
8
|
+
* their assigned organizations and the customers within them. Guarded by
|
|
9
|
+
* `orders:write`.
|
|
10
|
+
*/
|
|
11
|
+
export interface QuickOrderPreferenceAdminRoutesDeps {
|
|
12
|
+
service: DefaultPreferenceService;
|
|
13
|
+
/** `customer_accounts`' read model — the target customer's organisation. */
|
|
14
|
+
customerAccounts: CustomerAccountReadPort;
|
|
15
|
+
/** `organizations`' sales-rep scope — which orgs this admin may act on. */
|
|
16
|
+
salesRepScope: SalesRepAssignmentPort;
|
|
17
|
+
requireAdmin: RequireAdminFactory;
|
|
18
|
+
resolveAdminContext: (req: FastifyRequest) => {
|
|
19
|
+
adminUserId: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export declare function registerQuickOrderPreferenceAdminRoutes(app: FastifyInstance, deps: QuickOrderPreferenceAdminRoutesDeps): Promise<void>;
|
|
23
|
+
//# sourceMappingURL=routes.preferences.admin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.preferences.admin.d.ts","sourceRoot":"","sources":["../../src/backend/routes.preferences.admin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAIL,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC5B,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,0CAA0C,CAAC;AAEzF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E;;;;;GAKG;AACH,MAAM,WAAW,mCAAmC;IAClD,OAAO,EAAE,wBAAwB,CAAC;IAClC,4EAA4E;IAC5E,gBAAgB,EAAE,uBAAuB,CAAC;IAC1C,2EAA2E;IAC3E,aAAa,EAAE,sBAAsB,CAAC;IACtC,YAAY,EAAE,mBAAmB,CAAC;IAClC,mBAAmB,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;CACvE;AAmBD,wBAAsB,uCAAuC,CAC3D,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,mCAAmC,GACxC,OAAO,CAAC,IAAI,CAAC,CAiCf"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ERROR_CODES, quickOrderPreferenceScopeSchema, quickOrderPreferenceUpsertSchema, } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
import { canManagePreference } from './services/default-preference-authz.js';
|
|
4
|
+
/**
|
|
5
|
+
* "No assignments" is what makes an admin a platform admin here, and the port
|
|
6
|
+
* answers exactly that question — `listAssignedOrganizationIds` replaces an
|
|
7
|
+
* `em.find(OrganizationSalesRepAssignment, …)` against a table `organizations`
|
|
8
|
+
* owns (feature 075, Phase C).
|
|
9
|
+
*/
|
|
10
|
+
async function adminActor(salesRepScope, adminUserId) {
|
|
11
|
+
const assignedOrganizationIds = await salesRepScope.listAssignedOrganizationIds(adminUserId);
|
|
12
|
+
if (assignedOrganizationIds.length > 0) {
|
|
13
|
+
return { kind: 'salesperson', assignedOrganizationIds };
|
|
14
|
+
}
|
|
15
|
+
return { kind: 'platform_admin' };
|
|
16
|
+
}
|
|
17
|
+
export async function registerQuickOrderPreferenceAdminRoutes(app, deps) {
|
|
18
|
+
const { service, customerAccounts, salesRepScope, requireAdmin, resolveAdminContext } = deps;
|
|
19
|
+
app.get('/api/v1/admin/quick-order/preferences', { preHandler: requireAdmin('orders:write') }, async (request) => {
|
|
20
|
+
const query = request.query;
|
|
21
|
+
const scope = quickOrderPreferenceScopeSchema.parse(query.scope);
|
|
22
|
+
const scopeId = query.scopeId ?? '';
|
|
23
|
+
const actor = await adminActor(salesRepScope, resolveAdminContext(request).adminUserId);
|
|
24
|
+
const targetCustomerOrgId = scope === 'customer'
|
|
25
|
+
? ((await customerAccounts.findById(scopeId))?.organizationId ?? null)
|
|
26
|
+
: null;
|
|
27
|
+
if (!canManagePreference(actor, { scope, scopeId }, targetCustomerOrgId)) {
|
|
28
|
+
throw new HttpError(403, ERROR_CODES.FORBIDDEN, 'Not allowed to read these defaults.');
|
|
29
|
+
}
|
|
30
|
+
return { data: await service.readRaw(scope, scopeId) };
|
|
31
|
+
});
|
|
32
|
+
app.put('/api/v1/admin/quick-order/preferences', { preHandler: requireAdmin('orders:write'), schema: { body: quickOrderPreferenceUpsertSchema } }, async (request) => {
|
|
33
|
+
const body = quickOrderPreferenceUpsertSchema.parse(request.body);
|
|
34
|
+
const ctx = resolveAdminContext(request);
|
|
35
|
+
const actor = await adminActor(salesRepScope, ctx.adminUserId);
|
|
36
|
+
const result = await service.upsert(actor, body, { actorAdminUserId: ctx.adminUserId });
|
|
37
|
+
return { data: result };
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=routes.preferences.admin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.preferences.admin.js","sourceRoot":"","sources":["../../src/backend/routes.preferences.admin.ts"],"names":[],"mappings":"AACA,OAAO,EACL,WAAW,EACX,+BAA+B,EAC/B,gCAAgC,GAGjC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAE3D,OAAO,EAAE,mBAAmB,EAAwB,MAAM,wCAAwC,CAAC;AAmBnG;;;;;GAKG;AACH,KAAK,UAAU,UAAU,CACvB,aAAqC,EACrC,WAAmB;IAEnB,MAAM,uBAAuB,GAAG,MAAM,aAAa,CAAC,2BAA2B,CAAC,WAAW,CAAC,CAAC;IAC7F,IAAI,uBAAuB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvC,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,uBAAuB,EAAE,CAAC;IAC1D,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAC;AACpC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,uCAAuC,CAC3D,GAAoB,EACpB,IAAyC;IAEzC,MAAM,EAAE,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,YAAY,EAAE,mBAAmB,EAAE,GAAG,IAAI,CAAC;IAE7F,GAAG,CAAC,GAAG,CACL,uCAAuC,EACvC,EAAE,UAAU,EAAE,YAAY,CAAC,cAAc,CAAC,EAAE,EAC5C,KAAK,EAAE,OAAO,EAAE,EAAE;QAChB,MAAM,KAAK,GAAG,OAAO,CAAC,KAA6C,CAAC;QACpE,MAAM,KAAK,GAAG,+BAA+B,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACjE,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,aAAa,EAAE,mBAAmB,CAAC,OAAO,CAAC,CAAC,WAAW,CAAC,CAAC;QACxF,MAAM,mBAAmB,GACvB,KAAK,KAAK,UAAU;YAClB,CAAC,CAAC,CAAC,CAAC,MAAM,gBAAgB,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,cAAc,IAAI,IAAI,CAAC;YACtE,CAAC,CAAC,IAAI,CAAC;QACX,IAAI,CAAC,mBAAmB,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,mBAAmB,CAAC,EAAE,CAAC;YACzE,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,SAAS,EAAE,qCAAqC,CAAC,CAAC;QACzF,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;IACzD,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,GAAG,CACL,uCAAuC,EACvC,EAAE,UAAU,EAAE,YAAY,CAAC,cAAc,CAAC,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,gCAAgC,EAAE,EAAE,EAChG,KAAK,EAAE,OAAO,EAAE,EAAE;QAChB,MAAM,IAAI,GAAG,gCAAgC,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAClE,MAAM,GAAG,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;QACzC,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,aAAa,EAAE,GAAG,CAAC,WAAW,CAAC,CAAC;QAC/D,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,gBAAgB,EAAE,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;QACxF,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC1B,CAAC,CACF,CAAC;AACJ,CAAC"}
|