@bytebrand/export-cars 1.26.38 → 1.26.40
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/dist/configs/criteo.config.d.ts +10 -0
- package/dist/configs/criteo.config.js +26 -1
- package/dist/configs/criteo.config.js.map +1 -1
- package/dist/exporters/criteo.exporter.js +27 -23
- package/dist/exporters/criteo.exporter.js.map +1 -1
- package/dist/interfaces/export-item.interface.d.ts +5 -5
- package/dist/services/criteo-feed-run.d.ts +76 -0
- package/dist/services/criteo-feed-run.js +186 -0
- package/dist/services/criteo-feed-run.js.map +1 -0
- package/dist/transformers/criteo.transformer.js +13 -8
- package/dist/transformers/criteo.transformer.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/criteo/RUNBOOK.md +86 -0
- package/package.json +65 -65
- package/src/configs/criteo.config.ts +54 -17
- package/src/exporters/criteo.exporter.ts +63 -51
- package/src/interfaces/export-item.interface.ts +3 -3
- package/src/services/criteo-feed-run.ts +253 -0
- package/src/transformers/criteo.transformer.ts +22 -10
- package/test/criteo-feed-run.test.ts +365 -0
- package/test/criteo-mapping.test.ts +122 -0
- package/test/fixtures/criteo-20-cases.json +582 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Criteo catalog feed — runbook (ADV-319)
|
|
2
|
+
|
|
3
|
+
Status 06.11.2026, `@bytebrand/export-cars` 1.26.40. DEV only; PROD events `4.0` / `4.1` are disabled
|
|
4
|
+
and no Criteo account, contract or importer test exists yet. Nothing here allows a live upload.
|
|
5
|
+
|
|
6
|
+
## What the job does
|
|
7
|
+
|
|
8
|
+
`4.0 export criteo - part 1` (`exportType=criteo`) reads the base export snapshot
|
|
9
|
+
(`resources/cars-export/cars.<n>.json`) and writes the legacy Criteo RSS/XML file
|
|
10
|
+
`resources/criteo-export/api_export_cars_criteo.xml`. The chained `4.1 Export criteo - part 2` uploads that
|
|
11
|
+
file. Format, enums and field order are the legacy ones (decision 20.10.2026: restore as before).
|
|
12
|
+
|
|
13
|
+
Per car, in this order (first match wins, counted in `excluded`):
|
|
14
|
+
|
|
15
|
+
| Reason | Rule |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `no_id` | empty `_id` |
|
|
18
|
+
| `duplicate_id` | `_id` already seen in this run (first one is kept) |
|
|
19
|
+
| `filtered` | shared partner filter: `metaData.imagesCount > 0` and the Cronicle rule params |
|
|
20
|
+
| `reserved_or_sold` | `offer.offerAvailability` is reserved or sold (Criteo 16.10: only marketable stock) |
|
|
21
|
+
| `no_valid_price` | `price.currentSalesPrice` missing, not a number or ≤ 0 |
|
|
22
|
+
| `no_marketing_image` | no generated `criteo-<id>.jpg`; no noimage placeholder is ever sent |
|
|
23
|
+
|
|
24
|
+
A car that went offline is not in the snapshot, so it is missing from the next full file; Criteo's next
|
|
25
|
+
full import removes it. `published-ids.json` keeps the ids of the last published file, and the report
|
|
26
|
+
lists `ids.added` / `ids.removed` / `ids.kept` (+ up to 20 removed ids) for each run.
|
|
27
|
+
|
|
28
|
+
Item fields: `installment` and `custom_label_0` only when the car has a real rate (months and amount > 0);
|
|
29
|
+
`energy_efficiency_class` only for a known WLTP class.
|
|
30
|
+
|
|
31
|
+
## Fail-closed guard
|
|
32
|
+
|
|
33
|
+
Shared `FeedGuard` (same as meinestadt / AutoUncle / OpenAI). The new XML is written to
|
|
34
|
+
`api_export_cars_criteo.xml.tmp` and renamed over the published file only after all checks passed; the
|
|
35
|
+
previous file is kept as `.prev`. A blocked run keeps the published file, moves the temp file to
|
|
36
|
+
`.rejected`, and **fails the job** (Cronicle code 1), so `4.1` does not upload anything.
|
|
37
|
+
|
|
38
|
+
| Code | Meaning | Action |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `source_empty` | no snapshot chunk files (base export failed / wiped) | fix the base export, re-run |
|
|
41
|
+
| `source_partial` | chunk files with gaps | re-run the base export |
|
|
42
|
+
| `source_stale` | newest chunk older than `feedGuardMaxSourceAgeHours` (26) | base export did not run |
|
|
43
|
+
| `source_too_small` | snapshot below `feedGuardMinSourceItems` (1000) | check the base export |
|
|
44
|
+
| `published_too_small` | fewer items than `feedGuardMinPublishedItems` (500), e.g. all reserved/sold | check stock / images |
|
|
45
|
+
| `count_drop` | source or items dropped by more than `feedGuardMaxDropPercent` (20 %) vs last good | if the stock really dropped: re-run once with `feedGuardAcceptDrop=true` |
|
|
46
|
+
| `locked` | another run holds `resources/criteo-state/run.lock` (stale after 6 h) | wait / check the other job |
|
|
47
|
+
|
|
48
|
+
Stale fallback: each report carries `lastGoodAgeHours`; after `feedGuardMaxStaleHours` (48) without a
|
|
49
|
+
successful run, `stale: true` and the log line `criteo feed STALE`. The job itself already failed on every
|
|
50
|
+
blocked run, which is the alert. Whether Criteo should then get an empty file (to stop advertising
|
|
51
|
+
old stock) is an open partner question, not built.
|
|
52
|
+
|
|
53
|
+
All thresholds are Cronicle params of the `4.0` event (`feedGuardMinSourceItems`,
|
|
54
|
+
`feedGuardMinPublishedItems`, `feedGuardMaxDropPercent`, `feedGuardMaxSourceAgeHours`,
|
|
55
|
+
`feedGuardMaxStaleHours`, `feedGuardAcceptDrop`).
|
|
56
|
+
|
|
57
|
+
## Files
|
|
58
|
+
|
|
59
|
+
| Path (relative to `/opt/cronicle`) | Content |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `resources/criteo-export/api_export_cars_criteo.xml` | published file (what `4.1` uploads) |
|
|
62
|
+
| `resources/criteo-export/api_export_cars_criteo.xml.prev` | file before the last publish |
|
|
63
|
+
| `resources/criteo-export/api_export_cars_criteo.xml.rejected` | output of the last blocked run |
|
|
64
|
+
| `resources/criteo-state/last-good.json` | counts + time of the last publish |
|
|
65
|
+
| `resources/criteo-state/last-run-report.json` | report of the last run (also printed as `criteo feed report: {...}`) |
|
|
66
|
+
| `resources/criteo-state/published-ids.json` | ids of the published file |
|
|
67
|
+
|
|
68
|
+
Report metrics: source chunk age (`source.ageHours`), run time (`durationMs`), source cars, items,
|
|
69
|
+
exclusions by reason, items with a rate, items without energy class, id changes, last good age.
|
|
70
|
+
Criteo's accepted / rejected records come from the Criteo importer, not from this job.
|
|
71
|
+
|
|
72
|
+
## Rollback
|
|
73
|
+
|
|
74
|
+
1. Code: `sudo npm --prefix /opt/cronicle install @bytebrand/export-cars@<previous> --prefer-online`
|
|
75
|
+
(DEV before this release: 1.26.39). Only the export-cars package changes; back up
|
|
76
|
+
`/opt/cronicle/package.json` and `package-lock.json` first.
|
|
77
|
+
2. File: `cp resources/criteo-export/api_export_cars_criteo.xml.prev resources/criteo-export/api_export_cars_criteo.xml`.
|
|
78
|
+
3. State: `resources/criteo-state/` can be deleted; the next run then has no last good counts (no drop check
|
|
79
|
+
once) and reports all ids as added.
|
|
80
|
+
|
|
81
|
+
## Tests
|
|
82
|
+
|
|
83
|
+
`test/criteo-mapping.test.ts` (item mapping) and `test/criteo-feed-run.test.ts`: 20 anonymized DEV cars
|
|
84
|
+
(`test/fixtures/criteo-20-cases.json`, synthetic ids, mapping fields only) plus sold, reserved, offline,
|
|
85
|
+
no image, price change, invalid price, duplicate id, missing id, source outage, partial / stale snapshot,
|
|
86
|
+
count drop + acceptDrop, empty stock, lock, stale last good, atomic publish. Run: `npm test`.
|
package/package.json
CHANGED
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@bytebrand/export-cars",
|
|
3
|
-
"version": "1.26.
|
|
4
|
-
"description": "",
|
|
5
|
-
"author": "bytebrand",
|
|
6
|
-
"license": "UNLICENSED",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"prebuild": "rimraf dist",
|
|
9
|
-
"build": "nest build",
|
|
10
|
-
"prepublishOnly": "npm run build",
|
|
11
|
-
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
12
|
-
"lint": "eslint -c .eslintrc.js --ext .ts src/",
|
|
13
|
-
"start:dev": "nest start --watch",
|
|
14
|
-
"test": "ts-node --transpile-only test/google-feed-envkv.test.ts && ts-node --transpile-only test/google-feed-item-row.test.ts && ts-node --transpile-only test/publication-gate.test.ts && ts-node --transpile-only test/images-regression.test.ts && ts-node --transpile-only test/clean-images.test.ts && ts-node --transpile-only test/image-url-validator.test.ts && ts-node --transpile-only test/openai-image-fields.test.ts && ts-node --transpile-only test/openai-ads-mapping.test.ts && ts-node --transpile-only test/openai-ads-feed-run.test.ts && ts-node --transpile-only test/openai-ads-finance.test.ts && ts-node --transpile-only test/openai-ads-price-cap.test.ts && ts-node --transpile-only test/meinstadt-mapping.test.ts && ts-node --transpile-only test/meinstadt-feed-run.test.ts && ts-node --transpile-only test/autouncle-mapping.test.ts && ts-node --transpile-only test/autouncle-feed-run.test.ts && ts-node --transpile-only test/criteo-images.test.ts"
|
|
15
|
-
},
|
|
16
|
-
"dependencies": {
|
|
17
|
-
"@bytebrand/autode-request": "^1.19.2",
|
|
18
|
-
"@bytebrand/bigquery-logger": "^2.1.2",
|
|
19
|
-
"@bytebrand/car-basic-data-scope": "^9.11.24",
|
|
20
|
-
"@bytebrand/car-description": "^1.17.20",
|
|
21
|
-
"@bytebrand/car-schema": "^3.14.15",
|
|
22
|
-
"@bytebrand/chunkable": "^5.19.8",
|
|
23
|
-
"@bytebrand/cronicle-helper": "^1.21.8",
|
|
24
|
-
"@bytebrand/fe-financing": "^5.14.19",
|
|
25
|
-
"@bytebrand/i18n-dictionaries": "^1.5.0",
|
|
26
|
-
"@bytebrand/import-helper": "^0.45.0",
|
|
27
|
-
"@bytebrand/schema-tools": "^3.18.8",
|
|
28
|
-
"@bytebrand/zipper": "^2.2.0",
|
|
29
|
-
"@nestjs/cache-manager": "^2.1.0",
|
|
30
|
-
"@nestjs/common": "^10.2.7",
|
|
31
|
-
"@nestjs/core": "^10.2.7",
|
|
32
|
-
"adm-zip": "0.5.5",
|
|
33
|
-
"axios": "^1.5.1",
|
|
34
|
-
"bottleneck": "^2.19.5",
|
|
35
|
-
"cache-manager": "^5.2.4",
|
|
36
|
-
"csv-write-stream": "^2.0.0",
|
|
37
|
-
"google-spreadsheet": "^3.3.0",
|
|
38
|
-
"lodash": "^4.17.21",
|
|
39
|
-
"moment": "^2.29.4",
|
|
40
|
-
"nest-commander": "~3.15.0",
|
|
41
|
-
"reflect-metadata": "^0.1.13",
|
|
42
|
-
"vin-generator": "^1.0.1"
|
|
43
|
-
},
|
|
44
|
-
"devDependencies": {
|
|
45
|
-
"@nestjs/cli": "^10.1.18",
|
|
46
|
-
"@types/google-spreadsheet": "^3.3.0",
|
|
47
|
-
"@types/inquirer": "^8.2.4",
|
|
48
|
-
"@types/lodash": "^4.14.186",
|
|
49
|
-
"@types/node": "^16.18.58",
|
|
50
|
-
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
|
51
|
-
"@typescript-eslint/parser": "^6.8.0",
|
|
52
|
-
"eslint": "^8.0.1",
|
|
53
|
-
"eslint-config-prettier": "^8.3.0",
|
|
54
|
-
"eslint-plugin-prettier": "^4.0.0",
|
|
55
|
-
"gts": "^3.1.1",
|
|
56
|
-
"prettier": "^2.3.2",
|
|
57
|
-
"ts-node": "^10.0.0",
|
|
58
|
-
"tsconfig-paths": "4.1.0",
|
|
59
|
-
"typescript": "^5.2.2"
|
|
60
|
-
},
|
|
61
|
-
"engines": {
|
|
62
|
-
"node": ">=14.0.0"
|
|
63
|
-
},
|
|
64
|
-
"gitHead": "e827f35865032821bf3a081f03a026d3d3f8a540"
|
|
65
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@bytebrand/export-cars",
|
|
3
|
+
"version": "1.26.40",
|
|
4
|
+
"description": "",
|
|
5
|
+
"author": "bytebrand",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"prebuild": "rimraf dist",
|
|
9
|
+
"build": "nest build",
|
|
10
|
+
"prepublishOnly": "npm run build",
|
|
11
|
+
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
12
|
+
"lint": "eslint -c .eslintrc.js --ext .ts src/",
|
|
13
|
+
"start:dev": "nest start --watch",
|
|
14
|
+
"test": "ts-node --transpile-only test/google-feed-envkv.test.ts && ts-node --transpile-only test/google-feed-item-row.test.ts && ts-node --transpile-only test/publication-gate.test.ts && ts-node --transpile-only test/images-regression.test.ts && ts-node --transpile-only test/clean-images.test.ts && ts-node --transpile-only test/image-url-validator.test.ts && ts-node --transpile-only test/openai-image-fields.test.ts && ts-node --transpile-only test/openai-ads-mapping.test.ts && ts-node --transpile-only test/openai-ads-feed-run.test.ts && ts-node --transpile-only test/openai-ads-finance.test.ts && ts-node --transpile-only test/openai-ads-price-cap.test.ts && ts-node --transpile-only test/meinstadt-mapping.test.ts && ts-node --transpile-only test/meinstadt-feed-run.test.ts && ts-node --transpile-only test/autouncle-mapping.test.ts && ts-node --transpile-only test/autouncle-feed-run.test.ts && ts-node --transpile-only test/criteo-images.test.ts && ts-node --transpile-only test/criteo-mapping.test.ts && ts-node --transpile-only test/criteo-feed-run.test.ts"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@bytebrand/autode-request": "^1.19.2",
|
|
18
|
+
"@bytebrand/bigquery-logger": "^2.1.2",
|
|
19
|
+
"@bytebrand/car-basic-data-scope": "^9.11.24",
|
|
20
|
+
"@bytebrand/car-description": "^1.17.20",
|
|
21
|
+
"@bytebrand/car-schema": "^3.14.15",
|
|
22
|
+
"@bytebrand/chunkable": "^5.19.8",
|
|
23
|
+
"@bytebrand/cronicle-helper": "^1.21.8",
|
|
24
|
+
"@bytebrand/fe-financing": "^5.14.19",
|
|
25
|
+
"@bytebrand/i18n-dictionaries": "^1.5.0",
|
|
26
|
+
"@bytebrand/import-helper": "^0.45.0",
|
|
27
|
+
"@bytebrand/schema-tools": "^3.18.8",
|
|
28
|
+
"@bytebrand/zipper": "^2.2.0",
|
|
29
|
+
"@nestjs/cache-manager": "^2.1.0",
|
|
30
|
+
"@nestjs/common": "^10.2.7",
|
|
31
|
+
"@nestjs/core": "^10.2.7",
|
|
32
|
+
"adm-zip": "0.5.5",
|
|
33
|
+
"axios": "^1.5.1",
|
|
34
|
+
"bottleneck": "^2.19.5",
|
|
35
|
+
"cache-manager": "^5.2.4",
|
|
36
|
+
"csv-write-stream": "^2.0.0",
|
|
37
|
+
"google-spreadsheet": "^3.3.0",
|
|
38
|
+
"lodash": "^4.17.21",
|
|
39
|
+
"moment": "^2.29.4",
|
|
40
|
+
"nest-commander": "~3.15.0",
|
|
41
|
+
"reflect-metadata": "^0.1.13",
|
|
42
|
+
"vin-generator": "^1.0.1"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@nestjs/cli": "^10.1.18",
|
|
46
|
+
"@types/google-spreadsheet": "^3.3.0",
|
|
47
|
+
"@types/inquirer": "^8.2.4",
|
|
48
|
+
"@types/lodash": "^4.14.186",
|
|
49
|
+
"@types/node": "^16.18.58",
|
|
50
|
+
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
|
51
|
+
"@typescript-eslint/parser": "^6.8.0",
|
|
52
|
+
"eslint": "^8.0.1",
|
|
53
|
+
"eslint-config-prettier": "^8.3.0",
|
|
54
|
+
"eslint-plugin-prettier": "^4.0.0",
|
|
55
|
+
"gts": "^3.1.1",
|
|
56
|
+
"prettier": "^2.3.2",
|
|
57
|
+
"ts-node": "^10.0.0",
|
|
58
|
+
"tsconfig-paths": "4.1.0",
|
|
59
|
+
"typescript": "^5.2.2"
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=14.0.0"
|
|
63
|
+
},
|
|
64
|
+
"gitHead": "e827f35865032821bf3a081f03a026d3d3f8a540"
|
|
65
|
+
}
|
|
@@ -1,17 +1,54 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
import { toNumber } from 'lodash';
|
|
2
|
+
|
|
3
|
+
import { ExportType, FileType, ImageType } from 'src/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
|
+
// Same guard defaults as the meinestadt / OpenAI feeds; a job can override them via the
|
|
16
|
+
// feedGuard* Cronicle params.
|
|
17
|
+
export const CRITEO_DEFAULTS = {
|
|
18
|
+
minSourceItems: 1000,
|
|
19
|
+
minPublishedItems: 500,
|
|
20
|
+
maxDropPercent: 20,
|
|
21
|
+
maxSourceAgeHours: 26,
|
|
22
|
+
maxStaleHours: 48,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Criteo catalog (ADV-319): legacy RSS/XML file at the same path as before, which the chained
|
|
27
|
+
* `4.1 Export criteo - part 2` step uploads. Guard state lives in resources/criteo-state.
|
|
28
|
+
*/
|
|
29
|
+
export class CriteoConfig extends Config {
|
|
30
|
+
stateDir: string;
|
|
31
|
+
feedGuard: FeedGuardOptions;
|
|
32
|
+
|
|
33
|
+
constructor(params: Params) {
|
|
34
|
+
super(params);
|
|
35
|
+
this.campaign = ExportType.CRITEO;
|
|
36
|
+
this.pathesConfig.localConvertedDirectory = `resources/${ExportType.CRITEO}-export`;
|
|
37
|
+
this.stateDir = `resources/${ExportType.CRITEO}-state`;
|
|
38
|
+
this.imageType = ImageType.marketing;
|
|
39
|
+
this.marketingImageConfigId = this.environmentConfig[params.environment].criteoMarketingImagesConfigId;
|
|
40
|
+
this.feedGuard = {
|
|
41
|
+
stateDir: this.stateDir,
|
|
42
|
+
minSourceItems: positiveNumber(params.feedGuardMinSourceItems, CRITEO_DEFAULTS.minSourceItems),
|
|
43
|
+
minPublishedItems: positiveNumber(params.feedGuardMinPublishedItems, CRITEO_DEFAULTS.minPublishedItems),
|
|
44
|
+
maxDropPercent: positiveNumber(params.feedGuardMaxDropPercent, CRITEO_DEFAULTS.maxDropPercent),
|
|
45
|
+
maxSourceAgeHours: positiveNumber(params.feedGuardMaxSourceAgeHours, CRITEO_DEFAULTS.maxSourceAgeHours),
|
|
46
|
+
maxStaleHours: positiveNumber(params.feedGuardMaxStaleHours, CRITEO_DEFAULTS.maxStaleHours),
|
|
47
|
+
acceptDrop: text(params.feedGuardAcceptDrop).toLowerCase() === 'true',
|
|
48
|
+
};
|
|
49
|
+
this.outputFiles = [
|
|
50
|
+
`${this.pathesConfig.localConvertedDirectory}/api_export_cars_${ExportType.CRITEO}.${FileType.XML}`,
|
|
51
|
+
];
|
|
52
|
+
this.isCriteo = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -1,51 +1,63 @@
|
|
|
1
|
-
import { Injectable } from '@nestjs/common';
|
|
2
|
-
import { ModuleRef } from '@nestjs/core';
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
import { CarService, ConfigService, LoaderService, RulesService } from '../services';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { ModuleRef } from '@nestjs/core';
|
|
3
|
+
|
|
4
|
+
import { FsChunkInterface } from '@bytebrand/chunkable';
|
|
5
|
+
import { LoggerService } from '@bytebrand/cronicle-helper';
|
|
6
|
+
|
|
7
|
+
import { CarService, ConfigService, LoaderService, RulesService } from '../services';
|
|
8
|
+
import { FeedGuard } from '../services/feed-guard';
|
|
9
|
+
import { CriteoFeedRun, CriteoSourceCar } from '../services/criteo-feed-run';
|
|
10
|
+
import { Exporter } from '../interfaces/exporter.interface';
|
|
11
|
+
import { CriteoConfig } from '../configs/criteo.config';
|
|
12
|
+
import { CriteoTransformer } from 'src/transformers/criteo.transformer';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* ExportType.CRITEO (ADV-319). Wires the Cronicle/Nest services into CriteoFeedRun, which holds the
|
|
16
|
+
* logic. The output directory is no longer emptied: the new XML replaces the old one only after the
|
|
17
|
+
* fail-closed guard passed.
|
|
18
|
+
*/
|
|
19
|
+
@Injectable()
|
|
20
|
+
export class CriteoExporter implements Exporter {
|
|
21
|
+
constructor(
|
|
22
|
+
private carService: CarService,
|
|
23
|
+
private configService: ConfigService,
|
|
24
|
+
private rulesService: RulesService,
|
|
25
|
+
private loaderService: LoaderService,
|
|
26
|
+
private moduleRef: ModuleRef,
|
|
27
|
+
) { }
|
|
28
|
+
|
|
29
|
+
async export(): Promise<void> {
|
|
30
|
+
const config = this.configService.config as CriteoConfig;
|
|
31
|
+
const [xmlFile] = config.outputFiles;
|
|
32
|
+
const transformer = await this.moduleRef.create(CriteoTransformer);
|
|
33
|
+
|
|
34
|
+
const run = new CriteoFeedRun(
|
|
35
|
+
{
|
|
36
|
+
forEachChunk: fn => this.loaderService.process((chunk: FsChunkInterface) => fn(chunk.data as CriteoSourceCar[])),
|
|
37
|
+
filterCar: source => this.carService.filterCar(source, this.rulesService.rules),
|
|
38
|
+
transform: source => transformer.transform(source),
|
|
39
|
+
guard: new FeedGuard(config.feedGuard),
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
runId: config.runId,
|
|
43
|
+
domain: config.domain,
|
|
44
|
+
sourceDirectory: config.pathesConfig.localDirectory,
|
|
45
|
+
sourceBaseFileName: config.pathesConfig.baseFileName,
|
|
46
|
+
xmlFile,
|
|
47
|
+
stateDir: config.stateDir,
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const report = await run.run();
|
|
53
|
+
if (report.stale) {
|
|
54
|
+
console.log(`criteo feed STALE: last good file ${report.lastGoodAgeHours?.toFixed(1)} h old`);
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// main.ts would report a rejected run as {"complete":1,"code":0}; fail the job so the chained
|
|
58
|
+
// upload step does not run (report + lock already handled by the run).
|
|
59
|
+
LoggerService.exitError(`criteo feed not published: ${error?.message ?? error}`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -152,10 +152,10 @@ export interface CriteoXmlItem {
|
|
|
152
152
|
brand: any;
|
|
153
153
|
price: string;
|
|
154
154
|
sale_price: string;
|
|
155
|
-
energy_efficiency_class
|
|
155
|
+
energy_efficiency_class?: string;
|
|
156
156
|
color: string;
|
|
157
|
-
installment
|
|
158
|
-
custom_label_0
|
|
157
|
+
installment?: { paybackPeriod: number; monthlyInstallment: number };
|
|
158
|
+
custom_label_0?: string;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
export interface GoogleXmlItem {
|