@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/src/export.command.ts
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import { createInterface } from 'readline';
|
|
2
|
-
import { Command, CommandRunner } from 'nest-commander';
|
|
3
|
-
import { ConfigService } from './services';
|
|
4
|
-
import { Input } from './interfaces/input.interface';
|
|
5
|
-
import { exportSchema } from './export.schema';
|
|
6
|
-
import { ModuleRef } from '@nestjs/core';
|
|
7
|
-
import { Exporter } from './interfaces/exporter.interface';
|
|
8
|
-
import { LoggerService } from '@bytebrand/cronicle-helper';
|
|
9
|
-
|
|
10
|
-
@Command({
|
|
11
|
-
name: 'export',
|
|
12
|
-
options: { isDefault: true },
|
|
13
|
-
})
|
|
14
|
-
export class ExportRunner extends CommandRunner {
|
|
15
|
-
constructor(
|
|
16
|
-
private configService: ConfigService,
|
|
17
|
-
private moduleRef: ModuleRef,
|
|
18
|
-
) {
|
|
19
|
-
super();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async run(): Promise<void> {
|
|
23
|
-
const rl = createInterface({ input: process.stdin });
|
|
24
|
-
await new Promise<void>((resolve, reject) => {
|
|
25
|
-
rl.on('line', async (data: string) => {
|
|
26
|
-
const input = JSON.parse(data) as Input;
|
|
27
|
-
LoggerService.log(`Running job: ${input.id} - ${JSON.stringify(input)} \n`);
|
|
28
|
-
|
|
29
|
-
this.configService.config = new exportSchema[input.params.exportType].config(input.params);
|
|
30
|
-
this.configService.config.runId = input.id;
|
|
31
|
-
const exporter = await this.moduleRef.create<Exporter>(exportSchema[input.params.exportType].exporter);
|
|
32
|
-
await exporter.export().catch(reject);
|
|
33
|
-
|
|
34
|
-
resolve(rl.close());
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
}
|
|
1
|
+
import { createInterface } from 'readline';
|
|
2
|
+
import { Command, CommandRunner } from 'nest-commander';
|
|
3
|
+
import { ConfigService } from './services';
|
|
4
|
+
import { Input } from './interfaces/input.interface';
|
|
5
|
+
import { exportSchema } from './export.schema';
|
|
6
|
+
import { ModuleRef } from '@nestjs/core';
|
|
7
|
+
import { Exporter } from './interfaces/exporter.interface';
|
|
8
|
+
import { LoggerService } from '@bytebrand/cronicle-helper';
|
|
9
|
+
|
|
10
|
+
@Command({
|
|
11
|
+
name: 'export',
|
|
12
|
+
options: { isDefault: true },
|
|
13
|
+
})
|
|
14
|
+
export class ExportRunner extends CommandRunner {
|
|
15
|
+
constructor(
|
|
16
|
+
private configService: ConfigService,
|
|
17
|
+
private moduleRef: ModuleRef,
|
|
18
|
+
) {
|
|
19
|
+
super();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async run(): Promise<void> {
|
|
23
|
+
const rl = createInterface({ input: process.stdin });
|
|
24
|
+
await new Promise<void>((resolve, reject) => {
|
|
25
|
+
rl.on('line', async (data: string) => {
|
|
26
|
+
const input = JSON.parse(data) as Input;
|
|
27
|
+
LoggerService.log(`Running job: ${input.id} - ${JSON.stringify(input)} \n`);
|
|
28
|
+
|
|
29
|
+
this.configService.config = new exportSchema[input.params.exportType].config(input.params);
|
|
30
|
+
this.configService.config.runId = input.id;
|
|
31
|
+
const exporter = await this.moduleRef.create<Exporter>(exportSchema[input.params.exportType].exporter);
|
|
32
|
+
await exporter.export().catch(reject);
|
|
33
|
+
|
|
34
|
+
resolve(rl.close());
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/export.schema.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
AwinConfig,
|
|
24
24
|
EviumConfig,
|
|
25
25
|
SantanderConfig,
|
|
26
|
+
OpenAiAdsConfig,
|
|
26
27
|
} from './configs';
|
|
27
28
|
import {
|
|
28
29
|
As24Exporter,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
AwinExporter,
|
|
42
43
|
SantanderExporter,
|
|
43
44
|
CriteoExporter,
|
|
45
|
+
OpenAiAdsExporter,
|
|
44
46
|
} from './exporters';
|
|
45
47
|
|
|
46
48
|
export const exportSchema = {
|
|
@@ -132,4 +134,8 @@ export const exportSchema = {
|
|
|
132
134
|
config: SantanderConfig,
|
|
133
135
|
exporter: SantanderExporter,
|
|
134
136
|
},
|
|
137
|
+
[ExportType.OPENAI_ADS]: {
|
|
138
|
+
config: OpenAiAdsConfig,
|
|
139
|
+
exporter: OpenAiAdsExporter,
|
|
140
|
+
},
|
|
135
141
|
};
|
|
@@ -1,56 +1,67 @@
|
|
|
1
|
-
import { Injectable } from '@nestjs/common';
|
|
2
|
-
import { ModuleRef } from '@nestjs/core';
|
|
3
|
-
|
|
4
|
-
import { CronicleUtils } from '@bytebrand/cronicle-helper';
|
|
5
|
-
import { FsChunkInterface } from '@bytebrand/chunkable';
|
|
6
|
-
|
|
7
|
-
import { CarService, ConfigService, LoaderService, RulesService } from '../services';
|
|
8
|
-
import { GoogleTransformer } from '../transformers/google.transformer';
|
|
9
|
-
import { buildGoogleFeedItemRow } from '../transformers/google-feed-item-row';
|
|
10
|
-
import { FeedItemsBigQueryWriter } from '../services/feed-items-bigquery.writer';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
private
|
|
19
|
-
private
|
|
20
|
-
private
|
|
21
|
-
private
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
await
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { ModuleRef } from '@nestjs/core';
|
|
3
|
+
|
|
4
|
+
import { CronicleUtils } from '@bytebrand/cronicle-helper';
|
|
5
|
+
import { FsChunkInterface } from '@bytebrand/chunkable';
|
|
6
|
+
|
|
7
|
+
import { CarService, ConfigService, LoaderService, RulesService } from '../services';
|
|
8
|
+
import { GoogleTransformer } from '../transformers/google.transformer';
|
|
9
|
+
import { buildGoogleFeedItemRow } from '../transformers/google-feed-item-row';
|
|
10
|
+
import { FeedItemsBigQueryWriter } from '../services/feed-items-bigquery.writer';
|
|
11
|
+
import { loadPublicationGate } from '../services/publication-gate';
|
|
12
|
+
import { GoogleXmlBuilder } from '../formats/google-xml.builder';
|
|
13
|
+
import { Exporter } from '../interfaces/exporter.interface';
|
|
14
|
+
|
|
15
|
+
@Injectable()
|
|
16
|
+
export class GoogleExporter implements Exporter {
|
|
17
|
+
constructor(
|
|
18
|
+
private carService: CarService,
|
|
19
|
+
private configService: ConfigService,
|
|
20
|
+
private rulesService: RulesService,
|
|
21
|
+
private loaderService: LoaderService,
|
|
22
|
+
private moduleRef: ModuleRef,
|
|
23
|
+
) { }
|
|
24
|
+
|
|
25
|
+
async export(): Promise<void> {
|
|
26
|
+
// EnVKV C8: estimated EnVKV values (CBD enrichment log); off unless the job sets publicationGate.
|
|
27
|
+
// Read first: in enforce mode a failure stops the run before the output directory is emptied.
|
|
28
|
+
const gate = await loadPublicationGate(this.configService.config.publicationGate);
|
|
29
|
+
const [fileName] = this.configService.config.outputFiles;
|
|
30
|
+
await CronicleUtils.ensureDir(this.configService.config.pathesConfig.localConvertedDirectory);
|
|
31
|
+
await CronicleUtils.emptyDir(this.configService.config.pathesConfig.localConvertedDirectory);
|
|
32
|
+
|
|
33
|
+
const googleXmlBuilder = new GoogleXmlBuilder(fileName);
|
|
34
|
+
await googleXmlBuilder.buildStart(this.configService.config.domain);
|
|
35
|
+
|
|
36
|
+
const googleTransformer = await this.moduleRef.create(GoogleTransformer);
|
|
37
|
+
|
|
38
|
+
// Opt-in audit of the sent items (feedItemsBq* job params); null = no BigQuery calls at all.
|
|
39
|
+
const { feedItemsBigQuery, runId } = this.configService.config;
|
|
40
|
+
const feedItemsWriter = feedItemsBigQuery ? new FeedItemsBigQueryWriter(feedItemsBigQuery) : null;
|
|
41
|
+
const exportedAt = new Date();
|
|
42
|
+
let gatedItems = 0;
|
|
43
|
+
|
|
44
|
+
await this.loaderService.process(async (chunk: FsChunkInterface) => {
|
|
45
|
+
chunk.data = chunk.data.filter(car => this.carService.filterCar(car, this.rulesService.rules));
|
|
46
|
+
|
|
47
|
+
for (const data of chunk.data) {
|
|
48
|
+
const car = await googleTransformer.transform(data);
|
|
49
|
+
if (car) {
|
|
50
|
+
const estimatedAttributes = gate.estimatedAttributes(data.car);
|
|
51
|
+
if (estimatedAttributes.length) gatedItems += 1;
|
|
52
|
+
await googleXmlBuilder.addItem(gate.apply(car, estimatedAttributes));
|
|
53
|
+
const gateInfo = gate.mode === 'off' ? null : { mode: gate.mode, estimatedAttributes };
|
|
54
|
+
feedItemsWriter?.add(buildGoogleFeedItemRow(data.car, car, runId, exportedAt, gateInfo));
|
|
55
|
+
}
|
|
56
|
+
this.carService.counter.countCar(data.images);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await googleXmlBuilder.buildEnd();
|
|
61
|
+
if (gate.mode !== 'off') {
|
|
62
|
+
console.log(`Publication gate ${gate.mode}: ${gatedItems} items with estimated EnVKV values${gate.mode === 'enforce' ? ' (attributes left out)' : ''}`);
|
|
63
|
+
}
|
|
64
|
+
await feedItemsWriter?.close();
|
|
65
|
+
console.log(this.carService.counter.getCountedCars());
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/exporters/index.ts
CHANGED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { FsChunkInterface } from '@bytebrand/chunkable';
|
|
3
|
+
import { LoggerService } from '@bytebrand/cronicle-helper';
|
|
4
|
+
import { TranslatorExtended } from '@bytebrand/schema-tools';
|
|
5
|
+
import { de, deVehicleDetailsPage, deVehicleProps } from '@bytebrand/i18n-dictionaries';
|
|
6
|
+
|
|
7
|
+
import { CarService, ConfigService, ImagesService, LoaderService, RulesService } from '../services';
|
|
8
|
+
import { ImageUrlValidator } from '../services/image-url-validator.service';
|
|
9
|
+
import { FeedGuard } from '../services/feed-guard';
|
|
10
|
+
import { DealerStatus, OpenAiAdsFeedRun, SourceCar } from '../services/openai-ads-feed-run';
|
|
11
|
+
import { resolveOpenAiImageFields } from '../transformers/openai-image-fields';
|
|
12
|
+
import { OpenAiAdsConfig } from '../configs/openai-ads.config';
|
|
13
|
+
import { Exporter } from '../interfaces/exporter.interface';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ExportType.OPENAI_ADS (ADV-335). Wires the Cronicle/Nest services into OpenAiAdsFeedRun, which
|
|
17
|
+
* holds the logic. Unlike the other exporters it never empties its output directory: the new file
|
|
18
|
+
* replaces the old one only after the fail-closed guard passed.
|
|
19
|
+
*/
|
|
20
|
+
@Injectable()
|
|
21
|
+
export class OpenAiAdsExporter implements Exporter {
|
|
22
|
+
constructor(
|
|
23
|
+
private carService: CarService,
|
|
24
|
+
private configService: ConfigService,
|
|
25
|
+
private rulesService: RulesService,
|
|
26
|
+
private loaderService: LoaderService,
|
|
27
|
+
private imagesService: ImagesService,
|
|
28
|
+
) { }
|
|
29
|
+
|
|
30
|
+
async export(): Promise<void> {
|
|
31
|
+
const config = this.configService.config as OpenAiAdsConfig;
|
|
32
|
+
const translator = new TranslatorExtended({ ...de, ...deVehicleProps, ...deVehicleDetailsPage });
|
|
33
|
+
const validator = new ImageUrlValidator({ concurrency: config.imageConcurrency });
|
|
34
|
+
|
|
35
|
+
const run = new OpenAiAdsFeedRun(
|
|
36
|
+
{
|
|
37
|
+
forEachChunk: fn => this.loaderService.process((chunk: FsChunkInterface) => fn(chunk.data as SourceCar[])),
|
|
38
|
+
filterCar: source => this.carService.filterCar(source, this.rulesService.rules),
|
|
39
|
+
dealerStatus: ownerId => this.dealerStatus(ownerId),
|
|
40
|
+
resolveImages: ({ car, images }) =>
|
|
41
|
+
resolveOpenAiImageFields(this.imagesService.getCleanImages(car._id, images), validator),
|
|
42
|
+
imageStats: () => validator.stats,
|
|
43
|
+
guard: new FeedGuard(config.feedGuard),
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
runId: config.runId,
|
|
47
|
+
dryRun: config.dryRun,
|
|
48
|
+
sourceDirectory: config.pathesConfig.localDirectory,
|
|
49
|
+
sourceBaseFileName: config.pathesConfig.baseFileName,
|
|
50
|
+
outputFile: config.outputFiles[0],
|
|
51
|
+
stateDir: config.stateDir,
|
|
52
|
+
retentionDays: config.retentionDays,
|
|
53
|
+
mapping: {
|
|
54
|
+
domain: config.domain,
|
|
55
|
+
sellerName: config.sellerName,
|
|
56
|
+
translate: key => translator.translateVal(key),
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const report = await run.run();
|
|
63
|
+
if (report.stale) {
|
|
64
|
+
console.log(`OpenAI ads feed STALE: last good file ${report.lastGoodAgeHours?.toFixed(1)} h old`);
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
// nest-commander's default serviceErrorHandler only prints a rejected run, and main.ts then
|
|
68
|
+
// reports {"complete":1,"code":0} — a blocked feed would look green in Cronicle. Fail the job
|
|
69
|
+
// here (report + lock already handled by the run).
|
|
70
|
+
LoggerService.exitError(`OpenAI ads feed not published: ${error?.message ?? error}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// One UPA lookup per dealer and run. CarService.getOwner retries 4× and does not remember
|
|
76
|
+
// failures, so without this map every car of an unknown dealer would wait for the retries again.
|
|
77
|
+
private readonly dealerStatuses = new Map<string, Promise<DealerStatus>>();
|
|
78
|
+
|
|
79
|
+
// CBD already takes blacklisted dealers' cars offline, but a deactivated dealer's cars stay
|
|
80
|
+
// online — not advertised here. A missing `active` field counts as active (only explicit false).
|
|
81
|
+
private dealerStatus(ownerId: string): Promise<DealerStatus> {
|
|
82
|
+
if (!ownerId) {
|
|
83
|
+
return Promise.resolve('unknown');
|
|
84
|
+
}
|
|
85
|
+
if (!this.dealerStatuses.has(ownerId)) {
|
|
86
|
+
this.dealerStatuses.set(ownerId, this.carService.getOwner(ownerId).then(
|
|
87
|
+
owner => (!owner ? 'unknown' : owner.active === false || owner.blacklist === true ? 'inactive' : 'active'),
|
|
88
|
+
() => 'unknown' as DealerStatus,
|
|
89
|
+
));
|
|
90
|
+
}
|
|
91
|
+
return this.dealerStatuses.get(ownerId);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createWriteStream, WriteStream } from 'fs';
|
|
2
|
+
import { once } from 'events';
|
|
3
|
+
|
|
4
|
+
import { OPENAI_ADS_COLUMNS, OpenAiAdsRow } from '../interfaces/openai-ads.interface';
|
|
5
|
+
|
|
6
|
+
// RFC 4180 cell: quoted only when it contains a comma, a quote, CR or LF (the OpenAI product file
|
|
7
|
+
// rule); embedded quotes are doubled. Values are written as they are — callers keep free text on
|
|
8
|
+
// one line, but a stray newline still stays inside its quoted cell instead of breaking the row.
|
|
9
|
+
export const toCsvCell = (value: string | boolean | undefined | null): string => {
|
|
10
|
+
if (value === undefined || value === null) {
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
13
|
+
const text = typeof value === 'boolean' ? (value ? 'true' : 'false') : value;
|
|
14
|
+
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const toCsvLine = (cells: Array<string | boolean | undefined | null>): string => `${cells.map(toCsvCell).join(',')}\n`;
|
|
18
|
+
|
|
19
|
+
export const openAiAdsCsvHeader = (): string => toCsvLine([...OPENAI_ADS_COLUMNS]);
|
|
20
|
+
|
|
21
|
+
export const openAiAdsCsvRow = (row: OpenAiAdsRow): string => toCsvLine(OPENAI_ADS_COLUMNS.map(column => row[column]));
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* OpenAI product file (OpenAI format, not the Google-compatible profile): UTF-8 without BOM, one
|
|
25
|
+
* header row, one vehicle per row, fixed column order (OPENAI_ADS_COLUMNS). Not built on
|
|
26
|
+
* XMLBuilderBase and not shared with the Awin/Santander writers, which do not escape quotes.
|
|
27
|
+
*/
|
|
28
|
+
export class OpenAiAdsCsvBuilder {
|
|
29
|
+
private stream: WriteStream;
|
|
30
|
+
private rows = 0;
|
|
31
|
+
|
|
32
|
+
constructor(private readonly fileName: string) { }
|
|
33
|
+
|
|
34
|
+
async buildStart(): Promise<void> {
|
|
35
|
+
this.stream = createWriteStream(this.fileName, { encoding: 'utf8' });
|
|
36
|
+
await this.write(openAiAdsCsvHeader());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async addItem(row: OpenAiAdsRow): Promise<void> {
|
|
40
|
+
await this.write(openAiAdsCsvRow(row));
|
|
41
|
+
this.rows += 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async buildEnd(): Promise<number> {
|
|
45
|
+
this.stream.end();
|
|
46
|
+
await once(this.stream, 'finish');
|
|
47
|
+
return this.rows;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private async write(chunk: string): Promise<void> {
|
|
51
|
+
if (!this.stream.write(chunk)) {
|
|
52
|
+
await once(this.stream, 'drain');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// OpenAI product file, OpenAI format (developers.openai.com/commerce/specs/file-upload/products,
|
|
2
|
+
// read 2026-10-09). Only the columns below are sent: no gtin/mpn/VIN (never invented), no
|
|
3
|
+
// target_countries/store_country (market comes from the account setup), no Google-compatible names.
|
|
4
|
+
export const OPENAI_ADS_COLUMNS = [
|
|
5
|
+
'item_id',
|
|
6
|
+
'title',
|
|
7
|
+
'description',
|
|
8
|
+
'url',
|
|
9
|
+
'brand',
|
|
10
|
+
'seller_name',
|
|
11
|
+
'image_url',
|
|
12
|
+
'additional_image_urls',
|
|
13
|
+
'availability',
|
|
14
|
+
'price',
|
|
15
|
+
'condition',
|
|
16
|
+
'is_ads_eligible',
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
export type OpenAiAdsColumn = typeof OPENAI_ADS_COLUMNS[number];
|
|
20
|
+
|
|
21
|
+
export type OpenAiAdsAvailability = 'in_stock' | 'out_of_stock';
|
|
22
|
+
|
|
23
|
+
export type OpenAiAdsCondition = 'new' | 'used';
|
|
24
|
+
|
|
25
|
+
export interface OpenAiAdsRow {
|
|
26
|
+
// canonical auto.de vehicle id (car._id, the VDP id) — same value as GA4 item_id and the
|
|
27
|
+
// Pixel/CAPI contents[].id of ADV-337
|
|
28
|
+
item_id: string;
|
|
29
|
+
title: string;
|
|
30
|
+
description: string;
|
|
31
|
+
url: string;
|
|
32
|
+
brand: string;
|
|
33
|
+
seller_name: string;
|
|
34
|
+
image_url: string;
|
|
35
|
+
// one CSV cell, comma-separated, commas inside URLs as %2C; '' = none
|
|
36
|
+
additional_image_urls: string;
|
|
37
|
+
availability: OpenAiAdsAvailability;
|
|
38
|
+
// "17990.00 EUR": purchase price in major units — never a monthly rate
|
|
39
|
+
price: string;
|
|
40
|
+
condition: OpenAiAdsCondition;
|
|
41
|
+
is_ads_eligible: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Why a car of the source snapshot is not in the file as in_stock. Counted in the run report.
|
|
45
|
+
export enum OpenAiAdsExclusion {
|
|
46
|
+
noId = 'no_id',
|
|
47
|
+
duplicateId = 'duplicate_id',
|
|
48
|
+
filtered = 'filtered',
|
|
49
|
+
invalidPrice = 'invalid_price',
|
|
50
|
+
noTitle = 'no_title',
|
|
51
|
+
noBrand = 'no_brand',
|
|
52
|
+
conditionUnknown = 'condition_unknown',
|
|
53
|
+
dealerInactive = 'dealer_inactive',
|
|
54
|
+
dealerUnknown = 'dealer_unknown',
|
|
55
|
+
noImage = 'no_image',
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Why an item is sent as out_of_stock.
|
|
59
|
+
export enum OpenAiAdsOutOfStockReason {
|
|
60
|
+
reserved = 'reserved',
|
|
61
|
+
soldFlag = 'sold_flag',
|
|
62
|
+
// sent before, not in_stock in this run (sold/offline/deleted/now excluded)
|
|
63
|
+
notInSnapshot = 'not_in_snapshot',
|
|
64
|
+
}
|
|
@@ -1,29 +1,56 @@
|
|
|
1
|
-
import { Environment, ExportType } from '../enums';
|
|
2
|
-
|
|
3
|
-
export interface Params {
|
|
4
|
-
environment: Environment;
|
|
5
|
-
exportType?: ExportType;
|
|
6
|
-
apikey?: string;
|
|
7
|
-
String?: string;
|
|
8
|
-
Number?: string;
|
|
9
|
-
Date?: string;
|
|
10
|
-
isNewCar?: boolean;
|
|
11
|
-
cpc?: string;
|
|
12
|
-
ownerId?: string;
|
|
13
|
-
metaPushPriorityCsvUrl?: string;
|
|
14
|
-
// Cronicle plugin param (text field, so it arrives as a string); overrides the
|
|
15
|
-
// default MAX_FEED_SALES_PRICE_EUR cap in the configs that apply one.
|
|
16
|
-
maxCurrentSalesPrice?: string;
|
|
17
|
-
// Google feed only, opt-in: when all three are set, every exported item is also written to
|
|
18
|
-
// `<project>.<dataset>.<table>` in BigQuery (schema/feed-items.json, see README).
|
|
19
|
-
feedItemsBqProject?: string;
|
|
20
|
-
feedItemsBqDataset?: string;
|
|
21
|
-
feedItemsBqTable?: string;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
import { Environment, ExportType } from '../enums';
|
|
2
|
+
|
|
3
|
+
export interface Params {
|
|
4
|
+
environment: Environment;
|
|
5
|
+
exportType?: ExportType;
|
|
6
|
+
apikey?: string;
|
|
7
|
+
String?: string;
|
|
8
|
+
Number?: string;
|
|
9
|
+
Date?: string;
|
|
10
|
+
isNewCar?: boolean;
|
|
11
|
+
cpc?: string;
|
|
12
|
+
ownerId?: string;
|
|
13
|
+
metaPushPriorityCsvUrl?: string;
|
|
14
|
+
// Cronicle plugin param (text field, so it arrives as a string); overrides the
|
|
15
|
+
// default MAX_FEED_SALES_PRICE_EUR cap in the configs that apply one.
|
|
16
|
+
maxCurrentSalesPrice?: string;
|
|
17
|
+
// Google feed only, opt-in: when all three are set, every exported item is also written to
|
|
18
|
+
// `<project>.<dataset>.<table>` in BigQuery (schema/feed-items.json, see README).
|
|
19
|
+
feedItemsBqProject?: string;
|
|
20
|
+
feedItemsBqDataset?: string;
|
|
21
|
+
feedItemsBqTable?: string;
|
|
22
|
+
// Google feed only, EnVKV C8: off (default) | audit | enforce, and the car-basic-data enrichment log
|
|
23
|
+
// table the estimates are read from (see services/publication-gate.ts).
|
|
24
|
+
publicationGate?: string;
|
|
25
|
+
publicationGateBqProject?: string;
|
|
26
|
+
publicationGateBqDataset?: string;
|
|
27
|
+
publicationGateBqTable?: string;
|
|
28
|
+
// OpenAI Ads feed only (ADV-335). Cronicle text params, so all arrive as strings; see
|
|
29
|
+
// configs/openai-ads.config.ts for defaults. Live output needs openAiAdsDryRun=false explicitly.
|
|
30
|
+
openAiAdsDryRun?: string;
|
|
31
|
+
openAiAdsSellerName?: string;
|
|
32
|
+
openAiAdsRetentionDays?: string;
|
|
33
|
+
openAiAdsImageConcurrency?: string;
|
|
34
|
+
feedGuardMinSourceItems?: string;
|
|
35
|
+
feedGuardMinPublishedItems?: string;
|
|
36
|
+
feedGuardMaxDropPercent?: string;
|
|
37
|
+
feedGuardMaxSourceAgeHours?: string;
|
|
38
|
+
feedGuardMaxStaleHours?: string;
|
|
39
|
+
// "true" for ONE run after a real stock drop the guard blocked
|
|
40
|
+
feedGuardAcceptDrop?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Parsed from feedItemsBq* by GoogleConfig.
|
|
44
|
+
export interface FeedItemsBigQueryTarget {
|
|
45
|
+
project: string;
|
|
46
|
+
dataset: string;
|
|
47
|
+
table: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Parsed from publicationGate* by GoogleConfig.
|
|
51
|
+
export interface PublicationGateConfig {
|
|
52
|
+
mode: 'off' | 'audit' | 'enforce';
|
|
53
|
+
project: string;
|
|
54
|
+
dataset: string;
|
|
55
|
+
table: string;
|
|
56
|
+
}
|