@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.
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/dist/admin/api/sales-channels-client.d.ts +69 -0
- package/dist/admin/api/sales-channels-client.d.ts.map +1 -0
- package/dist/admin/api/sales-channels-client.js +74 -0
- package/dist/admin/api/sales-channels-client.js.map +1 -0
- package/dist/admin/components/ChannelIdentityForm.d.ts +24 -0
- package/dist/admin/components/ChannelIdentityForm.d.ts.map +1 -0
- package/dist/admin/components/ChannelIdentityForm.js +147 -0
- package/dist/admin/components/ChannelIdentityForm.js.map +1 -0
- package/dist/admin/components/DefaultChannelBadge.d.ts +21 -0
- package/dist/admin/components/DefaultChannelBadge.d.ts.map +1 -0
- package/dist/admin/components/DefaultChannelBadge.js +26 -0
- package/dist/admin/components/DefaultChannelBadge.js.map +1 -0
- package/dist/admin/components/EntityChannelMembership.d.ts +52 -0
- package/dist/admin/components/EntityChannelMembership.d.ts.map +1 -0
- package/dist/admin/components/EntityChannelMembership.js +98 -0
- package/dist/admin/components/EntityChannelMembership.js.map +1 -0
- package/dist/admin/index.d.ts +44 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +148 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/SalesChannelEditPage.d.ts +25 -0
- package/dist/admin/pages/SalesChannelEditPage.d.ts.map +1 -0
- package/dist/admin/pages/SalesChannelEditPage.js +185 -0
- package/dist/admin/pages/SalesChannelEditPage.js.map +1 -0
- package/dist/admin/pages/SalesChannelsListPage.d.ts +25 -0
- package/dist/admin/pages/SalesChannelsListPage.d.ts.map +1 -0
- package/dist/admin/pages/SalesChannelsListPage.js +91 -0
- package/dist/admin/pages/SalesChannelsListPage.js.map +1 -0
- package/dist/admin/zones/OrganizationChannelMembership.d.ts +34 -0
- package/dist/admin/zones/OrganizationChannelMembership.d.ts.map +1 -0
- package/dist/admin/zones/OrganizationChannelMembership.js +11 -0
- package/dist/admin/zones/OrganizationChannelMembership.js.map +1 -0
- package/dist/admin/zones/ProductChannelMembership.d.ts +23 -0
- package/dist/admin/zones/ProductChannelMembership.d.ts.map +1 -0
- package/dist/admin/zones/ProductChannelMembership.js +11 -0
- package/dist/admin/zones/ProductChannelMembership.js.map +1 -0
- package/dist/backend/commands/set-default.command.d.ts +53 -0
- package/dist/backend/commands/set-default.command.d.ts.map +1 -0
- package/dist/backend/commands/set-default.command.js +87 -0
- package/dist/backend/commands/set-default.command.js.map +1 -0
- package/dist/backend/index.d.ts +71 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +81 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +12 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +302 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.storefront.d.ts +36 -0
- package/dist/backend/routes.storefront.d.ts.map +1 -0
- package/dist/backend/routes.storefront.js +58 -0
- package/dist/backend/routes.storefront.js.map +1 -0
- package/dist/backend/services/index.d.ts +2 -0
- package/dist/backend/services/index.d.ts.map +1 -0
- package/dist/backend/services/index.js +2 -0
- package/dist/backend/services/index.js.map +1 -0
- package/dist/backend/services/sales-channel-attribution-registry.d.ts +28 -0
- package/dist/backend/services/sales-channel-attribution-registry.d.ts.map +1 -0
- package/dist/backend/services/sales-channel-attribution-registry.js +50 -0
- package/dist/backend/services/sales-channel-attribution-registry.js.map +1 -0
- package/dist/backend/services/sales-channels.service.d.ts +247 -0
- package/dist/backend/services/sales-channels.service.d.ts.map +1 -0
- package/dist/backend/services/sales-channels.service.js +535 -0
- package/dist/backend/services/sales-channels.service.js.map +1 -0
- package/dist/manifest.d.ts +185 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +204 -0
- package/dist/manifest.js.map +1 -0
- package/docs/sales_channels/admin-usage.md +77 -0
- package/docs/sales_channels/developer-guide.md +107 -0
- package/docs/sales_channels/index.md +95 -0
- package/i18n/en.json +75 -0
- package/i18n/pl.json +75 -0
- package/package.json +88 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { Link } from 'react-router-dom';
|
|
4
|
+
import { ApiError, useAuth } from '@endora-commerce/admin-kit/lib';
|
|
5
|
+
import { Alert, AlertDescription, Badge, Button, Card, CardContent, Checkbox, Label, PageHeader, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@endora-commerce/admin-kit/ui';
|
|
6
|
+
import { salesChannelsClient } from '../api/sales-channels-client.js';
|
|
7
|
+
import { DefaultChannelBadge } from '../components/DefaultChannelBadge.js';
|
|
8
|
+
import { useTranslation } from '@endora-commerce/admin-kit/i18n';
|
|
9
|
+
/**
|
|
10
|
+
* SalesChannelsListPage — feature 005 / T041.
|
|
11
|
+
*
|
|
12
|
+
* Top-level list view. The system-default channel always lives in
|
|
13
|
+
* the list; the activeOnly filter toggles whether deactivated
|
|
14
|
+
* channels are hidden. The "+ New channel" CTA sends the operator
|
|
15
|
+
* to the create form (`/sales-channels/new`).
|
|
16
|
+
*
|
|
17
|
+
* The system-default column is where the flag moves (feature 072 / D-51). The
|
|
18
|
+
* list is the right surface for it because the decision is comparative — which
|
|
19
|
+
* of these channels should be the fallback — and because the current default
|
|
20
|
+
* has to be visible while another one is promoted. The current default's cell
|
|
21
|
+
* is deliberately inert: the flag is moved by promoting a different channel,
|
|
22
|
+
* never by clearing it here, which is what keeps "exactly one default" true at
|
|
23
|
+
* every moment.
|
|
24
|
+
*/
|
|
25
|
+
export function SalesChannelsListPage() {
|
|
26
|
+
const t = useTranslation('sales_channels');
|
|
27
|
+
const { hasPermission } = useAuth();
|
|
28
|
+
const canWrite = hasPermission('sales_channels:write');
|
|
29
|
+
const [rows, setRows] = useState([]);
|
|
30
|
+
const [loading, setLoading] = useState(true);
|
|
31
|
+
const [error, setError] = useState(null);
|
|
32
|
+
const [activeOnly, setActiveOnly] = useState(false);
|
|
33
|
+
const [promoting, setPromoting] = useState(null);
|
|
34
|
+
const refresh = useCallback(async () => {
|
|
35
|
+
setLoading(true);
|
|
36
|
+
setError(null);
|
|
37
|
+
try {
|
|
38
|
+
const res = await salesChannelsClient.list({ activeOnly });
|
|
39
|
+
setRows(res.items);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
setError(err instanceof ApiError
|
|
43
|
+
? err.envelope.error.message
|
|
44
|
+
: t('list.error.load'));
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
setLoading(false);
|
|
48
|
+
}
|
|
49
|
+
}, [activeOnly, t]);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
void refresh();
|
|
52
|
+
}, [refresh]);
|
|
53
|
+
const makeDefault = useCallback(async (channel) => {
|
|
54
|
+
const previous = rows.find((c) => c.systemDefault)?.code ?? '—';
|
|
55
|
+
if (!window.confirm(t('list.action.makeDefault.confirm', { code: channel.code, previous }))) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
setPromoting(channel.code);
|
|
59
|
+
setError(null);
|
|
60
|
+
try {
|
|
61
|
+
await salesChannelsClient.setDefault(channel.code);
|
|
62
|
+
await refresh();
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
setError(err instanceof ApiError
|
|
66
|
+
? err.envelope.error.message
|
|
67
|
+
: t('list.error.makeDefault'));
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
setPromoting(null);
|
|
71
|
+
}
|
|
72
|
+
}, [refresh, rows, t]);
|
|
73
|
+
return (_jsxs(_Fragment, { children: [_jsx(PageHeader, { title: t('list.page.title'), description: t('list.page.description'), actions:
|
|
74
|
+
// Gated on the code the create route now enforces (feature 091, batch
|
|
75
|
+
// 14). It was ungated while `/sales-channels/new` was `App.tsx`'s and
|
|
76
|
+
// therefore ungated too; the route is this module's own now and takes
|
|
77
|
+
// `sales_channels:write`, so an ungated button would be a dead end for
|
|
78
|
+
// a read-only operator rather than a refusal they could act on.
|
|
79
|
+
canWrite ? (_jsx(Button, { asChild: true, children: _jsx(Link, { to: "/sales-channels/new", children: t('list.action.newChannel') }) })) : null }), error && (_jsx(Alert, { variant: "destructive", className: "mb-4", children: _jsx(AlertDescription, { children: error }) })), _jsx(Card, { className: "mb-4", children: _jsx(CardContent, { className: "pt-6", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: "sc-active-only", checked: activeOnly, onChange: (e) => setActiveOnly(e.target.checked) }), _jsx(Label, { htmlFor: "sc-active-only", className: "cursor-pointer", children: t('list.filter.activeOnly') })] }) }) }), _jsx(Card, { children: _jsx(CardContent, { className: "pt-6", children: loading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('list.loading') })) : rows.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('list.empty') })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: t('list.column.code') }), _jsx(TableHead, { children: t('list.column.name') }), _jsx(TableHead, { children: t('list.column.defaultLangCurrency') }), _jsx(TableHead, { children: t('list.column.status') }), _jsx(TableHead, { className: "w-32 text-right", children: t('list.column.version') }), _jsx(TableHead, { className: "w-48 text-right", children: t('list.column.systemDefault') })] }) }), _jsx(TableBody, { children: rows.map((c) => (_jsxs(TableRow, { children: [_jsxs(TableCell, { className: "font-mono text-xs", children: [_jsx(Link, { to: `/sales-channels/${encodeURIComponent(c.code)}`, className: "text-primary hover:underline", children: c.code }), _jsx(DefaultChannelBadge, { systemDefault: c.systemDefault })] }), _jsx(TableCell, { children: c.name['en-US'] ?? c.name['en'] ?? Object.values(c.name)[0] ?? '—' }), _jsxs(TableCell, { className: "text-xs text-muted-foreground", children: [c.defaultLanguage, " / ", c.defaultCurrency] }), _jsx(TableCell, { children: c.active ? (_jsx(Badge, { variant: "default", className: "text-[10px]", children: t('status.active') })) : (_jsx(Badge, { variant: "outline", className: "text-[10px]", children: t('status.inactive') })) }), _jsxs(TableCell, { className: "text-right text-xs text-muted-foreground", children: ["v", c.version] }), _jsx(TableCell, { className: "text-right", children: c.systemDefault ? (
|
|
80
|
+
// The current default: stated, not offered. Clearing the
|
|
81
|
+
// flag without giving it to another channel is the one
|
|
82
|
+
// state the platform must never be in.
|
|
83
|
+
_jsx("span", { className: "text-xs text-muted-foreground", children: t('list.action.makeDefault.current') })) : canWrite ? (_jsx(Button, { variant: "outline", size: "sm", disabled: !c.active || promoting !== null, title: c.active ? undefined : t('list.action.makeDefault.inactiveHelp'), onClick: () => void makeDefault(c), children: t('list.action.makeDefault') })) : null })] }, c.id))) })] })) }) })] }));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The registry loads a route component through a dynamic-import factory and
|
|
87
|
+
* reads its default export (feature 091, FR-013). The named export stays: it is
|
|
88
|
+
* the spelling this module's own code and its tests use.
|
|
89
|
+
*/
|
|
90
|
+
export default SalesChannelsListPage;
|
|
91
|
+
//# sourceMappingURL=SalesChannelsListPage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SalesChannelsListPage.js","sourceRoot":"","sources":["../../../src/admin/pages/SalesChannelsListPage.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { AdminZoneProps } from '@endora-commerce/contracts';
|
|
3
|
+
/**
|
|
4
|
+
* The channels an organization belongs to (feature 091, P7b).
|
|
5
|
+
*
|
|
6
|
+
* `organizations`' `OrganizationDetail.tsx` used to import
|
|
7
|
+
* `EntityChannelMembership` by path and render it with
|
|
8
|
+
* `entityType="organization"` — one of the three keys of
|
|
9
|
+
* `backend/scripts/ledgers/cross-module-imports/organizations.ts`. The place is
|
|
10
|
+
* `organization.detail.after` now, and the `entityType` is *this module's*
|
|
11
|
+
* constant: an organization's detail screen is the only thing it could be.
|
|
12
|
+
*
|
|
13
|
+
* **Two contributions over one component, not one contribution with a `match`**
|
|
14
|
+
* (Z4, Z13). This wrapper and `ProductChannelMembership` differ only in the
|
|
15
|
+
* `entityType` they pass, and that is the whole cost of `organization.detail.after`
|
|
16
|
+
* and `product.editor.channels` being two places rather than one name narrowed
|
|
17
|
+
* per mount.
|
|
18
|
+
*
|
|
19
|
+
* **No `match`, and it is a decision rather than an omission.** This place has
|
|
20
|
+
* one host and one mount, so there is nothing to narrow.
|
|
21
|
+
* `admin/test/modules/sales_channels/organization-channels-zone.test.tsx`
|
|
22
|
+
* asserts it absent so a later author cannot add one quietly.
|
|
23
|
+
*
|
|
24
|
+
* The host used to pass no `onChanged`, and neither does this: the panel's own
|
|
25
|
+
* refresh is what the operator sees, and a zone has no callback to give it.
|
|
26
|
+
*/
|
|
27
|
+
export type OrganizationChannelMembershipProps = AdminZoneProps<'organization.detail.after'>;
|
|
28
|
+
export declare function OrganizationChannelMembership({ organizationId, }: OrganizationChannelMembershipProps): ReactNode;
|
|
29
|
+
/**
|
|
30
|
+
* The zone renderer loads a contribution through a dynamic-import factory and
|
|
31
|
+
* reads its default export (feature 091, FR-013).
|
|
32
|
+
*/
|
|
33
|
+
export default OrganizationChannelMembership;
|
|
34
|
+
//# sourceMappingURL=OrganizationChannelMembership.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OrganizationChannelMembership.d.ts","sourceRoot":"","sources":["../../../src/admin/zones/OrganizationChannelMembership.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAGjE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,MAAM,kCAAkC,GAAG,cAAc,CAAC,2BAA2B,CAAC,CAAC;AAE7F,wBAAgB,6BAA6B,CAAC,EAC5C,cAAc,GACf,EAAE,kCAAkC,GAAG,SAAS,CAEhD;AAED;;;GAGG;AACH,eAAe,6BAA6B,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { EntityChannelMembership } from '../components/EntityChannelMembership.js';
|
|
3
|
+
export function OrganizationChannelMembership({ organizationId, }) {
|
|
4
|
+
return _jsx(EntityChannelMembership, { entityType: "organization", entityId: organizationId });
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The zone renderer loads a contribution through a dynamic-import factory and
|
|
8
|
+
* reads its default export (feature 091, FR-013).
|
|
9
|
+
*/
|
|
10
|
+
export default OrganizationChannelMembership;
|
|
11
|
+
//# sourceMappingURL=OrganizationChannelMembership.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OrganizationChannelMembership.js","sourceRoot":"","sources":["../../../src/admin/zones/OrganizationChannelMembership.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,uBAAuB,EAAE,MAAM,0CAA0C,CAAC;AA4BnF,MAAM,UAAU,6BAA6B,CAAC,EAC5C,cAAc,GACqB;IACnC,OAAO,KAAC,uBAAuB,IAAC,UAAU,EAAC,cAAc,EAAC,QAAQ,EAAE,cAAc,GAAI,CAAC;AACzF,CAAC;AAED;;;GAGG;AACH,eAAe,6BAA6B,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { AdminZoneProps } from '@endora-commerce/contracts';
|
|
3
|
+
/**
|
|
4
|
+
* The body of the product editor's Channels tab (feature 091, P7a).
|
|
5
|
+
*
|
|
6
|
+
* `catalog`'s `ProductEditor.tsx` used to import `EntityChannelMembership` and
|
|
7
|
+
* render it as the whole tab body, passing `entityType="product"`. The place is
|
|
8
|
+
* `product.editor.channels` now, and the `entityType` is *this module's*
|
|
9
|
+
* constant: the zone is the product editor's, so nothing else it could be.
|
|
10
|
+
*
|
|
11
|
+
* **No `match`, and it is a decision rather than an omission.** `match` narrows
|
|
12
|
+
* the mounts of one place (Z13); this place has one host and one mount.
|
|
13
|
+
* `admin/test/modules/sales_channels/product-channels-zone.test.tsx` asserts it
|
|
14
|
+
* absent so a later author cannot add one quietly.
|
|
15
|
+
*/
|
|
16
|
+
export type ProductChannelMembershipProps = AdminZoneProps<'product.editor.channels'>;
|
|
17
|
+
export declare function ProductChannelMembership({ productId, }: ProductChannelMembershipProps): ReactNode;
|
|
18
|
+
/**
|
|
19
|
+
* The zone renderer loads a contribution through a dynamic-import factory and
|
|
20
|
+
* reads its default export (feature 091, FR-013).
|
|
21
|
+
*/
|
|
22
|
+
export default ProductChannelMembership;
|
|
23
|
+
//# sourceMappingURL=ProductChannelMembership.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductChannelMembership.d.ts","sourceRoot":"","sources":["../../../src/admin/zones/ProductChannelMembership.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAGjE;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,6BAA6B,GAAG,cAAc,CAAC,yBAAyB,CAAC,CAAC;AAEtF,wBAAgB,wBAAwB,CAAC,EACvC,SAAS,GACV,EAAE,6BAA6B,GAAG,SAAS,CAE3C;AAED;;;GAGG;AACH,eAAe,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { EntityChannelMembership } from '../components/EntityChannelMembership.js';
|
|
3
|
+
export function ProductChannelMembership({ productId, }) {
|
|
4
|
+
return _jsx(EntityChannelMembership, { entityType: "product", entityId: productId });
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The zone renderer loads a contribution through a dynamic-import factory and
|
|
8
|
+
* reads its default export (feature 091, FR-013).
|
|
9
|
+
*/
|
|
10
|
+
export default ProductChannelMembership;
|
|
11
|
+
//# sourceMappingURL=ProductChannelMembership.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductChannelMembership.js","sourceRoot":"","sources":["../../../src/admin/zones/ProductChannelMembership.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,uBAAuB,EAAE,MAAM,0CAA0C,CAAC;AAiBnF,MAAM,UAAU,wBAAwB,CAAC,EACvC,SAAS,GACqB;IAC9B,OAAO,KAAC,uBAAuB,IAAC,UAAU,EAAC,SAAS,EAAC,QAAQ,EAAE,SAAS,GAAI,CAAC;AAC/E,CAAC;AAED;;;GAGG;AACH,eAAe,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Command } from '@endora-commerce/platform/commands';
|
|
2
|
+
import { SalesChannel } from '@endora-commerce/platform/kernel';
|
|
3
|
+
/**
|
|
4
|
+
* Moving the `system_default` flag — feature 072 / D-51, Principle XIII.
|
|
5
|
+
*
|
|
6
|
+
* The third property of the product owner's invariant: exactly one channel is
|
|
7
|
+
* the default, and the flag is **movable**. The first two have been enforced
|
|
8
|
+
* since feature 005 (a partial unique index, plus a boot reconciler and three
|
|
9
|
+
* service refusals); this is the one nothing in the platform could do.
|
|
10
|
+
*
|
|
11
|
+
* It is a Command because it is the definition of a sensitive write: it changes
|
|
12
|
+
* where every unattributed order, every platform-wide fallback read and every
|
|
13
|
+
* orphan rebind lands, and the operator who did it has to be recoverable from
|
|
14
|
+
* the audit trail. The audit row names both channels — `stateBefore` the one
|
|
15
|
+
* that held the flag, `stateAfter` the one that holds it now — because "which
|
|
16
|
+
* channel was the default in March" is the question this row exists to answer.
|
|
17
|
+
*
|
|
18
|
+
* **Not `reversible`.** The registry's `reversible` marks commands wired into
|
|
19
|
+
* the operator-facing undo of feature 054 (a `RevertRecord` set on a bulk
|
|
20
|
+
* operation row), and there is no such surface for a single channel flag. The
|
|
21
|
+
* move is undone by making the same call against the previous default, whose
|
|
22
|
+
* code the response and the audit row both carry — an inverse an operator can
|
|
23
|
+
* perform, not a stored revert state.
|
|
24
|
+
*/
|
|
25
|
+
export interface SetSystemDefaultChannelResult {
|
|
26
|
+
/** The channel that holds the flag once the command has run. */
|
|
27
|
+
readonly channel: SalesChannel;
|
|
28
|
+
/**
|
|
29
|
+
* The code that held the flag when the command started — the channel a
|
|
30
|
+
* subsequent `set-default` would move it back to. `null` only in the state the
|
|
31
|
+
* boot reconciler exists to repair: a table with rows but no default at all.
|
|
32
|
+
*/
|
|
33
|
+
readonly previousDefaultCode: string | null;
|
|
34
|
+
/** False when the target already held the flag; the call wrote nothing. */
|
|
35
|
+
readonly changed: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Demote-then-promote, in this order, inside the Command Bus's transaction.
|
|
39
|
+
*
|
|
40
|
+
* `sales_channels_one_system_default` is a partial unique index and Postgres
|
|
41
|
+
* cannot defer one, so the two updates are two statements with the demote
|
|
42
|
+
* first — the flag has to be off the old row before it can land on the new one.
|
|
43
|
+
* Assigning both and flushing once is **not** equivalent: MikroORM decides the
|
|
44
|
+
* statement order for a change set, and the promote-first ordering trips the
|
|
45
|
+
* index. That is also why a concurrent second move blocks on the index and then
|
|
46
|
+
* fails closed rather than racing, which is the behaviour we want.
|
|
47
|
+
*
|
|
48
|
+
* Both guards live inside the transaction rather than in the caller: the
|
|
49
|
+
* caller's lookup produces the 404 and a readable message, this one is what is
|
|
50
|
+
* still true at the moment of the write.
|
|
51
|
+
*/
|
|
52
|
+
export declare function makeSetSystemDefaultChannelCommand(channelId: string): Command<SetSystemDefaultChannelResult>;
|
|
53
|
+
//# sourceMappingURL=set-default.command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"set-default.command.d.ts","sourceRoot":"","sources":["../../../src/backend/commands/set-default.command.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAElE,OAAO,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAEhE;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,WAAW,6BAA6B;IAC5C,gEAAgE;IAChE,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kCAAkC,CAChD,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,6BAA6B,CAAC,CAmFxC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { ERROR_CODES } from '@endora-commerce/contracts';
|
|
3
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
4
|
+
import { SalesChannel } from '@endora-commerce/platform/kernel';
|
|
5
|
+
/**
|
|
6
|
+
* Demote-then-promote, in this order, inside the Command Bus's transaction.
|
|
7
|
+
*
|
|
8
|
+
* `sales_channels_one_system_default` is a partial unique index and Postgres
|
|
9
|
+
* cannot defer one, so the two updates are two statements with the demote
|
|
10
|
+
* first — the flag has to be off the old row before it can land on the new one.
|
|
11
|
+
* Assigning both and flushing once is **not** equivalent: MikroORM decides the
|
|
12
|
+
* statement order for a change set, and the promote-first ordering trips the
|
|
13
|
+
* index. That is also why a concurrent second move blocks on the index and then
|
|
14
|
+
* fails closed rather than racing, which is the behaviour we want.
|
|
15
|
+
*
|
|
16
|
+
* Both guards live inside the transaction rather than in the caller: the
|
|
17
|
+
* caller's lookup produces the 404 and a readable message, this one is what is
|
|
18
|
+
* still true at the moment of the write.
|
|
19
|
+
*/
|
|
20
|
+
export function makeSetSystemDefaultChannelCommand(channelId) {
|
|
21
|
+
return {
|
|
22
|
+
action: 'sales_channel.set_default',
|
|
23
|
+
objectType: 'sales_channel',
|
|
24
|
+
objectId: channelId,
|
|
25
|
+
run: async ({ em }) => {
|
|
26
|
+
const target = await em.findOne(SalesChannel, { id: channelId });
|
|
27
|
+
if (target === null) {
|
|
28
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'The sales channel to promote no longer exists.');
|
|
29
|
+
}
|
|
30
|
+
const current = await em.findOne(SalesChannel, { systemDefault: true });
|
|
31
|
+
// Already the default: a no-op success, not an error. A retried request and
|
|
32
|
+
// a double-clicked button answer the same 200, and `skipAudit` keeps "no
|
|
33
|
+
// write ⇒ no audit row" honest.
|
|
34
|
+
if (target.systemDefault) {
|
|
35
|
+
return {
|
|
36
|
+
result: { channel: target, previousDefaultCode: target.code, changed: false },
|
|
37
|
+
skipAudit: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// A default nobody can transact on is not a default. The mirror of the
|
|
41
|
+
// three refusals that keep the default channel from being deactivated or
|
|
42
|
+
// deleted (`sales-channels.service.ts:231/272/317`): they stop the flag's
|
|
43
|
+
// holder from going inactive, this stops an inactive channel from becoming
|
|
44
|
+
// the holder.
|
|
45
|
+
if (!target.active) {
|
|
46
|
+
throw new HttpError(422, ERROR_CODES.INACTIVE_SALES_CHANNEL, `Sales channel "${target.code}" is inactive and cannot become the system default. ` +
|
|
47
|
+
`Activate it first.`);
|
|
48
|
+
}
|
|
49
|
+
const before = current === null ? null : { id: current.id, code: current.code };
|
|
50
|
+
// 1. Demote, and get it into the database before step 2 exists.
|
|
51
|
+
if (current !== null) {
|
|
52
|
+
current.systemDefault = false;
|
|
53
|
+
current.version += 1;
|
|
54
|
+
await em.flush();
|
|
55
|
+
}
|
|
56
|
+
// 2. Promote.
|
|
57
|
+
target.systemDefault = true;
|
|
58
|
+
target.version += 1;
|
|
59
|
+
await em.flush();
|
|
60
|
+
return {
|
|
61
|
+
result: {
|
|
62
|
+
channel: target,
|
|
63
|
+
previousDefaultCode: current?.code ?? null,
|
|
64
|
+
changed: true,
|
|
65
|
+
},
|
|
66
|
+
before,
|
|
67
|
+
after: { id: target.id, code: target.code },
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
event: (result) => result.changed
|
|
71
|
+
? {
|
|
72
|
+
eventName: 'sales_channels.lifecycle_changed',
|
|
73
|
+
payload: {
|
|
74
|
+
eventId: randomUUID(),
|
|
75
|
+
occurredAt: new Date().toISOString(),
|
|
76
|
+
channelCode: result.channel.code,
|
|
77
|
+
// Two rows changed identity-bearing state, and the cache is keyed
|
|
78
|
+
// by code with `systemDefault` inside the cached value — so the
|
|
79
|
+
// demoted channel's entry is stale too. Same shape the delete path
|
|
80
|
+
// uses for the same reason.
|
|
81
|
+
invalidateAll: true,
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
: undefined,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=set-default.command.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"set-default.command.js","sourceRoot":"","sources":["../../../src/backend/commands/set-default.command.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { FastifyRequest } from 'fastify';
|
|
3
|
+
import type { DictionaryValidator, SalesChannelAttributionRegistryPort } from '@endora-commerce/contracts';
|
|
4
|
+
import type { CommandBus } from '@endora-commerce/platform/commands';
|
|
5
|
+
import type { EventBus } from '@endora-commerce/platform/events';
|
|
6
|
+
import type { AuditPort } from '@endora-commerce/platform/kernel';
|
|
7
|
+
import type { ModuleContext } from '@endora-commerce/platform/kernel';
|
|
8
|
+
import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
|
|
9
|
+
import type { SalesChannelsCache } from '@endora-commerce/platform/kernel';
|
|
10
|
+
import type { SalesChannelMembershipPort } from '@endora-commerce/platform/kernel';
|
|
11
|
+
import { SalesChannelsService } from './services/sales-channels.service.js';
|
|
12
|
+
import type { AdminAuditContext } from './services/sales-channels.service.js';
|
|
13
|
+
/**
|
|
14
|
+
* `sales_channels` — what is left after the kernel took the resolution
|
|
15
|
+
* machinery (feature 072, wave 2, T110).
|
|
16
|
+
*
|
|
17
|
+
* T019 moved the cache, the resolver, the membership service, the middleware
|
|
18
|
+
* and the default-channel reconciler into `kernel/sales-channels/`, and this
|
|
19
|
+
* conversion finishes the thought: a composition root composes those through
|
|
20
|
+
* `composeSalesChannelsKernel`, and the module keeps only what an operator
|
|
21
|
+
* would recognise as the module — the admin CRUD service and its routes.
|
|
22
|
+
*
|
|
23
|
+
* The division is not cosmetic. Resolving which channel a request belongs to
|
|
24
|
+
* backs every channel-scoped read in the platform (Principle XII), so it must
|
|
25
|
+
* not be gated on this module: switching the administration screens off would
|
|
26
|
+
* otherwise leave every storefront request with no resolved channel.
|
|
27
|
+
*
|
|
28
|
+
* **`dictionaryValidator` was optional, and never once supplied.** Neither
|
|
29
|
+
* composition passed it, so `SalesChannelsService` has been skipping the
|
|
30
|
+
* language- and currency-code checks it declares — creating a channel with a
|
|
31
|
+
* language nothing in the platform speaks is accepted today, silently. It is a
|
|
32
|
+
* declared dependency now, not an option: its absence removes a check rather
|
|
33
|
+
* than a capability, which is the line these conversions have been drawing.
|
|
34
|
+
*/
|
|
35
|
+
export interface SalesChannelsCradle {
|
|
36
|
+
readonly emFactory: () => EntityManager;
|
|
37
|
+
readonly eventBus: EventBus;
|
|
38
|
+
readonly auditLogService: AuditPort;
|
|
39
|
+
/** D-51 — moving the system-default flag is a Command (Principle XIII). */
|
|
40
|
+
readonly commandBus: CommandBus;
|
|
41
|
+
readonly requireAdmin: RequireAdminFactory;
|
|
42
|
+
readonly adminAuditActorResolver: (req: FastifyRequest) => AdminAuditContext;
|
|
43
|
+
/** Kernel-composed, so it is the same cache the resolver reads through. */
|
|
44
|
+
readonly salesChannelsCache: SalesChannelsCache;
|
|
45
|
+
readonly salesChannelMembershipPort: SalesChannelMembershipPort;
|
|
46
|
+
readonly dictionaryValidator: DictionaryValidator;
|
|
47
|
+
/**
|
|
48
|
+
* Owned here, contributed to from `orders` and `quote_requests` (feature 075,
|
|
49
|
+
* D-87). A plain registration, not a port: a contributor pushes into it from
|
|
50
|
+
* a boot hook, and a gate there would stop the backend from starting.
|
|
51
|
+
*/
|
|
52
|
+
readonly salesChannelAttributionRegistry: SalesChannelAttributionRegistryPort;
|
|
53
|
+
readonly salesChannelsService: SalesChannelsService;
|
|
54
|
+
}
|
|
55
|
+
export declare function registerModule(ctx: ModuleContext): void;
|
|
56
|
+
/**
|
|
57
|
+
* This module owns **no persisted entity**, and says so with an empty array
|
|
58
|
+
* rather than by omission (D-168).
|
|
59
|
+
*
|
|
60
|
+
* The two are not the same thing to the platform. When the package is
|
|
61
|
+
* *installed*, `src/packages/package-runtime.ts` reads `exported['entities']`
|
|
62
|
+
* and answers a missing export with `[]` — so "this module has no table" and
|
|
63
|
+
* "somebody forgot the array" arrive at the host as the same silence, and the
|
|
64
|
+
* only symptom of the second is a query against a table nobody created. The
|
|
65
|
+
* declaration is what makes the first case a statement.
|
|
66
|
+
*
|
|
67
|
+
* A first entity added here goes in this array in the same merge request, and
|
|
68
|
+
* `test/unit/packages/module-package-entity-surface.test.ts` is what says so.
|
|
69
|
+
*/
|
|
70
|
+
export declare const entities: readonly never[];
|
|
71
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EACV,mBAAmB,EACnB,mCAAmC,EACpC,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AACrE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AACjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAEtE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,KAAK,EACV,kBAAkB,EAEnB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AAEnF,OAAO,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAC5E,OAAO,KAAK,EAAE,iBAAiB,EAAqB,MAAM,sCAAsC,CAAC;AAIjG;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,aAAa,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,eAAe,EAAE,SAAS,CAAC;IACpC,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,QAAQ,CAAC,uBAAuB,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,iBAAiB,CAAC;IAC7E,2EAA2E;IAC3E,QAAQ,CAAC,kBAAkB,EAAE,kBAAkB,CAAC;IAChD,QAAQ,CAAC,0BAA0B,EAAE,0BAA0B,CAAC;IAChE,QAAQ,CAAC,mBAAmB,EAAE,mBAAmB,CAAC;IAClD;;;;OAIG;IACH,QAAQ,CAAC,+BAA+B,EAAE,mCAAmC,CAAC;IAC9E,QAAQ,CAAC,oBAAoB,EAAE,oBAAoB,CAAC;CACrD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAkFvD;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,QAAQ,EAAE,SAAS,KAAK,EAAO,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { lazyPort } from '@endora-commerce/platform/kernel';
|
|
2
|
+
import { SalesChannelAttributionRegistry } from './services/sales-channel-attribution-registry.js';
|
|
3
|
+
import { SalesChannelsService } from './services/sales-channels.service.js';
|
|
4
|
+
import { registerSalesChannelsAdminRoutes } from './routes.admin.js';
|
|
5
|
+
import { registerSalesChannelsStorefrontRoutes } from './routes.storefront.js';
|
|
6
|
+
export function registerModule(ctx) {
|
|
7
|
+
ctx.di.register({
|
|
8
|
+
/**
|
|
9
|
+
* "Who is still attributed to this channel?" — the contribution seam the
|
|
10
|
+
* FR-006 delete guard reads (feature 075, D-87). `SalesChannelsService`
|
|
11
|
+
* used to answer it here, with one statement naming `orders` and
|
|
12
|
+
* `quote_requests`; each owner counts its own rows now.
|
|
13
|
+
*
|
|
14
|
+
* `di.register`, not `di.providePort`: `orders` and `quote_requests` push
|
|
15
|
+
* into this from their own boot hooks, and a gate resolved there would
|
|
16
|
+
* refuse at a point in the boot that has nothing to answer. This module is
|
|
17
|
+
* `nonDeactivatable`, so there is no flip for a gate to protect either.
|
|
18
|
+
*/
|
|
19
|
+
salesChannelAttributionRegistry: ctx
|
|
20
|
+
.asFunction(() => new SalesChannelAttributionRegistry())
|
|
21
|
+
.singleton(),
|
|
22
|
+
});
|
|
23
|
+
ctx.di.providePort('salesChannelsService', ctx
|
|
24
|
+
.asFunction(({ emFactory, eventBus, auditLogService, commandBus, salesChannelAttributionRegistry, }) => new SalesChannelsService(emFactory, eventBus,
|
|
25
|
+
// Another module's port, so it is resolved per call rather than
|
|
26
|
+
// captured. Required since feature 075 — the branch its absence
|
|
27
|
+
// used to take was a raw `select` over `languages` and
|
|
28
|
+
// `currencies`, and it is gone.
|
|
29
|
+
lazyPort(ctx, 'dictionaryValidator'),
|
|
30
|
+
// This module's own registration, so it is captured like `emFactory`.
|
|
31
|
+
salesChannelAttributionRegistry,
|
|
32
|
+
// Feature 120 (FR-015) — which table holds each entity type's
|
|
33
|
+
// memberships, said by the module that owns the entity type. A
|
|
34
|
+
// kernel registration a root contributes, resolved per call rather
|
|
35
|
+
// than captured for the same reason `salesChannelsCache` is: the
|
|
36
|
+
// registry is filled by every owning module's boot hook, so a value
|
|
37
|
+
// captured while this module registers would be the empty one.
|
|
38
|
+
lazyPort(ctx, 'salesChannelBridgeRegistry'), auditLogService,
|
|
39
|
+
// Resolved per call rather than captured: the cache is composed by a
|
|
40
|
+
// root. Narrowed to the invalidating half (D-93): this service drops
|
|
41
|
+
// the cache at its write seam and must not be able to seed it, which
|
|
42
|
+
// is the resolver's job.
|
|
43
|
+
lazyPort(ctx, 'salesChannelsCache'),
|
|
44
|
+
// A deployment input, like `emFactory` and `eventBus` — not a port,
|
|
45
|
+
// so it is captured with them.
|
|
46
|
+
commandBus))
|
|
47
|
+
.singleton());
|
|
48
|
+
ctx.routes(async (app) => {
|
|
49
|
+
const { salesChannelMembershipPort, requireAdmin } = ctx.cradle();
|
|
50
|
+
await registerSalesChannelsAdminRoutes(app, {
|
|
51
|
+
// Lazily, even though the module owns this port: route *registration* runs
|
|
52
|
+
// inside `buildServer` whatever the module's effective state is, so
|
|
53
|
+
// destructuring the gate here would stop the next start instead of
|
|
54
|
+
// stopping the routes (D-40).
|
|
55
|
+
salesChannelsService: lazyPort(ctx, 'salesChannelsService'),
|
|
56
|
+
membershipService: salesChannelMembershipPort,
|
|
57
|
+
requireAdmin,
|
|
58
|
+
resolveAdminAuditContext: (req) => ctx.cradle().adminAuditActorResolver(req),
|
|
59
|
+
});
|
|
60
|
+
// The public read of the resolved channel. It resolves nothing itself and
|
|
61
|
+
// takes no dependency, so it is registered beside the admin surface rather
|
|
62
|
+
// than in a second `ctx.routes` call.
|
|
63
|
+
await registerSalesChannelsStorefrontRoutes(app);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* This module owns **no persisted entity**, and says so with an empty array
|
|
68
|
+
* rather than by omission (D-168).
|
|
69
|
+
*
|
|
70
|
+
* The two are not the same thing to the platform. When the package is
|
|
71
|
+
* *installed*, `src/packages/package-runtime.ts` reads `exported['entities']`
|
|
72
|
+
* and answers a missing export with `[]` — so "this module has no table" and
|
|
73
|
+
* "somebody forgot the array" arrive at the host as the same silence, and the
|
|
74
|
+
* only symptom of the second is a query against a table nobody created. The
|
|
75
|
+
* declaration is what makes the first case a statement.
|
|
76
|
+
*
|
|
77
|
+
* A first entity added here goes in this array in the same merge request, and
|
|
78
|
+
* `test/unit/packages/module-package-entity-surface.test.ts` is what says so.
|
|
79
|
+
*/
|
|
80
|
+
export const entities = [];
|
|
81
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAO5D,OAAO,EAAE,+BAA+B,EAAE,MAAM,kDAAkD,CAAC;AACnG,OAAO,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAE5E,OAAO,EAAE,gCAAgC,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,qCAAqC,EAAE,MAAM,wBAAwB,CAAC;AA8C/E,MAAM,UAAU,cAAc,CAAC,GAAkB;IAC/C,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC;QACd;;;;;;;;;;WAUG;QACH,+BAA+B,EAAE,GAAG;aACjC,UAAU,CACT,GAAwC,EAAE,CAAC,IAAI,+BAA+B,EAAE,CACjF;aACA,SAAS,EAAE;KACf,CAAC,CAAC;IAEH,GAAG,CAAC,EAAE,CAAC,WAAW,CAChB,sBAAsB,EACtB,GAAG;SACA,UAAU,CACT,CAAC,EACC,SAAS,EACT,QAAQ,EACR,eAAe,EACf,UAAU,EACV,+BAA+B,GACX,EAAE,EAAE,CACxB,IAAI,oBAAoB,CACtB,SAAS,EACT,QAAQ;IACR,gEAAgE;IAChE,gEAAgE;IAChE,uDAAuD;IACvD,gCAAgC;IAChC,QAAQ,CAAsB,GAAG,EAAE,qBAAqB,CAAC;IACzD,sEAAsE;IACtE,+BAA+B;IAC/B,8DAA8D;IAC9D,+DAA+D;IAC/D,mEAAmE;IACnE,iEAAiE;IACjE,oEAAoE;IACpE,+DAA+D;IAC/D,QAAQ,CAAoB,GAAG,EAAE,4BAA4B,CAAC,EAC9D,eAAe;IACf,qEAAqE;IACrE,qEAAqE;IACrE,qEAAqE;IACrE,yBAAyB;IACzB,QAAQ,CAAiC,GAAG,EAAE,oBAAoB,CAAC;IACnE,oEAAoE;IACpE,+BAA+B;IAC/B,UAAU,CACX,CACJ;SACA,SAAS,EAAE,CACf,CAAC;IAEF,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACvB,MAAM,EAAE,0BAA0B,EAAE,YAAY,EAAE,GAAG,GAAG,CAAC,MAAM,EAAuB,CAAC;QAEvF,MAAM,gCAAgC,CAAC,GAAG,EAAE;YAC1C,2EAA2E;YAC3E,oEAAoE;YACpE,mEAAmE;YACnE,8BAA8B;YAC9B,oBAAoB,EAAE,QAAQ,CAAuB,GAAG,EAAE,sBAAsB,CAAC;YACjF,iBAAiB,EAAE,0BAA0B;YAC7C,YAAY;YACZ,wBAAwB,EAAE,CAAC,GAAG,EAAE,EAAE,CAChC,GAAG,CAAC,MAAM,EAAuB,CAAC,uBAAuB,CAAC,GAAG,CAAC;SACjE,CAAC,CAAC;QAEH,0EAA0E;QAC1E,2EAA2E;QAC3E,sCAAsC;QACtC,MAAM,qCAAqC,CAAC,GAAG,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAqB,EAAE,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
|
2
|
+
import type { AdminAuditContext, SalesChannelsService } from './services/sales-channels.service.js';
|
|
3
|
+
import type { SalesChannelMembershipPort } from '@endora-commerce/platform/kernel';
|
|
4
|
+
import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
|
|
5
|
+
export interface SalesChannelsAdminDeps {
|
|
6
|
+
salesChannelsService: SalesChannelsService;
|
|
7
|
+
membershipService: SalesChannelMembershipPort;
|
|
8
|
+
requireAdmin: RequireAdminFactory;
|
|
9
|
+
resolveAdminAuditContext?: (req: FastifyRequest) => AdminAuditContext;
|
|
10
|
+
}
|
|
11
|
+
export declare function registerSalesChannelsAdminRoutes(app: FastifyInstance, deps: SalesChannelsAdminDeps): Promise<void>;
|
|
12
|
+
//# 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,EAAgB,cAAc,EAAE,MAAM,SAAS,CAAC;AAW7E,OAAO,KAAK,EACV,iBAAiB,EACjB,oBAAoB,EACrB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AAEnF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AA4D5E,MAAM,WAAW,sBAAsB;IACrC,oBAAoB,EAAE,oBAAoB,CAAC;IAC3C,iBAAiB,EAAE,0BAA0B,CAAC;IAC9C,YAAY,EAAE,mBAAmB,CAAC;IAClC,wBAAwB,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,iBAAiB,CAAC;CACvE;AAED,wBAAsB,gCAAgC,CACpD,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAoXf"}
|