@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.
- package/LICENSE +21 -0
- package/README.md +61 -2
- package/dist/admin/api/comparisons-client.d.ts +24 -0
- package/dist/admin/api/comparisons-client.d.ts.map +1 -0
- package/dist/admin/api/comparisons-client.js +15 -0
- package/dist/admin/api/comparisons-client.js.map +1 -0
- package/dist/admin/index.d.ts +37 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +49 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/ComparisonDetailPage.d.ts +19 -0
- package/dist/admin/pages/ComparisonDetailPage.d.ts.map +1 -0
- package/dist/admin/pages/ComparisonDetailPage.js +62 -0
- package/dist/admin/pages/ComparisonDetailPage.js.map +1 -0
- package/dist/admin/pages/ComparisonsListPage.d.ts +10 -0
- package/dist/admin/pages/ComparisonsListPage.d.ts.map +1 -0
- package/dist/admin/pages/ComparisonsListPage.js +71 -0
- package/dist/admin/pages/ComparisonsListPage.js.map +1 -0
- package/dist/backend/entities/comparison-product.entity.d.ts +22 -0
- package/dist/backend/entities/comparison-product.entity.d.ts.map +1 -0
- package/dist/backend/entities/comparison-product.entity.js +54 -0
- package/dist/backend/entities/comparison-product.entity.js.map +1 -0
- package/dist/backend/entities/comparison.entity.d.ts +61 -0
- package/dist/backend/entities/comparison.entity.d.ts.map +1 -0
- package/dist/backend/entities/comparison.entity.js +115 -0
- package/dist/backend/entities/comparison.entity.js.map +1 -0
- package/dist/backend/index.d.ts +61 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +117 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/request-actor.d.ts +22 -0
- package/dist/backend/request-actor.d.ts.map +1 -0
- package/dist/backend/request-actor.js +24 -0
- package/dist/backend/request-actor.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +22 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +41 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.public.d.ts +33 -0
- package/dist/backend/routes.public.d.ts.map +1 -0
- package/dist/backend/routes.public.js +215 -0
- package/dist/backend/routes.public.js.map +1 -0
- package/dist/backend/routes.share.d.ts +27 -0
- package/dist/backend/routes.share.d.ts.map +1 -0
- package/dist/backend/routes.share.js +49 -0
- package/dist/backend/routes.share.js.map +1 -0
- package/dist/backend/services/anonymous-token-cookie.d.ts +23 -0
- package/dist/backend/services/anonymous-token-cookie.d.ts.map +1 -0
- package/dist/backend/services/anonymous-token-cookie.js +33 -0
- package/dist/backend/services/anonymous-token-cookie.js.map +1 -0
- package/dist/backend/services/asset-byte-fetcher.d.ts +37 -0
- package/dist/backend/services/asset-byte-fetcher.d.ts.map +1 -0
- package/dist/backend/services/asset-byte-fetcher.js +70 -0
- package/dist/backend/services/asset-byte-fetcher.js.map +1 -0
- package/dist/backend/services/comparable-attribute-projection.d.ts +57 -0
- package/dist/backend/services/comparable-attribute-projection.d.ts.map +1 -0
- package/dist/backend/services/comparable-attribute-projection.js +136 -0
- package/dist/backend/services/comparable-attribute-projection.js.map +1 -0
- package/dist/backend/services/comparison-admin.service.d.ts +31 -0
- package/dist/backend/services/comparison-admin.service.d.ts.map +1 -0
- package/dist/backend/services/comparison-admin.service.js +184 -0
- package/dist/backend/services/comparison-admin.service.js.map +1 -0
- package/dist/backend/services/comparison-pdf-renderer.d.ts +57 -0
- package/dist/backend/services/comparison-pdf-renderer.d.ts.map +1 -0
- package/dist/backend/services/comparison-pdf-renderer.js +211 -0
- package/dist/backend/services/comparison-pdf-renderer.js.map +1 -0
- package/dist/backend/services/comparison-service.d.ts +339 -0
- package/dist/backend/services/comparison-service.d.ts.map +1 -0
- package/dist/backend/services/comparison-service.js +588 -0
- package/dist/backend/services/comparison-service.js.map +1 -0
- package/dist/backend/services/share-token-generator.d.ts +20 -0
- package/dist/backend/services/share-token-generator.d.ts.map +1 -0
- package/dist/backend/services/share-token-generator.js +23 -0
- package/dist/backend/services/share-token-generator.js.map +1 -0
- package/dist/manifest.d.ts +199 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +179 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260501T185834_comparisons_init.d.ts +32 -0
- package/dist/migrations/20260501T185834_comparisons_init.d.ts.map +1 -0
- package/dist/migrations/20260501T185834_comparisons_init.js +102 -0
- package/dist/migrations/20260501T185834_comparisons_init.js.map +1 -0
- package/dist/migrations/20260830T163143_comparisons_organization_attribution.d.ts +84 -0
- package/dist/migrations/20260830T163143_comparisons_organization_attribution.d.ts.map +1 -0
- package/dist/migrations/20260830T163143_comparisons_organization_attribution.js +147 -0
- package/dist/migrations/20260830T163143_comparisons_organization_attribution.js.map +1 -0
- package/dist/migrations/index.d.ts +28 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +31 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/comparisons.md +278 -0
- package/i18n/en.json +51 -0
- package/i18n/pl.json +51 -0
- package/package.json +102 -3
- 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"}
|
package/dist/manifest.js
ADDED
|
@@ -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"}
|