@endora-commerce/mod-comparisons 0.0.0-stage → 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 (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -2
  3. package/dist/admin/api/comparisons-client.d.ts +24 -0
  4. package/dist/admin/api/comparisons-client.d.ts.map +1 -0
  5. package/dist/admin/api/comparisons-client.js +15 -0
  6. package/dist/admin/api/comparisons-client.js.map +1 -0
  7. package/dist/admin/index.d.ts +37 -0
  8. package/dist/admin/index.d.ts.map +1 -0
  9. package/dist/admin/index.js +49 -0
  10. package/dist/admin/index.js.map +1 -0
  11. package/dist/admin/pages/ComparisonDetailPage.d.ts +19 -0
  12. package/dist/admin/pages/ComparisonDetailPage.d.ts.map +1 -0
  13. package/dist/admin/pages/ComparisonDetailPage.js +62 -0
  14. package/dist/admin/pages/ComparisonDetailPage.js.map +1 -0
  15. package/dist/admin/pages/ComparisonsListPage.d.ts +10 -0
  16. package/dist/admin/pages/ComparisonsListPage.d.ts.map +1 -0
  17. package/dist/admin/pages/ComparisonsListPage.js +71 -0
  18. package/dist/admin/pages/ComparisonsListPage.js.map +1 -0
  19. package/dist/backend/entities/comparison-product.entity.d.ts +22 -0
  20. package/dist/backend/entities/comparison-product.entity.d.ts.map +1 -0
  21. package/dist/backend/entities/comparison-product.entity.js +54 -0
  22. package/dist/backend/entities/comparison-product.entity.js.map +1 -0
  23. package/dist/backend/entities/comparison.entity.d.ts +61 -0
  24. package/dist/backend/entities/comparison.entity.d.ts.map +1 -0
  25. package/dist/backend/entities/comparison.entity.js +115 -0
  26. package/dist/backend/entities/comparison.entity.js.map +1 -0
  27. package/dist/backend/index.d.ts +61 -0
  28. package/dist/backend/index.d.ts.map +1 -0
  29. package/dist/backend/index.js +117 -0
  30. package/dist/backend/index.js.map +1 -0
  31. package/dist/backend/request-actor.d.ts +22 -0
  32. package/dist/backend/request-actor.d.ts.map +1 -0
  33. package/dist/backend/request-actor.js +24 -0
  34. package/dist/backend/request-actor.js.map +1 -0
  35. package/dist/backend/routes.admin.d.ts +22 -0
  36. package/dist/backend/routes.admin.d.ts.map +1 -0
  37. package/dist/backend/routes.admin.js +41 -0
  38. package/dist/backend/routes.admin.js.map +1 -0
  39. package/dist/backend/routes.public.d.ts +33 -0
  40. package/dist/backend/routes.public.d.ts.map +1 -0
  41. package/dist/backend/routes.public.js +215 -0
  42. package/dist/backend/routes.public.js.map +1 -0
  43. package/dist/backend/routes.share.d.ts +27 -0
  44. package/dist/backend/routes.share.d.ts.map +1 -0
  45. package/dist/backend/routes.share.js +49 -0
  46. package/dist/backend/routes.share.js.map +1 -0
  47. package/dist/backend/services/anonymous-token-cookie.d.ts +23 -0
  48. package/dist/backend/services/anonymous-token-cookie.d.ts.map +1 -0
  49. package/dist/backend/services/anonymous-token-cookie.js +33 -0
  50. package/dist/backend/services/anonymous-token-cookie.js.map +1 -0
  51. package/dist/backend/services/asset-byte-fetcher.d.ts +37 -0
  52. package/dist/backend/services/asset-byte-fetcher.d.ts.map +1 -0
  53. package/dist/backend/services/asset-byte-fetcher.js +70 -0
  54. package/dist/backend/services/asset-byte-fetcher.js.map +1 -0
  55. package/dist/backend/services/comparable-attribute-projection.d.ts +57 -0
  56. package/dist/backend/services/comparable-attribute-projection.d.ts.map +1 -0
  57. package/dist/backend/services/comparable-attribute-projection.js +136 -0
  58. package/dist/backend/services/comparable-attribute-projection.js.map +1 -0
  59. package/dist/backend/services/comparison-admin.service.d.ts +31 -0
  60. package/dist/backend/services/comparison-admin.service.d.ts.map +1 -0
  61. package/dist/backend/services/comparison-admin.service.js +184 -0
  62. package/dist/backend/services/comparison-admin.service.js.map +1 -0
  63. package/dist/backend/services/comparison-pdf-renderer.d.ts +57 -0
  64. package/dist/backend/services/comparison-pdf-renderer.d.ts.map +1 -0
  65. package/dist/backend/services/comparison-pdf-renderer.js +211 -0
  66. package/dist/backend/services/comparison-pdf-renderer.js.map +1 -0
  67. package/dist/backend/services/comparison-service.d.ts +339 -0
  68. package/dist/backend/services/comparison-service.d.ts.map +1 -0
  69. package/dist/backend/services/comparison-service.js +588 -0
  70. package/dist/backend/services/comparison-service.js.map +1 -0
  71. package/dist/backend/services/share-token-generator.d.ts +20 -0
  72. package/dist/backend/services/share-token-generator.d.ts.map +1 -0
  73. package/dist/backend/services/share-token-generator.js +23 -0
  74. package/dist/backend/services/share-token-generator.js.map +1 -0
  75. package/dist/manifest.d.ts +199 -0
  76. package/dist/manifest.d.ts.map +1 -0
  77. package/dist/manifest.js +179 -0
  78. package/dist/manifest.js.map +1 -0
  79. package/dist/migrations/20260501T185834_comparisons_init.d.ts +32 -0
  80. package/dist/migrations/20260501T185834_comparisons_init.d.ts.map +1 -0
  81. package/dist/migrations/20260501T185834_comparisons_init.js +102 -0
  82. package/dist/migrations/20260501T185834_comparisons_init.js.map +1 -0
  83. package/dist/migrations/20260830T163143_comparisons_organization_attribution.d.ts +84 -0
  84. package/dist/migrations/20260830T163143_comparisons_organization_attribution.d.ts.map +1 -0
  85. package/dist/migrations/20260830T163143_comparisons_organization_attribution.js +147 -0
  86. package/dist/migrations/20260830T163143_comparisons_organization_attribution.js.map +1 -0
  87. package/dist/migrations/index.d.ts +28 -0
  88. package/dist/migrations/index.d.ts.map +1 -0
  89. package/dist/migrations/index.js +31 -0
  90. package/dist/migrations/index.js.map +1 -0
  91. package/docs/comparisons.md +278 -0
  92. package/i18n/en.json +51 -0
  93. package/i18n/pl.json +51 -0
  94. package/package.json +102 -3
  95. package/tailwind.css +14 -0
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Built-in settings manifest for the comparisons module — feature 007.
3
+ *
4
+ * Reserves the `compare` group. The single `compare.max_products`
5
+ * setting is added in T013 (foundational), so US1 has a real cap to
6
+ * read from day one. Range bounds (`1..16`) are enforced by
7
+ * `ComparisonService.addProduct(...)` per research.md R-7, not by the
8
+ * manifest — the Settings module's value-type registry only recognises
9
+ * primitive shapes.
10
+ */
11
+ export declare const COMPARE_SETTING_CODES: {
12
+ readonly MAX_PRODUCTS: "compare.max_products";
13
+ };
14
+ export declare const DEFAULT_COMPARE_MAX_PRODUCTS = 4;
15
+ /** Module-lifecycle manifest (feature 018). */
16
+ export declare const manifest: {
17
+ id: string;
18
+ name: string;
19
+ version: string;
20
+ dependencies: string[];
21
+ description?: string | undefined;
22
+ acknowledgedDependencies?: {
23
+ moduleId: string;
24
+ port: string;
25
+ reason: string;
26
+ }[] | undefined;
27
+ nonBindingDependencies?: {
28
+ moduleId: string;
29
+ name: string;
30
+ kind: "contributes-to" | "degrades-without" | "refuses-without";
31
+ reason: string;
32
+ whenAbsent?: string | undefined;
33
+ }[] | undefined;
34
+ activation?: {
35
+ settingCode: string;
36
+ default: boolean;
37
+ } | {
38
+ nonDeactivatable: true;
39
+ reason: string;
40
+ } | undefined;
41
+ settings?: {
42
+ moduleCode: string;
43
+ groups: {
44
+ code: string;
45
+ name: string;
46
+ salesChannelCodes?: string[] | undefined;
47
+ isSystemProtected?: boolean | undefined;
48
+ }[];
49
+ settings: {
50
+ code: string;
51
+ name: string;
52
+ valueType: "string" | "number" | "boolean" | "json" | "string_list" | "secret" | "credential_ref";
53
+ defaultValue: unknown;
54
+ description?: string | undefined;
55
+ groupCode?: string | undefined;
56
+ previousDefaultValues?: unknown[] | undefined;
57
+ salesChannelCodes?: string[] | undefined;
58
+ enumOptions?: string[] | undefined;
59
+ configurationType?: string | undefined;
60
+ hidden?: boolean | undefined;
61
+ }[];
62
+ } | undefined;
63
+ i18n?: {
64
+ bundlesDir: string;
65
+ } | undefined;
66
+ docs?: false | {
67
+ dir: string;
68
+ } | undefined;
69
+ demo?: false | {
70
+ summary: string;
71
+ seed: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoSeedResult>;
72
+ reset: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoResetResult>;
73
+ after?: readonly string[] | undefined;
74
+ package?: string | undefined;
75
+ } | undefined;
76
+ actions?: {
77
+ id: string;
78
+ labelKey: string;
79
+ icon: "Plus" | "Sparkles" | "Settings" | "Search" | "Boxes" | "Layers" | "Menu" | "PlusCircle" | "PlusSquare" | "FilePlus" | "FolderPlus" | "Upload" | "FileUp" | "CloudUpload" | "Download" | "FileDown" | "FileText" | "BookOpen" | "Rss" | "Package" | "Tag" | "ShoppingCart" | "Receipt" | "CreditCard" | "Users" | "UserPlus" | "Inbox" | "ListChecks" | "ClipboardList" | "Image" | "Video" | "LayoutDashboard" | "PanelLeft" | "KeyRound" | "ShieldCheck" | "Edit" | "Archive" | "Box" | "Truck" | "CircleDollarSign" | "Activity" | "LineChart" | "Smartphone" | "Webhook" | "Scale" | "PlugZap" | "PercentDiamond" | "Newspaper" | "Languages" | "Eraser" | "Warehouse" | "TrendingDown" | "Bell" | "PackageOpen" | "Building2" | "Store" | "ClipboardCheck";
80
+ targetRoute: string;
81
+ keywords: string[];
82
+ weight: number;
83
+ descriptionKey?: string | undefined;
84
+ requiredPermission?: string | undefined;
85
+ }[] | undefined;
86
+ permissions?: {
87
+ code: string;
88
+ label: string;
89
+ module?: string | undefined;
90
+ description?: string | undefined;
91
+ requires?: string[] | undefined;
92
+ }[] | undefined;
93
+ transactionalEmails?: {
94
+ code: string;
95
+ name: string;
96
+ variables: {
97
+ key: string;
98
+ label: string;
99
+ sampleValue?: string | undefined;
100
+ description?: string | undefined;
101
+ }[];
102
+ description?: string | undefined;
103
+ group?: string | undefined;
104
+ }[] | undefined;
105
+ capabilities?: string[] | undefined;
106
+ exclusiveCapabilities?: {
107
+ key: string;
108
+ errorCode: string;
109
+ }[] | undefined;
110
+ errorCodes?: {
111
+ code: string;
112
+ tokens?: string[] | undefined;
113
+ }[] | undefined;
114
+ blocks?: {
115
+ name: string;
116
+ labelKey: string;
117
+ category: string;
118
+ contexts: ("email" | "invoice" | "cms" | "newsletter")[];
119
+ fields: Record<string, {
120
+ type: "number" | "object" | "array" | "text" | "textarea" | "select" | "radio" | "external" | "uuid" | "richtext";
121
+ label?: string | undefined;
122
+ required?: boolean | undefined;
123
+ options?: {
124
+ label: string;
125
+ value: string | number;
126
+ }[] | undefined;
127
+ refKind?: string | undefined;
128
+ }>;
129
+ descriptionKey?: string | undefined;
130
+ defaultProps?: Record<string, unknown> | undefined;
131
+ responsiveFields?: string[] | undefined;
132
+ previewIcon?: string | undefined;
133
+ weight?: number | undefined;
134
+ }[] | undefined;
135
+ blockCategories?: {
136
+ key: string;
137
+ titleKey: string;
138
+ contexts: ("email" | "invoice" | "cms" | "newsletter")[];
139
+ weight?: number | undefined;
140
+ visible?: boolean | undefined;
141
+ }[] | undefined;
142
+ env?: {
143
+ name: string;
144
+ describes: {
145
+ en: string;
146
+ pl: string;
147
+ };
148
+ requirement: {
149
+ kind: "required";
150
+ } | {
151
+ kind: "requiredWhen";
152
+ input: string;
153
+ equals: string;
154
+ } | {
155
+ kind: "optional";
156
+ without: {
157
+ en: string;
158
+ pl: string;
159
+ };
160
+ };
161
+ secret: boolean;
162
+ generable: boolean;
163
+ owner: {
164
+ kind: "platform";
165
+ } | {
166
+ kind: "application";
167
+ application: "admin" | "backend" | "storefront";
168
+ } | {
169
+ kind: "module";
170
+ moduleId: string;
171
+ };
172
+ consumers: ("admin" | "backend" | "storefront")[];
173
+ addressOf: "admin" | "backend" | "storefront" | null;
174
+ }[] | undefined;
175
+ };
176
+ /** Legacy export retained for backward compatibility. */
177
+ export declare const comparisonsManifest: {
178
+ moduleCode: string;
179
+ groups: {
180
+ code: string;
181
+ name: string;
182
+ salesChannelCodes?: string[] | undefined;
183
+ isSystemProtected?: boolean | undefined;
184
+ }[];
185
+ settings: {
186
+ code: string;
187
+ name: string;
188
+ valueType: "string" | "number" | "boolean" | "json" | "string_list" | "secret" | "credential_ref";
189
+ defaultValue: unknown;
190
+ description?: string | undefined;
191
+ groupCode?: string | undefined;
192
+ previousDefaultValues?: unknown[] | undefined;
193
+ salesChannelCodes?: string[] | undefined;
194
+ enumOptions?: string[] | undefined;
195
+ configurationType?: string | undefined;
196
+ hidden?: boolean | undefined;
197
+ }[];
198
+ };
199
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAKA;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB;;CAExB,CAAC;AAEX,eAAO,MAAM,4BAA4B,IAAI,CAAC;AAiC9C,+CAA+C;AAC/C,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoInB,CAAC;AAEH,yDAAyD;AACzD,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;CAAW,CAAC"}
@@ -0,0 +1,179 @@
1
+ import { defineModuleManifest, defineModuleSettingsManifest, } from '@endora-commerce/contracts';
2
+ /**
3
+ * Built-in settings manifest for the comparisons module — feature 007.
4
+ *
5
+ * Reserves the `compare` group. The single `compare.max_products`
6
+ * setting is added in T013 (foundational), so US1 has a real cap to
7
+ * read from day one. Range bounds (`1..16`) are enforced by
8
+ * `ComparisonService.addProduct(...)` per research.md R-7, not by the
9
+ * manifest — the Settings module's value-type registry only recognises
10
+ * primitive shapes.
11
+ */
12
+ export const COMPARE_SETTING_CODES = {
13
+ MAX_PRODUCTS: 'compare.max_products',
14
+ };
15
+ export const DEFAULT_COMPARE_MAX_PRODUCTS = 4;
16
+ const settings = defineModuleSettingsManifest({
17
+ moduleCode: 'comparisons',
18
+ groups: [
19
+ {
20
+ code: 'compare',
21
+ name: 'Compare',
22
+ },
23
+ ],
24
+ settings: [
25
+ {
26
+ // Feature 073 — the operator's activation control. Platform-wide.
27
+ code: 'comparisons.enabled',
28
+ name: 'Product comparison enabled',
29
+ description: 'Switches the product-comparison feature on or off: the storefront compare list, the shareable comparison links and the admin screens. Nothing is dropped — saved comparisons stay in the database and their share links work again when you switch it back on. While it is off, a comparison list an anonymous visitor built is no longer adopted when they sign in, and expires with its own cookie.',
30
+ groupCode: 'compare',
31
+ valueType: 'boolean',
32
+ defaultValue: true,
33
+ },
34
+ {
35
+ code: COMPARE_SETTING_CODES.MAX_PRODUCTS,
36
+ name: 'Maximum products per comparison',
37
+ description: 'Upper bound on how many products a customer can place in a single Comparison. The Compare page and PDF export are sized for this number; the storefront refuses to add a (max+1)-th product. Sensible range 1..16; defaults to 4.',
38
+ groupCode: 'compare',
39
+ valueType: 'number',
40
+ defaultValue: DEFAULT_COMPARE_MAX_PRODUCTS,
41
+ },
42
+ ],
43
+ });
44
+ /** Module-lifecycle manifest (feature 018). */
45
+ export const manifest = defineModuleManifest({
46
+ id: 'comparisons',
47
+ name: 'Compare Products',
48
+ description: 'Customer-facing product comparison feature with shareable links and PDF export.',
49
+ version: '1.0.0',
50
+ // `auth` owns the `requireAdmin` port the admin routes are gated by; feature
51
+ // 072 made it a container resolution rather than an optional argument that
52
+ // decided whether the admin surface existed at all.
53
+ //
54
+ // `price_lists` joined in issue #132: a comparison column shows a price, and
55
+ // it now resolves that price through the `pricingService` port instead of
56
+ // projecting the catalogue's legacy attribute. The edge binds — comparing
57
+ // products on prices the platform is refusing to serve is exactly the
58
+ // failure the port gate exists to prevent.
59
+ //
60
+ // `organizations` joined with the viewer-priced comparison: the columns are
61
+ // resolved for whoever is looking, and the engine selects a group-targeted
62
+ // price list by the buyer's customer group, which is a field on the
63
+ // organisation row.
64
+ dependencies: [
65
+ 'catalog',
66
+ 'customer_accounts',
67
+ 'organizations',
68
+ 'price_lists',
69
+ 'sales_channels',
70
+ 'settings',
71
+ 'auth',
72
+ ],
73
+ settings,
74
+ /**
75
+ * The operator-visible error codes this module owns — feature 090, Phase 3
76
+ * (`specs/090-module-owned-error-codes/migration-runbook.md`).
77
+ *
78
+ * The list is the incumbent prefix chain's *answer* for `comparisons`, copied
79
+ * from the frozen capture at
80
+ * `backend/test/fixtures/error-code-routing/chain-answers.ts`
81
+ * (`grep -oE "^ [A-Z0-9_]+: 'comparisons'," …`). It is a transcription, not a
82
+ * judgement: the migration is answer-preserving over all 289 codes and
83
+ * re-routing is out of scope (§6.2, §6.5).
84
+ *
85
+ * **Trap T1 does not bite here, and that was measured rather than assumed.**
86
+ * The chain answers `comparisons` from one prefix and one identity —
87
+ * `code.startsWith('COMPARISON_') || code === ERROR_CODES.PDF_GENERATION_FAILED`.
88
+ * Three enumeration members carry the prefix and all three survive to this
89
+ * rule; no earlier rule names `PDF_GENERATION_FAILED`. So reading the chain's
90
+ * source would have given the same four as reading its answer. `inventory` and
91
+ * `assets_library` are where it does bite; this is another module where the two
92
+ * agree, which is worth recording so the next reader knows the question was
93
+ * asked.
94
+ *
95
+ * **One code a reader will look for here and not find: `PRODUCT_NOT_IN_COMPARISON`.**
96
+ * It names this module's own noun, it is raised in this package and nowhere
97
+ * else — `routes.public.ts`' `translate()` converts
98
+ * `ProductNotInComparisonError` into a 404 with it — and it is `catalog`'s,
99
+ * declared there in !1117. The chain never reaches the `COMPARISON_` rule with
100
+ * it, because the `PRODUCT_` prefix runs first and wins; the runbook's trap T2
101
+ * names this exact code as a routing decision an earlier feature made
102
+ * deliberately. Do not move it here.
103
+ *
104
+ * **The inverse: this package raises four codes it does not own.** All four are
105
+ * in `routes.public.ts` — `PRODUCT_NOT_IN_COMPARISON` and `PRODUCT_NOT_FOUND`
106
+ * are `catalog`'s (both from `translate()`), and `INTERNAL` and
107
+ * `VALIDATION_FAILED` are the platform's. That is D-95.2 from the thrower's
108
+ * side: routing follows the domain noun, and "the product does not exist" stays
109
+ * a catalogue noun however it is reached. None is declared here.
110
+ *
111
+ * **No code is raised by nothing.** All four have a live raise site in this
112
+ * package, on routes mounted unconditionally: `COMPARISON_EMPTY` and
113
+ * `PDF_GENERATION_FAILED` in the `GET /api/v1/comparisons/me/pdf` handler
114
+ * (`index.ts` always supplies a real `ComparisonPdfRenderer`, so the `if
115
+ * (pdfRenderer)` guard around them is not a closed door), `COMPARISON_FULL`
116
+ * from `translate()` over `ComparisonFullError`, and `COMPARISON_NOT_FOUND`
117
+ * three times — the `notFoundComparison()` / `notFound()` factories in
118
+ * `routes.public.ts` and `routes.share.ts`, and an inline throw in
119
+ * `routes.admin.ts`. So this module contributes nothing to the deferred-defect
120
+ * register's entry on codes no client can receive.
121
+ *
122
+ * Both spellings were searched, which trap T12 asks for: `ERROR_CODES.<CODE>`
123
+ * and the bare quoted literal, over `packages` and `backend/src`. This package
124
+ * writes no bare-literal `new HttpError(<status>, '<CODE>', …)` at all.
125
+ *
126
+ * No `tokens`, derived rather than assumed. `refusalToken`
127
+ * (`packages/platform/src/http/error-envelope.ts`) reads `details.code` and
128
+ * nothing else; every raise site above was read and not one passes a fourth
129
+ * argument, so no `details.code` can exist. The runbook's §5 raise-site scan
130
+ * attributes the tree's ten token-carrying codes over 41 sites to `core`,
131
+ * `invoices` and `carts` and names none of these; in the other direction the
132
+ * module's bundles hold exactly four `errors.<CODE>` sentences in each language
133
+ * and no `errors.<CODE>.<token>` key, so there is no dead sentence either.
134
+ */
135
+ errorCodes: [
136
+ { code: 'COMPARISON_EMPTY' },
137
+ { code: 'COMPARISON_FULL' },
138
+ { code: 'COMPARISON_NOT_FOUND' },
139
+ { code: 'PDF_GENERATION_FAILED' },
140
+ ],
141
+ i18n: { bundlesDir: 'i18n' },
142
+ docs: { dir: 'docs' },
143
+ permissions: [{ code: 'comparisons:read', label: 'View product comparisons' }],
144
+ /**
145
+ * The module's command-palette entry — feature 091, Phase 4 (the plan's
146
+ * batch 6), and one of the fifteen Principle XVI entries
147
+ * `specs/deferred-defects.md` records as owed.
148
+ *
149
+ * It arrives with the drain rather than before it, by the mechanism that
150
+ * register predicts: the batch owes an off-state proof over every surface the
151
+ * module contributes, and until this declaration existed the `AppShell.tsx`
152
+ * `PALETTE_ITEMS` row that advertised /comparisons was the admin's own — a
153
+ * hand-written copy no server-side presence check was ever asked about, so it
154
+ * went on offering the screen to an operator who had switched the module off.
155
+ * The Actions group is resolved by `AdminActionsService` against the effective
156
+ * enabled-set, which is what makes the withdrawal real.
157
+ *
158
+ * `requiredPermission` is the code the target route enforces, which
159
+ * `check:action-route-permissions` compares against the registration on
160
+ * `GET /api/v1/admin/comparisons` itself. The admin side of this module is read-only, so the read
161
+ * code is the whole of what it gates.
162
+ */
163
+ actions: [
164
+ {
165
+ id: 'open-comparisons',
166
+ labelKey: 'actions.openComparisons.label',
167
+ descriptionKey: 'actions.openComparisons.description',
168
+ icon: 'Scale',
169
+ targetRoute: '/comparisons',
170
+ requiredPermission: 'comparisons:read',
171
+ keywords: ['compare', 'comparison', 'comparisons', 'porównanie', 'porównania'],
172
+ weight: 600,
173
+ },
174
+ ],
175
+ activation: { settingCode: 'comparisons.enabled', default: true },
176
+ });
177
+ /** Legacy export retained for backward compatibility. */
178
+ export const comparisonsManifest = settings;
179
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,4BAA4B,GAC7B,MAAM,4BAA4B,CAAC;AAEpC;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,YAAY,EAAE,sBAAsB;CAC5B,CAAC;AAEX,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,CAAC;AAE9C,MAAM,QAAQ,GAAG,4BAA4B,CAAC;IAC5C,UAAU,EAAE,aAAa;IACzB,MAAM,EAAE;QACN;YACE,IAAI,EAAE,SAAS;YACf,IAAI,EAAE,SAAS;SAChB;KACF;IACD,QAAQ,EAAE;QACR;YACE,kEAAkE;YAClE,IAAI,EAAE,qBAAqB;YAC3B,IAAI,EAAE,4BAA4B;YAClC,WAAW,EACT,uYAAuY;YACzY,SAAS,EAAE,SAAS;YACpB,SAAS,EAAE,SAAS;YACpB,YAAY,EAAE,IAAI;SACnB;QACD;YACE,IAAI,EAAE,qBAAqB,CAAC,YAAY;YACxC,IAAI,EAAE,iCAAiC;YACvC,WAAW,EACT,mOAAmO;YACrO,SAAS,EAAE,SAAS;YACpB,SAAS,EAAE,QAAQ;YACnB,YAAY,EAAE,4BAA4B;SAC3C;KACF;CACF,CAAC,CAAC;AAEH,+CAA+C;AAC/C,MAAM,CAAC,MAAM,QAAQ,GAAG,oBAAoB,CAAC;IAC3C,EAAE,EAAE,aAAa;IACjB,IAAI,EAAE,kBAAkB;IACxB,WAAW,EACT,iFAAiF;IACnF,OAAO,EAAE,OAAO;IAChB,6EAA6E;IAC7E,2EAA2E;IAC3E,oDAAoD;IACpD,EAAE;IACF,6EAA6E;IAC7E,0EAA0E;IAC1E,0EAA0E;IAC1E,sEAAsE;IACtE,2CAA2C;IAC3C,EAAE;IACF,4EAA4E;IAC5E,2EAA2E;IAC3E,oEAAoE;IACpE,oBAAoB;IACpB,YAAY,EAAE;QACZ,SAAS;QACT,mBAAmB;QACnB,eAAe;QACf,aAAa;QACb,gBAAgB;QAChB,UAAU;QACV,MAAM;KACP;IACD,QAAQ;IACR;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4DG;IACH,UAAU,EAAE;QACV,EAAE,IAAI,EAAE,kBAAkB,EAAE;QAC5B,EAAE,IAAI,EAAE,iBAAiB,EAAE;QAC3B,EAAE,IAAI,EAAE,sBAAsB,EAAE;QAChC,EAAE,IAAI,EAAE,uBAAuB,EAAE;KAClC;IACD,IAAI,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE;IAC5B,IAAI,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE;IACrB,WAAW,EAAE,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC;IAC9E;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,EAAE;QACP;YACE,EAAE,EAAE,kBAAkB;YACtB,QAAQ,EAAE,+BAA+B;YACzC,cAAc,EAAE,qCAAqC;YACrD,IAAI,EAAE,OAAO;YACb,WAAW,EAAE,cAAc;YAC3B,kBAAkB,EAAE,kBAAkB;YACtC,QAAQ,EAAE,CAAC,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,CAAC;YAC9E,MAAM,EAAE,GAAG;SACZ;KACF;IACD,UAAU,EAAE,EAAE,WAAW,EAAE,qBAAqB,EAAE,OAAO,EAAE,IAAI,EAAE;CAClE,CAAC,CAAC;AAEH,yDAAyD;AACzD,MAAM,CAAC,MAAM,mBAAmB,GAAG,QAAQ,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Comparisons module — feature 007 / T010 / data-model.md §1.1 + §1.2.
4
+ *
5
+ * Single migration owning both tables of the comparisons module:
6
+ *
7
+ * - `comparisons` — the Comparison resource. Owner is exclusively a
8
+ * customer account OR an anonymous browser session (CHECK enforces
9
+ * mutual exclusivity). `share_token` is a 22-char base64url string
10
+ * unique across all live comparisons; the share-token endpoint
11
+ * resolves it in O(1).
12
+ * - `comparison_products` — ordered bridge between a Comparison and
13
+ * the Products it holds. Composite PK on (comparison_id, product_id);
14
+ * `position` defines column order on the comparison page and PDF.
15
+ * Cascades on both parents: removing a Product removes it from every
16
+ * Comparison that referenced it; removing a Comparison removes its
17
+ * bridge rows.
18
+ *
19
+ * Indexing rationale comes from the data model:
20
+ * - UNIQUE(share_token) supports findByShareToken in O(1) (R-4 share
21
+ * tokens are unguessable; this is the lookup path).
22
+ * - Partial indexes on (customer_account_id) WHERE NOT NULL and on
23
+ * (anonymous_token) WHERE NOT NULL support "find my comparison" for
24
+ * each owner kind without bloating the index for the unused column.
25
+ * - (created_at desc) and (sales_channel_id, created_at desc) support
26
+ * the admin overview's default sort and channel filter (US5).
27
+ */
28
+ export declare class Migration20260501T185834ComparisonsInit extends Migration {
29
+ up(): Promise<void>;
30
+ down(): Promise<void>;
31
+ }
32
+ //# sourceMappingURL=20260501T185834_comparisons_init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260501T185834_comparisons_init.d.ts","sourceRoot":"","sources":["../../src/migrations/20260501T185834_comparisons_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,uCAAwC,SAAQ,SAAS;IACrD,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAyEnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAIrC"}
@@ -0,0 +1,102 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Comparisons module — feature 007 / T010 / data-model.md §1.1 + §1.2.
4
+ *
5
+ * Single migration owning both tables of the comparisons module:
6
+ *
7
+ * - `comparisons` — the Comparison resource. Owner is exclusively a
8
+ * customer account OR an anonymous browser session (CHECK enforces
9
+ * mutual exclusivity). `share_token` is a 22-char base64url string
10
+ * unique across all live comparisons; the share-token endpoint
11
+ * resolves it in O(1).
12
+ * - `comparison_products` — ordered bridge between a Comparison and
13
+ * the Products it holds. Composite PK on (comparison_id, product_id);
14
+ * `position` defines column order on the comparison page and PDF.
15
+ * Cascades on both parents: removing a Product removes it from every
16
+ * Comparison that referenced it; removing a Comparison removes its
17
+ * bridge rows.
18
+ *
19
+ * Indexing rationale comes from the data model:
20
+ * - UNIQUE(share_token) supports findByShareToken in O(1) (R-4 share
21
+ * tokens are unguessable; this is the lookup path).
22
+ * - Partial indexes on (customer_account_id) WHERE NOT NULL and on
23
+ * (anonymous_token) WHERE NOT NULL support "find my comparison" for
24
+ * each owner kind without bloating the index for the unused column.
25
+ * - (created_at desc) and (sales_channel_id, created_at desc) support
26
+ * the admin overview's default sort and channel filter (US5).
27
+ */
28
+ export class Migration20260501T185834ComparisonsInit extends Migration {
29
+ async up() {
30
+ this.addSql(`
31
+ create table "comparisons" (
32
+ "id" uuid not null,
33
+ "share_token" varchar(32) not null,
34
+ "customer_account_id" uuid null,
35
+ "anonymous_token" varchar(32) null,
36
+ "sales_channel_id" uuid not null,
37
+ "display_mode" varchar(16) not null default 'all',
38
+ "created_at" timestamptz not null default now(),
39
+ "updated_at" timestamptz not null default now(),
40
+ constraint "comparisons_pkey" primary key ("id"),
41
+ constraint "comparisons_share_token_uq" unique ("share_token"),
42
+ constraint "comparisons_owner_xor_chk"
43
+ check ((customer_account_id is null) <> (anonymous_token is null)),
44
+ constraint "comparisons_customer_account_fk"
45
+ foreign key ("customer_account_id") references "customer_accounts" ("id")
46
+ on delete cascade,
47
+ constraint "comparisons_sales_channel_fk"
48
+ foreign key ("sales_channel_id") references "sales_channels" ("id")
49
+ on delete restrict
50
+ );
51
+ `);
52
+ // Partial owner indexes — only one of the two columns is set per row;
53
+ // partial indexes keep them tight without a useless second key.
54
+ this.addSql(`
55
+ create index "idx_comparisons_owner_customer"
56
+ on "comparisons" ("customer_account_id")
57
+ where "customer_account_id" is not null;
58
+ `);
59
+ this.addSql(`
60
+ create index "idx_comparisons_owner_anon"
61
+ on "comparisons" ("anonymous_token")
62
+ where "anonymous_token" is not null;
63
+ `);
64
+ // Admin overview sort + channel filter (US5).
65
+ this.addSql(`
66
+ create index "idx_comparisons_admin_listing"
67
+ on "comparisons" ("created_at" desc);
68
+ `);
69
+ this.addSql(`
70
+ create index "idx_comparisons_channel_recent"
71
+ on "comparisons" ("sales_channel_id", "created_at" desc);
72
+ `);
73
+ // Bridge — ordered set of products in a comparison.
74
+ this.addSql(`
75
+ create table "comparison_products" (
76
+ "comparison_id" uuid not null,
77
+ "product_id" uuid not null,
78
+ "position" smallint not null,
79
+ "added_at" timestamptz not null default now(),
80
+ constraint "comparison_products_pkey"
81
+ primary key ("comparison_id", "product_id"),
82
+ constraint "comparison_products_comparison_fk"
83
+ foreign key ("comparison_id") references "comparisons" ("id")
84
+ on delete cascade,
85
+ constraint "comparison_products_product_fk"
86
+ foreign key ("product_id") references "products" ("id")
87
+ on delete cascade
88
+ );
89
+ `);
90
+ // Reverse lookup — supports "is this product in any comparison?" and
91
+ // makes the products → CASCADE deletion path efficient.
92
+ this.addSql(`
93
+ create index "idx_comparison_products_product"
94
+ on "comparison_products" ("product_id");
95
+ `);
96
+ }
97
+ async down() {
98
+ this.addSql('drop table if exists "comparison_products" cascade;');
99
+ this.addSql('drop table if exists "comparisons" cascade;');
100
+ }
101
+ }
102
+ //# sourceMappingURL=20260501T185834_comparisons_init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260501T185834_comparisons_init.js","sourceRoot":"","sources":["../../src/migrations/20260501T185834_comparisons_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,OAAO,uCAAwC,SAAQ,SAAS;IAC3D,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;KAqBX,CAAC,CAAC;QAEH,sEAAsE;QACtE,gEAAgE;QAChE,IAAI,CAAC,MAAM,CAAC;;;;KAIX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC;;;;KAIX,CAAC,CAAC;QAEH,8CAA8C;QAC9C,IAAI,CAAC,MAAM,CAAC;;;KAGX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC;;;KAGX,CAAC,CAAC;QAEH,oDAAoD;QACpD,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;KAeX,CAAC,CAAC;QAEH,qEAAqE;QACrE,wDAAwD;QACxD,IAAI,CAAC,MAAM,CAAC;;;KAGX,CAAC,CAAC;IACL,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,qDAAqD,CAAC,CAAC;QACnE,IAAI,CAAC,MAAM,CAAC,6CAA6C,CAAC,CAAC;IAC7D,CAAC;CACF"}
@@ -0,0 +1,84 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * D-187 — `comparisons` gains its organisation, and a row that names a customer
4
+ * account carries one.
5
+ *
6
+ * Feature 087 Group B, class 1 of 4. `Comparison` is `@CustomerScoped`, and
7
+ * until this migration the `allowed-set` arm of `customerFilterCond` had no
8
+ * column to grant on: a sales representative assigned to the buyer's own
9
+ * organisation was shown **none** of their comparisons and told so, through the
10
+ * `ORGANIZATION_ATTRIBUTION_PENDING` notice the refusing arm records. The
11
+ * column is what turns that into an answer.
12
+ *
13
+ * ## The column and the read arrive together, on purpose
14
+ *
15
+ * `customerOrganizationColumn` asks the ORM's own metadata whether the filtered
16
+ * entity carries `organizationId`, **per query**, so the entity property in
17
+ * this same merge request is what switches the grant on — there is no third
18
+ * artefact, no flag and no staging. At the same instant the notice retires
19
+ * itself: it is keyed on the column's *absence*.
20
+ *
21
+ * That pairing is why this migration is not "column and backfill". From the
22
+ * moment the grant is live, a row inserted without an organisation is invisible
23
+ * to the representative who serves that organisation, on a screen that has just
24
+ * stopped explaining itself — and MikroORM applies no filter to `INSERT`
25
+ * (`r1-spike.md` §4, measured), so the filter cannot refuse it. The `CHECK`
26
+ * below is the only refusal an `INSERT` has.
27
+ *
28
+ * ## An implication, not an equivalence
29
+ *
30
+ * `customer_account_id is null or organization_id is not null` is FR-010 and
31
+ * FR-011 together and nothing wider: an **owned** row has an organisation, an
32
+ * **ownerless** row need not. Half of this table is ownerless by construction —
33
+ * `comparisons_owner_xor_chk` makes every row that is not owned an anonymous
34
+ * one — and who an anonymous comparison belongs to is R-6's open question. The
35
+ * equivalence would answer it in the schema, which D-187 declines to do.
36
+ *
37
+ * ## No foreign key
38
+ *
39
+ * Matching `Cart`, which carries none. D-187 withdraws `r1-spike.md` §8's
40
+ * `on delete set null` recommendation as contradicting FR-011: it produces a
41
+ * row that names an account and no organisation, which is precisely the state
42
+ * this migration makes unreachable, and under the constraint below it would
43
+ * abort an unrelated organisation delete with a message about a table the
44
+ * operator was not touching. If one is ever taken here it must be `restrict`,
45
+ * and it is not this migration's decision.
46
+ *
47
+ * ## Derive, then count, then refuse — never delete
48
+ *
49
+ * The derivation is the owning account's own organisation, and on **this**
50
+ * table it is total by proof rather than by hope.
51
+ * `comparisons_customer_account_fk` is `on delete cascade`, so a row naming an
52
+ * account that is gone cannot exist; `comparisons_owner_xor_chk` makes every
53
+ * other row anonymous; and D-178 made `customer_accounts.organization_id`
54
+ * `NOT NULL`. *Derivable ⟺ has an account* is a theorem here, and the refusal
55
+ * branch is dead code the day it is written.
56
+ *
57
+ * It is written anyway, and that is the point of writing it: a proof that stops
58
+ * being true stops **silently**. Drop the foreign key, relax the XOR, or
59
+ * re-open D-178, and the count below is the only thing that would notice. It
60
+ * raises with the count and up to twenty ids and deletes nothing (D-184), in
61
+ * the shape `20260825T141659_customer_accounts_organization_required.ts`
62
+ * established and `20260830T112911_carts_organization_attribution_check.ts`
63
+ * repeated.
64
+ *
65
+ * ## Nothing to declare in the manifest
66
+ *
67
+ * `comparisons` already declares `organizations`, whose own
68
+ * `20260424T205317_organizations_init.ts` creates `customer_accounts` — far
69
+ * below `BASELINE_THROUGH`, so the table this reads exists whatever this
70
+ * module's position in the dependency order. No foreign key is added, so
71
+ * `fk-dependency-drift` has nothing to say either, and a migration naming
72
+ * another module's table is outside `check:module-boundary`'s population by
73
+ * that check's own rule.
74
+ *
75
+ * The standing guard is
76
+ * `backend/test/integration/tenancy/customer-scoped-organization-completeness.test.ts`,
77
+ * which derives its population from the ORM's metadata rather than from a list
78
+ * — so this class is covered by gaining the column, with no edit to that file.
79
+ */
80
+ export declare class Migration20260830T163143ComparisonsOrganizationAttribution extends Migration {
81
+ up(): Promise<void>;
82
+ down(): Promise<void>;
83
+ }
84
+ //# sourceMappingURL=20260830T163143_comparisons_organization_attribution.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260830T163143_comparisons_organization_attribution.d.ts","sourceRoot":"","sources":["../../src/migrations/20260830T163143_comparisons_organization_attribution.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6EG;AACH,qBAAa,0DAA2D,SAAQ,SAAS;IACxE,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IA6DnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAWrC"}