@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.
- package/README.md +128 -0
- package/dist/configs/config.d.ts +2 -1
- package/dist/configs/config.js +1 -0
- package/dist/configs/config.js.map +1 -1
- package/dist/configs/google.config.js +16 -0
- package/dist/configs/google.config.js.map +1 -1
- package/dist/configs/index.d.ts +1 -0
- package/dist/configs/index.js +1 -0
- package/dist/configs/index.js.map +1 -1
- package/dist/configs/openai-ads.config.d.ts +22 -0
- package/dist/configs/openai-ads.config.js +56 -0
- package/dist/configs/openai-ads.config.js.map +1 -0
- package/dist/enums/export-type.enum.d.ts +2 -1
- package/dist/enums/export-type.enum.js +1 -0
- package/dist/enums/export-type.enum.js.map +1 -1
- package/dist/enums/image-type.enum.d.ts +2 -1
- package/dist/enums/image-type.enum.js +1 -0
- package/dist/enums/image-type.enum.js.map +1 -1
- package/dist/export.schema.d.ts +6 -2
- package/dist/export.schema.js +4 -0
- package/dist/export.schema.js.map +1 -1
- package/dist/exporters/google.exporter.js +12 -2
- package/dist/exporters/google.exporter.js.map +1 -1
- package/dist/exporters/index.d.ts +1 -0
- package/dist/exporters/index.js +1 -0
- package/dist/exporters/index.js.map +1 -1
- package/dist/exporters/openai-ads.exporter.d.ts +13 -0
- package/dist/exporters/openai-ads.exporter.js +91 -0
- package/dist/exporters/openai-ads.exporter.js.map +1 -0
- package/dist/formats/openai-ads-csv.builder.d.ts +15 -0
- package/dist/formats/openai-ads-csv.builder.js +48 -0
- package/dist/formats/openai-ads-csv.builder.js.map +1 -0
- package/dist/interfaces/openai-ads.interface.d.ts +35 -0
- package/dist/interfaces/openai-ads.interface.js +37 -0
- package/dist/interfaces/openai-ads.interface.js.map +1 -0
- package/dist/interfaces/params.interface.d.ts +20 -0
- package/dist/services/feed-guard.d.ts +44 -0
- package/dist/services/feed-guard.js +156 -0
- package/dist/services/feed-guard.js.map +1 -0
- package/dist/services/image-url-validator.service.d.ts +45 -0
- package/dist/services/image-url-validator.service.js +141 -0
- package/dist/services/image-url-validator.service.js.map +1 -0
- package/dist/services/images.service.d.ts +12 -0
- package/dist/services/images.service.js +46 -1
- package/dist/services/images.service.js.map +1 -1
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/openai-ads-feed-run.d.ts +74 -0
- package/dist/services/openai-ads-feed-run.js +151 -0
- package/dist/services/openai-ads-feed-run.js.map +1 -0
- package/dist/services/openai-ads-ledger.d.ts +19 -0
- package/dist/services/openai-ads-ledger.js +72 -0
- package/dist/services/openai-ads-ledger.js.map +1 -0
- package/dist/services/publication-gate.d.ts +15 -0
- package/dist/services/publication-gate.js +107 -0
- package/dist/services/publication-gate.js.map +1 -0
- package/dist/transformers/google-feed-envkv.d.ts +2 -0
- package/dist/transformers/google-feed-envkv.js +62 -31
- package/dist/transformers/google-feed-envkv.js.map +1 -1
- package/dist/transformers/google-feed-item-row.d.ts +6 -1
- package/dist/transformers/google-feed-item-row.js +3 -1
- package/dist/transformers/google-feed-item-row.js.map +1 -1
- package/dist/transformers/openai-ads.transformer.d.ts +28 -0
- package/dist/transformers/openai-ads.transformer.js +144 -0
- package/dist/transformers/openai-ads.transformer.js.map +1 -0
- package/dist/transformers/openai-image-fields.d.ts +16 -0
- package/dist/transformers/openai-image-fields.js +34 -0
- package/dist/transformers/openai-image-fields.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/openai-ads/AUDIT.md +235 -0
- package/docs/openai-ads/RUNBOOK.md +94 -0
- package/package.json +64 -64
- package/schema/feed-items.json +134 -122
- package/src/configs/config.ts +99 -97
- package/src/configs/google.config.ts +41 -26
- package/src/configs/index.ts +1 -0
- package/src/configs/openai-ads.config.ts +70 -0
- package/src/enums/export-type.enum.ts +2 -0
- package/src/enums/image-type.enum.ts +5 -0
- package/src/export.command.ts +38 -38
- package/src/export.schema.ts +6 -0
- package/src/exporters/google.exporter.ts +67 -56
- package/src/exporters/index.ts +1 -0
- package/src/exporters/openai-ads.exporter.ts +93 -0
- package/src/formats/openai-ads-csv.builder.ts +55 -0
- package/src/interfaces/openai-ads.interface.ts +64 -0
- package/src/interfaces/params.interface.ts +56 -29
- package/src/services/feed-guard.ts +219 -0
- package/src/services/feed-items-bigquery.writer.ts +101 -101
- package/src/services/image-url-validator.service.ts +181 -0
- package/src/services/images.service.ts +72 -0
- package/src/services/index.ts +1 -0
- package/src/services/openai-ads-feed-run.ts +226 -0
- package/src/services/openai-ads-ledger.ts +116 -0
- package/src/services/publication-gate.ts +119 -0
- package/src/transformers/google-feed-envkv.ts +81 -33
- package/src/transformers/google-feed-item-row.ts +78 -71
- package/src/transformers/openai-ads.transformer.ts +211 -0
- package/src/transformers/openai-image-fields.ts +60 -0
- package/test/clean-images.test.ts +86 -0
- package/test/fixtures/images-service.harness.ts +32 -0
- package/test/fixtures/images.golden.json +218 -0
- package/test/fixtures/imbo-images.fixture.ts +77 -0
- package/test/fixtures/openai-ads.fixture.ts +89 -0
- package/test/google-feed-item-row.test.ts +135 -121
- package/test/image-url-validator.test.ts +110 -0
- package/test/images-regression.test.ts +23 -0
- package/test/openai-ads-feed-run.test.ts +278 -0
- package/test/openai-ads-mapping.test.ts +136 -0
- package/test/openai-image-fields.test.ts +99 -0
- package/test/publication-gate.test.ts +86 -0
package/schema/feed-items.json
CHANGED
|
@@ -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
|
+
]
|
package/src/configs/config.ts
CHANGED
|
@@ -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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
this.
|
|
72
|
-
this.
|
|
73
|
-
this.
|
|
74
|
-
this.
|
|
75
|
-
this.
|
|
76
|
-
this.
|
|
77
|
-
this.
|
|
78
|
-
this.
|
|
79
|
-
this.
|
|
80
|
-
this.
|
|
81
|
-
this.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
this.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
+
}
|
package/src/configs/index.ts
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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
|
}
|