@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.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -0
  3. package/dist/admin/api/quick-order-client.d.ts +76 -0
  4. package/dist/admin/api/quick-order-client.d.ts.map +1 -0
  5. package/dist/admin/api/quick-order-client.js +18 -0
  6. package/dist/admin/api/quick-order-client.js.map +1 -0
  7. package/dist/admin/components/DefaultPreferencesPanel.d.ts +44 -0
  8. package/dist/admin/components/DefaultPreferencesPanel.d.ts.map +1 -0
  9. package/dist/admin/components/DefaultPreferencesPanel.js +107 -0
  10. package/dist/admin/components/DefaultPreferencesPanel.js.map +1 -0
  11. package/dist/admin/index.d.ts +48 -0
  12. package/dist/admin/index.d.ts.map +1 -0
  13. package/dist/admin/index.js +120 -0
  14. package/dist/admin/index.js.map +1 -0
  15. package/dist/admin/pages/QuickOrderOnBehalfPage.d.ts +16 -0
  16. package/dist/admin/pages/QuickOrderOnBehalfPage.d.ts.map +1 -0
  17. package/dist/admin/pages/QuickOrderOnBehalfPage.js +85 -0
  18. package/dist/admin/pages/QuickOrderOnBehalfPage.js.map +1 -0
  19. package/dist/admin/zones/CustomerDefaults.d.ts +27 -0
  20. package/dist/admin/zones/CustomerDefaults.d.ts.map +1 -0
  21. package/dist/admin/zones/CustomerDefaults.js +11 -0
  22. package/dist/admin/zones/CustomerDefaults.js.map +1 -0
  23. package/dist/admin/zones/OrderEntryQuickTab.d.ts +21 -0
  24. package/dist/admin/zones/OrderEntryQuickTab.d.ts.map +1 -0
  25. package/dist/admin/zones/OrderEntryQuickTab.js +26 -0
  26. package/dist/admin/zones/OrderEntryQuickTab.js.map +1 -0
  27. package/dist/admin/zones/OrganizationDefaults.d.ts +30 -0
  28. package/dist/admin/zones/OrganizationDefaults.d.ts.map +1 -0
  29. package/dist/admin/zones/OrganizationDefaults.js +11 -0
  30. package/dist/admin/zones/OrganizationDefaults.js.map +1 -0
  31. package/dist/backend/entities/quick-order-default-preference.entity.d.ts +22 -0
  32. package/dist/backend/entities/quick-order-default-preference.entity.d.ts.map +1 -0
  33. package/dist/backend/entities/quick-order-default-preference.entity.js +76 -0
  34. package/dist/backend/entities/quick-order-default-preference.entity.js.map +1 -0
  35. package/dist/backend/index.d.ts +88 -0
  36. package/dist/backend/index.d.ts.map +1 -0
  37. package/dist/backend/index.js +235 -0
  38. package/dist/backend/index.js.map +1 -0
  39. package/dist/backend/routes.admin.d.ts +35 -0
  40. package/dist/backend/routes.admin.d.ts.map +1 -0
  41. package/dist/backend/routes.admin.js +64 -0
  42. package/dist/backend/routes.admin.js.map +1 -0
  43. package/dist/backend/routes.d.ts +35 -0
  44. package/dist/backend/routes.d.ts.map +1 -0
  45. package/dist/backend/routes.js +77 -0
  46. package/dist/backend/routes.js.map +1 -0
  47. package/dist/backend/routes.one-click.d.ts +22 -0
  48. package/dist/backend/routes.one-click.d.ts.map +1 -0
  49. package/dist/backend/routes.one-click.js +35 -0
  50. package/dist/backend/routes.one-click.js.map +1 -0
  51. package/dist/backend/routes.preferences.admin.d.ts +23 -0
  52. package/dist/backend/routes.preferences.admin.d.ts.map +1 -0
  53. package/dist/backend/routes.preferences.admin.js +40 -0
  54. package/dist/backend/routes.preferences.admin.js.map +1 -0
  55. package/dist/backend/routes.preferences.d.ts +21 -0
  56. package/dist/backend/routes.preferences.d.ts.map +1 -0
  57. package/dist/backend/routes.preferences.js +42 -0
  58. package/dist/backend/routes.preferences.js.map +1 -0
  59. package/dist/backend/services/catalog-lookup.d.ts +39 -0
  60. package/dist/backend/services/catalog-lookup.d.ts.map +1 -0
  61. package/dist/backend/services/catalog-lookup.js +104 -0
  62. package/dist/backend/services/catalog-lookup.js.map +1 -0
  63. package/dist/backend/services/default-preference-authz.d.ts +31 -0
  64. package/dist/backend/services/default-preference-authz.d.ts.map +1 -0
  65. package/dist/backend/services/default-preference-authz.js +26 -0
  66. package/dist/backend/services/default-preference-authz.js.map +1 -0
  67. package/dist/backend/services/default-preference-resolver.d.ts +28 -0
  68. package/dist/backend/services/default-preference-resolver.d.ts.map +1 -0
  69. package/dist/backend/services/default-preference-resolver.js +27 -0
  70. package/dist/backend/services/default-preference-resolver.js.map +1 -0
  71. package/dist/backend/services/default-preference-service.d.ts +63 -0
  72. package/dist/backend/services/default-preference-service.d.ts.map +1 -0
  73. package/dist/backend/services/default-preference-service.js +176 -0
  74. package/dist/backend/services/default-preference-service.js.map +1 -0
  75. package/dist/backend/services/excel-importer.d.ts +11 -0
  76. package/dist/backend/services/excel-importer.d.ts.map +1 -0
  77. package/dist/backend/services/excel-importer.js +47 -0
  78. package/dist/backend/services/excel-importer.js.map +1 -0
  79. package/dist/backend/services/import-from-request.d.ts +9 -0
  80. package/dist/backend/services/import-from-request.d.ts.map +1 -0
  81. package/dist/backend/services/import-from-request.js +17 -0
  82. package/dist/backend/services/import-from-request.js.map +1 -0
  83. package/dist/backend/services/import-pipeline.d.ts +89 -0
  84. package/dist/backend/services/import-pipeline.d.ts.map +1 -0
  85. package/dist/backend/services/import-pipeline.js +188 -0
  86. package/dist/backend/services/import-pipeline.js.map +1 -0
  87. package/dist/backend/services/import-rows.d.ts +43 -0
  88. package/dist/backend/services/import-rows.d.ts.map +1 -0
  89. package/dist/backend/services/import-rows.js +102 -0
  90. package/dist/backend/services/import-rows.js.map +1 -0
  91. package/dist/backend/services/one-click-service.d.ts +50 -0
  92. package/dist/backend/services/one-click-service.d.ts.map +1 -0
  93. package/dist/backend/services/one-click-service.js +83 -0
  94. package/dist/backend/services/one-click-service.js.map +1 -0
  95. package/dist/backend/services/quick-order-build-service.d.ts +35 -0
  96. package/dist/backend/services/quick-order-build-service.d.ts.map +1 -0
  97. package/dist/backend/services/quick-order-build-service.js +61 -0
  98. package/dist/backend/services/quick-order-build-service.js.map +1 -0
  99. package/dist/backend/services/variant-resolver.d.ts +40 -0
  100. package/dist/backend/services/variant-resolver.d.ts.map +1 -0
  101. package/dist/backend/services/variant-resolver.js +47 -0
  102. package/dist/backend/services/variant-resolver.js.map +1 -0
  103. package/dist/manifest.d.ts +190 -0
  104. package/dist/manifest.d.ts.map +1 -0
  105. package/dist/manifest.js +129 -0
  106. package/dist/manifest.js.map +1 -0
  107. package/dist/migrations/20260611T140359_quick_order_default_preferences.d.ts +15 -0
  108. package/dist/migrations/20260611T140359_quick_order_default_preferences.d.ts.map +1 -0
  109. package/dist/migrations/20260611T140359_quick_order_default_preferences.js +53 -0
  110. package/dist/migrations/20260611T140359_quick_order_default_preferences.js.map +1 -0
  111. package/dist/migrations/index.d.ts +27 -0
  112. package/dist/migrations/index.d.ts.map +1 -0
  113. package/dist/migrations/index.js +29 -0
  114. package/dist/migrations/index.js.map +1 -0
  115. package/docs/quick_order.md +98 -0
  116. package/i18n/en.json +47 -0
  117. package/i18n/pl.json +47 -0
  118. package/package.json +91 -0
  119. 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"}