@bytebrand/export-cars 1.26.27 → 1.26.29

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 (112) hide show
  1. package/README.md +128 -0
  2. package/dist/configs/config.d.ts +2 -1
  3. package/dist/configs/config.js +1 -0
  4. package/dist/configs/config.js.map +1 -1
  5. package/dist/configs/google.config.js +16 -0
  6. package/dist/configs/google.config.js.map +1 -1
  7. package/dist/configs/index.d.ts +1 -0
  8. package/dist/configs/index.js +1 -0
  9. package/dist/configs/index.js.map +1 -1
  10. package/dist/configs/openai-ads.config.d.ts +22 -0
  11. package/dist/configs/openai-ads.config.js +56 -0
  12. package/dist/configs/openai-ads.config.js.map +1 -0
  13. package/dist/enums/export-type.enum.d.ts +2 -1
  14. package/dist/enums/export-type.enum.js +1 -0
  15. package/dist/enums/export-type.enum.js.map +1 -1
  16. package/dist/enums/image-type.enum.d.ts +2 -1
  17. package/dist/enums/image-type.enum.js +1 -0
  18. package/dist/enums/image-type.enum.js.map +1 -1
  19. package/dist/export.schema.d.ts +6 -2
  20. package/dist/export.schema.js +4 -0
  21. package/dist/export.schema.js.map +1 -1
  22. package/dist/exporters/google.exporter.js +12 -2
  23. package/dist/exporters/google.exporter.js.map +1 -1
  24. package/dist/exporters/index.d.ts +1 -0
  25. package/dist/exporters/index.js +1 -0
  26. package/dist/exporters/index.js.map +1 -1
  27. package/dist/exporters/openai-ads.exporter.d.ts +13 -0
  28. package/dist/exporters/openai-ads.exporter.js +91 -0
  29. package/dist/exporters/openai-ads.exporter.js.map +1 -0
  30. package/dist/formats/openai-ads-csv.builder.d.ts +15 -0
  31. package/dist/formats/openai-ads-csv.builder.js +48 -0
  32. package/dist/formats/openai-ads-csv.builder.js.map +1 -0
  33. package/dist/interfaces/openai-ads.interface.d.ts +35 -0
  34. package/dist/interfaces/openai-ads.interface.js +37 -0
  35. package/dist/interfaces/openai-ads.interface.js.map +1 -0
  36. package/dist/interfaces/params.interface.d.ts +20 -0
  37. package/dist/services/feed-guard.d.ts +44 -0
  38. package/dist/services/feed-guard.js +156 -0
  39. package/dist/services/feed-guard.js.map +1 -0
  40. package/dist/services/image-url-validator.service.d.ts +45 -0
  41. package/dist/services/image-url-validator.service.js +141 -0
  42. package/dist/services/image-url-validator.service.js.map +1 -0
  43. package/dist/services/images.service.d.ts +12 -0
  44. package/dist/services/images.service.js +46 -1
  45. package/dist/services/images.service.js.map +1 -1
  46. package/dist/services/index.d.ts +1 -0
  47. package/dist/services/index.js +1 -0
  48. package/dist/services/index.js.map +1 -1
  49. package/dist/services/openai-ads-feed-run.d.ts +74 -0
  50. package/dist/services/openai-ads-feed-run.js +151 -0
  51. package/dist/services/openai-ads-feed-run.js.map +1 -0
  52. package/dist/services/openai-ads-ledger.d.ts +19 -0
  53. package/dist/services/openai-ads-ledger.js +72 -0
  54. package/dist/services/openai-ads-ledger.js.map +1 -0
  55. package/dist/services/publication-gate.d.ts +15 -0
  56. package/dist/services/publication-gate.js +107 -0
  57. package/dist/services/publication-gate.js.map +1 -0
  58. package/dist/transformers/google-feed-envkv.d.ts +2 -0
  59. package/dist/transformers/google-feed-envkv.js +62 -31
  60. package/dist/transformers/google-feed-envkv.js.map +1 -1
  61. package/dist/transformers/google-feed-item-row.d.ts +6 -1
  62. package/dist/transformers/google-feed-item-row.js +3 -1
  63. package/dist/transformers/google-feed-item-row.js.map +1 -1
  64. package/dist/transformers/openai-ads.transformer.d.ts +28 -0
  65. package/dist/transformers/openai-ads.transformer.js +144 -0
  66. package/dist/transformers/openai-ads.transformer.js.map +1 -0
  67. package/dist/transformers/openai-image-fields.d.ts +16 -0
  68. package/dist/transformers/openai-image-fields.js +34 -0
  69. package/dist/transformers/openai-image-fields.js.map +1 -0
  70. package/dist/tsconfig.build.tsbuildinfo +1 -1
  71. package/docs/openai-ads/AUDIT.md +235 -0
  72. package/docs/openai-ads/RUNBOOK.md +94 -0
  73. package/package.json +64 -64
  74. package/schema/feed-items.json +134 -122
  75. package/src/configs/config.ts +99 -97
  76. package/src/configs/google.config.ts +41 -26
  77. package/src/configs/index.ts +1 -0
  78. package/src/configs/openai-ads.config.ts +70 -0
  79. package/src/enums/export-type.enum.ts +2 -0
  80. package/src/enums/image-type.enum.ts +5 -0
  81. package/src/export.command.ts +38 -38
  82. package/src/export.schema.ts +6 -0
  83. package/src/exporters/google.exporter.ts +67 -56
  84. package/src/exporters/index.ts +1 -0
  85. package/src/exporters/openai-ads.exporter.ts +93 -0
  86. package/src/formats/openai-ads-csv.builder.ts +55 -0
  87. package/src/interfaces/openai-ads.interface.ts +64 -0
  88. package/src/interfaces/params.interface.ts +56 -29
  89. package/src/services/feed-guard.ts +219 -0
  90. package/src/services/feed-items-bigquery.writer.ts +101 -101
  91. package/src/services/image-url-validator.service.ts +181 -0
  92. package/src/services/images.service.ts +72 -0
  93. package/src/services/index.ts +1 -0
  94. package/src/services/openai-ads-feed-run.ts +226 -0
  95. package/src/services/openai-ads-ledger.ts +116 -0
  96. package/src/services/publication-gate.ts +119 -0
  97. package/src/transformers/google-feed-envkv.ts +81 -33
  98. package/src/transformers/google-feed-item-row.ts +78 -71
  99. package/src/transformers/openai-ads.transformer.ts +211 -0
  100. package/src/transformers/openai-image-fields.ts +60 -0
  101. package/test/clean-images.test.ts +86 -0
  102. package/test/fixtures/images-service.harness.ts +32 -0
  103. package/test/fixtures/images.golden.json +218 -0
  104. package/test/fixtures/imbo-images.fixture.ts +77 -0
  105. package/test/fixtures/openai-ads.fixture.ts +89 -0
  106. package/test/google-feed-item-row.test.ts +135 -121
  107. package/test/image-url-validator.test.ts +110 -0
  108. package/test/images-regression.test.ts +23 -0
  109. package/test/openai-ads-feed-run.test.ts +278 -0
  110. package/test/openai-ads-mapping.test.ts +136 -0
  111. package/test/openai-image-fields.test.ts +99 -0
  112. package/test/publication-gate.test.ts +86 -0
@@ -1,122 +1,134 @@
1
- [
2
- {
3
- "name": "partitionDay",
4
- "type": "DATE",
5
- "mode": "NULLABLE",
6
- "description": "UTC day of exportedAt; the table is partitioned by it"
7
- },
8
- {
9
- "name": "exportedAt",
10
- "type": "TIMESTAMP",
11
- "mode": "NULLABLE",
12
- "description": "Start of the export run (same for every row of a run)"
13
- },
14
- {
15
- "name": "feed",
16
- "type": "STRING",
17
- "mode": "NULLABLE",
18
- "description": "Feed the item was written to ('google')"
19
- },
20
- {
21
- "name": "runId",
22
- "type": "STRING",
23
- "mode": "NULLABLE",
24
- "description": "Cronicle job id of the run"
25
- },
26
- {
27
- "name": "itemId",
28
- "type": "STRING",
29
- "mode": "NULLABLE",
30
- "description": "<g:id> = CBD _id"
31
- },
32
- {
33
- "name": "vehicleId",
34
- "type": "STRING",
35
- "mode": "NULLABLE",
36
- "description": "metaData.vehicleId"
37
- },
38
- {
39
- "name": "make",
40
- "type": "STRING",
41
- "mode": "NULLABLE",
42
- "description": "<g:brand> as sent"
43
- },
44
- {
45
- "name": "model",
46
- "type": "STRING",
47
- "mode": "NULLABLE",
48
- "description": "<g:model> as sent"
49
- },
50
- {
51
- "name": "condition",
52
- "type": "STRING",
53
- "mode": "NULLABLE",
54
- "description": "<g:condition> as sent"
55
- },
56
- {
57
- "name": "fuel",
58
- "type": "STRING",
59
- "mode": "NULLABLE",
60
- "description": "consumption.fuel selector"
61
- },
62
- {
63
- "name": "hybridPlugin",
64
- "type": "BOOLEAN",
65
- "mode": "NULLABLE",
66
- "description": "engineData.hybridPlugin, null when unset"
67
- },
68
- {
69
- "name": "engine",
70
- "type": "STRING",
71
- "mode": "NULLABLE",
72
- "description": "<g:engine> as sent"
73
- },
74
- {
75
- "name": "electricRange",
76
- "type": "STRING",
77
- "mode": "NULLABLE",
78
- "description": "<g:electric_range> as sent, null when omitted"
79
- },
80
- {
81
- "name": "fuelConsumption",
82
- "type": "STRING",
83
- "mode": "NULLABLE",
84
- "description": "<g:fuel_consumption> as sent, null when omitted"
85
- },
86
- {
87
- "name": "fuelConsumptionDischarged",
88
- "type": "STRING",
89
- "mode": "NULLABLE",
90
- "description": "<g:fuel_consumption_discharged_battery> as sent, null when omitted"
91
- },
92
- {
93
- "name": "energyConsumption",
94
- "type": "STRING",
95
- "mode": "NULLABLE",
96
- "description": "<g:energy_consumption> as sent, null when omitted"
97
- },
98
- {
99
- "name": "co2Emissions",
100
- "type": "STRING",
101
- "mode": "NULLABLE",
102
- "description": "<g:co2_emissions> as sent, null when omitted"
103
- },
104
- {
105
- "name": "rangeSourceField",
106
- "type": "STRING",
107
- "mode": "NULLABLE",
108
- "description": "Car field electric_range came from: battery.wltpRange / battery.wltpTotalRange / battery.batteryRangeElectric"
109
- },
110
- {
111
- "name": "price",
112
- "type": "FLOAT",
113
- "mode": "NULLABLE",
114
- "description": "<g:price> amount as sent (EUR)"
115
- },
116
- {
117
- "name": "availability",
118
- "type": "STRING",
119
- "mode": "NULLABLE",
120
- "description": "<g:availability> as sent"
121
- }
122
- ]
1
+ [
2
+ {
3
+ "name": "partitionDay",
4
+ "type": "DATE",
5
+ "mode": "NULLABLE",
6
+ "description": "UTC day of exportedAt; the table is partitioned by it"
7
+ },
8
+ {
9
+ "name": "exportedAt",
10
+ "type": "TIMESTAMP",
11
+ "mode": "NULLABLE",
12
+ "description": "Start of the export run (same for every row of a run)"
13
+ },
14
+ {
15
+ "name": "feed",
16
+ "type": "STRING",
17
+ "mode": "NULLABLE",
18
+ "description": "Feed the item was written to ('google')"
19
+ },
20
+ {
21
+ "name": "runId",
22
+ "type": "STRING",
23
+ "mode": "NULLABLE",
24
+ "description": "Cronicle job id of the run"
25
+ },
26
+ {
27
+ "name": "itemId",
28
+ "type": "STRING",
29
+ "mode": "NULLABLE",
30
+ "description": "<g:id> = CBD _id"
31
+ },
32
+ {
33
+ "name": "vehicleId",
34
+ "type": "STRING",
35
+ "mode": "NULLABLE",
36
+ "description": "metaData.vehicleId"
37
+ },
38
+ {
39
+ "name": "make",
40
+ "type": "STRING",
41
+ "mode": "NULLABLE",
42
+ "description": "<g:brand> as sent"
43
+ },
44
+ {
45
+ "name": "model",
46
+ "type": "STRING",
47
+ "mode": "NULLABLE",
48
+ "description": "<g:model> as sent"
49
+ },
50
+ {
51
+ "name": "condition",
52
+ "type": "STRING",
53
+ "mode": "NULLABLE",
54
+ "description": "<g:condition> as sent"
55
+ },
56
+ {
57
+ "name": "fuel",
58
+ "type": "STRING",
59
+ "mode": "NULLABLE",
60
+ "description": "consumption.fuel selector"
61
+ },
62
+ {
63
+ "name": "hybridPlugin",
64
+ "type": "BOOLEAN",
65
+ "mode": "NULLABLE",
66
+ "description": "engineData.hybridPlugin, null when unset"
67
+ },
68
+ {
69
+ "name": "engine",
70
+ "type": "STRING",
71
+ "mode": "NULLABLE",
72
+ "description": "<g:engine> as sent"
73
+ },
74
+ {
75
+ "name": "electricRange",
76
+ "type": "STRING",
77
+ "mode": "NULLABLE",
78
+ "description": "<g:electric_range> as sent, null when omitted"
79
+ },
80
+ {
81
+ "name": "fuelConsumption",
82
+ "type": "STRING",
83
+ "mode": "NULLABLE",
84
+ "description": "<g:fuel_consumption> as sent, null when omitted"
85
+ },
86
+ {
87
+ "name": "fuelConsumptionDischarged",
88
+ "type": "STRING",
89
+ "mode": "NULLABLE",
90
+ "description": "<g:fuel_consumption_discharged_battery> as sent, null when omitted"
91
+ },
92
+ {
93
+ "name": "energyConsumption",
94
+ "type": "STRING",
95
+ "mode": "NULLABLE",
96
+ "description": "<g:energy_consumption> as sent, null when omitted"
97
+ },
98
+ {
99
+ "name": "co2Emissions",
100
+ "type": "STRING",
101
+ "mode": "NULLABLE",
102
+ "description": "<g:co2_emissions> as sent, null when omitted"
103
+ },
104
+ {
105
+ "name": "rangeSourceField",
106
+ "type": "STRING",
107
+ "mode": "NULLABLE",
108
+ "description": "Car field electric_range came from: battery.wltpRange / battery.wltpTotalRange / battery.batteryRangeElectric"
109
+ },
110
+ {
111
+ "name": "price",
112
+ "type": "FLOAT",
113
+ "mode": "NULLABLE",
114
+ "description": "<g:price> amount as sent (EUR)"
115
+ },
116
+ {
117
+ "name": "availability",
118
+ "type": "STRING",
119
+ "mode": "NULLABLE",
120
+ "description": "<g:availability> as sent"
121
+ },
122
+ {
123
+ "name": "publicationGate",
124
+ "type": "STRING",
125
+ "mode": "NULLABLE",
126
+ "description": "EnVKV C8 gate mode of the run (audit / enforce), null when off"
127
+ },
128
+ {
129
+ "name": "estimatedAttributes",
130
+ "type": "STRING",
131
+ "mode": "NULLABLE",
132
+ "description": "Comma-separated EnVKV attributes whose value CBD estimated; left out of the item in enforce mode"
133
+ }
134
+ ]
@@ -1,97 +1,99 @@
1
- import { Environment, ExportType, ImageType } from '../enums';
2
- import { FeedItemsBigQueryTarget, Params } from '../interfaces/params.interface';
3
-
4
- export class Config {
5
- apikey: string;
6
- campaign: string;
7
- userActionsUrl: string;
8
- marketingImageConfigId: number;
9
- upaUrl: string;
10
- imboUrl: string;
11
- imageUrl: string;
12
- baseImageUrl: string;
13
- marketingImageUrl: string;
14
- metaAiaImageUrl: string;
15
- imageType: ImageType;
16
- downloadImage: boolean;
17
- isNewCar: boolean;
18
- isCriteo: boolean;
19
- exportType: ExportType;
20
- domain: string;
21
- input: {
22
- stringIN: string;
23
- numberIN: string;
24
- dateIN: string;
25
- };
26
- // When set, CarService.filterCar exports a car only if price.currentSalesPrice
27
- // is a valid number in (0, maxCurrentSalesPrice] — missing/invalid price means
28
- // the car is not exported. Unset = no price cap for this export type.
29
- maxCurrentSalesPrice?: number;
30
- pathesConfig: {
31
- localDirectory: string;
32
- localConvertedDirectory: string;
33
- baseFileName: string;
34
- };
35
- outputFiles: string[];
36
- cpc: string;
37
- mappingSpreadsheetId: string;
38
- mappingSpreadsheetTitle: string;
39
- dealersSpreadsheetId: string;
40
- dealersSpreadsheetTitle: string;
41
- metaPushPriorityCsvUrl: string;
42
- // Cronicle job id of the run (set by ExportRunner), stored with the feed item rows.
43
- runId?: string;
44
- // Set only when the job configures the feed items BigQuery table; unset = no BigQuery calls.
45
- feedItemsBigQuery?: FeedItemsBigQueryTarget;
46
-
47
- protected environmentConfig = {
48
- [Environment.DEVELOP]: {
49
- domain: 'autode-dev.de',
50
- fbMarketingImageId: 8,
51
- criteoMarketingImagesConfigId: 12,
52
- localConvertedDirectory: 'resources/csv_dev',
53
- remoteDirectory: 'import_dev',
54
- imageUrl: 'https://images.autode-dev.de/cache_test',
55
- baseImageUrl: 'https://images.autode-dev.de',
56
- },
57
- [Environment.PRODUCTION]: {
58
- domain: 'auto.de',
59
- fbMarketingImageId: 1,
60
- criteoMarketingImagesConfigId: 3,
61
- localConvertedDirectory: 'resources/csv_prod',
62
- remoteDirectory: 'import',
63
- imageUrl: 'https://images.auto.de/generated_images',
64
- baseImageUrl: 'https://images.auto.de',
65
- },
66
- };
67
-
68
- constructor(params: Params) {
69
- this.apikey = params.apikey;
70
- this.exportType = params.exportType;
71
- this.cpc = params.cpc;
72
- this.domain = this.environmentConfig[params.environment].domain;
73
- this.baseImageUrl = this.environmentConfig[params.environment].baseImageUrl;
74
- this.marketingImageUrl = this.environmentConfig[params.environment].imageUrl;
75
- this.mappingSpreadsheetId = '147qz9sTgs1wi13ua8YKa0IgfxAn1X_qH6LkGnAnzeyY';
76
- this.dealersSpreadsheetId = '1gWrcA-uRTpna1h66Mbe2Y7NA0tIhlUaQXBcS4a_UzOw';
77
- this.dealersSpreadsheetTitle = 'dealers_mapping';
78
- this.metaPushPriorityCsvUrl = params.metaPushPriorityCsvUrl;
79
- this.userActionsUrl = `https://www.${this.domain}/service/user-actions`;
80
- this.imboUrl = `https://www.${this.domain}/service/image-imbo`;
81
- this.imageUrl = `https://images.${this.domain}`;
82
- // Same FTP_ROOT (/generated_images) image-generator's MetaAiaImagesService saves to —
83
- // independent of marketingImageUrl, which points legacy fb/criteo channels at their own path.
84
- this.metaAiaImageUrl = `${this.imageUrl}/generated_images`;
85
- this.upaUrl = `https://www.${this.domain}/service/user-profile-admin`;
86
- this.input = {
87
- stringIN: params.String,
88
- numberIN: params.Number,
89
- dateIN: params.Date,
90
- };
91
- this.pathesConfig = {
92
- localDirectory: 'resources/cars-export',
93
- localConvertedDirectory: 'resources/default-export',
94
- baseFileName: 'cars',
95
- };
96
- }
97
- }
1
+ import { Environment, ExportType, ImageType } from '../enums';
2
+ import { FeedItemsBigQueryTarget, Params, PublicationGateConfig } from '../interfaces/params.interface';
3
+
4
+ export class Config {
5
+ apikey: string;
6
+ campaign: string;
7
+ userActionsUrl: string;
8
+ marketingImageConfigId: number;
9
+ upaUrl: string;
10
+ imboUrl: string;
11
+ imageUrl: string;
12
+ baseImageUrl: string;
13
+ marketingImageUrl: string;
14
+ metaAiaImageUrl: string;
15
+ imageType: ImageType;
16
+ downloadImage: boolean;
17
+ isNewCar: boolean;
18
+ isCriteo: boolean;
19
+ exportType: ExportType;
20
+ domain: string;
21
+ input: {
22
+ stringIN: string;
23
+ numberIN: string;
24
+ dateIN: string;
25
+ };
26
+ // When set, CarService.filterCar exports a car only if price.currentSalesPrice
27
+ // is a valid number in (0, maxCurrentSalesPrice] — missing/invalid price means
28
+ // the car is not exported. Unset = no price cap for this export type.
29
+ maxCurrentSalesPrice?: number;
30
+ pathesConfig: {
31
+ localDirectory: string;
32
+ localConvertedDirectory: string;
33
+ baseFileName: string;
34
+ };
35
+ outputFiles: string[];
36
+ cpc: string;
37
+ mappingSpreadsheetId: string;
38
+ mappingSpreadsheetTitle: string;
39
+ dealersSpreadsheetId: string;
40
+ dealersSpreadsheetTitle: string;
41
+ metaPushPriorityCsvUrl: string;
42
+ // Cronicle job id of the run (set by ExportRunner), stored with the feed item rows.
43
+ runId?: string;
44
+ // Set only when the job configures the feed items BigQuery table; unset = no BigQuery calls.
45
+ feedItemsBigQuery?: FeedItemsBigQueryTarget;
46
+ // EnVKV C8 publication gate of the Google feed; unset = off.
47
+ publicationGate?: PublicationGateConfig;
48
+
49
+ protected environmentConfig = {
50
+ [Environment.DEVELOP]: {
51
+ domain: 'autode-dev.de',
52
+ fbMarketingImageId: 8,
53
+ criteoMarketingImagesConfigId: 12,
54
+ localConvertedDirectory: 'resources/csv_dev',
55
+ remoteDirectory: 'import_dev',
56
+ imageUrl: 'https://images.autode-dev.de/cache_test',
57
+ baseImageUrl: 'https://images.autode-dev.de',
58
+ },
59
+ [Environment.PRODUCTION]: {
60
+ domain: 'auto.de',
61
+ fbMarketingImageId: 1,
62
+ criteoMarketingImagesConfigId: 3,
63
+ localConvertedDirectory: 'resources/csv_prod',
64
+ remoteDirectory: 'import',
65
+ imageUrl: 'https://images.auto.de/generated_images',
66
+ baseImageUrl: 'https://images.auto.de',
67
+ },
68
+ };
69
+
70
+ constructor(params: Params) {
71
+ this.apikey = params.apikey;
72
+ this.exportType = params.exportType;
73
+ this.cpc = params.cpc;
74
+ this.domain = this.environmentConfig[params.environment].domain;
75
+ this.baseImageUrl = this.environmentConfig[params.environment].baseImageUrl;
76
+ this.marketingImageUrl = this.environmentConfig[params.environment].imageUrl;
77
+ this.mappingSpreadsheetId = '147qz9sTgs1wi13ua8YKa0IgfxAn1X_qH6LkGnAnzeyY';
78
+ this.dealersSpreadsheetId = '1gWrcA-uRTpna1h66Mbe2Y7NA0tIhlUaQXBcS4a_UzOw';
79
+ this.dealersSpreadsheetTitle = 'dealers_mapping';
80
+ this.metaPushPriorityCsvUrl = params.metaPushPriorityCsvUrl;
81
+ this.userActionsUrl = `https://www.${this.domain}/service/user-actions`;
82
+ this.imboUrl = `https://www.${this.domain}/service/image-imbo`;
83
+ this.imageUrl = `https://images.${this.domain}`;
84
+ // Same FTP_ROOT (/generated_images) image-generator's MetaAiaImagesService saves to —
85
+ // independent of marketingImageUrl, which points legacy fb/criteo channels at their own path.
86
+ this.metaAiaImageUrl = `${this.imageUrl}/generated_images`;
87
+ this.upaUrl = `https://www.${this.domain}/service/user-profile-admin`;
88
+ this.input = {
89
+ stringIN: params.String,
90
+ numberIN: params.Number,
91
+ dateIN: params.Date,
92
+ };
93
+ this.pathesConfig = {
94
+ localDirectory: 'resources/cars-export',
95
+ localConvertedDirectory: 'resources/default-export',
96
+ baseFileName: 'cars',
97
+ };
98
+ }
99
+ }
@@ -1,26 +1,41 @@
1
- import { ExportType, FileType, ImageType } from 'src/enums';
2
- import { resolveMaxFeedSalesPrice } from '../constants/feed-price.constants';
3
- import { Params } from '../interfaces/params.interface';
4
- import { Config } from './config';
5
-
6
- const trimmed = (value: unknown): string => (typeof value === 'string' ? value.trim() : '');
7
-
8
- export class GoogleConfig extends Config {
9
- constructor(params: Params) {
10
- super(params);
11
- this.pathesConfig.localConvertedDirectory = `resources/${ExportType.GOOGLE}-export`;
12
- this.imageType = ImageType.default;
13
- this.maxCurrentSalesPrice = resolveMaxFeedSalesPrice(params.maxCurrentSalesPrice);
14
- const feedItemsBigQuery = {
15
- project: trimmed(params.feedItemsBqProject),
16
- dataset: trimmed(params.feedItemsBqDataset),
17
- table: trimmed(params.feedItemsBqTable),
18
- };
19
- if (feedItemsBigQuery.project && feedItemsBigQuery.dataset && feedItemsBigQuery.table) {
20
- this.feedItemsBigQuery = feedItemsBigQuery;
21
- }
22
- this.outputFiles = [
23
- `${this.pathesConfig.localConvertedDirectory}/api_export_cars_${ExportType.GOOGLE}.${FileType.XML}`,
24
- ];
25
- }
26
- }
1
+ import { ExportType, FileType, ImageType } from 'src/enums';
2
+ import { resolveMaxFeedSalesPrice } from '../constants/feed-price.constants';
3
+ import { Params, PublicationGateConfig } from '../interfaces/params.interface';
4
+ import { Config } from './config';
5
+
6
+ const trimmed = (value: unknown): string => (typeof value === 'string' ? value.trim() : '');
7
+
8
+ export class GoogleConfig extends Config {
9
+ constructor(params: Params) {
10
+ super(params);
11
+ this.pathesConfig.localConvertedDirectory = `resources/${ExportType.GOOGLE}-export`;
12
+ this.imageType = ImageType.default;
13
+ this.maxCurrentSalesPrice = resolveMaxFeedSalesPrice(params.maxCurrentSalesPrice);
14
+ const feedItemsBigQuery = {
15
+ project: trimmed(params.feedItemsBqProject),
16
+ dataset: trimmed(params.feedItemsBqDataset),
17
+ table: trimmed(params.feedItemsBqTable),
18
+ };
19
+ if (feedItemsBigQuery.project && feedItemsBigQuery.dataset && feedItemsBigQuery.table) {
20
+ this.feedItemsBigQuery = feedItemsBigQuery;
21
+ }
22
+ const gateMode = trimmed(params.publicationGate).toLowerCase();
23
+ if (gateMode === 'audit' || gateMode === 'enforce') {
24
+ const gate = {
25
+ mode: gateMode as PublicationGateConfig['mode'],
26
+ project: trimmed(params.publicationGateBqProject),
27
+ dataset: trimmed(params.publicationGateBqDataset),
28
+ table: trimmed(params.publicationGateBqTable),
29
+ };
30
+ if (!gate.project || !gate.dataset || !gate.table) {
31
+ throw new Error(`publicationGate=${gateMode} needs publicationGateBqProject, publicationGateBqDataset and publicationGateBqTable`);
32
+ }
33
+ this.publicationGate = gate;
34
+ } else if (gateMode && gateMode !== 'off') {
35
+ throw new Error(`publicationGate must be off, audit or enforce, got "${gateMode}"`);
36
+ }
37
+ this.outputFiles = [
38
+ `${this.pathesConfig.localConvertedDirectory}/api_export_cars_${ExportType.GOOGLE}.${FileType.XML}`,
39
+ ];
40
+ }
41
+ }
@@ -20,3 +20,4 @@ export * from './mitula.config';
20
20
  export * from './awin.config';
21
21
  export * from './evium.config';
22
22
  export * from './santander.config';
23
+ export * from './openai-ads.config';
@@ -0,0 +1,70 @@
1
+ import { toNumber } from 'lodash';
2
+
3
+ import { ExportType, FileType, ImageType } from '../enums';
4
+ import { Params } from '../interfaces/params.interface';
5
+ import { FeedGuardOptions } from '../services/feed-guard';
6
+ import { Config } from './config';
7
+
8
+ const text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '');
9
+
10
+ const positiveNumber = (value: unknown, fallback: number): number => {
11
+ const parsed = toNumber(text(value));
12
+ return text(value) && Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
13
+ };
14
+
15
+ // Defaults sized for prod (~45k online cars, ~27k in the Google feed on 25.08). DEV (a few hundred
16
+ // cars) needs smaller minimums via the Cronicle params.
17
+ export const OPENAI_ADS_DEFAULTS = {
18
+ sellerName: 'auto.de',
19
+ retentionDays: 15,
20
+ imageConcurrency: 16,
21
+ minSourceItems: 1000,
22
+ minPublishedItems: 500,
23
+ maxDropPercent: 20,
24
+ maxSourceAgeHours: 26,
25
+ maxStaleHours: 48,
26
+ };
27
+
28
+ /**
29
+ * OpenAI ChatGPT Ads vehicle product feed (ADV-335): OpenAI-format CSV, clean ADV-336 images.
30
+ * Dry-run is the default: the file goes to resources/openai_ads-dryrun and nothing reads it.
31
+ * Only openAiAdsDryRun=false writes resources/openai_ads-export, the directory a transport step
32
+ * (hosted URL / SFTP, not part of this exporter) may publish from. Each mode has its own state
33
+ * (ledger, last good counts), so dry-runs never touch what was sent to OpenAI.
34
+ */
35
+ export class OpenAiAdsConfig extends Config {
36
+ dryRun: boolean;
37
+ sellerName: string;
38
+ retentionDays: number;
39
+ imageConcurrency: number;
40
+ stateDir: string;
41
+ feedGuard: FeedGuardOptions;
42
+
43
+ constructor(params: Params) {
44
+ super(params);
45
+ this.dryRun = text(params.openAiAdsDryRun).toLowerCase() !== 'false';
46
+ const mode = this.dryRun ? 'dryrun' : 'export';
47
+ this.pathesConfig.localConvertedDirectory = `resources/${ExportType.OPENAI_ADS}-${mode}`;
48
+ this.stateDir = `resources/${ExportType.OPENAI_ADS}-state/${mode}`;
49
+ this.imageType = ImageType.clean;
50
+ this.sellerName = text(params.openAiAdsSellerName) || OPENAI_ADS_DEFAULTS.sellerName;
51
+ this.retentionDays = positiveNumber(params.openAiAdsRetentionDays, OPENAI_ADS_DEFAULTS.retentionDays);
52
+ this.imageConcurrency = positiveNumber(params.openAiAdsImageConcurrency, OPENAI_ADS_DEFAULTS.imageConcurrency);
53
+ // Shared price cap stays opt-in here (open question to Marketing): only an explicit
54
+ // maxCurrentSalesPrice param applies it, no 80k default as for Google/Meta.
55
+ const cap = positiveNumber(params.maxCurrentSalesPrice, 0);
56
+ this.maxCurrentSalesPrice = cap || undefined;
57
+ this.feedGuard = {
58
+ stateDir: this.stateDir,
59
+ minSourceItems: positiveNumber(params.feedGuardMinSourceItems, OPENAI_ADS_DEFAULTS.minSourceItems),
60
+ minPublishedItems: positiveNumber(params.feedGuardMinPublishedItems, OPENAI_ADS_DEFAULTS.minPublishedItems),
61
+ maxDropPercent: positiveNumber(params.feedGuardMaxDropPercent, OPENAI_ADS_DEFAULTS.maxDropPercent),
62
+ maxSourceAgeHours: positiveNumber(params.feedGuardMaxSourceAgeHours, OPENAI_ADS_DEFAULTS.maxSourceAgeHours),
63
+ maxStaleHours: positiveNumber(params.feedGuardMaxStaleHours, OPENAI_ADS_DEFAULTS.maxStaleHours),
64
+ acceptDrop: text(params.feedGuardAcceptDrop).toLowerCase() === 'true',
65
+ };
66
+ this.outputFiles = [
67
+ `${this.pathesConfig.localConvertedDirectory}/api_export_cars_${ExportType.OPENAI_ADS}.${FileType.CSV}`,
68
+ ];
69
+ }
70
+ }
@@ -30,4 +30,6 @@ export enum ExportType {
30
30
  AWIN = 'awin',
31
31
  SANTANDER = 'santander',
32
32
  EVIUM = 'evium',
33
+ // ChatGPT Ads product feed (ADV-335)
34
+ OPENAI_ADS = 'openai_ads',
33
35
  }
@@ -1,4 +1,9 @@
1
1
  export enum ImageType {
2
2
  default = 'default',
3
3
  marketing = 'marketing',
4
+ /**
5
+ * ADV-336: real photos only, explicit JPEG, 3/4 exterior hero first, then the remaining
6
+ * exterior and interior views — no feature close-ups, no marketing overlays, no placeholders.
7
+ */
8
+ clean = 'clean',
4
9
  }