@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.
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/admin/api/google-analytics-client.d.ts +17 -0
- package/dist/admin/api/google-analytics-client.d.ts.map +1 -0
- package/dist/admin/api/google-analytics-client.js +21 -0
- package/dist/admin/api/google-analytics-client.js.map +1 -0
- package/dist/admin/index.d.ts +29 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +63 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/CustomEventEditPage.d.ts +3 -0
- package/dist/admin/pages/CustomEventEditPage.d.ts.map +1 -0
- package/dist/admin/pages/CustomEventEditPage.js +179 -0
- package/dist/admin/pages/CustomEventEditPage.js.map +1 -0
- package/dist/admin/pages/CustomEventsListPage.d.ts +8 -0
- package/dist/admin/pages/CustomEventsListPage.d.ts.map +1 -0
- package/dist/admin/pages/CustomEventsListPage.js +36 -0
- package/dist/admin/pages/CustomEventsListPage.js.map +1 -0
- package/dist/backend/entities/ga-custom-event.entity.d.ts +26 -0
- package/dist/backend/entities/ga-custom-event.entity.d.ts.map +1 -0
- package/dist/backend/entities/ga-custom-event.entity.js +82 -0
- package/dist/backend/entities/ga-custom-event.entity.js.map +1 -0
- package/dist/backend/index.d.ts +85 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +102 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +15 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +35 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.storefront.d.ts +17 -0
- package/dist/backend/routes.storefront.d.ts.map +1 -0
- package/dist/backend/routes.storefront.js +30 -0
- package/dist/backend/routes.storefront.js.map +1 -0
- package/dist/backend/services/cookie-consent-block-seeder.d.ts +29 -0
- package/dist/backend/services/cookie-consent-block-seeder.d.ts.map +1 -0
- package/dist/backend/services/cookie-consent-block-seeder.js +51 -0
- package/dist/backend/services/cookie-consent-block-seeder.js.map +1 -0
- package/dist/backend/services/custom-events.service.d.ts +54 -0
- package/dist/backend/services/custom-events.service.d.ts.map +1 -0
- package/dist/backend/services/custom-events.service.js +158 -0
- package/dist/backend/services/custom-events.service.js.map +1 -0
- package/dist/backend/services/ga-config.service.d.ts +27 -0
- package/dist/backend/services/ga-config.service.d.ts.map +1 -0
- package/dist/backend/services/ga-config.service.js +66 -0
- package/dist/backend/services/ga-config.service.js.map +1 -0
- package/dist/backend/services/ga4-mp-client.d.ts +32 -0
- package/dist/backend/services/ga4-mp-client.d.ts.map +1 -0
- package/dist/backend/services/ga4-mp-client.js +46 -0
- package/dist/backend/services/ga4-mp-client.js.map +1 -0
- package/dist/backend/services/ss-delivery-queue.d.ts +32 -0
- package/dist/backend/services/ss-delivery-queue.d.ts.map +1 -0
- package/dist/backend/services/ss-delivery-queue.js +29 -0
- package/dist/backend/services/ss-delivery-queue.js.map +1 -0
- package/dist/backend/services/ss-delivery.service.d.ts +24 -0
- package/dist/backend/services/ss-delivery.service.d.ts.map +1 -0
- package/dist/backend/services/ss-delivery.service.js +52 -0
- package/dist/backend/services/ss-delivery.service.js.map +1 -0
- package/dist/manifest.d.ts +194 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +134 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260715T171116_google_analytics_init.d.ts +14 -0
- package/dist/migrations/20260715T171116_google_analytics_init.d.ts.map +1 -0
- package/dist/migrations/20260715T171116_google_analytics_init.js +35 -0
- package/dist/migrations/20260715T171116_google_analytics_init.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 +28 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/google-analytics.md +106 -0
- package/i18n/en.json +33 -0
- package/i18n/pl.json +33 -0
- package/package.json +98 -0
- 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":"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"}
|
|
@@ -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
|