@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 @@
1
+ {"version":3,"file":"comparison-pdf-renderer.js","sourceRoot":"","sources":["../../../src/backend/services/comparison-pdf-renderer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,339 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { type CatalogAttributeReadPort, type CatalogGalleryPort, type CatalogProductReadPort, type ComparisonOwnerView, type ComparisonDisplayMode, type CustomerAccountReadPort, type ListingPricePort, type OrganizationDetailsPort, type ProductAudience } from '@endora-commerce/contracts';
3
+ import type { SalesChannelMembershipPort } from '@endora-commerce/platform/kernel';
4
+ import type { SettingsReadPort } from '@endora-commerce/platform/kernel';
5
+ import { Comparison } from '../entities/comparison.entity.js';
6
+ import type { ShareTokenGenerator } from './share-token-generator.js';
7
+ import type { ComparableAttributeProjection } from './comparable-attribute-projection.js';
8
+ /**
9
+ * ComparisonService — feature 007 / T023 + T024.
10
+ *
11
+ * Stateful service owning the Comparison resource. Every cross-module read
12
+ * goes through a published port — {@link CatalogProductReadPort},
13
+ * {@link CatalogAttributeReadPort}, {@link ListingPricePort} and
14
+ * {@link SettingsReadPort}. The products a comparison holds are `catalog`'s
15
+ * rows, not this module's: reading them with `em.find(Product, …)` was a query
16
+ * nothing could gate, so a comparison kept resolving names and availability
17
+ * out of a module an operator had switched off (feature 075, Phase C).
18
+ *
19
+ * Behaviour summary (per `data-model.md`, `research.md`, and
20
+ * `contracts/public-comparisons-crud.md`):
21
+ *
22
+ * - `getForOwner` returns the caller's live Comparison or `null`. No
23
+ * side effects.
24
+ * - `addProduct` enforces `compare.max_products` on add (R-7). Throws
25
+ * {@link ComparisonFullError} when the cap would be exceeded.
26
+ * Idempotent on duplicates: re-adding a product already in the set
27
+ * is a no-op that returns the current comparison.
28
+ * - `removeProduct` throws {@link ProductNotInComparisonError} when
29
+ * the product is not in the set; emptying the set is allowed.
30
+ * - `setDisplayMode` only updates the persisted mode; refuses with
31
+ * {@link ComparisonNotFoundError} when the caller has no comparison.
32
+ * - `deleteForOwner` hard-deletes; cascades onto `comparison_products`
33
+ * by FK. Invalidates the share token (the row is gone — the share
34
+ * endpoint will return 404).
35
+ * - `findByShareToken` resolves the token in O(1) via the UNIQUE
36
+ * index. Used in US2 for the recipient view.
37
+ * - `buildOwnerView` produces the on-the-wire shape for `GET /me`
38
+ * (and is reused for the share-token recipient view in US2 and the
39
+ * admin detail view in US5).
40
+ */
41
+ /**
42
+ * Who is reading a comparison.
43
+ *
44
+ * Two kinds rather than one `ProductAudience`, because an administrator is not
45
+ * a buyer and there is no audience that means "may see everything": the
46
+ * predicate in {@link isProductVisibleTo} answers for a *shopper*, and every
47
+ * audience an administrator could be given would drop the organisation-
48
+ * restricted rows the audit screen exists to show. Making that an explicit
49
+ * kind is what keeps it from being expressed as a permissive audience
50
+ * somebody later reuses on a storefront path.
51
+ */
52
+ export type ComparisonViewer = {
53
+ readonly kind: 'buyer';
54
+ readonly audience: ProductAudience;
55
+ } | {
56
+ readonly kind: 'administrator';
57
+ };
58
+ /**
59
+ * The most restrictive viewer there is — the public. The right default for a
60
+ * caller that has not been taught to resolve its reader, for the same reason
61
+ * {@link ANONYMOUS_PRODUCT_AUDIENCE} is: everything it may see, every other
62
+ * viewer may see too.
63
+ */
64
+ export declare const ANONYMOUS_COMPARISON_VIEWER: ComparisonViewer;
65
+ export declare class ComparisonService {
66
+ #private;
67
+ private readonly emFactory;
68
+ private readonly projection;
69
+ private readonly tokens;
70
+ /**
71
+ * `catalog`'s product read model. Not optional: a comparison is a list of
72
+ * products, so a service that cannot read one has nothing to answer with —
73
+ * where the settings and pricing arguments below have a defined fallback,
74
+ * this has none.
75
+ */
76
+ private readonly catalogProducts;
77
+ /**
78
+ * The sanctioned bridge accessor (Constitution XII), for the assortment
79
+ * gate `addProduct` owes (issue #259). Required for the same reason
80
+ * `catalogProducts` is: an optional one would spell "this rig wired no
81
+ * membership service" and "this channel publishes the product" as the same
82
+ * answer.
83
+ */
84
+ private readonly channelMembership;
85
+ /**
86
+ * Feature 075 / D-87 — `catalog`'s gallery read model, for the base image
87
+ * of each column. Required for the same reason `catalogProducts` is: the
88
+ * alternative to a wired port is not "a comparison without pictures", it is
89
+ * a service that cannot tell an operator who set no base image from a rig
90
+ * that wired no owner.
91
+ *
92
+ * `baseImageUrls`, not `list`: the read is a batch, it must tolerate an id
93
+ * whose product has since been removed, and the value a column renders is a
94
+ * **url**, which `catalog` resolves through the asset port it holds. Those
95
+ * three are exactly why the single join this replaced could not be swapped
96
+ * for the per-product port that already existed.
97
+ */
98
+ private readonly catalogGallery;
99
+ /**
100
+ * Optional settings service. When undefined, `compare.max_products`
101
+ * defaults to {@link DEFAULT_COMPARE_MAX_PRODUCTS} on every call —
102
+ * useful for foundation tests that pre-date Settings wiring.
103
+ */
104
+ private readonly settingsService?;
105
+ /**
106
+ * Feature 061 — the catalog's composed attribute read model (Principle I:
107
+ * replaces the former direct `ProductAttribute` entity find).
108
+ */
109
+ private readonly catalogAttributes?;
110
+ /**
111
+ * Issue #132 — the pricing engine, through the `pricingService` port. A
112
+ * comparison exists so a buyer can put prices side by side, so the figures
113
+ * in its columns have to be the ones a price list stands behind.
114
+ */
115
+ private readonly listingPrices?;
116
+ /**
117
+ * `organizations`' read model, for the one field the pricing engine needs
118
+ * beyond the viewer's organisation id: the customer group it belongs to,
119
+ * which a group-targeted price list is selected by. Without it a buyer's
120
+ * comparison column and their own cart line would resolve against
121
+ * different lists and quote different figures for one product.
122
+ */
123
+ private readonly organizationDetails?;
124
+ /**
125
+ * Feature 087 Group B / D-187 — `customer_accounts`' read model, for the
126
+ * one field the owner stamp needs: the organisation the owning account
127
+ * belongs to.
128
+ *
129
+ * The organisation is derived from the **row**, never from the ambient
130
+ * request context, and that is what makes one implementation serve both
131
+ * write paths: `addProduct` runs on a storefront request and
132
+ * `adoptAnonymousComparison` runs inside another module's login hook, where
133
+ * there is no comparison caller to read a context from.
134
+ *
135
+ * Optional in the constructor and required at the two call sites, in the
136
+ * shape {@link organizationDetails} already takes here. A rig that wired no
137
+ * port cannot stamp, and the honest answer to that is a refusal at the
138
+ * write rather than a row `comparisons_organization_attribution_chk` will
139
+ * reject with a message about a constraint the caller never heard of.
140
+ */
141
+ private readonly customerAccounts?;
142
+ constructor(emFactory: () => EntityManager, projection: ComparableAttributeProjection, tokens: ShareTokenGenerator,
143
+ /**
144
+ * `catalog`'s product read model. Not optional: a comparison is a list of
145
+ * products, so a service that cannot read one has nothing to answer with —
146
+ * where the settings and pricing arguments below have a defined fallback,
147
+ * this has none.
148
+ */
149
+ catalogProducts: CatalogProductReadPort,
150
+ /**
151
+ * The sanctioned bridge accessor (Constitution XII), for the assortment
152
+ * gate `addProduct` owes (issue #259). Required for the same reason
153
+ * `catalogProducts` is: an optional one would spell "this rig wired no
154
+ * membership service" and "this channel publishes the product" as the same
155
+ * answer.
156
+ */
157
+ channelMembership: SalesChannelMembershipPort,
158
+ /**
159
+ * Feature 075 / D-87 — `catalog`'s gallery read model, for the base image
160
+ * of each column. Required for the same reason `catalogProducts` is: the
161
+ * alternative to a wired port is not "a comparison without pictures", it is
162
+ * a service that cannot tell an operator who set no base image from a rig
163
+ * that wired no owner.
164
+ *
165
+ * `baseImageUrls`, not `list`: the read is a batch, it must tolerate an id
166
+ * whose product has since been removed, and the value a column renders is a
167
+ * **url**, which `catalog` resolves through the asset port it holds. Those
168
+ * three are exactly why the single join this replaced could not be swapped
169
+ * for the per-product port that already existed.
170
+ */
171
+ catalogGallery: CatalogGalleryPort,
172
+ /**
173
+ * Optional settings service. When undefined, `compare.max_products`
174
+ * defaults to {@link DEFAULT_COMPARE_MAX_PRODUCTS} on every call —
175
+ * useful for foundation tests that pre-date Settings wiring.
176
+ */
177
+ settingsService?: SettingsReadPort | undefined,
178
+ /**
179
+ * Feature 061 — the catalog's composed attribute read model (Principle I:
180
+ * replaces the former direct `ProductAttribute` entity find).
181
+ */
182
+ catalogAttributes?: CatalogAttributeReadPort | undefined,
183
+ /**
184
+ * Issue #132 — the pricing engine, through the `pricingService` port. A
185
+ * comparison exists so a buyer can put prices side by side, so the figures
186
+ * in its columns have to be the ones a price list stands behind.
187
+ */
188
+ listingPrices?: ListingPricePort | undefined,
189
+ /**
190
+ * `organizations`' read model, for the one field the pricing engine needs
191
+ * beyond the viewer's organisation id: the customer group it belongs to,
192
+ * which a group-targeted price list is selected by. Without it a buyer's
193
+ * comparison column and their own cart line would resolve against
194
+ * different lists and quote different figures for one product.
195
+ */
196
+ organizationDetails?: OrganizationDetailsPort | undefined,
197
+ /**
198
+ * Feature 087 Group B / D-187 — `customer_accounts`' read model, for the
199
+ * one field the owner stamp needs: the organisation the owning account
200
+ * belongs to.
201
+ *
202
+ * The organisation is derived from the **row**, never from the ambient
203
+ * request context, and that is what makes one implementation serve both
204
+ * write paths: `addProduct` runs on a storefront request and
205
+ * `adoptAnonymousComparison` runs inside another module's login hook, where
206
+ * there is no comparison caller to read a context from.
207
+ *
208
+ * Optional in the constructor and required at the two call sites, in the
209
+ * shape {@link organizationDetails} already takes here. A rig that wired no
210
+ * port cannot stamp, and the honest answer to that is a refusal at the
211
+ * write rather than a row `comparisons_organization_attribution_chk` will
212
+ * reject with a message about a constraint the caller never heard of.
213
+ */
214
+ customerAccounts?: CustomerAccountReadPort | undefined);
215
+ getForOwner(owner: ComparisonOwner): Promise<Comparison | null>;
216
+ /**
217
+ * Resolve a share token to its comparison, across customers.
218
+ *
219
+ * `Comparison` is `@CustomerScoped`, so the ambient filter confines this read
220
+ * to the caller's own rows — which is right for every other method here and
221
+ * is the one thing a share link has to cross. An **anonymous** recipient
222
+ * already crossed it by accident: the composition roots run anonymous
223
+ * requests under `systemTenantContext('actor:anonymous')`, so the filter is a
224
+ * no-op for them, while a *signed-in* recipient got a 404 on a link that
225
+ * worked when they were logged out. That is the shape a guard takes when a
226
+ * deliberate cross-tenant grant is left implicit.
227
+ *
228
+ * So it is explicit, through the one sanctioned crossing (feature 050,
229
+ * FR-005): the token is the authorization, exactly as it is on the public
230
+ * product-feed endpoint, and the widening covers this single lookup by a
231
+ * unique index and nothing after it. **What the recipient then sees is not
232
+ * widened at all** — `buildOwnerView` filters the products by their own
233
+ * audience and prices them for their own organisation.
234
+ */
235
+ findByShareToken(token: string): Promise<Comparison | null>;
236
+ /**
237
+ * Anonymous → authenticated merge (research.md R-2 / spec FR-005).
238
+ * Called from the post-login hook with the cookie-bound
239
+ * `anonymousToken` the caller was carrying. Behaviour:
240
+ *
241
+ * - customer has no Comparison → reassign the anonymous one
242
+ * (set customer_account_id, clear anonymous_token).
243
+ * - customer already has a Comparison → discard the anonymous one
244
+ * (the customer's existing curated set wins, per R-2 — merging
245
+ * two sets is more surprising than honouring the persistent
246
+ * identity).
247
+ *
248
+ * No-op when neither side resolves.
249
+ *
250
+ * **This is the one place in the platform where a comparison becomes owned**
251
+ * — `Cart` has no counterpart, because its anonymous merge *completes* the
252
+ * anonymous cart rather than re-owning it. So it is the write feature 087's
253
+ * invariant would have been forgotten at: the row arrives here with a null
254
+ * `organization_id` that was correct while it was anonymous, and the
255
+ * organisation is set in the same unit of work as the account, from the same
256
+ * account. Nothing here is conditional on the column being null — an
257
+ * anonymous row's organisation is null by construction, and if it ever is
258
+ * not, the account it is being handed to is the truth.
259
+ */
260
+ adoptAnonymousComparison(customerAccountId: string, anonymousToken: string): Promise<void>;
261
+ /**
262
+ * True when the supplied owner identity matches the Comparison's
263
+ * stored owner. Used by the share-token endpoint to populate
264
+ * `meta.viewerIsOwner`.
265
+ */
266
+ isOwnedBy(comparison: Comparison, owner: ComparisonOwner | null): boolean;
267
+ addProduct(owner: ComparisonOwner, salesChannelId: string, productId: string,
268
+ /**
269
+ * Who is comparing (issue #227). Defaulted to the anonymous audience so a
270
+ * composition that has not been taught to resolve its caller refuses a
271
+ * restricted product rather than adding it — the fail-closed end. The
272
+ * public route passes the real one.
273
+ */
274
+ audience?: ProductAudience): Promise<Comparison>;
275
+ removeProduct(owner: ComparisonOwner, productId: string): Promise<Comparison>;
276
+ setDisplayMode(owner: ComparisonOwner, mode: ComparisonDisplayMode): Promise<Comparison>;
277
+ deleteForOwner(owner: ComparisonOwner): Promise<void>;
278
+ /**
279
+ * Build the wire-format `ComparisonOwnerView` for the given comparison
280
+ * in the viewer's sales-channel context. Reusable across the three
281
+ * consumers (owner / recipient / admin); the recipient + admin views
282
+ * pass `viewerIsOwner=false` and consume only the slice they need.
283
+ *
284
+ * **The viewer's identity decides what the viewer sees. Always.** A share
285
+ * token grants access to the *comparison*, never to anything in it, so it
286
+ * settles two questions the same way:
287
+ *
288
+ * - **the prices** — the owner sees their organisation's figures, a
289
+ * signed-in recipient from another organisation sees **theirs**, and an
290
+ * anonymous recipient sees the channel's. That is what makes a shared
291
+ * link trustworthy: the numbers are true for whoever is looking, and a
292
+ * recipient who acted on the sender's negotiated price would be acting on
293
+ * a price nobody will sell them at.
294
+ * - **the products** — a row restricted to the sender's organisation is not
295
+ * disclosed to a recipient outside it, while a recipient who *is* on the
296
+ * allow-list sees it. The owner was entitled to add it (`addProduct`
297
+ * enforces that, issue #227); a second reader has an entitlement of their
298
+ * own, and a product may be restricted after it was added.
299
+ *
300
+ * The two run in that order — filter, then price — because a price resolved
301
+ * for a row the reader may not see is work done to produce a figure that must
302
+ * not be sent.
303
+ */
304
+ buildOwnerView(comparison: Comparison, viewerSalesChannelId: string,
305
+ /**
306
+ * Who is looking. Defaulted to the anonymous buyer so a caller that has not
307
+ * been taught to resolve its reader shows the public set at the channel
308
+ * price — the answer every reader may see — rather than one buyer's.
309
+ */
310
+ viewer?: ComparisonViewer): Promise<ComparisonOwnerView>;
311
+ /**
312
+ * Read `compare.max_products` from Settings; fall back to the manifest
313
+ * default on any error (Settings hiccup must not break the storefront).
314
+ */
315
+ private resolveMaxProducts;
316
+ }
317
+ export type ComparisonOwner = {
318
+ kind: 'customer';
319
+ customerAccountId: string;
320
+ } | {
321
+ kind: 'anonymous';
322
+ anonymousToken: string;
323
+ };
324
+ export declare class ComparisonFullError extends Error {
325
+ readonly max: number;
326
+ constructor(max: number);
327
+ }
328
+ export declare class ComparisonNotFoundError extends Error {
329
+ constructor();
330
+ }
331
+ export declare class ProductNotInComparisonError extends Error {
332
+ readonly productId: string;
333
+ constructor(productId: string);
334
+ }
335
+ export declare class ProductNotFoundError extends Error {
336
+ readonly productId: string;
337
+ constructor(productId: string);
338
+ }
339
+ //# sourceMappingURL=comparison-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comparison-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/comparison-service.ts"],"names":[],"mappings":"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"}