@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,195 @@
1
+ /**
2
+ * LinkedIn Ads module (feature 063). Puts the LinkedIn Insight Tag on the
3
+ * storefront per sales channel and reports mapped conversions, either from the
4
+ * browser via `lintrk` or — opt-in per channel — from the backend through
5
+ * LinkedIn's Conversions API.
6
+ *
7
+ * Per-channel configuration is stored through the Settings module; the
8
+ * Conversions API access token uses the `secret` value type (feature 043,
9
+ * AES-256-GCM at rest, write-only at the admin boundary).
10
+ */
11
+ export declare const linkedInAdsSettingsManifest: {
12
+ moduleCode: string;
13
+ groups: {
14
+ code: string;
15
+ name: string;
16
+ salesChannelCodes?: string[] | undefined;
17
+ isSystemProtected?: boolean | undefined;
18
+ }[];
19
+ settings: {
20
+ code: string;
21
+ name: string;
22
+ valueType: "string" | "number" | "boolean" | "json" | "string_list" | "secret" | "credential_ref";
23
+ defaultValue: unknown;
24
+ description?: string | undefined;
25
+ groupCode?: string | undefined;
26
+ previousDefaultValues?: unknown[] | undefined;
27
+ salesChannelCodes?: string[] | undefined;
28
+ enumOptions?: string[] | undefined;
29
+ configurationType?: string | undefined;
30
+ hidden?: boolean | undefined;
31
+ }[];
32
+ };
33
+ export declare const LINKEDIN_ADS_READ_PERMISSION = "linkedin_ads:read";
34
+ export declare const LINKEDIN_ADS_WRITE_PERMISSION = "linkedin_ads:write";
35
+ export declare const manifest: {
36
+ id: string;
37
+ name: string;
38
+ version: string;
39
+ dependencies: string[];
40
+ description?: string | undefined;
41
+ acknowledgedDependencies?: {
42
+ moduleId: string;
43
+ port: string;
44
+ reason: string;
45
+ }[] | undefined;
46
+ nonBindingDependencies?: {
47
+ moduleId: string;
48
+ name: string;
49
+ kind: "contributes-to" | "degrades-without" | "refuses-without";
50
+ reason: string;
51
+ whenAbsent?: string | undefined;
52
+ }[] | undefined;
53
+ activation?: {
54
+ settingCode: string;
55
+ default: boolean;
56
+ } | {
57
+ nonDeactivatable: true;
58
+ reason: string;
59
+ } | undefined;
60
+ settings?: {
61
+ moduleCode: string;
62
+ groups: {
63
+ code: string;
64
+ name: string;
65
+ salesChannelCodes?: string[] | undefined;
66
+ isSystemProtected?: boolean | undefined;
67
+ }[];
68
+ settings: {
69
+ code: string;
70
+ name: string;
71
+ valueType: "string" | "number" | "boolean" | "json" | "string_list" | "secret" | "credential_ref";
72
+ defaultValue: unknown;
73
+ description?: string | undefined;
74
+ groupCode?: string | undefined;
75
+ previousDefaultValues?: unknown[] | undefined;
76
+ salesChannelCodes?: string[] | undefined;
77
+ enumOptions?: string[] | undefined;
78
+ configurationType?: string | undefined;
79
+ hidden?: boolean | undefined;
80
+ }[];
81
+ } | undefined;
82
+ i18n?: {
83
+ bundlesDir: string;
84
+ } | undefined;
85
+ docs?: false | {
86
+ dir: string;
87
+ } | undefined;
88
+ demo?: false | {
89
+ summary: string;
90
+ seed: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoSeedResult>;
91
+ reset: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoResetResult>;
92
+ after?: readonly string[] | undefined;
93
+ package?: string | undefined;
94
+ } | undefined;
95
+ actions?: {
96
+ id: string;
97
+ labelKey: string;
98
+ icon: "Plus" | "Sparkles" | "Settings" | "Search" | "Boxes" | "Layers" | "Menu" | "PlusCircle" | "PlusSquare" | "FilePlus" | "FolderPlus" | "Upload" | "FileUp" | "CloudUpload" | "Download" | "FileDown" | "FileText" | "BookOpen" | "Rss" | "Package" | "Tag" | "ShoppingCart" | "Receipt" | "CreditCard" | "Users" | "UserPlus" | "Inbox" | "ListChecks" | "ClipboardList" | "Image" | "Video" | "LayoutDashboard" | "PanelLeft" | "KeyRound" | "ShieldCheck" | "Edit" | "Archive" | "Box" | "Truck" | "CircleDollarSign" | "Activity" | "LineChart" | "Smartphone" | "Webhook" | "Scale" | "PlugZap" | "PercentDiamond" | "Newspaper" | "Languages" | "Eraser" | "Warehouse" | "TrendingDown" | "Bell" | "PackageOpen" | "Building2" | "Store" | "ClipboardCheck";
99
+ targetRoute: string;
100
+ keywords: string[];
101
+ weight: number;
102
+ descriptionKey?: string | undefined;
103
+ requiredPermission?: string | undefined;
104
+ }[] | undefined;
105
+ permissions?: {
106
+ code: string;
107
+ label: string;
108
+ module?: string | undefined;
109
+ description?: string | undefined;
110
+ requires?: string[] | undefined;
111
+ }[] | undefined;
112
+ transactionalEmails?: {
113
+ code: string;
114
+ name: string;
115
+ variables: {
116
+ key: string;
117
+ label: string;
118
+ sampleValue?: string | undefined;
119
+ description?: string | undefined;
120
+ }[];
121
+ description?: string | undefined;
122
+ group?: string | undefined;
123
+ }[] | undefined;
124
+ capabilities?: string[] | undefined;
125
+ exclusiveCapabilities?: {
126
+ key: string;
127
+ errorCode: string;
128
+ }[] | undefined;
129
+ errorCodes?: {
130
+ code: string;
131
+ tokens?: string[] | undefined;
132
+ }[] | undefined;
133
+ blocks?: {
134
+ name: string;
135
+ labelKey: string;
136
+ category: string;
137
+ contexts: ("invoice" | "email" | "cms" | "newsletter")[];
138
+ fields: Record<string, {
139
+ type: "number" | "object" | "array" | "text" | "textarea" | "select" | "radio" | "external" | "uuid" | "richtext";
140
+ label?: string | undefined;
141
+ required?: boolean | undefined;
142
+ options?: {
143
+ label: string;
144
+ value: string | number;
145
+ }[] | undefined;
146
+ refKind?: string | undefined;
147
+ }>;
148
+ descriptionKey?: string | undefined;
149
+ defaultProps?: Record<string, unknown> | undefined;
150
+ responsiveFields?: string[] | undefined;
151
+ previewIcon?: string | undefined;
152
+ weight?: number | undefined;
153
+ }[] | undefined;
154
+ blockCategories?: {
155
+ key: string;
156
+ titleKey: string;
157
+ contexts: ("invoice" | "email" | "cms" | "newsletter")[];
158
+ weight?: number | undefined;
159
+ visible?: boolean | undefined;
160
+ }[] | undefined;
161
+ env?: {
162
+ name: string;
163
+ describes: {
164
+ en: string;
165
+ pl: string;
166
+ };
167
+ requirement: {
168
+ kind: "required";
169
+ } | {
170
+ kind: "requiredWhen";
171
+ input: string;
172
+ equals: string;
173
+ } | {
174
+ kind: "optional";
175
+ without: {
176
+ en: string;
177
+ pl: string;
178
+ };
179
+ };
180
+ secret: boolean;
181
+ generable: boolean;
182
+ owner: {
183
+ kind: "platform";
184
+ } | {
185
+ kind: "application";
186
+ application: "admin" | "backend" | "storefront";
187
+ } | {
188
+ kind: "module";
189
+ moduleId: string;
190
+ };
191
+ consumers: ("admin" | "backend" | "storefront")[];
192
+ addressOf: "admin" | "backend" | "storefront" | null;
193
+ }[] | undefined;
194
+ };
195
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;CAkEtC,CAAC;AAEH,eAAO,MAAM,4BAA4B,sBAAsB,CAAC;AAChE,eAAO,MAAM,6BAA6B,uBAAuB,CAAC;AAElE,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0CnB,CAAC"}
@@ -0,0 +1,119 @@
1
+ import { defineModuleManifest, defineModuleSettingsManifest } from '@endora-commerce/contracts';
2
+ import { LINKEDIN_ADS_SETTING_CODES } from '@endora-commerce/contracts';
3
+ /**
4
+ * LinkedIn Ads module (feature 063). Puts the LinkedIn Insight Tag on the
5
+ * storefront per sales channel and reports mapped conversions, either from the
6
+ * browser via `lintrk` or — opt-in per channel — from the backend through
7
+ * LinkedIn's Conversions API.
8
+ *
9
+ * Per-channel configuration is stored through the Settings module; the
10
+ * Conversions API access token uses the `secret` value type (feature 043,
11
+ * AES-256-GCM at rest, write-only at the admin boundary).
12
+ */
13
+ export const linkedInAdsSettingsManifest = defineModuleSettingsManifest({
14
+ moduleCode: 'linkedin_ads',
15
+ groups: [{ code: 'linkedin_ads', name: 'LinkedIn Ads' }],
16
+ settings: [
17
+ {
18
+ // Feature 073 — the operator's activation control. **Platform-wide, and
19
+ // deliberately NOT `linkedin_ads.enabled`.** That code already exists and
20
+ // means something else: it is per-sales-channel and answers "is the
21
+ // tag is live on a given storefront channel". This one answers "does this client have the LinkedIn Ads
22
+ // capability at all". Adopting the existing code would have collapsed two
23
+ // orthogonal questions into one switch — and, because an activation
24
+ // control may only be written through the activation endpoint, would have
25
+ // made the per-channel setting unwritable through the settings screen.
26
+ code: 'linkedin_ads.module_enabled',
27
+ name: 'LinkedIn Ads module enabled',
28
+ description: 'Switches the LinkedIn Insight Tag, the conversion mappings and the admin screen on or off for the whole platform. Separate from the per-channel switch, which decides where the tag actually loads. Nothing is dropped: mappings stay in the database and every setting keeps its value.',
29
+ groupCode: 'linkedin_ads',
30
+ valueType: 'boolean',
31
+ defaultValue: true,
32
+ },
33
+ {
34
+ code: LINKEDIN_ADS_SETTING_CODES.ENABLED,
35
+ name: 'Enable LinkedIn Ads',
36
+ description: 'Master switch for the module. Per-channel overridable.',
37
+ groupCode: 'linkedin_ads',
38
+ valueType: 'boolean',
39
+ defaultValue: false,
40
+ },
41
+ {
42
+ code: LINKEDIN_ADS_SETTING_CODES.PARTNER_ID,
43
+ name: 'Partner ID',
44
+ description: 'LinkedIn Insight Tag Partner ID from Campaign Manager. Blank means the channel is untracked.',
45
+ groupCode: 'linkedin_ads',
46
+ valueType: 'string',
47
+ defaultValue: '',
48
+ },
49
+ {
50
+ code: LINKEDIN_ADS_SETTING_CODES.REQUIRE_CONSENT,
51
+ name: 'Require analytics consent',
52
+ description: 'When enabled, nothing LinkedIn-related runs until the visitor accepts the cookie banner.',
53
+ groupCode: 'linkedin_ads',
54
+ valueType: 'boolean',
55
+ defaultValue: true,
56
+ },
57
+ {
58
+ code: LINKEDIN_ADS_SETTING_CODES.SERVER_SIDE_ENABLED,
59
+ name: 'Server-side conversions',
60
+ description: "Report mapped conversions from the backend through LinkedIn's Conversions API instead of the browser. Requires an access token.",
61
+ groupCode: 'linkedin_ads',
62
+ valueType: 'boolean',
63
+ defaultValue: false,
64
+ },
65
+ {
66
+ code: LINKEDIN_ADS_SETTING_CODES.ACCESS_TOKEN,
67
+ name: 'Conversions API access token',
68
+ description: 'OAuth access token authorised for the Conversions API. Stored encrypted at rest; write-only.',
69
+ groupCode: 'linkedin_ads',
70
+ valueType: 'secret',
71
+ defaultValue: '',
72
+ },
73
+ ],
74
+ });
75
+ export const LINKEDIN_ADS_READ_PERMISSION = 'linkedin_ads:read';
76
+ export const LINKEDIN_ADS_WRITE_PERMISSION = 'linkedin_ads:write';
77
+ export const manifest = defineModuleManifest({
78
+ id: 'linkedin_ads',
79
+ name: 'LinkedIn Ads',
80
+ description: 'LinkedIn Ads integration: per-sales-channel Insight Tag, consent-gated tracking, configurable conversion mappings, and optional server-side reporting through the Conversions API.',
81
+ version: '1.0.0',
82
+ // `auth` owns the `requireAdmin` port the admin routes are gated by; feature
83
+ // 072 made it a container resolution rather than a constructor argument.
84
+ dependencies: ['audit_logs', 'sales_channels', 'settings', 'auth'],
85
+ settings: linkedInAdsSettingsManifest,
86
+ i18n: { bundlesDir: 'i18n' },
87
+ docs: { dir: 'docs' },
88
+ permissions: [
89
+ { code: LINKEDIN_ADS_READ_PERMISSION, label: 'View LinkedIn Ads configuration' },
90
+ {
91
+ code: LINKEDIN_ADS_WRITE_PERMISSION,
92
+ label: 'Manage LinkedIn Ads configuration and conversion mappings',
93
+ },
94
+ ],
95
+ actions: [
96
+ {
97
+ id: 'open-linkedin-ads',
98
+ labelKey: 'actions.openLinkedInAds.label',
99
+ descriptionKey: 'actions.openLinkedInAds.description',
100
+ icon: 'Sparkles',
101
+ targetRoute: '/linkedin-ads',
102
+ requiredPermission: LINKEDIN_ADS_READ_PERMISSION,
103
+ keywords: ['linkedin', 'ads', 'insight tag', 'conversion', 'konwersje', 'reklamy'],
104
+ weight: 242,
105
+ },
106
+ {
107
+ id: 'new-linkedin-conversion-mapping',
108
+ labelKey: 'actions.newConversionMapping.label',
109
+ descriptionKey: 'actions.newConversionMapping.description',
110
+ icon: 'Plus',
111
+ targetRoute: '/linkedin-ads/new',
112
+ requiredPermission: LINKEDIN_ADS_WRITE_PERMISSION,
113
+ keywords: ['linkedin', 'conversion', 'mapping', 'konwersja', 'mapowanie'],
114
+ weight: 243,
115
+ },
116
+ ],
117
+ activation: { settingCode: 'linkedin_ads.module_enabled', default: true },
118
+ });
119
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Feature 063 — LinkedIn Ads. Creates `linkedin_conversion_mappings`.
4
+ *
5
+ * Channel configuration (enabled, partner id, consent, server-side, access
6
+ * token) lives in the Settings module and needs no schema here.
7
+ */
8
+ export declare class Migration20260727T233211LinkedinAdsInit extends Migration {
9
+ up(): Promise<void>;
10
+ down(): Promise<void>;
11
+ }
12
+ //# sourceMappingURL=20260727T233211_linkedin_ads_init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260727T233211_linkedin_ads_init.d.ts","sourceRoot":"","sources":["../../src/migrations/20260727T233211_linkedin_ads_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;GAKG;AACH,qBAAa,uCAAwC,SAAQ,SAAS;IACrD,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IA0BnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAGrC"}
@@ -0,0 +1,36 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Feature 063 — LinkedIn Ads. Creates `linkedin_conversion_mappings`.
4
+ *
5
+ * Channel configuration (enabled, partner id, consent, server-side, access
6
+ * token) lives in the Settings module and needs no schema here.
7
+ */
8
+ export class Migration20260727T233211LinkedinAdsInit extends Migration {
9
+ async up() {
10
+ this.addSql(`
11
+ create table "linkedin_conversion_mappings" (
12
+ "id" uuid not null,
13
+ "sales_channel_id" uuid null,
14
+ "trigger_action" varchar(32) not null,
15
+ "conversion_id" varchar(32) not null,
16
+ "conversion_rule_urn" varchar(128) null,
17
+ "enabled" boolean not null default true,
18
+ "version" int not null default 1,
19
+ "created_at" timestamptz not null,
20
+ "updated_at" timestamptz not null,
21
+ constraint "linkedin_conversion_mappings_pkey" primary key ("id")
22
+ );
23
+ `);
24
+ this.addSql(`
25
+ alter table "linkedin_conversion_mappings"
26
+ add constraint "linkedin_conversion_mappings_sales_channel_id_foreign"
27
+ foreign key ("sales_channel_id") references "sales_channels" ("id")
28
+ on update cascade on delete cascade;
29
+ `);
30
+ this.addSql('create index "linkedin_conversion_mappings_channel_action_idx" on "linkedin_conversion_mappings" ("sales_channel_id", "trigger_action");');
31
+ }
32
+ async down() {
33
+ this.addSql('drop table if exists "linkedin_conversion_mappings" cascade;');
34
+ }
35
+ }
36
+ //# sourceMappingURL=20260727T233211_linkedin_ads_init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260727T233211_linkedin_ads_init.js","sourceRoot":"","sources":["../../src/migrations/20260727T233211_linkedin_ads_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;GAKG;AACH,MAAM,OAAO,uCAAwC,SAAQ,SAAS;IAC3D,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;KAaX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC;;;;;KAKX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CACT,0IAA0I,CAC3I,CAAC;IACJ,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,8DAA8D,CAAC,CAAC;IAC9E,CAAC;CACF"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The `./migrations` subpath — every migration class this module owns, as one
3
+ * ordered `migrations` array.
4
+ *
5
+ * The array is what the platform reads when this module is **installed**:
6
+ * `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
7
+ * the package outright when it is absent (D-168).
8
+ *
9
+ * Listed in ascending timestamp, which is the order of this module's own
10
+ * migrations and of nothing else (feature 081): a manifest `dependencies` array
11
+ * is the only thing ordering this block against another module's.
12
+ *
13
+ * The **named** exports stay beside the array, and the asymmetry with
14
+ * `./backend` — which publishes an array and no named class (D-168) — is
15
+ * deliberate. `db/migrations-registry.generated.ts` imports each class by name
16
+ * from this specifier, and a migration class name is contract in a way an entity
17
+ * class name is not: `mikro_orm_migrations` persists it, so it is a string every
18
+ * already-migrated database holds.
19
+ *
20
+ * A class that is in neither the array nor the barrel is a migration that does
21
+ * not run: `migration:pending` reports nothing pending and the first symptom is
22
+ * a query against a table nobody created.
23
+ */
24
+ import { Migration20260727T233211LinkedinAdsInit } from './20260727T233211_linkedin_ads_init.js';
25
+ export declare const migrations: (typeof Migration20260727T233211LinkedinAdsInit)[];
26
+ export { Migration20260727T233211LinkedinAdsInit, };
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,uCAAuC,EAAE,MAAM,wCAAwC,CAAC;AAEjG,eAAO,MAAM,UAAU,oDAEtB,CAAC;AAEF,OAAO,EACL,uCAAuC,GACxC,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The `./migrations` subpath — every migration class this module owns, as one
3
+ * ordered `migrations` array.
4
+ *
5
+ * The array is what the platform reads when this module is **installed**:
6
+ * `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
7
+ * the package outright when it is absent (D-168).
8
+ *
9
+ * Listed in ascending timestamp, which is the order of this module's own
10
+ * migrations and of nothing else (feature 081): a manifest `dependencies` array
11
+ * is the only thing ordering this block against another module's.
12
+ *
13
+ * The **named** exports stay beside the array, and the asymmetry with
14
+ * `./backend` — which publishes an array and no named class (D-168) — is
15
+ * deliberate. `db/migrations-registry.generated.ts` imports each class by name
16
+ * from this specifier, and a migration class name is contract in a way an entity
17
+ * class name is not: `mikro_orm_migrations` persists it, so it is a string every
18
+ * already-migrated database holds.
19
+ *
20
+ * A class that is in neither the array nor the barrel is a migration that does
21
+ * not run: `migration:pending` reports nothing pending and the first symptom is
22
+ * a query against a table nobody created.
23
+ */
24
+ import { Migration20260727T233211LinkedinAdsInit } from './20260727T233211_linkedin_ads_init.js';
25
+ export const migrations = [
26
+ Migration20260727T233211LinkedinAdsInit,
27
+ ];
28
+ export { Migration20260727T233211LinkedinAdsInit, };
29
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,uCAAuC,EAAE,MAAM,wCAAwC,CAAC;AAEjG,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,uCAAuC;CACxC,CAAC;AAEF,OAAO,EACL,uCAAuC,GACxC,CAAC"}
@@ -0,0 +1,73 @@
1
+ ---
2
+ title: LinkedIn Ads
3
+ description: LinkedIn Insight Tag per sales channel, reporting storefront actions against Campaign Manager conversion rules
4
+ ---
5
+
6
+ # LinkedIn Ads
7
+
8
+ Puts the **LinkedIn Insight Tag** on the storefront per sales channel and reports storefront
9
+ actions against conversion rules defined in LinkedIn Campaign Manager.
10
+
11
+ Module id `linkedin_ads`.
12
+
13
+ ## Configuration (Settings module)
14
+
15
+ All values live in the **LinkedIn Ads** settings group and are overridable per sales channel.
16
+
17
+ | Setting | Type | Default | Meaning |
18
+ | --- | --- | --- | --- |
19
+ | `linkedin_ads.enabled` | boolean | `false` | Master switch for the channel. |
20
+ | `linkedin_ads.partner_id` | string | `''` | Insight Tag Partner ID from Campaign Manager. |
21
+ | `linkedin_ads.require_consent` | boolean | `true` | Gate all LinkedIn behaviour on the cookie-banner decision. |
22
+ | `linkedin_ads.server_side_enabled` | boolean | `false` | Reserved for Conversions API reporting (not yet implemented). |
23
+ | `linkedin_ads.access_token` | secret | `''` | Conversions API token. Encrypted at rest, write-only — it is never returned by any API. |
24
+
25
+ A channel is tracked only when the master switch is on **and** the Partner ID is non-blank. A blank
26
+ Partner ID is treated as "not configured", never as an error the visitor can see, so clearing the
27
+ field is a safe way to switch a channel off.
28
+
29
+ ## Storefront behaviour
30
+
31
+ The Insight Tag is injected client-side, after interaction, so it never blocks the first paint.
32
+ Nothing LinkedIn-related runs — no script, no cookie, no request — until the visitor accepts the
33
+ cookie banner, unless the operator has turned the consent requirement off for that channel. A
34
+ visitor who accepts mid-session is tracked from that moment without a page reload.
35
+
36
+ Consent is the storefront's single decision, shared with Google Analytics, so there is only ever
37
+ one banner and one stored answer.
38
+
39
+ ## Conversion mappings
40
+
41
+ Define your conversions in Campaign Manager first; each gets a numeric conversion ID. Then map
42
+ storefront actions to those IDs under **Admin → LinkedIn Ads**.
43
+
44
+ Supported actions: product viewed, added to cart, added to quote request, added to shopping list,
45
+ checkout started, Place Order clicked, order completed, contact form submitted. The list is exactly
46
+ what the storefront emits — you cannot map an action that could never fire.
47
+
48
+ A mapping can target one sales channel or all of them, and can be disabled without deleting it. An
49
+ action with no enabled mapping simply reports nothing — that is a normal state, not a
50
+ misconfiguration. One action may map to several conversion IDs; each reports once.
51
+
52
+ Mappings are served to the storefront through a cached per-channel config; creating, editing, or
53
+ deleting one revalidates that cache immediately rather than waiting for the TTL.
54
+
55
+ ## Permissions
56
+
57
+ | Code | Grants |
58
+ | --- | --- |
59
+ | `linkedin_ads:read` | View conversion mappings and the module page. |
60
+ | `linkedin_ads:write` | Create, edit, and delete conversion mappings. |
61
+
62
+ Both appear on `/admin-roles`. Mapping changes are audited with the acting operator.
63
+
64
+ Conversions are reported from the browser via `lintrk`, and only when the visitor has consented.
65
+ An action mapped more than once reports once per mapping.
66
+
67
+ ## Not yet implemented
68
+
69
+ - **Server-side reporting through the Conversions API.** Specified and settings-ready, so enabling
70
+ it later needs no contract change; `linkedin_ads.server_side_enabled` stays off. When it is
71
+ turned on, the browser path for mapped conversions is suppressed so each conversion has exactly
72
+ one transport.
73
+ - **Conversion retraction** for refunded or cancelled orders.
package/i18n/en.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "actions.openLinkedInAds.label": "LinkedIn Ads",
3
+ "actions.openLinkedInAds.description": "Configure LinkedIn conversion mappings",
4
+ "actions.newConversionMapping.label": "New LinkedIn conversion mapping",
5
+ "actions.newConversionMapping.description": "Map a storefront action to a LinkedIn conversion",
6
+ "nav.linkedInAds.label": "LinkedIn Ads",
7
+ "mappings.title": "Conversion mappings",
8
+ "mappings.subtitle": "Report storefront actions against conversion rules defined in LinkedIn Campaign Manager.",
9
+ "mappings.empty": "No conversion mappings yet",
10
+ "mappings.new": "New mapping",
11
+ "mappings.edit": "Edit mapping",
12
+ "mappings.triggerAction": "Storefront action",
13
+ "mappings.conversionId": "Conversion ID",
14
+ "mappings.conversionIdHint": "The numeric ID shown next to the conversion in Campaign Manager.",
15
+ "mappings.conversionRuleUrn": "Conversion rule URN",
16
+ "mappings.conversionRuleUrnHint": "Only needed for server-side reporting, and only when it differs from the derived value.",
17
+ "mappings.salesChannel": "Sales channel",
18
+ "mappings.allChannels": "All channels",
19
+ "mappings.enabled": "Enabled",
20
+ "mappings.save": "Save",
21
+ "mappings.delete": "Delete",
22
+ "mappings.deleteConfirm": "Delete this conversion mapping?",
23
+ "error.generic": "Something went wrong.",
24
+ "error.notFound": "Conversion mapping not found.",
25
+ "error.versionConflict": "Someone else changed this mapping. Reload and reapply your edit.",
26
+ "error.validationFailed": "Some fields are invalid.",
27
+ "mappings.action.product_viewed": "Product viewed",
28
+ "mappings.action.add_to_cart": "Added to cart",
29
+ "mappings.action.add_to_quote_request": "Added to quote request",
30
+ "mappings.action.add_to_shopping_list": "Added to shopping list",
31
+ "mappings.action.begin_checkout": "Checkout started",
32
+ "mappings.action.place_order_clicked": "Place Order clicked",
33
+ "mappings.action.purchase": "Order completed",
34
+ "mappings.action.contact_form_submitted": "Contact form submitted"
35
+ }
package/i18n/pl.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "actions.openLinkedInAds.label": "LinkedIn Ads",
3
+ "actions.openLinkedInAds.description": "Konfiguruj mapowania konwersji LinkedIn",
4
+ "actions.newConversionMapping.label": "Nowe mapowanie konwersji LinkedIn",
5
+ "actions.newConversionMapping.description": "Przypisz akcję w Storefront do konwersji LinkedIn",
6
+ "nav.linkedInAds.label": "LinkedIn Ads",
7
+ "mappings.title": "Mapowania konwersji",
8
+ "mappings.subtitle": "Raportuj akcje w Storefront do reguł konwersji zdefiniowanych w LinkedIn Campaign Manager.",
9
+ "mappings.empty": "Brak mapowań konwersji",
10
+ "mappings.new": "Nowe mapowanie",
11
+ "mappings.edit": "Edytuj mapowanie",
12
+ "mappings.triggerAction": "Akcja w Storefront",
13
+ "mappings.conversionId": "ID konwersji",
14
+ "mappings.conversionIdHint": "Numeryczne ID widoczne przy konwersji w Campaign Manager.",
15
+ "mappings.conversionRuleUrn": "URN reguły konwersji",
16
+ "mappings.conversionRuleUrnHint": "Potrzebne tylko przy raportowaniu server-side i tylko gdy różni się od wartości wyliczonej.",
17
+ "mappings.salesChannel": "Kanał sprzedaży",
18
+ "mappings.allChannels": "Wszystkie kanały",
19
+ "mappings.enabled": "Aktywne",
20
+ "mappings.save": "Zapisz",
21
+ "mappings.delete": "Usuń",
22
+ "mappings.deleteConfirm": "Usunąć to mapowanie konwersji?",
23
+ "error.generic": "Coś poszło nie tak.",
24
+ "error.notFound": "Nie znaleziono mapowania konwersji.",
25
+ "error.versionConflict": "Ktoś inny zmienił to mapowanie. Odśwież stronę i ponów zmianę.",
26
+ "error.validationFailed": "Niektóre pola są nieprawidłowe.",
27
+ "mappings.action.product_viewed": "Wyświetlenie produktu",
28
+ "mappings.action.add_to_cart": "Dodanie do koszyka",
29
+ "mappings.action.add_to_quote_request": "Dodanie do zapytania ofertowego",
30
+ "mappings.action.add_to_shopping_list": "Dodanie do listy zakupowej",
31
+ "mappings.action.begin_checkout": "Rozpoczęcie zamówienia",
32
+ "mappings.action.place_order_clicked": "Kliknięcie „Złóż zamówienie”",
33
+ "mappings.action.purchase": "Złożenie zamówienia",
34
+ "mappings.action.contact_form_submitted": "Zapis formularza kontaktowego"
35
+ }