@endora-commerce/mod-sales-channels 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 (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +67 -0
  3. package/dist/admin/api/sales-channels-client.d.ts +69 -0
  4. package/dist/admin/api/sales-channels-client.d.ts.map +1 -0
  5. package/dist/admin/api/sales-channels-client.js +74 -0
  6. package/dist/admin/api/sales-channels-client.js.map +1 -0
  7. package/dist/admin/components/ChannelIdentityForm.d.ts +24 -0
  8. package/dist/admin/components/ChannelIdentityForm.d.ts.map +1 -0
  9. package/dist/admin/components/ChannelIdentityForm.js +147 -0
  10. package/dist/admin/components/ChannelIdentityForm.js.map +1 -0
  11. package/dist/admin/components/DefaultChannelBadge.d.ts +21 -0
  12. package/dist/admin/components/DefaultChannelBadge.d.ts.map +1 -0
  13. package/dist/admin/components/DefaultChannelBadge.js +26 -0
  14. package/dist/admin/components/DefaultChannelBadge.js.map +1 -0
  15. package/dist/admin/components/EntityChannelMembership.d.ts +52 -0
  16. package/dist/admin/components/EntityChannelMembership.d.ts.map +1 -0
  17. package/dist/admin/components/EntityChannelMembership.js +98 -0
  18. package/dist/admin/components/EntityChannelMembership.js.map +1 -0
  19. package/dist/admin/index.d.ts +44 -0
  20. package/dist/admin/index.d.ts.map +1 -0
  21. package/dist/admin/index.js +148 -0
  22. package/dist/admin/index.js.map +1 -0
  23. package/dist/admin/pages/SalesChannelEditPage.d.ts +25 -0
  24. package/dist/admin/pages/SalesChannelEditPage.d.ts.map +1 -0
  25. package/dist/admin/pages/SalesChannelEditPage.js +185 -0
  26. package/dist/admin/pages/SalesChannelEditPage.js.map +1 -0
  27. package/dist/admin/pages/SalesChannelsListPage.d.ts +25 -0
  28. package/dist/admin/pages/SalesChannelsListPage.d.ts.map +1 -0
  29. package/dist/admin/pages/SalesChannelsListPage.js +91 -0
  30. package/dist/admin/pages/SalesChannelsListPage.js.map +1 -0
  31. package/dist/admin/zones/OrganizationChannelMembership.d.ts +34 -0
  32. package/dist/admin/zones/OrganizationChannelMembership.d.ts.map +1 -0
  33. package/dist/admin/zones/OrganizationChannelMembership.js +11 -0
  34. package/dist/admin/zones/OrganizationChannelMembership.js.map +1 -0
  35. package/dist/admin/zones/ProductChannelMembership.d.ts +23 -0
  36. package/dist/admin/zones/ProductChannelMembership.d.ts.map +1 -0
  37. package/dist/admin/zones/ProductChannelMembership.js +11 -0
  38. package/dist/admin/zones/ProductChannelMembership.js.map +1 -0
  39. package/dist/backend/commands/set-default.command.d.ts +53 -0
  40. package/dist/backend/commands/set-default.command.d.ts.map +1 -0
  41. package/dist/backend/commands/set-default.command.js +87 -0
  42. package/dist/backend/commands/set-default.command.js.map +1 -0
  43. package/dist/backend/index.d.ts +71 -0
  44. package/dist/backend/index.d.ts.map +1 -0
  45. package/dist/backend/index.js +81 -0
  46. package/dist/backend/index.js.map +1 -0
  47. package/dist/backend/routes.admin.d.ts +12 -0
  48. package/dist/backend/routes.admin.d.ts.map +1 -0
  49. package/dist/backend/routes.admin.js +302 -0
  50. package/dist/backend/routes.admin.js.map +1 -0
  51. package/dist/backend/routes.storefront.d.ts +36 -0
  52. package/dist/backend/routes.storefront.d.ts.map +1 -0
  53. package/dist/backend/routes.storefront.js +58 -0
  54. package/dist/backend/routes.storefront.js.map +1 -0
  55. package/dist/backend/services/index.d.ts +2 -0
  56. package/dist/backend/services/index.d.ts.map +1 -0
  57. package/dist/backend/services/index.js +2 -0
  58. package/dist/backend/services/index.js.map +1 -0
  59. package/dist/backend/services/sales-channel-attribution-registry.d.ts +28 -0
  60. package/dist/backend/services/sales-channel-attribution-registry.d.ts.map +1 -0
  61. package/dist/backend/services/sales-channel-attribution-registry.js +50 -0
  62. package/dist/backend/services/sales-channel-attribution-registry.js.map +1 -0
  63. package/dist/backend/services/sales-channels.service.d.ts +247 -0
  64. package/dist/backend/services/sales-channels.service.d.ts.map +1 -0
  65. package/dist/backend/services/sales-channels.service.js +535 -0
  66. package/dist/backend/services/sales-channels.service.js.map +1 -0
  67. package/dist/manifest.d.ts +185 -0
  68. package/dist/manifest.d.ts.map +1 -0
  69. package/dist/manifest.js +204 -0
  70. package/dist/manifest.js.map +1 -0
  71. package/docs/sales_channels/admin-usage.md +77 -0
  72. package/docs/sales_channels/developer-guide.md +107 -0
  73. package/docs/sales_channels/index.md +95 -0
  74. package/i18n/en.json +75 -0
  75. package/i18n/pl.json +75 -0
  76. package/package.json +88 -0
  77. package/tailwind.css +14 -0
@@ -0,0 +1,302 @@
1
+ import { ChannelMemberEntityTypeSchema, ERROR_CODES, SalesChannelCreateBodySchema, SalesChannelSetDefaultBodySchema, SalesChannelUpdateBodySchema, } from '@endora-commerce/contracts';
2
+ import { z } from 'zod';
3
+ import { HttpError } from '@endora-commerce/platform/http';
4
+ /**
5
+ * Admin HTTP surface — feature 005 / US2 (T034).
6
+ *
7
+ * Mounts under `/api/v1/admin/sales-channels/*`:
8
+ * - GET / — list (sales_channels:read)
9
+ * - GET /:code — detail (sales_channels:read)
10
+ * - POST / — create (sales_channels:write)
11
+ * - PATCH /:code — update (sales_channels:write); optimistic-
12
+ * concurrency via the body's `expectedVersion`
13
+ * field (matches feature 003/004 pattern; the
14
+ * `If-Match` header in the contract markdown
15
+ * is sugar that drops here later).
16
+ * - POST /:code/deactivate — deactivate (sales_channels:write)
17
+ * - POST /:code/activate — activate (sales_channels:write)
18
+ * - POST /:code/set-default — move the system-default flag here
19
+ * (sales_channels:write); a Command, so the
20
+ * move is audited and the demote/promote pair
21
+ * is one transaction (D-51).
22
+ * - DELETE /:code — hard delete (sales_channels:write); accepts
23
+ * `?fallbackToDefault=true` to rebind orphan
24
+ * members in the same transaction.
25
+ *
26
+ * Permission codes are placeholders that match the existing settings /
27
+ * admin-roles vocabulary; the constitution authority gate (Principle II)
28
+ * is upheld by the `requireAdmin(<permission>)` factory injected by the
29
+ * composition root.
30
+ */
31
+ const SC_READ = 'sales_channels:read';
32
+ const SC_WRITE = 'sales_channels:write';
33
+ const ListQuerySchema = z.object({
34
+ page: z.coerce.number().int().nonnegative().optional(),
35
+ pageSize: z.coerce.number().int().positive().max(200).optional(),
36
+ activeOnly: z
37
+ .union([z.boolean(), z.enum(['true', 'false'])])
38
+ .transform((v) => (typeof v === 'string' ? v === 'true' : v))
39
+ .optional(),
40
+ });
41
+ const DeleteQuerySchema = z.object({
42
+ fallbackToDefault: z
43
+ .union([z.boolean(), z.enum(['true', 'false'])])
44
+ .transform((v) => (typeof v === 'string' ? v === 'true' : v))
45
+ .optional(),
46
+ });
47
+ /**
48
+ * The PATCH body's optimistic-concurrency token rides alongside the
49
+ * partial identity fields. We pull out `expectedVersion` and parse the
50
+ * rest with the contract's update schema so the cross-field
51
+ * `defaultLanguage ∈ languages` rule still applies (the contract's
52
+ * superRefine cannot be `extended()`).
53
+ */
54
+ const ExpectedVersionShape = z.object({
55
+ expectedVersion: z.number().int().positive(),
56
+ });
57
+ export async function registerSalesChannelsAdminRoutes(app, deps) {
58
+ const { salesChannelsService, membershipService, requireAdmin, resolveAdminAuditContext, } = deps;
59
+ const auditContext = (request) => {
60
+ const ctx = resolveAdminAuditContext?.(request) ?? { actorAdminUserId: null };
61
+ const rid = request.headers['x-request-id'];
62
+ return {
63
+ ...ctx,
64
+ requestId: typeof rid === 'string' ? rid : null,
65
+ };
66
+ };
67
+ function setEtag(reply, channel) {
68
+ reply.header('etag', `W/"${channel.id}:${channel.version}"`);
69
+ }
70
+ function serializeSummary(c) {
71
+ return {
72
+ id: c.id,
73
+ code: c.code,
74
+ name: c.name,
75
+ active: c.active,
76
+ systemDefault: c.systemDefault,
77
+ defaultLanguage: c.defaultLanguage,
78
+ defaultCurrency: c.defaultCurrency,
79
+ themeCode: c.themeCode ?? null,
80
+ logoAssetId: c.logoAssetId ?? null,
81
+ version: c.version,
82
+ createdAt: c.createdAt.toISOString(),
83
+ updatedAt: c.updatedAt.toISOString(),
84
+ };
85
+ }
86
+ function serializeDetail(c) {
87
+ return {
88
+ ...serializeSummary(c),
89
+ languages: c.languages,
90
+ currencies: c.currencies,
91
+ // logoUrl resolved through the assets module is left null here for
92
+ // the initial v1; T038's admin client just reads logoAssetId and
93
+ // resolves the URL via the existing assets endpoint. A follow-up
94
+ // can fold the resolution server-side if storefront use grows.
95
+ logoUrl: null,
96
+ };
97
+ }
98
+ // -- List -----------------------------------------------------------------
99
+ app.get('/api/v1/admin/sales-channels', { preHandler: requireAdmin(SC_READ) }, async (request) => {
100
+ const q = ListQuerySchema.parse(request.query ?? {});
101
+ const result = await salesChannelsService.list({
102
+ ...(q.page !== undefined ? { page: q.page } : {}),
103
+ ...(q.pageSize !== undefined ? { pageSize: q.pageSize } : {}),
104
+ ...(q.activeOnly !== undefined ? { activeOnly: q.activeOnly } : {}),
105
+ });
106
+ return {
107
+ items: result.items.map(serializeSummary),
108
+ page: result.page,
109
+ pageSize: result.pageSize,
110
+ total: result.total,
111
+ };
112
+ });
113
+ // -- Detail ---------------------------------------------------------------
114
+ app.get('/api/v1/admin/sales-channels/:code', { preHandler: requireAdmin(SC_READ) }, async (request, reply) => {
115
+ const channel = await salesChannelsService.getByCode(request.params.code);
116
+ if (channel === null) {
117
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, `Sales channel "${request.params.code}" was not found.`);
118
+ }
119
+ setEtag(reply, channel);
120
+ return serializeDetail(channel);
121
+ });
122
+ // -- Create ---------------------------------------------------------------
123
+ app.post('/api/v1/admin/sales-channels', { preHandler: requireAdmin(SC_WRITE) }, async (request, reply) => {
124
+ const body = SalesChannelCreateBodySchema.parse(request.body);
125
+ const channel = await salesChannelsService.create(body, auditContext(request));
126
+ reply.code(201);
127
+ setEtag(reply, channel);
128
+ return serializeDetail(channel);
129
+ });
130
+ // -- Update ---------------------------------------------------------------
131
+ app.patch('/api/v1/admin/sales-channels/:code', { preHandler: requireAdmin(SC_WRITE) }, async (request, reply) => {
132
+ const raw = (request.body ?? {});
133
+ const { expectedVersion } = ExpectedVersionShape.parse({
134
+ expectedVersion: raw['expectedVersion'],
135
+ });
136
+ const { expectedVersion: _ignored, ...rest } = raw;
137
+ const body = SalesChannelUpdateBodySchema.parse(rest);
138
+ const channel = await salesChannelsService.update(request.params.code, body, expectedVersion, auditContext(request));
139
+ setEtag(reply, channel);
140
+ return serializeDetail(channel);
141
+ });
142
+ // -- Lifecycle ------------------------------------------------------------
143
+ app.post('/api/v1/admin/sales-channels/:code/deactivate', { preHandler: requireAdmin(SC_WRITE) }, async (request, reply) => {
144
+ const channel = await salesChannelsService.deactivate(request.params.code, auditContext(request));
145
+ setEtag(reply, channel);
146
+ return serializeDetail(channel);
147
+ });
148
+ app.post('/api/v1/admin/sales-channels/:code/activate', { preHandler: requireAdmin(SC_WRITE) }, async (request, reply) => {
149
+ const channel = await salesChannelsService.activate(request.params.code, auditContext(request));
150
+ setEtag(reply, channel);
151
+ return serializeDetail(channel);
152
+ });
153
+ /**
154
+ * Move the system-default flag — feature 072 / D-51.
155
+ *
156
+ * Gated by the same `sales_channels:write` that gates every other mutation on
157
+ * this surface: it is a channel-registry write, and an operator who may retire
158
+ * a channel may certainly choose which one is the fallback. No new permission
159
+ * code, so `/admin-roles` and the permission inventory are unchanged.
160
+ *
161
+ * Mounted before the membership routes' `/:code/:entityType` shapes, which
162
+ * mount no POST — and Fastify prefers the static `set-default` segment anyway.
163
+ */
164
+ app.post('/api/v1/admin/sales-channels/:code/set-default', { preHandler: requireAdmin(SC_WRITE) }, async (request, reply) => {
165
+ SalesChannelSetDefaultBodySchema.parse(request.body ?? {});
166
+ const result = await salesChannelsService.setDefault(request.params.code);
167
+ setEtag(reply, result.channel);
168
+ return {
169
+ channel: serializeDetail(result.channel),
170
+ previousDefaultCode: result.previousDefaultCode,
171
+ changed: result.changed,
172
+ };
173
+ });
174
+ app.delete('/api/v1/admin/sales-channels/:code', { preHandler: requireAdmin(SC_WRITE) }, async (request, reply) => {
175
+ const q = DeleteQuerySchema.parse(request.query ?? {});
176
+ await salesChannelsService.delete(request.params.code, { fallbackToDefault: q.fallbackToDefault ?? false }, auditContext(request));
177
+ reply.code(204);
178
+ return reply.send();
179
+ });
180
+ // -------------------------------------------------------------------------
181
+ // Membership routes — feature 005 / Phase 5b (T049-T058 consolidated).
182
+ //
183
+ // Both directions of the bidirectional relationship are mounted under
184
+ // `/api/v1/admin/sales-channels/*` so all 9 entity types (no
185
+ // `inventory-location` per the deferral) get coverage in one
186
+ // centralised file. The plan's per-owning-module mounts under
187
+ // `/admin/products/{id}/sales-channels` etc. would have required
188
+ // touching 9 modules' route files; consolidating here keeps the
189
+ // feature reviewable and the membership service is the single
190
+ // source of truth either way (FR-010).
191
+ //
192
+ // GET /api/v1/admin/sales-channels/:code/:entityType
193
+ // PUT /api/v1/admin/sales-channels/:code/:entityType/:entityId
194
+ // DELETE /api/v1/admin/sales-channels/:code/:entityType/:entityId
195
+ // ?fallbackToDefault=true
196
+ // GET /api/v1/admin/sales-channels/by-entity/:entityType/:entityId
197
+ //
198
+ // The "by-entity" path replaces the contract's
199
+ // `/admin/{T}/{entityId}/sales-channels` shape — functionally
200
+ // equivalent for the admin UI's membership panel and avoids
201
+ // dispersing the implementation across each owning module's
202
+ // routes.admin.ts.
203
+ // -------------------------------------------------------------------------
204
+ const PageQuerySchema = z.object({
205
+ page: z.coerce.number().int().nonnegative().optional(),
206
+ pageSize: z.coerce.number().int().positive().max(200).optional(),
207
+ });
208
+ const MembershipDeleteQuerySchema = z.object({
209
+ fallbackToDefault: z
210
+ .union([z.boolean(), z.enum(['true', 'false'])])
211
+ .transform((v) => (typeof v === 'string' ? v === 'true' : v))
212
+ .optional(),
213
+ });
214
+ function parseEntityType(raw) {
215
+ const result = ChannelMemberEntityTypeSchema.safeParse(raw);
216
+ if (!result.success) {
217
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, `Unknown channel-scoped entity type "${raw}".`);
218
+ }
219
+ return result.data;
220
+ }
221
+ async function requireChannelByCode(code) {
222
+ const channel = await salesChannelsService.getByCode(code);
223
+ if (channel === null) {
224
+ throw new HttpError(404, ERROR_CODES.UNKNOWN_SALES_CHANNEL, `Sales channel "${code}" was not found.`);
225
+ }
226
+ return channel;
227
+ }
228
+ // -- Channel-side: list members of a channel for one entity type ---------
229
+ app.get('/api/v1/admin/sales-channels/:code/:entityType', { preHandler: requireAdmin(SC_READ) }, async (request) => {
230
+ const entityType = parseEntityType(request.params.entityType);
231
+ const channel = await requireChannelByCode(request.params.code);
232
+ const q = PageQuerySchema.parse(request.query ?? {});
233
+ const page = q.page ?? 0;
234
+ const pageSize = q.pageSize ?? 100;
235
+ const result = await membershipService.listEntityIdsForChannel(channel.id, entityType, page, pageSize);
236
+ return {
237
+ salesChannelCode: channel.code,
238
+ entityType,
239
+ entityIds: result.entityIds,
240
+ page,
241
+ pageSize,
242
+ total: result.total,
243
+ };
244
+ });
245
+ // -- Channel-side: add membership ----------------------------------------
246
+ app.put('/api/v1/admin/sales-channels/:code/:entityType/:entityId', { preHandler: requireAdmin(SC_WRITE) }, async (request, reply) => {
247
+ const entityType = parseEntityType(request.params.entityType);
248
+ const channel = await requireChannelByCode(request.params.code);
249
+ const ctx = auditContext(request);
250
+ const result = await membershipService.addToChannel(channel.id, entityType, request.params.entityId, { actorAdminUserId: ctx.actorAdminUserId });
251
+ reply.code(result.changed ? 201 : 200);
252
+ return {
253
+ salesChannelCode: channel.code,
254
+ entityType,
255
+ entityId: request.params.entityId,
256
+ changed: result.changed,
257
+ };
258
+ });
259
+ // -- Channel-side: remove membership -------------------------------------
260
+ app.delete('/api/v1/admin/sales-channels/:code/:entityType/:entityId', { preHandler: requireAdmin(SC_WRITE) }, async (request, reply) => {
261
+ const entityType = parseEntityType(request.params.entityType);
262
+ const channel = await requireChannelByCode(request.params.code);
263
+ const q = MembershipDeleteQuerySchema.parse(request.query ?? {});
264
+ const ctx = auditContext(request);
265
+ const result = await membershipService.removeFromChannel(channel.id, entityType, request.params.entityId, {
266
+ fallbackToDefault: q.fallbackToDefault ?? false,
267
+ actorAdminUserId: ctx.actorAdminUserId,
268
+ });
269
+ reply.code(200);
270
+ return {
271
+ salesChannelCode: channel.code,
272
+ entityType,
273
+ entityId: request.params.entityId,
274
+ changed: result.changed,
275
+ fallbackAppliedToDefault: result.fallbackAppliedToDefault ?? false,
276
+ };
277
+ });
278
+ // -- Entity-side: list channels of an entity -----------------------------
279
+ app.get('/api/v1/admin/sales-channels/by-entity/:entityType/:entityId', { preHandler: requireAdmin(SC_READ) }, async (request) => {
280
+ const entityType = parseEntityType(request.params.entityType);
281
+ const channels = await membershipService.listChannelsForEntity(entityType, request.params.entityId);
282
+ return {
283
+ entityType,
284
+ entityId: request.params.entityId,
285
+ channels: channels.map((c) => ({
286
+ id: c.id,
287
+ code: c.code,
288
+ name: c.name,
289
+ active: c.active,
290
+ systemDefault: c.systemDefault,
291
+ defaultLanguage: c.defaultLanguage,
292
+ defaultCurrency: c.defaultCurrency,
293
+ themeCode: c.themeCode ?? null,
294
+ logoAssetId: c.logoAssetId ?? null,
295
+ version: c.version,
296
+ createdAt: c.createdAt.toISOString(),
297
+ updatedAt: c.updatedAt.toISOString(),
298
+ })),
299
+ };
300
+ });
301
+ }
302
+ //# 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,36 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ /**
3
+ * Public read of the resolved sales channel.
4
+ *
5
+ * GET /api/v1/storefront/sales-channel
6
+ *
7
+ * `PublicSalesChannelSchema` has existed since feature `005-sales-channels`
8
+ * and its own doc block describes this endpoint as "what the storefront app
9
+ * fetches on first paint to know its theme / language / currency". The schema
10
+ * shipped; the route did not. Measured on 2026-08-29: the path appeared in the
11
+ * repository exactly twice, in that comment and in the 005 prose contract, and
12
+ * in no route registration anywhere — so `theme_code`, `logoAssetId`, and the
13
+ * per-channel language and currency scopes had no way of reaching the buyer's
14
+ * first paint at all.
15
+ *
16
+ * ## Channel resolution
17
+ *
18
+ * `getResolvedChannel()` — the sanctioned accessor (Constitution XII). The
19
+ * resolver middleware has already filled the request scope from the api-key
20
+ * binding, the `X-Sales-Channel` header, `?salesChannel`, the host map or the
21
+ * system default, in that order; re-deriving any of that here would be a second
22
+ * answer to a question the platform has already answered, and would be the
23
+ * hand-rolled query Principle XII refuses. Nothing in this file reads
24
+ * `sales_channels` or any bridge table.
25
+ *
26
+ * ## Why the module gate is not a hazard here
27
+ *
28
+ * `sales_channels` declares `activation.nonDeactivatable`, so the seam
29
+ * `ctx.routes` wraps this registration in has no off state to take the
30
+ * storefront's first paint down with. The route would still be the wrong place
31
+ * for a fallback if it did: a buyer-facing page that cannot learn its channel
32
+ * is answered by the storefront's own default theme, not by a backend that
33
+ * invents a channel.
34
+ */
35
+ export declare function registerSalesChannelsStorefrontRoutes(app: FastifyInstance): Promise<void>;
36
+ //# sourceMappingURL=routes.storefront.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.storefront.d.ts","sourceRoot":"","sources":["../../src/backend/routes.storefront.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAsB,qCAAqC,CACzD,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,IAAI,CAAC,CAsBf"}
@@ -0,0 +1,58 @@
1
+ import { getResolvedChannel } from '@endora-commerce/platform/kernel';
2
+ /**
3
+ * Public read of the resolved sales channel.
4
+ *
5
+ * GET /api/v1/storefront/sales-channel
6
+ *
7
+ * `PublicSalesChannelSchema` has existed since feature `005-sales-channels`
8
+ * and its own doc block describes this endpoint as "what the storefront app
9
+ * fetches on first paint to know its theme / language / currency". The schema
10
+ * shipped; the route did not. Measured on 2026-08-29: the path appeared in the
11
+ * repository exactly twice, in that comment and in the 005 prose contract, and
12
+ * in no route registration anywhere — so `theme_code`, `logoAssetId`, and the
13
+ * per-channel language and currency scopes had no way of reaching the buyer's
14
+ * first paint at all.
15
+ *
16
+ * ## Channel resolution
17
+ *
18
+ * `getResolvedChannel()` — the sanctioned accessor (Constitution XII). The
19
+ * resolver middleware has already filled the request scope from the api-key
20
+ * binding, the `X-Sales-Channel` header, `?salesChannel`, the host map or the
21
+ * system default, in that order; re-deriving any of that here would be a second
22
+ * answer to a question the platform has already answered, and would be the
23
+ * hand-rolled query Principle XII refuses. Nothing in this file reads
24
+ * `sales_channels` or any bridge table.
25
+ *
26
+ * ## Why the module gate is not a hazard here
27
+ *
28
+ * `sales_channels` declares `activation.nonDeactivatable`, so the seam
29
+ * `ctx.routes` wraps this registration in has no off state to take the
30
+ * storefront's first paint down with. The route would still be the wrong place
31
+ * for a fallback if it did: a buyer-facing page that cannot learn its channel
32
+ * is answered by the storefront's own default theme, not by a backend that
33
+ * invents a channel.
34
+ */
35
+ export async function registerSalesChannelsStorefrontRoutes(app) {
36
+ app.get('/api/v1/storefront/sales-channel', async () => {
37
+ const channel = getResolvedChannel();
38
+ const data = {
39
+ code: channel.code,
40
+ name: channel.name,
41
+ defaultLanguage: channel.defaultLanguage,
42
+ defaultCurrency: channel.defaultCurrency,
43
+ languages: [...channel.languages],
44
+ currencies: [...channel.currencies],
45
+ themeCode: channel.themeCode,
46
+ // Null for the same reason the admin detail returns null: resolving an
47
+ // asset id to an absolute URL needs the assets module, and this route is
48
+ // not the merge request that adds that edge. `logoAssetId` is on the
49
+ // admin shapes and the public schema deliberately carries a URL rather
50
+ // than an id, so the two are answered together or not at all. Recorded
51
+ // here rather than left to be rediscovered — the theme half of this
52
+ // schema is the half this change makes true.
53
+ logoUrl: null,
54
+ };
55
+ return { data };
56
+ });
57
+ }
58
+ //# sourceMappingURL=routes.storefront.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.storefront.js","sourceRoot":"","sources":["../../src/backend/routes.storefront.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAEtE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,CAAC,KAAK,UAAU,qCAAqC,CACzD,GAAoB;IAEpB,GAAG,CAAC,GAAG,CAAC,kCAAkC,EAAE,KAAK,IAAI,EAAE;QACrD,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;QACrC,MAAM,IAAI,GAAuB;YAC/B,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,eAAe,EAAE,OAAO,CAAC,eAAe;YACxC,eAAe,EAAE,OAAO,CAAC,eAAe;YACxC,SAAS,EAAE,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC;YACjC,UAAU,EAAE,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC;YACnC,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,uEAAuE;YACvE,yEAAyE;YACzE,qEAAqE;YACrE,uEAAuE;YACvE,uEAAuE;YACvE,oEAAoE;YACpE,6CAA6C;YAC7C,OAAO,EAAE,IAAI;SACd,CAAC;QACF,OAAO,EAAE,IAAI,EAAE,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/backend/services/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/backend/services/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { SalesChannelAttribution, SalesChannelAttributionDescriptor, SalesChannelAttributionRegistryPort } from '@endora-commerce/contracts';
2
+ /**
3
+ * In-process registry of "who is still attributed to this sales channel?"
4
+ * counters (feature 075, D-87 drain).
5
+ *
6
+ * Registered under `salesChannelAttributionRegistry` and contributed to from
7
+ * other modules' boot hooks. `SalesChannelsService.delete` is the only reader
8
+ * today, and it used to answer the question itself — one raw statement naming
9
+ * `orders` and `quote_requests`, two other modules' tables, invisible to the
10
+ * import-level boundary check because SQL names no specifier.
11
+ *
12
+ * **Enumeration policy: honoured while the contributing module is absent.**
13
+ * The reasoning is at `SalesChannelAttributionRegistryPort` in
14
+ * `@endora-commerce/contracts`, and the short of it is that this is referential integrity
15
+ * rather than a surface: a switched-off module still owns the rows carrying the
16
+ * channel id, so skipping its counter would let an operator delete the channel
17
+ * underneath them. That is why this class takes no presence predicate — unlike
18
+ * `AuditReferenceRegistry`, which resolves a deep link into a contributor's own
19
+ * admin screen and therefore has one.
20
+ */
21
+ export declare class SalesChannelAttributionRegistry implements SalesChannelAttributionRegistryPort {
22
+ private readonly descriptors;
23
+ register(descriptor: SalesChannelAttributionDescriptor): void;
24
+ /** The contributing module of every registered descriptor, in registration order. */
25
+ owners(): readonly string[];
26
+ countForChannel(salesChannelId: string): Promise<SalesChannelAttribution[]>;
27
+ }
28
+ //# sourceMappingURL=sales-channel-attribution-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sales-channel-attribution-registry.d.ts","sourceRoot":"","sources":["../../../src/backend/services/sales-channel-attribution-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,uBAAuB,EACvB,iCAAiC,EACjC,mCAAmC,EACpC,MAAM,4BAA4B,CAAC;AAEpC;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,+BAAgC,YAAW,mCAAmC;IACzF,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA2C;IAEvE,QAAQ,CAAC,UAAU,EAAE,iCAAiC,GAAG,IAAI;IAM7D,qFAAqF;IACrF,MAAM,IAAI,SAAS,MAAM,EAAE;IAIrB,eAAe,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,EAAE,CAAC;CAiBlF"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * In-process registry of "who is still attributed to this sales channel?"
3
+ * counters (feature 075, D-87 drain).
4
+ *
5
+ * Registered under `salesChannelAttributionRegistry` and contributed to from
6
+ * other modules' boot hooks. `SalesChannelsService.delete` is the only reader
7
+ * today, and it used to answer the question itself — one raw statement naming
8
+ * `orders` and `quote_requests`, two other modules' tables, invisible to the
9
+ * import-level boundary check because SQL names no specifier.
10
+ *
11
+ * **Enumeration policy: honoured while the contributing module is absent.**
12
+ * The reasoning is at `SalesChannelAttributionRegistryPort` in
13
+ * `@endora-commerce/contracts`, and the short of it is that this is referential integrity
14
+ * rather than a surface: a switched-off module still owns the rows carrying the
15
+ * channel id, so skipping its counter would let an operator delete the channel
16
+ * underneath them. That is why this class takes no presence predicate — unlike
17
+ * `AuditReferenceRegistry`, which resolves a deep link into a contributor's own
18
+ * admin screen and therefore has one.
19
+ */
20
+ export class SalesChannelAttributionRegistry {
21
+ descriptors = [];
22
+ register(descriptor) {
23
+ if (!this.descriptors.includes(descriptor)) {
24
+ this.descriptors.push(descriptor);
25
+ }
26
+ }
27
+ /** The contributing module of every registered descriptor, in registration order. */
28
+ owners() {
29
+ return this.descriptors.map((d) => d.ownerModuleId);
30
+ }
31
+ async countForChannel(salesChannelId) {
32
+ const found = [];
33
+ for (const descriptor of this.descriptors) {
34
+ const count = await descriptor.countForChannel(salesChannelId);
35
+ // A consumer that points at nothing is not an attribution. Reporting it
36
+ // would put a module's name in a refusal message that is not refusing.
37
+ if (count <= 0)
38
+ continue;
39
+ found.push({
40
+ ownerModuleId: descriptor.ownerModuleId,
41
+ consumer: descriptor.consumer,
42
+ tableName: descriptor.tableName,
43
+ columnName: descriptor.columnName,
44
+ count,
45
+ });
46
+ }
47
+ return found;
48
+ }
49
+ }
50
+ //# sourceMappingURL=sales-channel-attribution-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sales-channel-attribution-registry.js","sourceRoot":"","sources":["../../../src/backend/services/sales-channel-attribution-registry.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,OAAO,+BAA+B;IACzB,WAAW,GAAwC,EAAE,CAAC;IAEvE,QAAQ,CAAC,UAA6C;QACpD,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;YAC3C,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IAED,qFAAqF;IACrF,MAAM;QACJ,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;IACtD,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,cAAsB;QAC1C,MAAM,KAAK,GAA8B,EAAE,CAAC;QAC5C,KAAK,MAAM,UAAU,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YAC1C,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC;YAC/D,wEAAwE;YACxE,uEAAuE;YACvE,IAAI,KAAK,IAAI,CAAC;gBAAE,SAAS;YACzB,KAAK,CAAC,IAAI,CAAC;gBACT,aAAa,EAAE,UAAU,CAAC,aAAa;gBACvC,QAAQ,EAAE,UAAU,CAAC,QAAQ;gBAC7B,SAAS,EAAE,UAAU,CAAC,SAAS;gBAC/B,UAAU,EAAE,UAAU,CAAC,UAAU;gBACjC,KAAK;aACN,CAAC,CAAC;QACL,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;CACF"}