@endora-commerce/mod-linkedin-ads 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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -0
  3. package/dist/admin/api/linkedin-ads-client.d.ts +32 -0
  4. package/dist/admin/api/linkedin-ads-client.d.ts.map +1 -0
  5. package/dist/admin/api/linkedin-ads-client.js +34 -0
  6. package/dist/admin/api/linkedin-ads-client.js.map +1 -0
  7. package/dist/admin/index.d.ts +32 -0
  8. package/dist/admin/index.d.ts.map +1 -0
  9. package/dist/admin/index.js +64 -0
  10. package/dist/admin/index.js.map +1 -0
  11. package/dist/admin/pages/ConversionMappingEditPage.d.ts +4 -0
  12. package/dist/admin/pages/ConversionMappingEditPage.d.ts.map +1 -0
  13. package/dist/admin/pages/ConversionMappingEditPage.js +89 -0
  14. package/dist/admin/pages/ConversionMappingEditPage.js.map +1 -0
  15. package/dist/admin/pages/ConversionMappingsListPage.d.ts +7 -0
  16. package/dist/admin/pages/ConversionMappingsListPage.d.ts.map +1 -0
  17. package/dist/admin/pages/ConversionMappingsListPage.js +84 -0
  18. package/dist/admin/pages/ConversionMappingsListPage.js.map +1 -0
  19. package/dist/backend/entities/linkedin-conversion-mapping.entity.d.ts +30 -0
  20. package/dist/backend/entities/linkedin-conversion-mapping.entity.d.ts.map +1 -0
  21. package/dist/backend/entities/linkedin-conversion-mapping.entity.js +85 -0
  22. package/dist/backend/entities/linkedin-conversion-mapping.entity.js.map +1 -0
  23. package/dist/backend/index.d.ts +58 -0
  24. package/dist/backend/index.d.ts.map +1 -0
  25. package/dist/backend/index.js +60 -0
  26. package/dist/backend/index.js.map +1 -0
  27. package/dist/backend/routes.admin.d.ts +16 -0
  28. package/dist/backend/routes.admin.d.ts.map +1 -0
  29. package/dist/backend/routes.admin.js +31 -0
  30. package/dist/backend/routes.admin.js.map +1 -0
  31. package/dist/backend/routes.storefront.d.ts +12 -0
  32. package/dist/backend/routes.storefront.d.ts.map +1 -0
  33. package/dist/backend/routes.storefront.js +15 -0
  34. package/dist/backend/routes.storefront.js.map +1 -0
  35. package/dist/backend/services/conversion-mappings.service.d.ts +51 -0
  36. package/dist/backend/services/conversion-mappings.service.d.ts.map +1 -0
  37. package/dist/backend/services/conversion-mappings.service.js +155 -0
  38. package/dist/backend/services/conversion-mappings.service.js.map +1 -0
  39. package/dist/backend/services/linkedin-config.service.d.ts +27 -0
  40. package/dist/backend/services/linkedin-config.service.d.ts.map +1 -0
  41. package/dist/backend/services/linkedin-config.service.js +57 -0
  42. package/dist/backend/services/linkedin-config.service.js.map +1 -0
  43. package/dist/manifest.d.ts +195 -0
  44. package/dist/manifest.d.ts.map +1 -0
  45. package/dist/manifest.js +119 -0
  46. package/dist/manifest.js.map +1 -0
  47. package/dist/migrations/20260727T233211_linkedin_ads_init.d.ts +12 -0
  48. package/dist/migrations/20260727T233211_linkedin_ads_init.d.ts.map +1 -0
  49. package/dist/migrations/20260727T233211_linkedin_ads_init.js +36 -0
  50. package/dist/migrations/20260727T233211_linkedin_ads_init.js.map +1 -0
  51. package/dist/migrations/index.d.ts +27 -0
  52. package/dist/migrations/index.d.ts.map +1 -0
  53. package/dist/migrations/index.js +29 -0
  54. package/dist/migrations/index.js.map +1 -0
  55. package/docs/linkedin-ads.md +73 -0
  56. package/i18n/en.json +35 -0
  57. package/i18n/pl.json +35 -0
  58. package/package.json +92 -0
  59. package/tailwind.css +14 -0
@@ -0,0 +1,58 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { FastifyRequest } from 'fastify';
3
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
4
+ import type { ModuleContext } from '@endora-commerce/platform/kernel';
5
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
6
+ import type { SettingsReadPort } from '@endora-commerce/platform/kernel';
7
+ import { StorefrontRevalidator } from '@endora-commerce/platform/http';
8
+ import { LinkedInConfigService } from './services/linkedin-config.service.js';
9
+ import { LinkedInConversionMappingsService, type LinkedInAuditContext } from './services/conversion-mappings.service.js';
10
+ import { LinkedInConversionMapping } from './entities/linkedin-conversion-mapping.entity.js';
11
+ /**
12
+ * `linkedin_ads` — converted with `meta_ads`, which it is a near-copy of
13
+ * (feature 072, wave 2, T106).
14
+ *
15
+ * Same off-state leak, same fix: the routes were gated by `defineModuleRoutes`
16
+ * but the `settings.value_changed` subscription arrived from a root as a bare
17
+ * `eventBus.on`, so a switched-off module still answered every
18
+ * `linkedin_ads.*` settings change with an outbound storefront revalidation.
19
+ * `ctx.subscribe` is `subscribeForModule`, so the subscription now stops with
20
+ * the module.
21
+ *
22
+ * `auditLog` stops being optional here too — see the note in `meta_ads`.
23
+ *
24
+ * The two modules are converted in one commit rather than one after the other.
25
+ * They are the same forty lines twice over, both roots build them from adjacent
26
+ * blocks, and splitting them would have meant leaving the shared
27
+ * `adminAuditActorResolver` contribution half-introduced between commits.
28
+ */
29
+ interface LinkedInAdsServices {
30
+ readonly revalidator: StorefrontRevalidator;
31
+ readonly mappings: LinkedInConversionMappingsService;
32
+ readonly configService: LinkedInConfigService;
33
+ readonly invalidateConfig: () => void;
34
+ }
35
+ export interface LinkedInAdsCradle {
36
+ readonly emFactory: () => EntityManager;
37
+ readonly auditLogService: AuditPort;
38
+ readonly settingsReadPort: SettingsReadPort;
39
+ readonly requireAdmin: RequireAdminFactory;
40
+ /** How this composition names the acting admin; `null` for a non-admin caller. */
41
+ readonly adminAuditActorResolver: (request: FastifyRequest) => LinkedInAuditContext;
42
+ readonly linkedInAdsServices: LinkedInAdsServices;
43
+ }
44
+ export declare function registerModule(ctx: ModuleContext): void;
45
+ /**
46
+ * The module's persisted entity classes, on the `./backend` subpath, as one
47
+ * array and **no named class export** (D-168).
48
+ *
49
+ * This is the shape the platform reads when the package is *installed*: the
50
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
51
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
52
+ * which is the third source of `check:module-boundary`'s `table→owner` map and
53
+ * the package pass of `check-entity-tenant-classification`. A missing array is
54
+ * answered with `[]` — zero entities registered, no error anywhere.
55
+ */
56
+ export declare const entities: (typeof LinkedInConversionMapping)[];
57
+ export {};
58
+ //# 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,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAEtE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EACL,iCAAiC,EACjC,KAAK,oBAAoB,EAC1B,MAAM,2CAA2C,CAAC;AAGnD,OAAO,EAAE,yBAAyB,EAAE,MAAM,kDAAkD,CAAC;AAE7F;;;;;;;;;;;;;;;;;GAiBG;AAEH,UAAU,mBAAmB;IAC3B,QAAQ,CAAC,WAAW,EAAE,qBAAqB,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,iCAAiC,CAAC;IACrD,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC;IAC9C,QAAQ,CAAC,gBAAgB,EAAE,MAAM,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,iBAAiB;IAChC,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,kFAAkF;IAClF,QAAQ,CAAC,uBAAuB,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,oBAAoB,CAAC;IACpF,QAAQ,CAAC,mBAAmB,EAAE,mBAAmB,CAAC;CACnD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAoDvD;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,sCAEpB,CAAC"}
@@ -0,0 +1,60 @@
1
+ import { lazyPort } from '@endora-commerce/platform/kernel';
2
+ import { StorefrontRevalidator } from '@endora-commerce/platform/http';
3
+ import { LinkedInConfigService } from './services/linkedin-config.service.js';
4
+ import { LinkedInConversionMappingsService, } from './services/conversion-mappings.service.js';
5
+ import { registerLinkedInAdsAdminRoutes } from './routes.admin.js';
6
+ import { registerLinkedInAdsStorefrontRoutes } from './routes.storefront.js';
7
+ import { LinkedInConversionMapping } from './entities/linkedin-conversion-mapping.entity.js';
8
+ export function registerModule(ctx) {
9
+ ctx.di.register({
10
+ linkedInAdsServices: ctx
11
+ .asFunction(({ emFactory, auditLogService, }) => {
12
+ const revalidator = new StorefrontRevalidator({
13
+ baseUrl: process.env['STOREFRONT_BASE_URL'],
14
+ secret: process.env['REVALIDATE_SECRET'],
15
+ });
16
+ const invalidateConfig = () => {
17
+ void revalidator.revalidate(['linkedin:config']);
18
+ };
19
+ const mappings = new LinkedInConversionMappingsService(emFactory, auditLogService, invalidateConfig);
20
+ const configService = new LinkedInConfigService(lazyPort(ctx, 'settingsReadPort'), (channelId) => mappings.loadForChannel(channelId));
21
+ return { revalidator, mappings, configService, invalidateConfig };
22
+ })
23
+ .singleton(),
24
+ });
25
+ // Config edits propagate immediately rather than after the cache TTL — while
26
+ // the module is on.
27
+ ctx.subscribe('settings.value_changed', (payload) => {
28
+ const settingCode = payload.settingCode;
29
+ if (typeof settingCode !== 'string' || !settingCode.startsWith('linkedin_ads.'))
30
+ return;
31
+ ctx.cradle().linkedInAdsServices.invalidateConfig();
32
+ });
33
+ ctx.routes(async (app) => {
34
+ const { linkedInAdsServices, requireAdmin, adminAuditActorResolver } = ctx.cradle();
35
+ linkedInAdsServices.revalidator.setLogger(app.log);
36
+ await registerLinkedInAdsStorefrontRoutes(app, {
37
+ configService: linkedInAdsServices.configService,
38
+ });
39
+ await registerLinkedInAdsAdminRoutes(app, {
40
+ mappings: linkedInAdsServices.mappings,
41
+ requireAdmin,
42
+ resolveAuditContext: adminAuditActorResolver,
43
+ });
44
+ });
45
+ }
46
+ /**
47
+ * The module's persisted entity classes, on the `./backend` subpath, as one
48
+ * array and **no named class export** (D-168).
49
+ *
50
+ * This is the shape the platform reads when the package is *installed*: the
51
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
52
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
53
+ * which is the third source of `check:module-boundary`'s `table→owner` map and
54
+ * the package pass of `check-entity-tenant-classification`. A missing array is
55
+ * answered with `[]` — zero entities registered, no error anywhere.
56
+ */
57
+ export const entities = [
58
+ LinkedInConversionMapping,
59
+ ];
60
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAG5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EACL,iCAAiC,GAElC,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,8BAA8B,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAE,mCAAmC,EAAE,MAAM,wBAAwB,CAAC;AAC7E,OAAO,EAAE,yBAAyB,EAAE,MAAM,kDAAkD,CAAC;AAsC7F,MAAM,UAAU,cAAc,CAAC,GAAkB;IAC/C,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC;QACd,mBAAmB,EAAE,GAAG;aACrB,UAAU,CACT,CAAC,EACC,SAAS,EACT,eAAe,GACG,EAAuB,EAAE;YAC3C,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,iBAAiB,CAAC,CAAC,CAAC;YACnD,CAAC,CAAC;YACF,MAAM,QAAQ,GAAG,IAAI,iCAAiC,CACpD,SAAS,EACT,eAAe,EACf,gBAAgB,CACjB,CAAC;YACF,MAAM,aAAa,GAAG,IAAI,qBAAqB,CAC7C,QAAQ,CAAmB,GAAG,EAAE,kBAAkB,CAAC,EACnD,CAAC,SAAS,EAAE,EAAE,CACd,QAAQ,CAAC,cAAc,CAAC,SAAS,CAAC,CACnC,CAAC;YACF,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,aAAa,EAAE,gBAAgB,EAAE,CAAC;QACpE,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,eAAe,CAAC;YAAE,OAAO;QACxF,GAAG,CAAC,MAAM,EAAqB,CAAC,mBAAmB,CAAC,gBAAgB,EAAE,CAAC;IACzE,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACvB,MAAM,EAAE,mBAAmB,EAAE,YAAY,EAAE,uBAAuB,EAAE,GAClE,GAAG,CAAC,MAAM,EAAqB,CAAC;QAClC,mBAAmB,CAAC,WAAW,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACnD,MAAM,mCAAmC,CAAC,GAAG,EAAE;YAC7C,aAAa,EAAE,mBAAmB,CAAC,aAAa;SACjD,CAAC,CAAC;QACH,MAAM,8BAA8B,CAAC,GAAG,EAAE;YACxC,QAAQ,EAAE,mBAAmB,CAAC,QAAQ;YACtC,YAAY;YACZ,mBAAmB,EAAE,uBAAuB;SAC7C,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,yBAAyB;CAC1B,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
2
+ import type { LinkedInAuditContext, LinkedInConversionMappingsService } from './services/conversion-mappings.service.js';
3
+ export interface LinkedInAdsAdminDeps {
4
+ mappings: LinkedInConversionMappingsService;
5
+ requireAdmin: (permission?: string) => (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
6
+ resolveAuditContext: (req: FastifyRequest) => LinkedInAuditContext;
7
+ }
8
+ /**
9
+ * Admin routes for the LinkedIn Ads module (feature 063, US3). Conversion-mapping
10
+ * CRUD plus the closed action catalogue that drives the admin picker.
11
+ *
12
+ * Per-channel Settings (Partner ID, consent, server-side, access token) are
13
+ * managed through the generic Settings admin surface, not here.
14
+ */
15
+ export declare function registerLinkedInAdsAdminRoutes(app: FastifyInstance, deps: LinkedInAdsAdminDeps): Promise<void>;
16
+ //# 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;AAU7E,OAAO,KAAK,EACV,oBAAoB,EACpB,iCAAiC,EAClC,MAAM,2CAA2C,CAAC;AAEnD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,iCAAiC,CAAC;IAC5C,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,oBAAoB,CAAC;CACpE;AAED;;;;;;GAMG;AACH,wBAAsB,8BAA8B,CAClD,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,oBAAoB,GACzB,OAAO,CAAC,IAAI,CAAC,CAsDf"}
@@ -0,0 +1,31 @@
1
+ import { LINKEDIN_TRIGGER_ACTIONS, createLinkedInConversionMappingSchema, updateLinkedInConversionMappingSchema, } from '@endora-commerce/contracts';
2
+ import { LINKEDIN_ADS_READ_PERMISSION, LINKEDIN_ADS_WRITE_PERMISSION, } from '../manifest.js';
3
+ /**
4
+ * Admin routes for the LinkedIn Ads module (feature 063, US3). Conversion-mapping
5
+ * CRUD plus the closed action catalogue that drives the admin picker.
6
+ *
7
+ * Per-channel Settings (Partner ID, consent, server-side, access token) are
8
+ * managed through the generic Settings admin surface, not here.
9
+ */
10
+ export async function registerLinkedInAdsAdminRoutes(app, deps) {
11
+ const { mappings, requireAdmin, resolveAuditContext } = deps;
12
+ const BASE = '/api/v1/admin/linkedin-ads';
13
+ app.get(`${BASE}/action-catalogue`, { preHandler: requireAdmin(LINKEDIN_ADS_READ_PERMISSION) }, async () => ({ data: LINKEDIN_TRIGGER_ACTIONS }));
14
+ app.get(`${BASE}/conversion-mappings`, { preHandler: requireAdmin(LINKEDIN_ADS_READ_PERMISSION) }, async () => ({ data: await mappings.list() }));
15
+ app.get(`${BASE}/conversion-mappings/:id`, { preHandler: requireAdmin(LINKEDIN_ADS_READ_PERMISSION) }, async (request) => ({ data: await mappings.get(request.params.id) }));
16
+ app.post(`${BASE}/conversion-mappings`, { preHandler: requireAdmin(LINKEDIN_ADS_WRITE_PERMISSION) }, async (request, reply) => {
17
+ const body = createLinkedInConversionMappingSchema.parse(request.body);
18
+ const created = await mappings.create(body, resolveAuditContext(request));
19
+ return reply.status(201).send({ data: created });
20
+ });
21
+ app.patch(`${BASE}/conversion-mappings/:id`, { preHandler: requireAdmin(LINKEDIN_ADS_WRITE_PERMISSION) }, async (request) => {
22
+ const body = updateLinkedInConversionMappingSchema.parse(request.body);
23
+ const updated = await mappings.update(request.params.id, body, resolveAuditContext(request));
24
+ return { data: updated };
25
+ });
26
+ app.delete(`${BASE}/conversion-mappings/:id`, { preHandler: requireAdmin(LINKEDIN_ADS_WRITE_PERMISSION) }, async (request, reply) => {
27
+ await mappings.remove(request.params.id, resolveAuditContext(request));
28
+ return reply.status(204).send();
29
+ });
30
+ }
31
+ //# 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,wBAAwB,EACxB,qCAAqC,EACrC,qCAAqC,GACtC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,4BAA4B,EAC5B,6BAA6B,GAC9B,MAAM,gBAAgB,CAAC;AAYxB;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAClD,GAAoB,EACpB,IAA0B;IAE1B,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,mBAAmB,EAAE,GAAG,IAAI,CAAC;IAC7D,MAAM,IAAI,GAAG,4BAA4B,CAAC;IAE1C,GAAG,CAAC,GAAG,CACL,GAAG,IAAI,mBAAmB,EAC1B,EAAE,UAAU,EAAE,YAAY,CAAC,4BAA4B,CAAC,EAAE,EAC1D,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,wBAAwB,EAAE,CAAC,CACjD,CAAC;IAEF,GAAG,CAAC,GAAG,CACL,GAAG,IAAI,sBAAsB,EAC7B,EAAE,UAAU,EAAE,YAAY,CAAC,4BAA4B,CAAC,EAAE,EAC1D,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAC9C,CAAC;IAEF,GAAG,CAAC,GAAG,CACL,GAAG,IAAI,0BAA0B,EACjC,EAAE,UAAU,EAAE,YAAY,CAAC,4BAA4B,CAAC,EAAE,EAC1D,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CACrE,CAAC;IAEF,GAAG,CAAC,IAAI,CACN,GAAG,IAAI,sBAAsB,EAC7B,EAAE,UAAU,EAAE,YAAY,CAAC,6BAA6B,CAAC,EAAE,EAC3D,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACvB,MAAM,IAAI,GAAG,qCAAqC,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACvE,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC;QAC1E,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IACnD,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,KAAK,CACP,GAAG,IAAI,0BAA0B,EACjC,EAAE,UAAU,EAAE,YAAY,CAAC,6BAA6B,CAAC,EAAE,EAC3D,KAAK,EAAE,OAAO,EAAE,EAAE;QAChB,MAAM,IAAI,GAAG,qCAAqC,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACvE,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,MAAM,CACnC,OAAO,CAAC,MAAM,CAAC,EAAE,EACjB,IAAI,EACJ,mBAAmB,CAAC,OAAO,CAAC,CAC7B,CAAC;QACF,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IAC3B,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,MAAM,CACR,GAAG,IAAI,0BAA0B,EACjC,EAAE,UAAU,EAAE,YAAY,CAAC,6BAA6B,CAAC,EAAE,EAC3D,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACvB,MAAM,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC;QACvE,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAClC,CAAC,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { LinkedInConfigService } from './services/linkedin-config.service.js';
3
+ export interface LinkedInAdsStorefrontDeps {
4
+ configService: LinkedInConfigService;
5
+ }
6
+ /**
7
+ * Public storefront routes for the LinkedIn Ads module (feature 063). Wrapped by
8
+ * `defineModuleRoutes` in plugin.ts so they 503 when the module is disabled.
9
+ * Channel comes from the shared sales-channel resolver.
10
+ */
11
+ export declare function registerLinkedInAdsStorefrontRoutes(app: FastifyInstance, deps: LinkedInAdsStorefrontDeps): Promise<void>;
12
+ //# 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;AAE/C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAEnF,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,qBAAqB,CAAC;CACtC;AAED;;;;GAIG;AACH,wBAAsB,mCAAmC,CACvD,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,yBAAyB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAOf"}
@@ -0,0 +1,15 @@
1
+ import { getResolvedChannel } from '@endora-commerce/platform/kernel';
2
+ /**
3
+ * Public storefront routes for the LinkedIn Ads module (feature 063). Wrapped by
4
+ * `defineModuleRoutes` in plugin.ts so they 503 when the module is disabled.
5
+ * Channel comes from the shared sales-channel resolver.
6
+ */
7
+ export async function registerLinkedInAdsStorefrontRoutes(app, deps) {
8
+ // Per-channel public config for the browser. Never returns the access token.
9
+ app.get('/api/v1/storefront/linkedin-ads/config', async (request, reply) => {
10
+ const channel = getResolvedChannel(request);
11
+ const config = await deps.configService.getConfig(channel.id);
12
+ return reply.send({ data: config });
13
+ });
14
+ }
15
+ //# 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,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAOtE;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,mCAAmC,CACvD,GAAoB,EACpB,IAA+B;IAE/B,6EAA6E;IAC7E,GAAG,CAAC,GAAG,CAAC,wCAAwC,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACzE,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;AACL,CAAC"}
@@ -0,0 +1,51 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { type CreateLinkedInConversionMapping, type LinkedInConversionMapping as LinkedInConversionMappingDto, type LinkedInStorefrontMapping, type LinkedInTriggerAction, type UpdateLinkedInConversionMapping } from '@endora-commerce/contracts';
3
+ /** Audit context resolved from the request at the route layer. */
4
+ export interface LinkedInAuditContext {
5
+ actorAdminUserId: string | null;
6
+ }
7
+ export interface LinkedInAuditSink {
8
+ record(input: {
9
+ action: string;
10
+ objectType: string;
11
+ objectId: string;
12
+ actorAdminUserId?: string | null;
13
+ stateBefore?: Record<string, unknown>;
14
+ stateAfter?: Record<string, unknown>;
15
+ }): Promise<unknown>;
16
+ }
17
+ /**
18
+ * CRUD + storefront resolution for LinkedIn conversion mappings (feature 063,
19
+ * US3). Boundary validation lives in the Zod schemas at the route; this service
20
+ * owns the optimistic-version guard, persistence, and audit.
21
+ */
22
+ export declare class LinkedInConversionMappingsService {
23
+ private readonly emFactory;
24
+ private readonly audit?;
25
+ /** Fired after a create/update/delete so callers can invalidate caches. */
26
+ private readonly onChange?;
27
+ constructor(emFactory: () => EntityManager, audit?: LinkedInAuditSink | undefined,
28
+ /** Fired after a create/update/delete so callers can invalidate caches. */
29
+ onChange?: (() => void) | undefined);
30
+ list(): Promise<LinkedInConversionMappingDto[]>;
31
+ get(id: string): Promise<LinkedInConversionMappingDto>;
32
+ create(input: CreateLinkedInConversionMapping, ctx: LinkedInAuditContext): Promise<LinkedInConversionMappingDto>;
33
+ update(id: string, input: UpdateLinkedInConversionMapping, ctx: LinkedInAuditContext): Promise<LinkedInConversionMappingDto>;
34
+ remove(id: string, ctx: LinkedInAuditContext): Promise<void>;
35
+ /**
36
+ * Enabled mappings applicable to a channel (channel-scoped OR all-channels),
37
+ * in the storefront shape. A NULL `salesChannelId` means "every channel" by
38
+ * explicit design, not by fail-open (Principle XII).
39
+ */
40
+ loadForChannel(salesChannelId: string): Promise<LinkedInStorefrontMapping[]>;
41
+ /**
42
+ * Enabled mappings for one action on one channel, with the conversion rule
43
+ * URN resolved — server-side delivery addresses a rule by URN, not by the
44
+ * numeric id shown in Campaign Manager.
45
+ */
46
+ loadRuleUrnsFor(salesChannelId: string, triggerAction: LinkedInTriggerAction): Promise<string[]>;
47
+ private findEnabledForChannel;
48
+ private notFound;
49
+ private toDto;
50
+ }
51
+ //# sourceMappingURL=conversion-mappings.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversion-mappings.service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/conversion-mappings.service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAGL,KAAK,+BAA+B,EACpC,KAAK,yBAAyB,IAAI,4BAA4B,EAC9D,KAAK,yBAAyB,EAC9B,KAAK,qBAAqB,EAC1B,KAAK,+BAA+B,EACrC,MAAM,4BAA4B,CAAC;AAIpC,kEAAkE;AAClE,MAAM,WAAW,oBAAoB;IACnC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,iBAAiB;IAChC,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;;;;GAIG;AACH,qBAAa,iCAAiC;IAE1C,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;IACvB,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAHT,SAAS,EAAE,MAAM,aAAa,EAC9B,KAAK,CAAC,EAAE,iBAAiB,YAAA;IAC1C,2EAA2E;IAC1D,QAAQ,CAAC,GAAE,MAAM,IAAI,aAAA;IAGlC,IAAI,IAAI,OAAO,CAAC,4BAA4B,EAAE,CAAC;IAM/C,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,4BAA4B,CAAC;IAOtD,MAAM,CACV,KAAK,EAAE,+BAA+B,EACtC,GAAG,EAAE,oBAAoB,GACxB,OAAO,CAAC,4BAA4B,CAAC;IAqBlC,MAAM,CACV,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,+BAA+B,EACtC,GAAG,EAAE,oBAAoB,GACxB,OAAO,CAAC,4BAA4B,CAAC;IAyClC,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBlE;;;;OAIG;IACG,cAAc,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,yBAAyB,EAAE,CAAC;IAQlF;;;;OAIG;IACG,eAAe,CACnB,cAAc,EAAE,MAAM,EACtB,aAAa,EAAE,qBAAqB,GACnC,OAAO,CAAC,MAAM,EAAE,CAAC;YAON,qBAAqB;IAWnC,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,KAAK;CAgBd"}
@@ -0,0 +1,155 @@
1
+ import { ERROR_CODES, conversionRuleUrnFor, } from '@endora-commerce/contracts';
2
+ import { HttpError } from '@endora-commerce/platform/http';
3
+ import { LinkedInConversionMapping } from '../entities/linkedin-conversion-mapping.entity.js';
4
+ /**
5
+ * CRUD + storefront resolution for LinkedIn conversion mappings (feature 063,
6
+ * US3). Boundary validation lives in the Zod schemas at the route; this service
7
+ * owns the optimistic-version guard, persistence, and audit.
8
+ */
9
+ export class LinkedInConversionMappingsService {
10
+ emFactory;
11
+ audit;
12
+ onChange;
13
+ constructor(emFactory, audit,
14
+ /** Fired after a create/update/delete so callers can invalidate caches. */
15
+ onChange) {
16
+ this.emFactory = emFactory;
17
+ this.audit = audit;
18
+ this.onChange = onChange;
19
+ }
20
+ async list() {
21
+ const em = this.emFactory();
22
+ const rows = await em.find(LinkedInConversionMapping, {}, { orderBy: { createdAt: 'asc' } });
23
+ return rows.map((r) => this.toDto(r));
24
+ }
25
+ async get(id) {
26
+ const em = this.emFactory();
27
+ const row = await em.findOne(LinkedInConversionMapping, { id });
28
+ if (!row)
29
+ throw this.notFound(id);
30
+ return this.toDto(row);
31
+ }
32
+ async create(input, ctx) {
33
+ const em = this.emFactory();
34
+ const row = em.create(LinkedInConversionMapping, {
35
+ salesChannelId: input.salesChannelId ?? null,
36
+ triggerAction: input.triggerAction,
37
+ conversionId: input.conversionId,
38
+ conversionRuleUrn: input.conversionRuleUrn ?? null,
39
+ enabled: input.enabled ?? true,
40
+ });
41
+ await em.persistAndFlush(row);
42
+ await this.audit?.record({
43
+ action: 'linkedin_ads.conversion_mapping.created',
44
+ objectType: 'linkedin_conversion_mapping',
45
+ objectId: row.id,
46
+ actorAdminUserId: ctx.actorAdminUserId,
47
+ stateAfter: { triggerAction: row.triggerAction, conversionId: row.conversionId },
48
+ });
49
+ this.onChange?.();
50
+ return this.toDto(row);
51
+ }
52
+ async update(id, input, ctx) {
53
+ const em = this.emFactory();
54
+ const row = await em.findOne(LinkedInConversionMapping, { id });
55
+ if (!row)
56
+ throw this.notFound(id);
57
+ if (row.version !== input.version) {
58
+ throw new HttpError(409, ERROR_CODES.VERSION_CONFLICT, 'Conversion mapping was modified concurrently.');
59
+ }
60
+ const before = {
61
+ triggerAction: row.triggerAction,
62
+ conversionId: row.conversionId,
63
+ enabled: row.enabled,
64
+ };
65
+ if (input.salesChannelId !== undefined)
66
+ row.salesChannelId = input.salesChannelId;
67
+ if (input.triggerAction !== undefined)
68
+ row.triggerAction = input.triggerAction;
69
+ if (input.conversionId !== undefined)
70
+ row.conversionId = input.conversionId;
71
+ if (input.conversionRuleUrn !== undefined)
72
+ row.conversionRuleUrn = input.conversionRuleUrn;
73
+ if (input.enabled !== undefined)
74
+ row.enabled = input.enabled;
75
+ row.version += 1;
76
+ await em.persistAndFlush(row);
77
+ await this.audit?.record({
78
+ action: 'linkedin_ads.conversion_mapping.updated',
79
+ objectType: 'linkedin_conversion_mapping',
80
+ objectId: row.id,
81
+ actorAdminUserId: ctx.actorAdminUserId,
82
+ stateBefore: before,
83
+ stateAfter: {
84
+ triggerAction: row.triggerAction,
85
+ conversionId: row.conversionId,
86
+ enabled: row.enabled,
87
+ },
88
+ });
89
+ this.onChange?.();
90
+ return this.toDto(row);
91
+ }
92
+ async remove(id, ctx) {
93
+ const em = this.emFactory();
94
+ const row = await em.findOne(LinkedInConversionMapping, { id });
95
+ if (!row)
96
+ throw this.notFound(id);
97
+ const before = { triggerAction: row.triggerAction, conversionId: row.conversionId };
98
+ await em.removeAndFlush(row);
99
+ await this.audit?.record({
100
+ action: 'linkedin_ads.conversion_mapping.deleted',
101
+ objectType: 'linkedin_conversion_mapping',
102
+ objectId: id,
103
+ actorAdminUserId: ctx.actorAdminUserId,
104
+ stateBefore: before,
105
+ });
106
+ this.onChange?.();
107
+ }
108
+ /**
109
+ * Enabled mappings applicable to a channel (channel-scoped OR all-channels),
110
+ * in the storefront shape. A NULL `salesChannelId` means "every channel" by
111
+ * explicit design, not by fail-open (Principle XII).
112
+ */
113
+ async loadForChannel(salesChannelId) {
114
+ const rows = await this.findEnabledForChannel(salesChannelId);
115
+ return rows.map((r) => ({
116
+ triggerAction: r.triggerAction,
117
+ conversionId: r.conversionId,
118
+ }));
119
+ }
120
+ /**
121
+ * Enabled mappings for one action on one channel, with the conversion rule
122
+ * URN resolved — server-side delivery addresses a rule by URN, not by the
123
+ * numeric id shown in Campaign Manager.
124
+ */
125
+ async loadRuleUrnsFor(salesChannelId, triggerAction) {
126
+ const rows = await this.findEnabledForChannel(salesChannelId);
127
+ return rows
128
+ .filter((r) => r.triggerAction === triggerAction)
129
+ .map((r) => r.conversionRuleUrn ?? conversionRuleUrnFor(r.conversionId));
130
+ }
131
+ async findEnabledForChannel(salesChannelId) {
132
+ const em = this.emFactory();
133
+ return em.find(LinkedInConversionMapping, { enabled: true, $or: [{ salesChannelId: null }, { salesChannelId }] }, { orderBy: { createdAt: 'asc' } });
134
+ }
135
+ notFound(id) {
136
+ return new HttpError(404, ERROR_CODES.NOT_FOUND, `Conversion mapping not found: ${id}`);
137
+ }
138
+ toDto(row) {
139
+ return {
140
+ id: row.id,
141
+ // `?? null` rather than a bare read: a freshly-created entity leaves an
142
+ // omitted optional property undefined, and JSON.stringify would drop the
143
+ // key entirely instead of sending the null the contract promises.
144
+ salesChannelId: row.salesChannelId ?? null,
145
+ triggerAction: row.triggerAction,
146
+ conversionId: row.conversionId,
147
+ conversionRuleUrn: row.conversionRuleUrn ?? null,
148
+ enabled: row.enabled,
149
+ version: row.version,
150
+ createdAt: row.createdAt.toISOString(),
151
+ updatedAt: row.updatedAt.toISOString(),
152
+ };
153
+ }
154
+ }
155
+ //# sourceMappingURL=conversion-mappings.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversion-mappings.service.js","sourceRoot":"","sources":["../../../src/backend/services/conversion-mappings.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import { type LinkedInStorefrontConfig, type LinkedInStorefrontMapping } from '@endora-commerce/contracts';
2
+ import type { SettingsReadPort } from '@endora-commerce/platform/kernel';
3
+ /**
4
+ * Loader for a channel's enabled conversion mappings in the storefront shape.
5
+ * Injected so the config service stays decoupled from mapping persistence.
6
+ */
7
+ export type ConversionMappingsLoader = (salesChannelId: string) => Promise<LinkedInStorefrontMapping[]>;
8
+ /**
9
+ * Resolves the per-sales-channel LinkedIn Ads configuration exposed to the
10
+ * storefront, from the module's Settings values plus the channel's enabled
11
+ * conversion mappings.
12
+ *
13
+ * A blank or absent Partner ID means the channel is untracked (FR-004): the
14
+ * returned config is fully disabled regardless of the master switch, so the
15
+ * storefront loads nothing. Every setting read degrades to its default rather
16
+ * than throwing — an unregistered or out-of-scope setting must not be able to
17
+ * fail a storefront render (FR-019).
18
+ */
19
+ export declare class LinkedInConfigService {
20
+ private readonly settings;
21
+ private readonly loadMappings;
22
+ constructor(settings: SettingsReadPort, loadMappings?: ConversionMappingsLoader);
23
+ getConfig(salesChannelId: string): Promise<LinkedInStorefrontConfig>;
24
+ private readBool;
25
+ private readString;
26
+ }
27
+ //# sourceMappingURL=linkedin-config.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linkedin-config.service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/linkedin-config.service.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,EAC/B,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAEzE;;;GAGG;AACH,MAAM,MAAM,wBAAwB,GAAG,CACrC,cAAc,EAAE,MAAM,KACnB,OAAO,CAAC,yBAAyB,EAAE,CAAC,CAAC;AAE1C;;;;;;;;;;GAUG;AACH,qBAAa,qBAAqB;IAE9B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,YAAY;gBADZ,QAAQ,EAAE,gBAAgB,EAC1B,YAAY,GAAE,wBAAyC;IAGpE,SAAS,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,wBAAwB,CAAC;YAwB5D,QAAQ;YAYR,UAAU;CAWzB"}
@@ -0,0 +1,57 @@
1
+ import { z } from 'zod';
2
+ import { LINKEDIN_ADS_SETTING_CODES, LINKEDIN_DISABLED_CONFIG, } from '@endora-commerce/contracts';
3
+ /**
4
+ * Resolves the per-sales-channel LinkedIn Ads configuration exposed to the
5
+ * storefront, from the module's Settings values plus the channel's enabled
6
+ * conversion mappings.
7
+ *
8
+ * A blank or absent Partner ID means the channel is untracked (FR-004): the
9
+ * returned config is fully disabled regardless of the master switch, so the
10
+ * storefront loads nothing. Every setting read degrades to its default rather
11
+ * than throwing — an unregistered or out-of-scope setting must not be able to
12
+ * fail a storefront render (FR-019).
13
+ */
14
+ export class LinkedInConfigService {
15
+ settings;
16
+ loadMappings;
17
+ constructor(settings, loadMappings = async () => []) {
18
+ this.settings = settings;
19
+ this.loadMappings = loadMappings;
20
+ }
21
+ async getConfig(salesChannelId) {
22
+ const [enabled, partnerIdRaw, requireConsent] = await Promise.all([
23
+ this.readBool(LINKEDIN_ADS_SETTING_CODES.ENABLED, salesChannelId, false),
24
+ this.readString(LINKEDIN_ADS_SETTING_CODES.PARTNER_ID, salesChannelId, ''),
25
+ this.readBool(LINKEDIN_ADS_SETTING_CODES.REQUIRE_CONSENT, salesChannelId, true),
26
+ ]);
27
+ const partnerId = partnerIdRaw.trim() || null;
28
+ // Untracked channel: master switch off, or no partner id (FR-004). The
29
+ // operator's consent choice is still reported so the storefront's consent
30
+ // banner behaves consistently across channels.
31
+ if (!enabled || partnerId === null) {
32
+ return { ...LINKEDIN_DISABLED_CONFIG, requireConsent };
33
+ }
34
+ const [serverSide, conversionMappings] = await Promise.all([
35
+ this.readBool(LINKEDIN_ADS_SETTING_CODES.SERVER_SIDE_ENABLED, salesChannelId, false),
36
+ this.loadMappings(salesChannelId),
37
+ ]);
38
+ return { enabled: true, partnerId, requireConsent, serverSide, conversionMappings };
39
+ }
40
+ async readBool(code, salesChannelId, fallback) {
41
+ try {
42
+ return await this.settings.get(code, salesChannelId, z.boolean());
43
+ }
44
+ catch {
45
+ return fallback;
46
+ }
47
+ }
48
+ async readString(code, salesChannelId, fallback) {
49
+ try {
50
+ return await this.settings.get(code, salesChannelId, z.string());
51
+ }
52
+ catch {
53
+ return fallback;
54
+ }
55
+ }
56
+ }
57
+ //# sourceMappingURL=linkedin-config.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linkedin-config.service.js","sourceRoot":"","sources":["../../../src/backend/services/linkedin-config.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,0BAA0B,EAC1B,wBAAwB,GAGzB,MAAM,4BAA4B,CAAC;AAWpC;;;;;;;;;;GAUG;AACH,MAAM,OAAO,qBAAqB;IAEb;IACA;IAFnB,YACmB,QAA0B,EAC1B,eAAyC,KAAK,IAAI,EAAE,CAAC,EAAE;QADvD,aAAQ,GAAR,QAAQ,CAAkB;QAC1B,iBAAY,GAAZ,YAAY,CAA2C;IACvE,CAAC;IAEJ,KAAK,CAAC,SAAS,CAAC,cAAsB;QACpC,MAAM,CAAC,OAAO,EAAE,YAAY,EAAE,cAAc,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YAChE,IAAI,CAAC,QAAQ,CAAC,0BAA0B,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC;YACxE,IAAI,CAAC,UAAU,CAAC,0BAA0B,CAAC,UAAU,EAAE,cAAc,EAAE,EAAE,CAAC;YAC1E,IAAI,CAAC,QAAQ,CAAC,0BAA0B,CAAC,eAAe,EAAE,cAAc,EAAE,IAAI,CAAC;SAChF,CAAC,CAAC;QAEH,MAAM,SAAS,GAAG,YAAY,CAAC,IAAI,EAAE,IAAI,IAAI,CAAC;QAE9C,uEAAuE;QACvE,0EAA0E;QAC1E,+CAA+C;QAC/C,IAAI,CAAC,OAAO,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;YACnC,OAAO,EAAE,GAAG,wBAAwB,EAAE,cAAc,EAAE,CAAC;QACzD,CAAC;QAED,MAAM,CAAC,UAAU,EAAE,kBAAkB,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YACzD,IAAI,CAAC,QAAQ,CAAC,0BAA0B,CAAC,mBAAmB,EAAE,cAAc,EAAE,KAAK,CAAC;YACpF,IAAI,CAAC,YAAY,CAAC,cAAc,CAAC;SAClC,CAAC,CAAC;QAEH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,cAAc,EAAE,UAAU,EAAE,kBAAkB,EAAE,CAAC;IACtF,CAAC;IAEO,KAAK,CAAC,QAAQ,CACpB,IAAY,EACZ,cAAsB,EACtB,QAAiB;QAEjB,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QACpE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,QAAQ,CAAC;QAClB,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,UAAU,CACtB,IAAY,EACZ,cAAsB,EACtB,QAAgB;QAEhB,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;QACnE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,QAAQ,CAAC;QAClB,CAAC;IACH,CAAC;CACF"}