@bytebrand/export-cars 1.26.33 → 1.26.35
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/config.js +1 -1
- package/dist/configs/config.js.map +1 -1
- package/dist/configs/openai-ads.config.js +2 -2
- package/dist/configs/openai-ads.config.js.map +1 -1
- package/dist/exporters/openai-ads.exporter.d.ts +3 -0
- package/dist/exporters/openai-ads.exporter.js +13 -2
- package/dist/exporters/openai-ads.exporter.js.map +1 -1
- package/dist/interfaces/openai-ads.interface.d.ts +1 -0
- package/dist/interfaces/openai-ads.interface.js +1 -0
- package/dist/interfaces/openai-ads.interface.js.map +1 -1
- package/dist/services/openai-ads-feed-run.d.ts +2 -1
- package/dist/services/openai-ads-feed-run.js +3 -2
- package/dist/services/openai-ads-feed-run.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/openai-ads/AUDIT.md +3 -3
- package/docs/openai-ads/RUNBOOK.md +10 -1
- package/package.json +2 -2
- package/src/configs/config.ts +2 -1
- package/src/configs/openai-ads.config.ts +4 -4
- package/src/exporters/openai-ads.exporter.ts +18 -1
- package/src/interfaces/openai-ads.interface.ts +2 -0
- package/src/services/openai-ads-feed-run.ts +6 -4
- package/test/openai-ads-feed-run.test.ts +20 -1
- package/test/openai-ads-price-cap.test.ts +69 -0
package/docs/openai-ads/AUDIT.md
CHANGED
|
@@ -147,7 +147,7 @@ Null policy "EXCLUDE" = row not written + counted in the run report under the re
|
|
|
147
147
|
Delta (deferred, only with API access): `products[].id = item_id`, price `{"amount": round(currentSalesPrice*100), "currency":"EUR"}`.
|
|
148
148
|
|
|
149
149
|
Filters before mapping (all reasons counted in the run report):
|
|
150
|
-
`imagesCount > 0` (existing `filterCar`), price valid,
|
|
150
|
+
`imagesCount > 0` (existing `filterCar`), price valid, €80k price cap (same as Google/Meta, Q9 closed 14.10; `price_cap`), dealer `active && !blacklist` via `CarService.getOwner` (cached per dealer, failure → EXCLUDE `dealer_unknown`), title/brand/image present.
|
|
151
151
|
|
|
152
152
|
---
|
|
153
153
|
|
|
@@ -177,7 +177,7 @@ Filters before mapping (all reasons counted in the run report):
|
|
|
177
177
|
| Q6 | Description: reuse dealer summary templates (same as Google) — do they contain finance claims? Or plain generated German text only? | PM/Legal | description |
|
|
178
178
|
| Q7 | `ads_metadata` keys configured in Ads Manager (if any) | Marketing | optional |
|
|
179
179
|
| Q8 | `product_category` wanted? | Marketing | optional |
|
|
180
|
-
| Q9 | Apply the 80k price cap of Google/Meta? | Marketing |
|
|
180
|
+
| Q9 | Apply the 80k price cap of Google/Meta? — **closed 14.10: yes**, same cap and resolver (1.26.34) | Marketing | done |
|
|
181
181
|
| Q10 | Import method available in the account: hosted URL (needs fetch IP/auth info) or SFTP (credentials into secret store); API access (delta, upload status)? | Marketing | transport |
|
|
182
182
|
| Q11 | OAI-AdsBot / OAI-SearchBot reachability of `www.auto.de` VDP and `images.auto.de` behind Cloudflare (js_challenge rules from ADV-193). Test only, no CF change without approval. | Eng + approval | go-live |
|
|
183
183
|
| Q12 | Image: ADV-336 clean profile is used (no overlay). Approval of plain photos + `ext_lr_main` question open in ADV-336 | ADV-336 | image_url |
|
|
@@ -222,7 +222,7 @@ retention 15 days; reinstatement keeps the id), run report, `RUNBOOK.md`, 23 tes
|
|
|
222
222
|
Open questions decided in code for now (each reversible by a param or a one-line change):
|
|
223
223
|
Q3 `seller_name` = `auto.de` (param); Q4 reserved/sold flag → out_of_stock, and never advertised when not sent before;
|
|
224
224
|
Q5 unknown condition → `used` only with a past first registration; Q6 description from structured data only;
|
|
225
|
-
Q9
|
|
225
|
+
Q9 (closed 14.10) €80k cap as Google/Meta; Q7/Q8 `ads_metadata` / `product_category` not sent.
|
|
226
226
|
|
|
227
227
|
Found while running the compiled job (`node dist/main.js` with a stdin job): **export-cars reports every failed
|
|
228
228
|
run as success.** nest-commander's default `serviceErrorHandler` only prints the error, and `main.ts` then
|
|
@@ -53,7 +53,7 @@ only. By user decision it has no schedule and no failure e-mail — nothing to c
|
|
|
53
53
|
| `openAiAdsSellerName` | `auto.de` | `seller_name` (open question Q3) |
|
|
54
54
|
| `openAiAdsRetentionDays` | 15 | days an item stays as out_of_stock after it was last in stock |
|
|
55
55
|
| `openAiAdsImageConcurrency` | 16 | parallel image HEAD checks against images.auto.de |
|
|
56
|
-
| `maxCurrentSalesPrice` |
|
|
56
|
+
| `maxCurrentSalesPrice` | 80000 | €80k selling-price cap, same as Google/Meta (decision 14.10); a valid positive value overrides, empty/invalid = 80000 |
|
|
57
57
|
| `feedGuardMinSourceItems` | 1000 | DEV: set ~50 |
|
|
58
58
|
| `feedGuardMinPublishedItems` | 500 | DEV: set ~20 |
|
|
59
59
|
| `feedGuardMaxDropPercent` | 20 | vs the last good run |
|
|
@@ -94,6 +94,15 @@ running after a failure as before.
|
|
|
94
94
|
Only `openai_ads` is protected: the feed guard keeps the last good file, and the job exits with code 1
|
|
95
95
|
(`exitError` + exit 1 in `openai-ads.exporter.ts`). It has no chain, so this stops nothing else.
|
|
96
96
|
|
|
97
|
+
## €80k price cap (`price_cap`, since 1.26.34)
|
|
98
|
+
|
|
99
|
+
Decision 14.10: the OpenAI feed uses the same selling-price cap as Google/Meta. `OpenAiAdsConfig` resolves
|
|
100
|
+
`maxCurrentSalesPrice` with the shared `resolveMaxFeedSalesPrice` (default `MAX_FEED_SALES_PRICE_EUR` = 80,000), and the
|
|
101
|
+
shared gate `CarService.filterCar` applies it: price must be finite, > 0 and ≤ cap (80,000.00 included, 80,000.01 not).
|
|
102
|
+
Cars above the cap are counted as `excluded.price_cap`. A car that was advertised and then crosses the cap stays in the
|
|
103
|
+
file as out_of_stock (same id, last advertised data) via the ledger — it does not just disappear.
|
|
104
|
+
⚠️ DEV google/fb/facebookshop events have carried a 20000 value in the past; check the param of any new event.
|
|
105
|
+
|
|
97
106
|
## No finance claims (`finance_claim`, since 1.26.33)
|
|
98
107
|
|
|
99
108
|
OpenAI's ad policy lists "Auto loans and leasing" outside the US as "Restricted to Select Approved Advertisers"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebrand/export-cars",
|
|
3
|
-
"version": "1.26.
|
|
3
|
+
"version": "1.26.35",
|
|
4
4
|
"description": "",
|
|
5
5
|
"author": "bytebrand",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
12
12
|
"lint": "eslint -c .eslintrc.js --ext .ts src/",
|
|
13
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/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"
|
|
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"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@bytebrand/autode-request": "^1.19.2",
|
package/src/configs/config.ts
CHANGED
|
@@ -53,7 +53,8 @@ export class Config {
|
|
|
53
53
|
criteoMarketingImagesConfigId: 12,
|
|
54
54
|
localConvertedDirectory: 'resources/csv_dev',
|
|
55
55
|
remoteDirectory: 'import_dev',
|
|
56
|
-
|
|
56
|
+
// image-generator writes marketing images (criteo, fb) to /generated_images on DEV too — same as PROD
|
|
57
|
+
imageUrl: 'https://images.autode-dev.de/generated_images',
|
|
57
58
|
baseImageUrl: 'https://images.autode-dev.de',
|
|
58
59
|
},
|
|
59
60
|
[Environment.PRODUCTION]: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { toNumber } from 'lodash';
|
|
2
2
|
|
|
3
3
|
import { ExportType, FileType, ImageType } from '../enums';
|
|
4
|
+
import { resolveMaxFeedSalesPrice } from '../constants/feed-price.constants';
|
|
4
5
|
import { Params } from '../interfaces/params.interface';
|
|
5
6
|
import { FeedGuardOptions } from '../services/feed-guard';
|
|
6
7
|
import { Config } from './config';
|
|
@@ -50,10 +51,9 @@ export class OpenAiAdsConfig extends Config {
|
|
|
50
51
|
this.sellerName = text(params.openAiAdsSellerName) || OPENAI_ADS_DEFAULTS.sellerName;
|
|
51
52
|
this.retentionDays = positiveNumber(params.openAiAdsRetentionDays, OPENAI_ADS_DEFAULTS.retentionDays);
|
|
52
53
|
this.imageConcurrency = positiveNumber(params.openAiAdsImageConcurrency, OPENAI_ADS_DEFAULTS.imageConcurrency);
|
|
53
|
-
//
|
|
54
|
-
// maxCurrentSalesPrice param
|
|
55
|
-
|
|
56
|
-
this.maxCurrentSalesPrice = cap || undefined;
|
|
54
|
+
// Same €80k selling-price cap as Google/Meta (decision 14.10): a valid positive
|
|
55
|
+
// maxCurrentSalesPrice param wins, else MAX_FEED_SALES_PRICE_EUR. Applied by CarService.filterCar.
|
|
56
|
+
this.maxCurrentSalesPrice = resolveMaxFeedSalesPrice(params.maxCurrentSalesPrice);
|
|
57
57
|
this.feedGuard = {
|
|
58
58
|
stateDir: this.stateDir,
|
|
59
59
|
minSourceItems: positiveNumber(params.feedGuardMinSourceItems, OPENAI_ADS_DEFAULTS.minSourceItems),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { get, toNumber } from 'lodash';
|
|
2
3
|
import { FsChunkInterface } from '@bytebrand/chunkable';
|
|
3
4
|
import { LoggerService } from '@bytebrand/cronicle-helper';
|
|
4
5
|
import { TranslatorExtended } from '@bytebrand/schema-tools';
|
|
@@ -11,6 +12,20 @@ import { DealerStatus, OpenAiAdsFeedRun, SourceCar } from '../services/openai-ad
|
|
|
11
12
|
import { resolveOpenAiImageFields } from '../transformers/openai-image-fields';
|
|
12
13
|
import { OpenAiAdsConfig } from '../configs/openai-ads.config';
|
|
13
14
|
import { Exporter } from '../interfaces/exporter.interface';
|
|
15
|
+
import { OpenAiAdsExclusion } from '../interfaces/openai-ads.interface';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* CarService.filterCar decides (images, €80k cap, Cronicle rules); this only names the reason for the
|
|
19
|
+
* run report: a rejected car with a valid price above the cap counts as price_cap, else filtered.
|
|
20
|
+
*/
|
|
21
|
+
export const classifyPartnerFilter = (passed: boolean, source: SourceCar, cap: number | undefined): OpenAiAdsExclusion | null => {
|
|
22
|
+
if (passed) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const price = toNumber(get(source, 'car.price.currentSalesPrice', null));
|
|
26
|
+
const hasImages = (source?.car?.metaData?.imagesCount ?? 0) > 0;
|
|
27
|
+
return cap && hasImages && Number.isFinite(price) && price > cap ? OpenAiAdsExclusion.priceCap : OpenAiAdsExclusion.filtered;
|
|
28
|
+
};
|
|
14
29
|
|
|
15
30
|
/**
|
|
16
31
|
* ExportType.OPENAI_ADS (ADV-335). Wires the Cronicle/Nest services into OpenAiAdsFeedRun, which
|
|
@@ -35,7 +50,9 @@ export class OpenAiAdsExporter implements Exporter {
|
|
|
35
50
|
const run = new OpenAiAdsFeedRun(
|
|
36
51
|
{
|
|
37
52
|
forEachChunk: fn => this.loaderService.process((chunk: FsChunkInterface) => fn(chunk.data as SourceCar[])),
|
|
38
|
-
filterCar: source =>
|
|
53
|
+
filterCar: source => classifyPartnerFilter(
|
|
54
|
+
this.carService.filterCar(source, this.rulesService.rules), source, config.maxCurrentSalesPrice,
|
|
55
|
+
),
|
|
39
56
|
dealerStatus: ownerId => this.dealerStatus(ownerId),
|
|
40
57
|
resolveImages: ({ car, images }) =>
|
|
41
58
|
resolveOpenAiImageFields(this.imagesService.getCleanImages(car._id, images), validator),
|
|
@@ -46,6 +46,8 @@ export enum OpenAiAdsExclusion {
|
|
|
46
46
|
noId = 'no_id',
|
|
47
47
|
duplicateId = 'duplicate_id',
|
|
48
48
|
filtered = 'filtered',
|
|
49
|
+
// price.currentSalesPrice above the feed cap (€80k default, maxCurrentSalesPrice param)
|
|
50
|
+
priceCap = 'price_cap',
|
|
49
51
|
invalidPrice = 'invalid_price',
|
|
50
52
|
noTitle = 'no_title',
|
|
51
53
|
noBrand = 'no_brand',
|
|
@@ -30,8 +30,9 @@ export type DealerStatus = 'active' | 'inactive' | 'unknown';
|
|
|
30
30
|
export interface OpenAiAdsRunDeps {
|
|
31
31
|
// iterates the base export snapshot chunk by chunk (LoaderService.process)
|
|
32
32
|
forEachChunk: (fn: (cars: SourceCar[]) => Promise<void>) => Promise<unknown>;
|
|
33
|
-
// existing shared partner filter (images count,
|
|
34
|
-
|
|
33
|
+
// existing shared partner filter (images count, €80k price cap, Cronicle rules): null = passes,
|
|
34
|
+
// else the exclusion reason (price_cap or filtered)
|
|
35
|
+
filterCar: (source: SourceCar) => OpenAiAdsExclusion | null;
|
|
35
36
|
dealerStatus: (ownerId: string) => Promise<DealerStatus>;
|
|
36
37
|
// ADV-336 clean images, validated
|
|
37
38
|
resolveImages: (source: SourceCar) => Promise<OpenAiImageSelection>;
|
|
@@ -146,8 +147,9 @@ export class OpenAiAdsFeedRun {
|
|
|
146
147
|
continue;
|
|
147
148
|
}
|
|
148
149
|
seen.add(id);
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
const filtered = this.deps.filterCar(source);
|
|
151
|
+
if (filtered) {
|
|
152
|
+
increment(report.excluded, filtered);
|
|
151
153
|
continue;
|
|
152
154
|
}
|
|
153
155
|
const mapped = mapOpenAiAdsVehicle(source.car, mapping);
|
|
@@ -9,6 +9,7 @@ import { join } from 'path';
|
|
|
9
9
|
import { FeedGuard, FeedGuardError, FeedGuardOptions } from '../src/services/feed-guard';
|
|
10
10
|
import { DealerStatus, OpenAiAdsFeedRun, SourceCar } from '../src/services/openai-ads-feed-run';
|
|
11
11
|
import { selectOpenAiImageFields } from '../src/transformers/openai-image-fields';
|
|
12
|
+
import { OpenAiAdsExclusion } from '../src/interfaces/openai-ads.interface';
|
|
12
13
|
import { buildImagesService } from './fixtures/images-service.harness';
|
|
13
14
|
import { NOW, car, csvObjects, images, translate } from './fixtures/openai-ads.fixture';
|
|
14
15
|
|
|
@@ -69,7 +70,10 @@ const run = async (world: World, at: Date = NOW, extra: Partial<FeedGuardOptions
|
|
|
69
70
|
const feed = new OpenAiAdsFeedRun(
|
|
70
71
|
{
|
|
71
72
|
forEachChunk,
|
|
72
|
-
filterCar: ({ car: c }) =>
|
|
73
|
+
filterCar: ({ car: c }) => {
|
|
74
|
+
if (!((c?.metaData?.imagesCount ?? 0) > 0)) return OpenAiAdsExclusion.filtered;
|
|
75
|
+
return Number(c?.price?.currentSalesPrice) > 80_000 ? OpenAiAdsExclusion.priceCap : null;
|
|
76
|
+
},
|
|
73
77
|
dealerStatus: async ownerId => world.dealers[ownerId] ?? 'active',
|
|
74
78
|
resolveImages: async ({ car: c, images: list }) => {
|
|
75
79
|
const clean = imagesService.getCleanImages(c._id, list);
|
|
@@ -160,6 +164,21 @@ const cases: Array<[string, () => Promise<void>]> = [
|
|
|
160
164
|
assert.equal(back['car-1'].availability, 'in_stock');
|
|
161
165
|
assert.equal(Object.keys(back).length, 5);
|
|
162
166
|
}],
|
|
167
|
+
['price crosses the €80k cap: advertised car switches to out_of_stock (same id), 80,000.00 stays', async () => {
|
|
168
|
+
const w = await newWorld();
|
|
169
|
+
await writeSource(w, [src('car-0', { 'price.currentSalesPrice': 79_990 }), src('car-1', { 'price.currentSalesPrice': 79_000 }), ...fleet(5).slice(2)]);
|
|
170
|
+
await run(w);
|
|
171
|
+
const next = new Date(NOW.getTime() + DAY);
|
|
172
|
+
await writeSource(w, [src('car-0', { 'price.currentSalesPrice': 80_000.01 }), src('car-1', { 'price.currentSalesPrice': 80_000 }), ...fleet(5).slice(2)], 2, next);
|
|
173
|
+
const report = await run(w, next);
|
|
174
|
+
const rows = byId(await published(w));
|
|
175
|
+
assert.equal(rows['car-0'].availability, 'out_of_stock');
|
|
176
|
+
assert.equal(rows['car-0'].price, '79990.00 EUR', 'last advertised data, not the new price');
|
|
177
|
+
assert.equal(rows['car-1'].availability, 'in_stock');
|
|
178
|
+
assert.equal(rows['car-1'].price, '80000.00 EUR');
|
|
179
|
+
assert.equal(report.excluded.price_cap, 1);
|
|
180
|
+
assert.deepEqual(report.outOfStock, { not_in_snapshot: 1 });
|
|
181
|
+
}],
|
|
163
182
|
['out_of_stock rows stop after the retention window', async () => {
|
|
164
183
|
const w = await newWorld();
|
|
165
184
|
await writeSource(w, fleet(5));
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Run: npm test (ts-node, no test framework needed)
|
|
2
|
+
// Decision 14.10: the OpenAI feed uses the same €80k selling-price cap as Google/Meta.
|
|
3
|
+
import * as assert from 'node:assert/strict';
|
|
4
|
+
|
|
5
|
+
import { OpenAiAdsConfig } from '../src/configs/openai-ads.config';
|
|
6
|
+
import { MAX_FEED_SALES_PRICE_EUR, resolveMaxFeedSalesPrice } from '../src/constants/feed-price.constants';
|
|
7
|
+
import { CarService } from '../src/services/car.service';
|
|
8
|
+
import { classifyPartnerFilter } from '../src/exporters/openai-ads.exporter';
|
|
9
|
+
import { OpenAiAdsExclusion as X } from '../src/interfaces/openai-ads.interface';
|
|
10
|
+
import { car } from './fixtures/openai-ads.fixture';
|
|
11
|
+
|
|
12
|
+
const openAiConfig = (maxCurrentSalesPrice?: string) =>
|
|
13
|
+
new OpenAiAdsConfig({ environment: 'develop', exportType: 'openai_ads', maxCurrentSalesPrice } as any);
|
|
14
|
+
|
|
15
|
+
// The real shared gate, with the OpenAI config (no Cronicle rules).
|
|
16
|
+
const filterCar = (config: OpenAiAdsConfig, price: unknown, imagesCount = 3) => {
|
|
17
|
+
const service = new CarService(null as any, null as any, { config } as any);
|
|
18
|
+
return service.filterCar({ car: car('id', { 'price.currentSalesPrice': price, 'metaData.imagesCount': imagesCount }) }, []);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const cases: Array<[string, () => void]> = [
|
|
22
|
+
['config: default cap is the shared 80,000 (same resolver as Google/Meta)', () => {
|
|
23
|
+
assert.equal(MAX_FEED_SALES_PRICE_EUR, 80_000);
|
|
24
|
+
for (const raw of [undefined, '', ' ', 'abc', '0', '-5']) {
|
|
25
|
+
assert.equal(openAiConfig(raw).maxCurrentSalesPrice, 80_000, String(raw));
|
|
26
|
+
// google/fb/facebook-shop configs call the same resolver
|
|
27
|
+
assert.equal(openAiConfig(raw).maxCurrentSalesPrice, resolveMaxFeedSalesPrice(raw));
|
|
28
|
+
}
|
|
29
|
+
}],
|
|
30
|
+
['config: a valid positive param wins (50000)', () => {
|
|
31
|
+
assert.equal(openAiConfig('50000').maxCurrentSalesPrice, 50_000);
|
|
32
|
+
}],
|
|
33
|
+
['gate (CarService.filterCar): 80,000.00 included, 80,000.01 excluded', () => {
|
|
34
|
+
const config = openAiConfig();
|
|
35
|
+
assert.equal(filterCar(config, 80_000), true);
|
|
36
|
+
assert.equal(filterCar(config, '80000.00'), true);
|
|
37
|
+
assert.equal(filterCar(config, 79_999.99), true);
|
|
38
|
+
assert.equal(filterCar(config, 80_000.01), false);
|
|
39
|
+
assert.equal(filterCar(config, 222_148), false);
|
|
40
|
+
for (const bad of [0, -1, null, 'abc']) {
|
|
41
|
+
assert.equal(filterCar(config, bad), false, String(bad));
|
|
42
|
+
}
|
|
43
|
+
}],
|
|
44
|
+
['gate with param 50000: 50,000 in, 50,000.01 out', () => {
|
|
45
|
+
const config = openAiConfig('50000');
|
|
46
|
+
assert.equal(filterCar(config, 50_000), true);
|
|
47
|
+
assert.equal(filterCar(config, 50_000.01), false);
|
|
48
|
+
}],
|
|
49
|
+
['report reason: above the cap = price_cap; no photos or invalid price = filtered; passed = null', () => {
|
|
50
|
+
const source = (price: unknown, imagesCount = 3) => ({ car: car('id', { 'price.currentSalesPrice': price, 'metaData.imagesCount': imagesCount }), images: [] });
|
|
51
|
+
assert.equal(classifyPartnerFilter(false, source(80_000.01), 80_000), X.priceCap);
|
|
52
|
+
assert.equal(classifyPartnerFilter(false, source(95_000, 0), 80_000), X.filtered);
|
|
53
|
+
assert.equal(classifyPartnerFilter(false, source(0), 80_000), X.filtered);
|
|
54
|
+
assert.equal(classifyPartnerFilter(false, source(17_990), 80_000), X.filtered);
|
|
55
|
+
assert.equal(classifyPartnerFilter(true, source(17_990), 80_000), null);
|
|
56
|
+
}],
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
let failed = 0;
|
|
60
|
+
for (const [name, fn] of cases) {
|
|
61
|
+
try {
|
|
62
|
+
fn();
|
|
63
|
+
console.log(`ok ${name}`);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
failed += 1;
|
|
66
|
+
console.error(`FAIL ${name}\n ${e.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (failed) process.exit(1);
|