@endora-commerce/mod-google-analytics 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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/dist/admin/api/google-analytics-client.d.ts +17 -0
  4. package/dist/admin/api/google-analytics-client.d.ts.map +1 -0
  5. package/dist/admin/api/google-analytics-client.js +21 -0
  6. package/dist/admin/api/google-analytics-client.js.map +1 -0
  7. package/dist/admin/index.d.ts +29 -0
  8. package/dist/admin/index.d.ts.map +1 -0
  9. package/dist/admin/index.js +63 -0
  10. package/dist/admin/index.js.map +1 -0
  11. package/dist/admin/pages/CustomEventEditPage.d.ts +3 -0
  12. package/dist/admin/pages/CustomEventEditPage.d.ts.map +1 -0
  13. package/dist/admin/pages/CustomEventEditPage.js +179 -0
  14. package/dist/admin/pages/CustomEventEditPage.js.map +1 -0
  15. package/dist/admin/pages/CustomEventsListPage.d.ts +8 -0
  16. package/dist/admin/pages/CustomEventsListPage.d.ts.map +1 -0
  17. package/dist/admin/pages/CustomEventsListPage.js +36 -0
  18. package/dist/admin/pages/CustomEventsListPage.js.map +1 -0
  19. package/dist/backend/entities/ga-custom-event.entity.d.ts +26 -0
  20. package/dist/backend/entities/ga-custom-event.entity.d.ts.map +1 -0
  21. package/dist/backend/entities/ga-custom-event.entity.js +82 -0
  22. package/dist/backend/entities/ga-custom-event.entity.js.map +1 -0
  23. package/dist/backend/index.d.ts +85 -0
  24. package/dist/backend/index.d.ts.map +1 -0
  25. package/dist/backend/index.js +102 -0
  26. package/dist/backend/index.js.map +1 -0
  27. package/dist/backend/routes.admin.d.ts +15 -0
  28. package/dist/backend/routes.admin.d.ts.map +1 -0
  29. package/dist/backend/routes.admin.js +35 -0
  30. package/dist/backend/routes.admin.js.map +1 -0
  31. package/dist/backend/routes.storefront.d.ts +17 -0
  32. package/dist/backend/routes.storefront.d.ts.map +1 -0
  33. package/dist/backend/routes.storefront.js +30 -0
  34. package/dist/backend/routes.storefront.js.map +1 -0
  35. package/dist/backend/services/cookie-consent-block-seeder.d.ts +29 -0
  36. package/dist/backend/services/cookie-consent-block-seeder.d.ts.map +1 -0
  37. package/dist/backend/services/cookie-consent-block-seeder.js +51 -0
  38. package/dist/backend/services/cookie-consent-block-seeder.js.map +1 -0
  39. package/dist/backend/services/custom-events.service.d.ts +54 -0
  40. package/dist/backend/services/custom-events.service.d.ts.map +1 -0
  41. package/dist/backend/services/custom-events.service.js +158 -0
  42. package/dist/backend/services/custom-events.service.js.map +1 -0
  43. package/dist/backend/services/ga-config.service.d.ts +27 -0
  44. package/dist/backend/services/ga-config.service.d.ts.map +1 -0
  45. package/dist/backend/services/ga-config.service.js +66 -0
  46. package/dist/backend/services/ga-config.service.js.map +1 -0
  47. package/dist/backend/services/ga4-mp-client.d.ts +32 -0
  48. package/dist/backend/services/ga4-mp-client.d.ts.map +1 -0
  49. package/dist/backend/services/ga4-mp-client.js +46 -0
  50. package/dist/backend/services/ga4-mp-client.js.map +1 -0
  51. package/dist/backend/services/ss-delivery-queue.d.ts +32 -0
  52. package/dist/backend/services/ss-delivery-queue.d.ts.map +1 -0
  53. package/dist/backend/services/ss-delivery-queue.js +29 -0
  54. package/dist/backend/services/ss-delivery-queue.js.map +1 -0
  55. package/dist/backend/services/ss-delivery.service.d.ts +24 -0
  56. package/dist/backend/services/ss-delivery.service.d.ts.map +1 -0
  57. package/dist/backend/services/ss-delivery.service.js +52 -0
  58. package/dist/backend/services/ss-delivery.service.js.map +1 -0
  59. package/dist/manifest.d.ts +194 -0
  60. package/dist/manifest.d.ts.map +1 -0
  61. package/dist/manifest.js +134 -0
  62. package/dist/manifest.js.map +1 -0
  63. package/dist/migrations/20260715T171116_google_analytics_init.d.ts +14 -0
  64. package/dist/migrations/20260715T171116_google_analytics_init.d.ts.map +1 -0
  65. package/dist/migrations/20260715T171116_google_analytics_init.js +35 -0
  66. package/dist/migrations/20260715T171116_google_analytics_init.js.map +1 -0
  67. package/dist/migrations/index.d.ts +28 -0
  68. package/dist/migrations/index.d.ts.map +1 -0
  69. package/dist/migrations/index.js +28 -0
  70. package/dist/migrations/index.js.map +1 -0
  71. package/docs/google-analytics.md +106 -0
  72. package/i18n/en.json +33 -0
  73. package/i18n/pl.json +33 -0
  74. package/package.json +98 -0
  75. package/tailwind.css +14 -0
@@ -0,0 +1,85 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { FastifyRequest } from 'fastify';
3
+ import type { Redis } from 'ioredis';
4
+ import type { AuditPort, ModuleContext, RequireAdminFactory, SettingsReadPort } from '@endora-commerce/platform/kernel';
5
+ import { StorefrontRevalidator } from '@endora-commerce/platform/http';
6
+ import { GaConfigService } from './services/ga-config.service.js';
7
+ import { GaCustomEventsService, type GaChannelPort, type GaAuditContext } from './services/custom-events.service.js';
8
+ import { makeEnqueuer } from './services/ss-delivery.service.js';
9
+ import { GaCustomEvent } from './entities/ga-custom-event.entity.js';
10
+ /**
11
+ * `google_analytics` — the last of the four ad modules, and the only one with
12
+ * something to say about *where* a seed belongs (feature 072, wave 2, T102).
13
+ *
14
+ * The off-state leak is the same one `meta_ads`, `linkedin_ads` and
15
+ * `google_tag_manager` carried: routes gated by `defineModuleRoutes`, but the
16
+ * `settings.value_changed` subscription arriving from a root as a bare
17
+ * `eventBus.on`, so a switched-off module kept POSTing to the storefront.
18
+ * `ctx.subscribe` is `subscribeForModule`; the worker moves to `ctx.worker`,
19
+ * which is `defineModuleWorker`. That closes the fourth and last of them.
20
+ *
21
+ * **The CMS block seed stays in `ctx.routes`, not `ctx.onBoot`.** It is the one
22
+ * decision here that is not mechanical. `ensureCookieConsentBlock` seeds a
23
+ * predefined CMS block, so it needs `cms`' tables to exist and it needs sales
24
+ * channels. `ctx.onBoot` would run it earlier, which buys nothing it needs and
25
+ * costs the ordering guarantee it currently has for free by running at plugin
26
+ * attach. `_i18n` taught this lesson the expensive way in wave 1: a seed moved
27
+ * to `onBoot` that silently found nothing.
28
+ *
29
+ * **What changed under it is who writes** (feature 075 / D-87). The seeder used
30
+ * to insert into `cms_blocks` and `cms_block_sales_channels` itself and was
31
+ * wrapped in a `try/catch` that downgraded any failure to a warning, so that a
32
+ * platform composed without the CMS module could still boot. It asks `cms`
33
+ * through `cmsBlockSeedPort` now and decides that module's presence before it
34
+ * calls, so the absence is an answer this body logs rather than an exception a
35
+ * `catch` has to be trusted to tell apart from a real one.
36
+ *
37
+ * `channels` is `salesChannelCodeIdPort` — the code⇄id lookup, which belongs to
38
+ * `sales_channels` and is registered by a root until that module converts
39
+ * (T110). `auditLog` stops being optional, as in the other three.
40
+ */
41
+ interface GaServices {
42
+ readonly revalidator: StorefrontRevalidator;
43
+ readonly customEvents: GaCustomEventsService;
44
+ readonly configService: GaConfigService;
45
+ readonly invalidateConfig: () => void;
46
+ readonly enqueueCollect: ReturnType<typeof makeEnqueuer> | undefined;
47
+ }
48
+ export interface GoogleAnalyticsCradle {
49
+ readonly emFactory: () => EntityManager;
50
+ readonly auditLogService: AuditPort;
51
+ readonly settingsReadPort: SettingsReadPort;
52
+ readonly requireAdmin: RequireAdminFactory;
53
+ /** Owned by `sales_channels`; a root registers it until that module converts. */
54
+ readonly salesChannelCodeIdPort: GaChannelPort;
55
+ /** How this composition names the acting admin; `null` for a non-admin caller. */
56
+ readonly adminAuditActorResolver: (request: FastifyRequest) => GaAuditContext;
57
+ /** Absent in a composition that wants no BullMQ queue — see `google_tag_manager`. */
58
+ readonly moduleQueueRedis: Redis | undefined;
59
+ readonly googleAnalyticsServices: GaServices;
60
+ }
61
+ export declare function registerModule(ctx: ModuleContext): void;
62
+ /**
63
+ * The module's persisted entity classes, on the `./backend` subpath, as one
64
+ * array and **no named class export** (D-168).
65
+ *
66
+ * This is the shape the platform reads when the package is *installed*: the
67
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
68
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
69
+ * which is the third source of `check:module-boundary`'s `table→owner` map and
70
+ * the package pass of `check-entity-tenant-classification`. A missing array is
71
+ * answered with `[]` — zero entities registered, no error anywhere.
72
+ *
73
+ * One class, and the absent named export matters here for a reason this module
74
+ * makes concrete: `ga_custom_events` carries a foreign key into `sales_channels`
75
+ * (D-169), and a foreign key needs the **table**, never the owner's entity
76
+ * class. There is nothing another module can legitimately do with `GaCustomEvent`
77
+ * — D-32 forbids an ORM relation into it, and every cross-module read of this
78
+ * module's data goes through the storefront config shape. With a named export,
79
+ * `import type { GaCustomEvent } from '@endora-commerce/mod-google-analytics/backend'`
80
+ * would compile in any consumer's tree and only a check whose population is this
81
+ * repository could object.
82
+ */
83
+ export declare const entities: (typeof GaCustomEvent)[];
84
+ export {};
85
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,KAAK,EACV,SAAS,EACT,aAAa,EACb,mBAAmB,EACnB,gBAAgB,EACjB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,EACL,qBAAqB,EACrB,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,qCAAqC,CAAC;AAK7C,OAAO,EAAE,YAAY,EAAiB,MAAM,mCAAmC,CAAC;AAGhF,OAAO,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAIrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,UAAU,UAAU;IAClB,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC;IAC5C,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC;IAC7C,QAAQ,CAAC,aAAa,EAAE,eAAe,CAAC;IACxC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,IAAI,CAAC;IACtC,QAAQ,CAAC,cAAc,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,GAAG,SAAS,CAAC;CACtE;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,aAAa,CAAC;IACxC,QAAQ,CAAC,eAAe,EAAE,SAAS,CAAC;IACpC,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC;IAC5C,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,iFAAiF;IACjF,QAAQ,CAAC,sBAAsB,EAAE,aAAa,CAAC;IAC/C,kFAAkF;IAClF,QAAQ,CAAC,uBAAuB,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,cAAc,CAAC;IAC9E,qFAAqF;IACrF,QAAQ,CAAC,gBAAgB,EAAE,KAAK,GAAG,SAAS,CAAC;IAC7C,QAAQ,CAAC,uBAAuB,EAAE,UAAU,CAAC;CAC9C;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAoGvD;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,QAAQ,0BAAkB,CAAC"}
@@ -0,0 +1,102 @@
1
+ import { lazyPort } from '@endora-commerce/platform/kernel';
2
+ import { StorefrontRevalidator } from '@endora-commerce/platform/http';
3
+ import { GaConfigService } from './services/ga-config.service.js';
4
+ import { GaCustomEventsService, } from './services/custom-events.service.js';
5
+ import { createGaDeliveryQueue, createGaDeliveryWorker, } from './services/ss-delivery-queue.js';
6
+ import { makeEnqueuer, makeProcessor } from './services/ss-delivery.service.js';
7
+ import { Ga4MpClient } from './services/ga4-mp-client.js';
8
+ import { ensureCookieConsentBlock } from './services/cookie-consent-block-seeder.js';
9
+ import { GaCustomEvent } from './entities/ga-custom-event.entity.js';
10
+ import { registerGoogleAnalyticsAdminRoutes } from './routes.admin.js';
11
+ import { registerGoogleAnalyticsStorefrontRoutes } from './routes.storefront.js';
12
+ export function registerModule(ctx) {
13
+ ctx.di.register({
14
+ googleAnalyticsServices: ctx
15
+ .asFunction(({ emFactory, auditLogService, moduleQueueRedis, }) => {
16
+ const revalidator = new StorefrontRevalidator({
17
+ baseUrl: process.env['STOREFRONT_BASE_URL'],
18
+ secret: process.env['REVALIDATE_SECRET'],
19
+ });
20
+ const invalidateConfig = () => {
21
+ void revalidator.revalidate(['ga:config']);
22
+ };
23
+ const customEvents = new GaCustomEventsService(emFactory, lazyPort(ctx, 'salesChannelCodeIdPort'), auditLogService, invalidateConfig);
24
+ const configService = new GaConfigService(lazyPort(ctx, 'settingsReadPort'), (channelId) => customEvents.loadForChannel(channelId));
25
+ // Producer side: the API enqueues whether or not this process also
26
+ // runs the consumer.
27
+ const deliveryQueue = moduleQueueRedis
28
+ ? createGaDeliveryQueue(moduleQueueRedis)
29
+ : undefined;
30
+ return {
31
+ revalidator,
32
+ customEvents,
33
+ configService,
34
+ invalidateConfig,
35
+ enqueueCollect: deliveryQueue ? makeEnqueuer(deliveryQueue) : undefined,
36
+ };
37
+ })
38
+ .singleton(),
39
+ });
40
+ // Admin config edits propagate immediately rather than after the TTL — while
41
+ // the module is on.
42
+ ctx.subscribe('settings.value_changed', (payload) => {
43
+ const settingCode = payload.settingCode;
44
+ if (typeof settingCode !== 'string' || !settingCode.startsWith('google_analytics.'))
45
+ return;
46
+ ctx.cradle().googleAnalyticsServices.invalidateConfig();
47
+ });
48
+ ctx.routes(async (app) => {
49
+ const { googleAnalyticsServices, settingsReadPort, requireAdmin, adminAuditActorResolver, moduleQueueRedis, } = ctx.cradle();
50
+ googleAnalyticsServices.revalidator.setLogger(app.log);
51
+ // Seed the predefined cookie-consent CMS block (idempotent; runs once
52
+ // channels exist). `cms` owns the write since feature 075 — the seeder asks
53
+ // it through `cmsBlockSeedPort` and answers `cms-absent` rather than
54
+ // throwing when the CMS is not there, so this no longer needs a `catch` to
55
+ // keep a CMS-less platform booting. `lazyPort` built here resolves nothing:
56
+ // it defers to the method call, which the seeder makes only after it has
57
+ // decided presence.
58
+ const seeded = await ensureCookieConsentBlock(lazyPort(ctx, 'cmsBlockSeedPort'));
59
+ if (seeded === 'cms-absent') {
60
+ app.log.info('[google_analytics] cookie-consent CMS block seed skipped — cms is not present');
61
+ }
62
+ // Queue consumer (Principle X): a separable worker entrypoint, gated on
63
+ // role. `ctx.worker` wraps it in `defineModuleWorker`.
64
+ if (process.env['BACKEND_ROLE'] !== 'api' && moduleQueueRedis) {
65
+ ctx.worker(createGaDeliveryWorker(moduleQueueRedis, makeProcessor({ settings: settingsReadPort, client: new Ga4MpClient() })), { logger: app.log });
66
+ }
67
+ await registerGoogleAnalyticsStorefrontRoutes(app, {
68
+ configService: googleAnalyticsServices.configService,
69
+ ...(googleAnalyticsServices.enqueueCollect
70
+ ? { enqueueCollect: googleAnalyticsServices.enqueueCollect }
71
+ : {}),
72
+ });
73
+ await registerGoogleAnalyticsAdminRoutes(app, {
74
+ customEvents: googleAnalyticsServices.customEvents,
75
+ requireAdmin,
76
+ resolveAuditContext: adminAuditActorResolver,
77
+ });
78
+ });
79
+ }
80
+ /**
81
+ * The module's persisted entity classes, on the `./backend` subpath, as one
82
+ * array and **no named class export** (D-168).
83
+ *
84
+ * This is the shape the platform reads when the package is *installed*: the
85
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
86
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
87
+ * which is the third source of `check:module-boundary`'s `table→owner` map and
88
+ * the package pass of `check-entity-tenant-classification`. A missing array is
89
+ * answered with `[]` — zero entities registered, no error anywhere.
90
+ *
91
+ * One class, and the absent named export matters here for a reason this module
92
+ * makes concrete: `ga_custom_events` carries a foreign key into `sales_channels`
93
+ * (D-169), and a foreign key needs the **table**, never the owner's entity
94
+ * class. There is nothing another module can legitimately do with `GaCustomEvent`
95
+ * — D-32 forbids an ORM relation into it, and every cross-module read of this
96
+ * module's data goes through the storefront config shape. With a named export,
97
+ * `import type { GaCustomEvent } from '@endora-commerce/mod-google-analytics/backend'`
98
+ * would compile in any consumer's tree and only a check whose population is this
99
+ * repository could object.
100
+ */
101
+ export const entities = [GaCustomEvent];
102
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,EACL,qBAAqB,GAGtB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EACL,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAAE,wBAAwB,EAAE,MAAM,2CAA2C,CAAC;AACrF,OAAO,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AACrE,OAAO,EAAE,kCAAkC,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,uCAAuC,EAAE,MAAM,wBAAwB,CAAC;AAwDjF,MAAM,UAAU,cAAc,CAAC,GAAkB;IAC/C,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC;QACd,uBAAuB,EAAE,GAAG;aACzB,UAAU,CACT,CAAC,EACC,SAAS,EACT,eAAe,EACf,gBAAgB,GACM,EAAc,EAAE;YACtC,MAAM,WAAW,GAAG,IAAI,qBAAqB,CAAC;gBAC5C,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC;gBAC3C,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;aACzC,CAAC,CAAC;YACH,MAAM,gBAAgB,GAAG,GAAS,EAAE;gBAClC,KAAK,WAAW,CAAC,UAAU,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;YAC7C,CAAC,CAAC;YACF,MAAM,YAAY,GAAG,IAAI,qBAAqB,CAC5C,SAAS,EACT,QAAQ,CAAgB,GAAG,EAAE,wBAAwB,CAAC,EACtD,eAAe,EACf,gBAAgB,CACjB,CAAC;YACF,MAAM,aAAa,GAAG,IAAI,eAAe,CACvC,QAAQ,CAAmB,GAAG,EAAE,kBAAkB,CAAC,EACnD,CAAC,SAAS,EAAE,EAAE,CACd,YAAY,CAAC,cAAc,CAAC,SAAS,CAAC,CACvC,CAAC;YACF,mEAAmE;YACnE,qBAAqB;YACrB,MAAM,aAAa,GAAG,gBAAgB;gBACpC,CAAC,CAAC,qBAAqB,CAAC,gBAAgB,CAAC;gBACzC,CAAC,CAAC,SAAS,CAAC;YACd,OAAO;gBACL,WAAW;gBACX,YAAY;gBACZ,aAAa;gBACb,gBAAgB;gBAChB,cAAc,EAAE,aAAa,CAAC,CAAC,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,SAAS;aACxE,CAAC;QACJ,CAAC,CACF;aACA,SAAS,EAAE;KACf,CAAC,CAAC;IAEH,6EAA6E;IAC7E,oBAAoB;IACpB,GAAG,CAAC,SAAS,CAAC,wBAAwB,EAAE,CAAC,OAAO,EAAE,EAAE;QAClD,MAAM,WAAW,GAAI,OAAqC,CAAC,WAAW,CAAC;QACvE,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,mBAAmB,CAAC;YAAE,OAAO;QAC5F,GAAG,CAAC,MAAM,EAAyB,CAAC,uBAAuB,CAAC,gBAAgB,EAAE,CAAC;IACjF,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACvB,MAAM,EACJ,uBAAuB,EACvB,gBAAgB,EAChB,YAAY,EACZ,uBAAuB,EACvB,gBAAgB,GACjB,GAAG,GAAG,CAAC,MAAM,EAAyB,CAAC;QACxC,uBAAuB,CAAC,WAAW,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAEvD,sEAAsE;QACtE,4EAA4E;QAC5E,qEAAqE;QACrE,2EAA2E;QAC3E,4EAA4E;QAC5E,yEAAyE;QACzE,oBAAoB;QACpB,MAAM,MAAM,GAAG,MAAM,wBAAwB,CAC3C,QAAQ,CAAmB,GAAG,EAAE,kBAAkB,CAAC,CACpD,CAAC;QACF,IAAI,MAAM,KAAK,YAAY,EAAE,CAAC;YAC5B,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,+EAA+E,CAAC,CAAC;QAChG,CAAC;QAED,wEAAwE;QACxE,uDAAuD;QACvD,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,KAAK,KAAK,IAAI,gBAAgB,EAAE,CAAC;YAC9D,GAAG,CAAC,MAAM,CACR,sBAAsB,CACpB,gBAAgB,EAChB,aAAa,CAAC,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,EAAE,IAAI,WAAW,EAAE,EAAE,CAAC,CACzE,EACD,EAAE,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE,CACpB,CAAC;QACJ,CAAC;QAED,MAAM,uCAAuC,CAAC,GAAG,EAAE;YACjD,aAAa,EAAE,uBAAuB,CAAC,aAAa;YACpD,GAAG,CAAC,uBAAuB,CAAC,cAAc;gBACxC,CAAC,CAAC,EAAE,cAAc,EAAE,uBAAuB,CAAC,cAAc,EAAE;gBAC5D,CAAC,CAAC,EAAE,CAAC;SACR,CAAC,CAAC;QACH,MAAM,kCAAkC,CAAC,GAAG,EAAE;YAC5C,YAAY,EAAE,uBAAuB,CAAC,YAAY;YAClD,YAAY;YACZ,mBAAmB,EAAE,uBAAuB;SAC7C,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,aAAa,CAAC,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
2
+ import type { GaCustomEventsService, GaAuditContext } from './services/custom-events.service.js';
3
+ export interface GoogleAnalyticsAdminDeps {
4
+ customEvents: GaCustomEventsService;
5
+ requireAdmin: (permission?: string) => (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
6
+ resolveAuditContext: (req: FastifyRequest) => GaAuditContext;
7
+ }
8
+ /**
9
+ * Admin routes for the Google Analytics module (feature 049, US3). Custom-event
10
+ * CRUD + the static action-field catalogue that drives the admin field picker.
11
+ * Per-channel Settings (Measurement ID, Enhanced Ecommerce, server-side) are
12
+ * managed through the generic Settings admin surface, not here.
13
+ */
14
+ export declare function registerGoogleAnalyticsAdminRoutes(app: FastifyInstance, deps: GoogleAnalyticsAdminDeps): Promise<void>;
15
+ //# sourceMappingURL=routes.admin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.admin.d.ts","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAO7E,OAAO,KAAK,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAEjG,MAAM,WAAW,wBAAwB;IACvC,YAAY,EAAE,qBAAqB,CAAC;IACpC,YAAY,EAAE,CAAC,UAAU,CAAC,EAAE,MAAM,KAAK,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnG,mBAAmB,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,cAAc,CAAC;CAC9D;AAED;;;;;GAKG;AACH,wBAAsB,kCAAkC,CACtD,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,wBAAwB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAwDf"}
@@ -0,0 +1,35 @@
1
+ import { gaCustomEventCreateSchema, gaCustomEventUpdateSchema, gaCustomEventListQuerySchema, GA_ACTION_FIELD_CATALOGUE, } from '@endora-commerce/contracts';
2
+ /**
3
+ * Admin routes for the Google Analytics module (feature 049, US3). Custom-event
4
+ * CRUD + the static action-field catalogue that drives the admin field picker.
5
+ * Per-channel Settings (Measurement ID, Enhanced Ecommerce, server-side) are
6
+ * managed through the generic Settings admin surface, not here.
7
+ */
8
+ export async function registerGoogleAnalyticsAdminRoutes(app, deps) {
9
+ const { customEvents, requireAdmin, resolveAuditContext } = deps;
10
+ app.get('/api/v1/admin/google-analytics/action-catalogue', { preHandler: requireAdmin('google_analytics:read') }, async () => ({ data: GA_ACTION_FIELD_CATALOGUE }));
11
+ app.get('/api/v1/admin/google-analytics/custom-events', { preHandler: requireAdmin('google_analytics:read') }, async (request) => {
12
+ const query = gaCustomEventListQuerySchema.parse(request.query);
13
+ const items = await customEvents.list(query);
14
+ return { data: items };
15
+ });
16
+ app.get('/api/v1/admin/google-analytics/custom-events/:id', { preHandler: requireAdmin('google_analytics:read') }, async (request) => {
17
+ const item = await customEvents.get(request.params.id);
18
+ return { data: item };
19
+ });
20
+ app.post('/api/v1/admin/google-analytics/custom-events', { preHandler: requireAdmin('google_analytics:write') }, async (request, reply) => {
21
+ const body = gaCustomEventCreateSchema.parse(request.body);
22
+ const item = await customEvents.create(body, resolveAuditContext(request));
23
+ return reply.status(201).send({ data: item });
24
+ });
25
+ app.patch('/api/v1/admin/google-analytics/custom-events/:id', { preHandler: requireAdmin('google_analytics:write') }, async (request) => {
26
+ const body = gaCustomEventUpdateSchema.parse(request.body);
27
+ const item = await customEvents.update(request.params.id, body, resolveAuditContext(request));
28
+ return { data: item };
29
+ });
30
+ app.delete('/api/v1/admin/google-analytics/custom-events/:id', { preHandler: requireAdmin('google_analytics:write') }, async (request, reply) => {
31
+ await customEvents.remove(request.params.id, resolveAuditContext(request));
32
+ return reply.status(204).send();
33
+ });
34
+ }
35
+ //# sourceMappingURL=routes.admin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.admin.js","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"AACA,OAAO,EACL,yBAAyB,EACzB,yBAAyB,EACzB,4BAA4B,EAC5B,yBAAyB,GAC1B,MAAM,4BAA4B,CAAC;AASpC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kCAAkC,CACtD,GAAoB,EACpB,IAA8B;IAE9B,MAAM,EAAE,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,GAAG,IAAI,CAAC;IAEjE,GAAG,CAAC,GAAG,CACL,iDAAiD,EACjD,EAAE,UAAU,EAAE,YAAY,CAAC,uBAAuB,CAAC,EAAE,EACrD,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,yBAAyB,EAAE,CAAC,CAClD,CAAC;IAEF,GAAG,CAAC,GAAG,CACL,8CAA8C,EAC9C,EAAE,UAAU,EAAE,YAAY,CAAC,uBAAuB,CAAC,EAAE,EACrD,KAAK,EAAE,OAAO,EAAE,EAAE;QAChB,MAAM,KAAK,GAAG,4BAA4B,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAChE,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC7C,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IACzB,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,GAAG,CACL,kDAAkD,EAClD,EAAE,UAAU,EAAE,YAAY,CAAC,uBAAuB,CAAC,EAAE,EACrD,KAAK,EAAE,OAAO,EAAE,EAAE;QAChB,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACvD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACxB,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,IAAI,CACN,8CAA8C,EAC9C,EAAE,UAAU,EAAE,YAAY,CAAC,wBAAwB,CAAC,EAAE,EACtD,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACvB,MAAM,IAAI,GAAG,yBAAyB,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC3D,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC;QAC3E,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,KAAK,CACP,kDAAkD,EAClD,EAAE,UAAU,EAAE,YAAY,CAAC,wBAAwB,CAAC,EAAE,EACtD,KAAK,EAAE,OAAO,EAAE,EAAE;QAChB,MAAM,IAAI,GAAG,yBAAyB,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC3D,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC;QAC9F,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACxB,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,MAAM,CACR,kDAAkD,EAClD,EAAE,UAAU,EAAE,YAAY,CAAC,wBAAwB,CAAC,EAAE,EACtD,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACvB,MAAM,YAAY,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC;QAC3E,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAClC,CAAC,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { GaConfigService } from './services/ga-config.service.js';
3
+ import type { GaCollectEnqueuer } from './services/ss-delivery-queue.js';
4
+ export interface GoogleAnalyticsStorefrontDeps {
5
+ configService: GaConfigService;
6
+ /** Producer that enqueues server-side delivery jobs (US4). Absent ⇒ 503. */
7
+ enqueueCollect?: GaCollectEnqueuer;
8
+ }
9
+ /**
10
+ * Public storefront routes for the Google Analytics module (feature 049).
11
+ * Registered through `ctx.routes` in this package's `backend/index.ts`, which
12
+ * is `defineModuleRoutes`, so they 503 `MODULE_DISABLED` when the module is
13
+ * switched off. Channel is resolved from the `X-Sales-Channel` header by the
14
+ * shared sales-channel-resolver middleware.
15
+ */
16
+ export declare function registerGoogleAnalyticsStorefrontRoutes(app: FastifyInstance, deps: GoogleAnalyticsStorefrontDeps): Promise<void>;
17
+ //# sourceMappingURL=routes.storefront.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.storefront.d.ts","sourceRoot":"","sources":["../../src/backend/routes.storefront.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAEzE,MAAM,WAAW,6BAA6B;IAC5C,aAAa,EAAE,eAAe,CAAC;IAC/B,4EAA4E;IAC5E,cAAc,CAAC,EAAE,iBAAiB,CAAC;CACpC;AAED;;;;;;GAMG;AACH,wBAAsB,uCAAuC,CAC3D,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,6BAA6B,GAClC,OAAO,CAAC,IAAI,CAAC,CAoBf"}
@@ -0,0 +1,30 @@
1
+ import { gaCollectRequestSchema } from '@endora-commerce/contracts';
2
+ import { getResolvedChannel } from '@endora-commerce/platform/kernel';
3
+ /**
4
+ * Public storefront routes for the Google Analytics module (feature 049).
5
+ * Registered through `ctx.routes` in this package's `backend/index.ts`, which
6
+ * is `defineModuleRoutes`, so they 503 `MODULE_DISABLED` when the module is
7
+ * switched off. Channel is resolved from the `X-Sales-Channel` header by the
8
+ * shared sales-channel-resolver middleware.
9
+ */
10
+ export async function registerGoogleAnalyticsStorefrontRoutes(app, deps) {
11
+ // Per-channel public config for the browser. Never returns the API secret.
12
+ app.get('/api/v1/storefront/google-analytics/config', async (request, reply) => {
13
+ const channel = getResolvedChannel(request);
14
+ const config = await deps.configService.getConfig(channel.id);
15
+ return reply.send({ data: config });
16
+ });
17
+ // Server-side collect — producer only (Principle X): validate → enqueue → 202.
18
+ app.post('/api/v1/storefront/google-analytics/collect', async (request, reply) => {
19
+ if (!deps.enqueueCollect) {
20
+ return reply.status(503).send({
21
+ error: { code: 'server_side_unavailable', message: 'Server-side tagging is not available.' },
22
+ });
23
+ }
24
+ const channel = getResolvedChannel(request);
25
+ const body = gaCollectRequestSchema.parse(request.body);
26
+ const accepted = await deps.enqueueCollect(channel.id, body);
27
+ return reply.status(202).send({ data: { accepted } });
28
+ });
29
+ }
30
+ //# sourceMappingURL=routes.storefront.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.storefront.js","sourceRoot":"","sources":["../../src/backend/routes.storefront.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAUtE;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,uCAAuC,CAC3D,GAAoB,EACpB,IAAmC;IAEnC,2EAA2E;IAC3E,GAAG,CAAC,GAAG,CAAC,4CAA4C,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QAC7E,MAAM,OAAO,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAC5C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC9D,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,+EAA+E;IAC/E,GAAG,CAAC,IAAI,CAAC,6CAA6C,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QAC/E,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;YACzB,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBAC5B,KAAK,EAAE,EAAE,IAAI,EAAE,yBAAyB,EAAE,OAAO,EAAE,uCAAuC,EAAE;aAC7F,CAAC,CAAC;QACL,CAAC;QACD,MAAM,OAAO,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAC5C,MAAM,IAAI,GAAG,sBAAsB,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACxD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QAC7D,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,29 @@
1
+ import type { CmsBlockSeedPort } from '@endora-commerce/contracts';
2
+ /**
3
+ * Seeds a predefined **CMS block** `cookieconsent.message` (feature 049) that
4
+ * the storefront cookie-consent banner renders as its message text.
5
+ *
6
+ * CMS blocks resolve per sales channel (they require a channel binding), and
7
+ * the system-default channel is created at boot rather than by migration, so
8
+ * this runs as an idempotent boot-time seed — the block is inserted once and
9
+ * bound to any channel not yet bound.
10
+ *
11
+ * **The text is this module's; the storage is `cms`'.** Until feature 075 this
12
+ * file wrote `cms_blocks` and `cms_block_sales_channels` itself, in raw SQL —
13
+ * two ledgered reaches per table that named no import specifier, so the
14
+ * boundary they crossed compiled and returned rows (D-87). It hands `cms` a
15
+ * descriptor now and owns none of the writing.
16
+ */
17
+ export declare const COOKIE_CONSENT_BLOCK_CODE = "cookieconsent.message";
18
+ /**
19
+ * What the seed did, so `cms` being off is an **answer** rather than an
20
+ * exception somebody has to catch.
21
+ *
22
+ * `cms-absent` is the operator having switched the CMS off, or a deployment
23
+ * shipping without it. It costs nothing that is not recovered: deactivation
24
+ * drops no rows, so a block seeded earlier is still there, and the first boot
25
+ * after `cms` comes back binds it to whatever channels appeared meanwhile.
26
+ */
27
+ export type CookieConsentBlockSeedOutcome = 'seeded' | 'cms-absent';
28
+ export declare function ensureCookieConsentBlock(blocks: CmsBlockSeedPort): Promise<CookieConsentBlockSeedOutcome>;
29
+ //# sourceMappingURL=cookie-consent-block-seeder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookie-consent-block-seeder.d.ts","sourceRoot":"","sources":["../../../src/backend/services/cookie-consent-block-seeder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAsB,MAAM,4BAA4B,CAAC;AAGvF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,yBAAyB,0BAA0B,CAAC;AA0BjE;;;;;;;;GAQG;AACH,MAAM,MAAM,6BAA6B,GAAG,QAAQ,GAAG,YAAY,CAAC;AAEpE,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,6BAA6B,CAAC,CAcxC"}
@@ -0,0 +1,51 @@
1
+ import { effectiveState } from '@endora-commerce/platform/kernel';
2
+ /**
3
+ * Seeds a predefined **CMS block** `cookieconsent.message` (feature 049) that
4
+ * the storefront cookie-consent banner renders as its message text.
5
+ *
6
+ * CMS blocks resolve per sales channel (they require a channel binding), and
7
+ * the system-default channel is created at boot rather than by migration, so
8
+ * this runs as an idempotent boot-time seed — the block is inserted once and
9
+ * bound to any channel not yet bound.
10
+ *
11
+ * **The text is this module's; the storage is `cms`'.** Until feature 075 this
12
+ * file wrote `cms_blocks` and `cms_block_sales_channels` itself, in raw SQL —
13
+ * two ledgered reaches per table that named no import specifier, so the
14
+ * boundary they crossed compiled and returned rows (D-87). It hands `cms` a
15
+ * descriptor now and owns none of the writing.
16
+ */
17
+ export const COOKIE_CONSENT_BLOCK_CODE = 'cookieconsent.message';
18
+ const LANGUAGES = ['en-US', 'pl-PL'];
19
+ function tree(html) {
20
+ return {
21
+ root: { props: {} },
22
+ content: [{ type: 'cms.Text', props: { id: 'cookieconsent', tiptapContent: null, html } }],
23
+ zones: {},
24
+ };
25
+ }
26
+ const CONTENT_ENVELOPE = {
27
+ schema_version: 1,
28
+ languages: {
29
+ 'en-US': tree('<p>We use cookies for analytics (Google Analytics). You can accept or reject ' +
30
+ 'statistics. See our <a href="/privacy-policy">Privacy Policy</a>.</p>'),
31
+ 'pl-PL': tree('<p>Używamy plików cookie do analityki (Google Analytics). Możesz zaakceptować lub ' +
32
+ 'odrzucić statystyki. Zobacz naszą <a href="/privacy-policy">Politykę prywatności</a>.</p>'),
33
+ },
34
+ };
35
+ export async function ensureCookieConsentBlock(blocks) {
36
+ // First, and outside any `try`: a closed gate throws rather than answering,
37
+ // so asking after the call is asking too late — and this call happens at
38
+ // route registration, where a `ModuleDisabledError` has nowhere to go but
39
+ // into a failed start. An operator switching `cms` off must not stop
40
+ // `google_analytics` starting (Constitution XVII).
41
+ if (!effectiveState.isPresent('cms'))
42
+ return 'cms-absent';
43
+ await blocks.ensureSeededBlock({
44
+ code: COOKIE_CONSENT_BLOCK_CODE,
45
+ name: 'Cookie consent message',
46
+ languages: LANGUAGES,
47
+ content: CONTENT_ENVELOPE,
48
+ });
49
+ return 'seeded';
50
+ }
51
+ //# sourceMappingURL=cookie-consent-block-seeder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookie-consent-block-seeder.js","sourceRoot":"","sources":["../../../src/backend/services/cookie-consent-block-seeder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAElE;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,uBAAuB,CAAC;AAEjE,MAAM,SAAS,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AAErC,SAAS,IAAI,CAAC,IAAY;IACxB,OAAO;QACL,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;QACnB,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,eAAe,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC;QAC1F,KAAK,EAAE,EAAE;KACV,CAAC;AACJ,CAAC;AAED,MAAM,gBAAgB,GAAuB;IAC3C,cAAc,EAAE,CAAC;IACjB,SAAS,EAAE;QACT,OAAO,EAAE,IAAI,CACX,+EAA+E;YAC7E,uEAAuE,CAC1E;QACD,OAAO,EAAE,IAAI,CACX,oFAAoF;YAClF,2FAA2F,CAC9F;KACF;CACF,CAAC;AAaF,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,MAAwB;IAExB,4EAA4E;IAC5E,yEAAyE;IACzE,0EAA0E;IAC1E,qEAAqE;IACrE,mDAAmD;IACnD,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,KAAK,CAAC;QAAE,OAAO,YAAY,CAAC;IAC1D,MAAM,MAAM,CAAC,iBAAiB,CAAC;QAC7B,IAAI,EAAE,yBAAyB;QAC/B,IAAI,EAAE,wBAAwB;QAC9B,SAAS,EAAE,SAAS;QACpB,OAAO,EAAE,gBAAgB;KAC1B,CAAC,CAAC;IACH,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,54 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { type GaCustomEventCreate, type GaCustomEventUpdate, type GaCustomEventResponse, type GaCustomEventListQuery, type GaStorefrontCustomEvent } from '@endora-commerce/contracts';
3
+ /**
4
+ * Sales-channel code ⇄ id port. Injected so the module stays isolated from the
5
+ * sales_channels internals (Principle I).
6
+ */
7
+ export interface GaChannelPort {
8
+ idByCode(code: string): Promise<string | null>;
9
+ codeById(id: string): Promise<string | null>;
10
+ }
11
+ /** Audit context resolved from the request at the route layer. */
12
+ export interface GaAuditContext {
13
+ actorAdminUserId: string | null;
14
+ }
15
+ export interface GaAuditSink {
16
+ record(input: {
17
+ action: string;
18
+ objectType: string;
19
+ objectId: string;
20
+ actorAdminUserId?: string | null;
21
+ stateBefore?: Record<string, unknown>;
22
+ stateAfter?: Record<string, unknown>;
23
+ }): Promise<unknown>;
24
+ }
25
+ /**
26
+ * CRUD + storefront resolution for GA custom events (feature 049, US3).
27
+ * Boundary validation (buttonId⇄action, static-field-set) is enforced by the
28
+ * Zod schemas at the route; this service owns channel-code resolution, the
29
+ * optimistic-version guard, persistence, and audit.
30
+ */
31
+ export declare class GaCustomEventsService {
32
+ private readonly emFactory;
33
+ private readonly channels;
34
+ private readonly audit?;
35
+ /** Fired after a create/update/delete so callers can invalidate caches. */
36
+ private readonly onChange?;
37
+ constructor(emFactory: () => EntityManager, channels: GaChannelPort, audit?: GaAuditSink | undefined,
38
+ /** Fired after a create/update/delete so callers can invalidate caches. */
39
+ onChange?: (() => void) | undefined);
40
+ list(query: GaCustomEventListQuery): Promise<GaCustomEventResponse[]>;
41
+ get(id: string): Promise<GaCustomEventResponse>;
42
+ create(input: GaCustomEventCreate, ctx: GaAuditContext): Promise<GaCustomEventResponse>;
43
+ update(id: string, input: GaCustomEventUpdate, ctx: GaAuditContext): Promise<GaCustomEventResponse>;
44
+ remove(id: string, ctx: GaAuditContext): Promise<void>;
45
+ /**
46
+ * Enabled events applicable to a channel (channel-scoped OR all-channels),
47
+ * mapped to the storefront shape. Used by GaConfigService.
48
+ */
49
+ loadForChannel(salesChannelId: string): Promise<GaStorefrontCustomEvent[]>;
50
+ private resolveChannelId;
51
+ private notFound;
52
+ private toDto;
53
+ }
54
+ //# sourceMappingURL=custom-events.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"custom-events.service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/custom-events.service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAE7B,MAAM,4BAA4B,CAAC;AAIpC;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/C,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC9C;AAED,kEAAkE;AAClE,MAAM,WAAW,cAAc;IAC7B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,KAAK,EAAE;QACZ,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACjC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACtC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACtC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtB;AAED;;;;;GAKG;AACH,qBAAa,qBAAqB;IAE9B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;IACvB,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAJT,SAAS,EAAE,MAAM,aAAa,EAC9B,QAAQ,EAAE,aAAa,EACvB,KAAK,CAAC,EAAE,WAAW,YAAA;IACpC,2EAA2E;IAC1D,QAAQ,CAAC,GAAE,MAAM,IAAI,aAAA;IAGlC,IAAI,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC;IAcrE,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAO/C,MAAM,CAAC,KAAK,EAAE,mBAAmB,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAuBvF,MAAM,CACV,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,mBAAmB,EAC1B,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,qBAAqB,CAAC;IAiC3B,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAe5D;;;OAGG;IACG,cAAc,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,EAAE,CAAC;YAelE,gBAAgB;IAS9B,OAAO,CAAC,QAAQ;YAIF,KAAK;CAgBpB"}
@@ -0,0 +1,158 @@
1
+ import { ERROR_CODES, } from '@endora-commerce/contracts';
2
+ import { HttpError } from '@endora-commerce/platform/http';
3
+ import { GaCustomEvent } from '../entities/ga-custom-event.entity.js';
4
+ /**
5
+ * CRUD + storefront resolution for GA custom events (feature 049, US3).
6
+ * Boundary validation (buttonId⇄action, static-field-set) is enforced by the
7
+ * Zod schemas at the route; this service owns channel-code resolution, the
8
+ * optimistic-version guard, persistence, and audit.
9
+ */
10
+ export class GaCustomEventsService {
11
+ emFactory;
12
+ channels;
13
+ audit;
14
+ onChange;
15
+ constructor(emFactory, channels, audit,
16
+ /** Fired after a create/update/delete so callers can invalidate caches. */
17
+ onChange) {
18
+ this.emFactory = emFactory;
19
+ this.channels = channels;
20
+ this.audit = audit;
21
+ this.onChange = onChange;
22
+ }
23
+ async list(query) {
24
+ const em = this.emFactory();
25
+ const where = {};
26
+ if (query.salesChannelCode !== undefined) {
27
+ const id = await this.channels.idByCode(query.salesChannelCode);
28
+ where['salesChannelId'] = id;
29
+ }
30
+ if (query.triggerAction !== undefined) {
31
+ where['triggerAction'] = query.triggerAction;
32
+ }
33
+ const rows = await em.find(GaCustomEvent, where, { orderBy: { createdAt: 'asc' } });
34
+ return Promise.all(rows.map((r) => this.toDto(r)));
35
+ }
36
+ async get(id) {
37
+ const em = this.emFactory();
38
+ const row = await em.findOne(GaCustomEvent, { id });
39
+ if (!row)
40
+ throw this.notFound(id);
41
+ return this.toDto(row);
42
+ }
43
+ async create(input, ctx) {
44
+ const em = this.emFactory();
45
+ const salesChannelId = await this.resolveChannelId(input.salesChannelCode);
46
+ const row = em.create(GaCustomEvent, {
47
+ salesChannelId,
48
+ eventName: input.eventName,
49
+ triggerAction: input.triggerAction,
50
+ buttonId: input.buttonId ?? null,
51
+ enabled: input.enabled,
52
+ fields: input.fields,
53
+ });
54
+ await em.persistAndFlush(row);
55
+ await this.audit?.record({
56
+ action: 'google_analytics.custom_event.created',
57
+ objectType: 'ga_custom_event',
58
+ objectId: row.id,
59
+ actorAdminUserId: ctx.actorAdminUserId,
60
+ stateAfter: { eventName: row.eventName, triggerAction: row.triggerAction },
61
+ });
62
+ this.onChange?.();
63
+ return this.toDto(row);
64
+ }
65
+ async update(id, input, ctx) {
66
+ const em = this.emFactory();
67
+ const row = await em.findOne(GaCustomEvent, { id });
68
+ if (!row)
69
+ throw this.notFound(id);
70
+ if (row.version !== input.version) {
71
+ throw new HttpError(409, ERROR_CODES.VERSION_CONFLICT, 'Custom event was modified concurrently.');
72
+ }
73
+ const before = { eventName: row.eventName, triggerAction: row.triggerAction, enabled: row.enabled };
74
+ if (input.salesChannelCode !== undefined) {
75
+ row.salesChannelId =
76
+ input.salesChannelCode === null ? null : await this.resolveChannelId(input.salesChannelCode);
77
+ }
78
+ if (input.eventName !== undefined)
79
+ row.eventName = input.eventName;
80
+ if (input.triggerAction !== undefined)
81
+ row.triggerAction = input.triggerAction;
82
+ if (input.buttonId !== undefined)
83
+ row.buttonId = input.buttonId;
84
+ if (input.enabled !== undefined)
85
+ row.enabled = input.enabled;
86
+ if (input.fields !== undefined)
87
+ row.fields = input.fields;
88
+ row.version += 1;
89
+ await em.persistAndFlush(row);
90
+ await this.audit?.record({
91
+ action: 'google_analytics.custom_event.updated',
92
+ objectType: 'ga_custom_event',
93
+ objectId: row.id,
94
+ actorAdminUserId: ctx.actorAdminUserId,
95
+ stateBefore: before,
96
+ stateAfter: { eventName: row.eventName, triggerAction: row.triggerAction, enabled: row.enabled },
97
+ });
98
+ this.onChange?.();
99
+ return this.toDto(row);
100
+ }
101
+ async remove(id, ctx) {
102
+ const em = this.emFactory();
103
+ const row = await em.findOne(GaCustomEvent, { id });
104
+ if (!row)
105
+ throw this.notFound(id);
106
+ await em.removeAndFlush(row);
107
+ await this.audit?.record({
108
+ action: 'google_analytics.custom_event.deleted',
109
+ objectType: 'ga_custom_event',
110
+ objectId: id,
111
+ actorAdminUserId: ctx.actorAdminUserId,
112
+ stateBefore: { eventName: row.eventName, triggerAction: row.triggerAction },
113
+ });
114
+ this.onChange?.();
115
+ }
116
+ /**
117
+ * Enabled events applicable to a channel (channel-scoped OR all-channels),
118
+ * mapped to the storefront shape. Used by GaConfigService.
119
+ */
120
+ async loadForChannel(salesChannelId) {
121
+ const em = this.emFactory();
122
+ const rows = await em.find(GaCustomEvent, { enabled: true, $or: [{ salesChannelId: null }, { salesChannelId }] }, { orderBy: { createdAt: 'asc' } });
123
+ return rows.map((r) => ({
124
+ eventName: r.eventName,
125
+ triggerAction: r.triggerAction,
126
+ buttonId: r.buttonId,
127
+ fields: r.fields.map((f) => ({ fieldKey: f.fieldKey, payloadKey: f.payloadKey ?? f.fieldKey })),
128
+ }));
129
+ }
130
+ async resolveChannelId(code) {
131
+ if (code === undefined)
132
+ return null;
133
+ const id = await this.channels.idByCode(code);
134
+ if (id === null) {
135
+ throw new HttpError(400, ERROR_CODES.VALIDATION_FAILED, `Unknown sales channel: ${code}`);
136
+ }
137
+ return id;
138
+ }
139
+ notFound(id) {
140
+ return new HttpError(404, ERROR_CODES.NOT_FOUND, `Custom event not found: ${id}`);
141
+ }
142
+ async toDto(row) {
143
+ const salesChannelCode = row.salesChannelId === null ? null : await this.channels.codeById(row.salesChannelId);
144
+ return {
145
+ id: row.id,
146
+ salesChannelCode,
147
+ eventName: row.eventName,
148
+ triggerAction: row.triggerAction,
149
+ buttonId: row.buttonId ?? null,
150
+ enabled: row.enabled,
151
+ fields: row.fields ?? [],
152
+ createdAt: row.createdAt.toISOString(),
153
+ updatedAt: row.updatedAt.toISOString(),
154
+ version: row.version,
155
+ };
156
+ }
157
+ }
158
+ //# sourceMappingURL=custom-events.service.js.map