@bytebrand/export-cars 1.26.47 → 1.26.48
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/ooyyo.config.d.ts +14 -0
- package/dist/configs/ooyyo.config.js +40 -6
- package/dist/configs/ooyyo.config.js.map +1 -1
- package/dist/exporters/ooyyo.exporter.d.ts +5 -4
- package/dist/exporters/ooyyo.exporter.js +45 -25
- package/dist/exporters/ooyyo.exporter.js.map +1 -1
- package/dist/formats/ooyyo-xml.builder.d.ts +7 -2
- package/dist/formats/ooyyo-xml.builder.js +59 -36
- package/dist/formats/ooyyo-xml.builder.js.map +1 -1
- package/dist/interfaces/export-item.interface.d.ts +30 -2
- package/dist/interfaces/params.interface.d.ts +3 -0
- package/dist/services/ooyyo-feed-run.d.ts +81 -0
- package/dist/services/ooyyo-feed-run.js +183 -0
- package/dist/services/ooyyo-feed-run.js.map +1 -0
- package/dist/services/ooyyo-feed-verify.d.ts +13 -0
- package/dist/services/ooyyo-feed-verify.js +104 -0
- package/dist/services/ooyyo-feed-verify.js.map +1 -0
- package/dist/transformers/ooyyo.transformer.d.ts +51 -11
- package/dist/transformers/ooyyo.transformer.js +268 -95
- package/dist/transformers/ooyyo.transformer.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/marktde/AUDIT.md +84 -84
- package/docs/marktde/PARTNER-QUESTIONS.md +49 -49
- package/docs/marktde/RUNBOOK.md +58 -58
- package/docs/marktde/TRACKING.md +75 -69
- package/docs/meinestadt/PARTNER-QUESTIONS.md +17 -0
- package/docs/meinestadt/TRACKING.md +107 -0
- package/docs/ooyyo/AUDIT.md +81 -0
- package/docs/ooyyo/PARTNER-QUESTIONS.md +32 -0
- package/docs/ooyyo/RUNBOOK.md +67 -0
- package/docs/ooyyo/TRACKING.md +54 -0
- package/package.json +66 -65
- package/src/configs/marktde.config.ts +70 -70
- package/src/configs/ooyyo.config.ts +65 -15
- package/src/exporters/marktde.exporter.ts +78 -78
- package/src/exporters/ooyyo.exporter.ts +75 -48
- package/src/formats/ooyyo-xml.builder.ts +87 -48
- package/src/interfaces/export-item.interface.ts +43 -2
- package/src/interfaces/params.interface.ts +5 -0
- package/src/services/marktde-feed-run.ts +233 -233
- package/src/services/ooyyo-feed-run.ts +265 -0
- package/src/services/ooyyo-feed-verify.ts +131 -0
- package/src/transformers/marktde.transformer.ts +255 -255
- package/src/transformers/ooyyo.transformer.ts +378 -109
- package/test/fixtures/ooyyo/OoyyoFormat.xml +125 -0
- package/test/fixtures/ooyyo/OoyyoFormat.xsd +116 -0
- package/test/fixtures/ooyyo.fixture.ts +53 -0
- package/test/mapping-csv.test.ts +65 -65
- package/test/marktde-feed-run.test.ts +263 -263
- package/test/marktde-mapping.test.ts +280 -280
- package/test/ooyyo-feed-run.test.ts +293 -0
- package/test/ooyyo-mapping.test.ts +276 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# meinestadt.de — measurement design (ADV-327)
|
|
2
|
+
|
|
3
|
+
Status 13.11.2026: DEV only. Implemented: the listing link (export-cars 1.26.44), the reporting channel
|
|
4
|
+
`CLASSIFIEDS_MEINESTADT` (web-funnel-tracking-health 1.5.5) and the lead source for the VDP enquiry
|
|
5
|
+
(user-actions-contact, ADV-327 fix). Not implemented, by design, until meinestadt confirms a path in
|
|
6
|
+
writing: partner click ids, postbacks / pixels, the daily report import. No production switch.
|
|
7
|
+
|
|
8
|
+
## 1. Evidence
|
|
9
|
+
|
|
10
|
+
| Source | Link parameters | Note |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| Original 32-field workbook (tab `meinestadt`) | `source=meineStadt&medium=cpc&campaign=listing` | historic |
|
|
13
|
+
| export-cars ≤ 1.26.43 / Confluence | `source=classifieds&medium=listing&campaign=meinestadt` | partner only in `campaign` |
|
|
14
|
+
| export-cars ≥ 1.26.44 (current DEV) | `source=meinestadt&medium=listing&campaign=listing` + identical `utm_source/utm_medium/utm_campaign` | partner in `source`; no paid medium |
|
|
15
|
+
|
|
16
|
+
- ADV-260: fe-main stores `source / medium / campaign / utm_*` + click ids in the `marketing` cookie
|
|
17
|
+
(`CommonUtils.ts` `MARKETING_PARAM_REGISTRY`, merge per key, 24 h) and the first landing URL / external
|
|
18
|
+
referrer in `landing_context` (first wins). car-basic-data (VDP), car-search-endpoint (SRL),
|
|
19
|
+
user-actions-contact (forms, checkout) read both cookies server-side into the BigQuery event rows.
|
|
20
|
+
- Before 1.5.5 every variant fell into generic rules: the workbook variant (`medium=cpc`) as `PAID_OTHER`,
|
|
21
|
+
the legacy variant as `UNRESOLVED` (`medium=listing` is unmapped). Partner identity was lost.
|
|
22
|
+
- Before the ADV-327 fix the VDP enquiry "Frage zum Fahrzeug" (`VehicleDetailedNextPage.onEnquirySubmit`)
|
|
23
|
+
sent no `marketing` object, and user-actions-contact built the Odoo message only from the body, so
|
|
24
|
+
those REQs reached Odoo without utm source / medium / campaign. DEV 09.11.2026, same visit with a
|
|
25
|
+
Criteo cookie: enquiry → source "(nicht bekannt)", offer request → `criteo / cpc / retargeting`.
|
|
26
|
+
This affected every channel, not only meinestadt.
|
|
27
|
+
- No meinestadt click id, postback, pixel or reporting API is known. Daily e-mails from
|
|
28
|
+
auto@meinestadt.de with `meinestadt-daily_reporting_AUTODE_<date>.csv` existed historically; no file was
|
|
29
|
+
available for this ticket, so its schema is not verified.
|
|
30
|
+
|
|
31
|
+
## 2. Path
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
meinestadt listing ──listingUrl──► https://www.auto.de/search/vehicle/<car _id>?source=meinestadt&medium=listing&campaign=listing&utm_*=…
|
|
35
|
+
→ fe-main: marketing cookie + landing_context (ADV-260) raw values unchanged in every event row
|
|
36
|
+
→ views: reporting_channel CLASSIFIEDS_MEINESTADT, family Classifieds, detail meinestadt.de, publisher MEINESTADT
|
|
37
|
+
→ form (VDP enquiry / offer request / checkout) → REQ → Odoo utm source "meinestadt" (body marketing, or the cookie)
|
|
38
|
+
→ fact_lead.crm_source → source_resolved_v1 = 'meinestadt' (customer SQL, no change needed for the new link)
|
|
39
|
+
→ rpt_episode_touchpoint → canonical 90-day intent episode (original episode_start_date) → fact_sale / CM
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- Grain: VISITOR session (traffic_class VISITOR; KNOWN_CRAWLER / BOT_SUSPECT rules run before attribution).
|
|
43
|
+
- Raw vs normalized: `source / medium / campaign / utm_*` stay as received in the event rows and in Odoo;
|
|
44
|
+
the channel is derived in the views. Historical rows are not rewritten — the views classify at query
|
|
45
|
+
time, the producer label (`resolved_channel`, message-schemes) is untouched.
|
|
46
|
+
- Rule (web-funnel-tracking-health `MEINESTADT_UTM`): `source` (legacy or utm, lower-cased) in
|
|
47
|
+
`meinestadt` / `meinstadt`, or `source = classifieds` and `campaign` in `meinestadt` / `meinstadt`; any
|
|
48
|
+
medium; after the click-id rules (a Google click id wins), before the paid UTM rules; not `PAID_*`, so
|
|
49
|
+
the paid click-id health rates are unaffected (paid / free is a contract attribute).
|
|
50
|
+
- Listing-level join without a partner id: feed `listingUrl` car `_id` = VDP id = lead car id.
|
|
51
|
+
- Click reconciliation: platform `meinestadt` in `buildClickReconciliationView`, aggregate only (no click
|
|
52
|
+
id). Used only once `fact_marketing_media` holds meinestadt rows. No artificial 1:1 mapping.
|
|
53
|
+
- Off-site leads (phone, partner-hosted forms) never create a website session.
|
|
54
|
+
- Consent: the cookie capture is first-party and not consent-gated today (open compliance question noted
|
|
55
|
+
in fe-main `CookieUtils.getConsentState`); every event row carries `marketingConsent` /
|
|
56
|
+
`analyticsConsent` from the request headers. Nothing is sent to meinestadt (no pixel / postback).
|
|
57
|
+
Finance / bank data never leaves auto.de.
|
|
58
|
+
|
|
59
|
+
## 3. Episodes and sales (customer SQL, prod-only, not applied)
|
|
60
|
+
|
|
61
|
+
- New link: `crm_source = meinestadt` → `source_resolved_v1/v2 = 'meinestadt'` with the existing rules.
|
|
62
|
+
- Legacy link (`classifieds` + campaign `meinestadt`): resolves to `classifieds` today; proposal D-rule
|
|
63
|
+
`source_resolved_v2 = 'meinestadt'`, method `PARTNER_PARAM_EXACT` (raw values kept). File:
|
|
64
|
+
`release-runbooks/adv-327/ADV-327-customer-sql-proposal.sql`.
|
|
65
|
+
- Same person within a fixed 90-day episode → no new acquisition (`rpt_customer_intent_episode`).
|
|
66
|
+
Sales: distinct `fact_sale` ids per episode, cohort = `episode_start_date`, `sales_reported_at` separate.
|
|
67
|
+
|
|
68
|
+
## 4. Costs (blocked on the real report and a contract)
|
|
69
|
+
|
|
70
|
+
- Target: the existing `fact_marketing_media` / `fact_marketing_cost` (no new table, no data mart).
|
|
71
|
+
`source_system = meinestadt_daily_report`, `channel = meinestadt`, report date (partner timezone →
|
|
72
|
+
Europe/Berlin), billable / non-billable clicks, spend, currency, credits as negative rows, invoice
|
|
73
|
+
number, contract version, file name + SHA-256 for idempotent upsert and correction replay, status
|
|
74
|
+
`actual` / `estimated` / `missing` per day.
|
|
75
|
+
- The feed field `cpc` is a bid / configuration value, never actual cost; no cost from the historic 0.08.
|
|
76
|
+
- `rpt_lead_cost_live.paid_channel_v3` gets `meinestadt` only while a CPC contract is effective.
|
|
77
|
+
- The parser is written against a real CSV only (separator, encoding, decimal comma, timezone, columns).
|
|
78
|
+
|
|
79
|
+
## 5. QA (DEV)
|
|
80
|
+
|
|
81
|
+
| Case | Expected | Status |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| new link, legacy link, workbook link (`meineStadt` + `cpc`) | CLASSIFIEDS_MEINESTADT event + session | ✅ 12.11 real DEV requests + unit tests |
|
|
84
|
+
| other classifieds campaign | not meinestadt | ✅ control → UNRESOLVED |
|
|
85
|
+
| Google click id + meinestadt utm | click id wins | ✅ rule order test |
|
|
86
|
+
| raw values kept | event rows hold `meineStadt` / `classifieds` as sent | ✅ BQ |
|
|
87
|
+
| fe-main capture of the link | cookie = link parameters, no parameterConflict | ✅ fe-main function run on the URLs |
|
|
88
|
+
| health metrics | Classifieds in `channel_family_share`, 0 WARN | ✅ day run 12.11 |
|
|
89
|
+
| VDP enquiry → REQ → Odoo source (no body marketing, cookie only) | utm source from the cookie | ✅ 13.11 DEV: REQ584784 `meinestadt / listing / listing`, REQ584783 `classifieds / listing / meinestadt` (raw); sessions CLASSIFIEDS_MEINESTADT, `develop_web_journey_lead_v1` CONFIRMED |
|
|
90
|
+
| offer request with body marketing | unchanged (body wins) | ✅ unit test |
|
|
91
|
+
| no consent | capture unchanged, consent flag on rows | documented, not changed |
|
|
92
|
+
| bot | KNOWN_CRAWLER / BOT_SUSPECT, not counted | existing rules |
|
|
93
|
+
| new / repeat episode, 2 sales, CSV replay / credit / no-report day, DST | — | prod customer SQL / blocked on CSV |
|
|
94
|
+
|
|
95
|
+
Note: Odoo `cookieid` is filled only for signed-in senders (user-actions-contact, unchanged); the website
|
|
96
|
+
session ↔ REQ link is `lead_funnel_event` (session id + Pub/Sub messageId → REQ), as for every form.
|
|
97
|
+
|
|
98
|
+
## 6. Launch blockers
|
|
99
|
+
|
|
100
|
+
Contract with auto.de GmbH, partner spec and approval of the link parameters, real daily CSV + field
|
|
101
|
+
definitions, legal / privacy sign-off, customer-SQL owner OK, feed and image acceptance (ADV-325/326).
|
|
102
|
+
|
|
103
|
+
## 7. Rollback
|
|
104
|
+
|
|
105
|
+
- Link: export-cars ≤ 1.26.43 (dry-run feed only, nothing published).
|
|
106
|
+
- Channel: web-funnel-tracking-health 1.5.4 + view rebuild (event `emubc2hjemu`).
|
|
107
|
+
- Lead source fallback: revert the user-actions-contact commit and rebuild the service.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# OOYYO feed — audit and contract (ADV-331)
|
|
2
|
+
|
|
3
|
+
Status 13.11.2026: DEV only, dry-run by default. Nothing is hosted, sent or submitted; OOYYO's
|
|
4
|
+
crawler is not enabled. Export type `ooyyo`, export-cars ≥ 1.26.48.
|
|
5
|
+
|
|
6
|
+
## Sources
|
|
7
|
+
|
|
8
|
+
| What | Where |
|
|
9
|
+
|---|---|
|
|
10
|
+
| Official XSD / XML template | `test/fixtures/ooyyo/OoyyoFormat.{xsd,xml}` — downloaded 13.11.2026 from the links on ooyyo.com/c=CDA31D7114F5854F111BFE6F9D671453 (d10y2oj5s0rxih.cloudfront.net/ooyyo_format/), 4,883 / 5,595 bytes = the attachments of Aleksandar Ciric's mail of 16.10.2026. sha256 xsd `58947bf2…ac07c3`, xml `08b3e74a…adefd` |
|
|
11
|
+
| Importer rules (binding) | Aleksandar Ciric (OOYYO), 16.10. and 19.10.2026, quoted in ADV-331 / ADV-333 |
|
|
12
|
+
| Legacy exporter | `OoyyoExporter` / `OoyyoTransformer` / `OoyyoXmlBuilder` before 1.26.48; DEV Cronicle `6.0 Export Ooyyo - part 1` (ejtpmrpb002) → `6.1 … part 2` (ejtpmsrmw03, shell `ftp put` of `autode-ooyyo.tar.gz` to the legacy Jelastic FTP) |
|
|
13
|
+
|
|
14
|
+
## What the XSD does and does not enforce
|
|
15
|
+
|
|
16
|
+
`<ooyyo>` = `xsd:choice minOccurs=0 maxOccurs=unbounded` of `<ad>`; `<ad>` = choice of 25 optional
|
|
17
|
+
elements; all values `xsd:string`; `price`, `engine_size`, `mileage`, `power` need a `unit` attribute
|
|
18
|
+
(any string). So the XSD accepts an empty file, an ad without price / photo / year, and `unit="euro"`.
|
|
19
|
+
The partner's real acceptance rules are therefore implemented separately (`validateOoyyoItem`) and
|
|
20
|
+
applied twice: to every item before it is written and to the finished file when it is read back
|
|
21
|
+
(`verifyOoyyoXml`). The test suite re-derives the element names and required-unit list from the XSD
|
|
22
|
+
file, so a schema change fails the build.
|
|
23
|
+
|
|
24
|
+
## Partner acceptance rules (19.10.2026) → implementation
|
|
25
|
+
|
|
26
|
+
| Rule | Implementation |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `<price unit="EUR">` exactly; missing / other unit = rejected | builder writes `unit="EUR"`; verifier allows only `EUR` |
|
|
29
|
+
| price = integer vehicle purchase price | `price.currentSalesPrice` only, whole euros; never `rate.*` (leasing) or `price.purchasePrice` (dealer cost) |
|
|
30
|
+
| rejected without photo | ≥ 1 validated clean photo (ADV-336 selection: 3/4 hero first, URL check), else `no_valid_image` |
|
|
31
|
+
| rejected without year | first-registration year, else stored construction year; nothing defaulted, else `no_valid_year` |
|
|
32
|
+
| rejected when make / model not recognisable | empty / `SONSTIGE` / `Andere` / `selector_unknown` … → `no_make` / `no_recognisable_model` |
|
|
33
|
+
| vehicle URL and picture URLs ≤ 1,500 chars, HTTPS | `url_too_long` / `no_valid_image`; all URLs `https://` |
|
|
34
|
+
| partly valid file REPLACES the whole OOYYO inventory | FeedGuard (complete + fresh snapshot, min / drop vs last good, lock) + read-back verify of every ad + count equality; temp → atomic rename; blocked run keeps the last good `.xml` / `.xml.gz` and exits 1 |
|
|
35
|
+
| download failure / 0 readable ads: OOYYO keeps old inventory | not relied upon — an empty file is never published (`published_too_small`) |
|
|
36
|
+
| one XML at a fixed HTTPS URL, plain or gzip ending `.gz`, FTP discouraged | outputs `ooyyo.xml` + `ooyyo.xml.gz` (real gzip of the XML, round-trip checked); the legacy `autode-ooyyo.tar.gz` (tar, FTP) is no longer produced. Hosting is NOT set up |
|
|
37
|
+
|
|
38
|
+
## Field mapping (source → XML)
|
|
39
|
+
|
|
40
|
+
| Element | Source | Rule |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `url` | `_id` | `https://www.<domain>/search/vehicle/<id>?source=ooyyo&medium=listing&campaign=listing&utm_source=ooyyo&utm_medium=listing&utm_campaign=listing` (ADV-333) |
|
|
43
|
+
| `title` | `mainData.make model subModel` | German, missing parts left out (legacy prefixed `constructionYear`, also for used cars) |
|
|
44
|
+
| `content` | `offer.offerDescription` | plain text: mobile.de `\\` → line break, `**` dropped; nothing added; empty → left out |
|
|
45
|
+
| `price unit="EUR"` | `price.currentSalesPrice` | integer > 0 |
|
|
46
|
+
| `make` / `model` / `trim` | `mainData.make` / `model` / `subModel` | as stored |
|
|
47
|
+
| `engine_size unit="cc"` | `engineData.cubicCapacity` | > 0 else left out (BEV) |
|
|
48
|
+
| `body_type` / `color` / `fuel_type` / `transmission` | selectors | German labels (`de` dictionary), `Außenfarbe ` prefix removed, unknown → left out |
|
|
49
|
+
| `year` | `mainData.firstRegistration` / `constructionYear` | see rules |
|
|
50
|
+
| `mileage unit="km"` | `mainData.mileage` | ≥ 0 (0 for new cars is real) |
|
|
51
|
+
| `euronorm` | `environmentEmissions.emissionClass` | `1`…`6`, `6b`, `6c`, `6d`, `6d-TEMP` (legacy values); unknown left out (legacy wrote `default`) |
|
|
52
|
+
| `power unit="kw"` | `engineData.powerKW` | > 0 |
|
|
53
|
+
| `doors` / `seats` / `gears` | `mainData.doors` / `seats`, `driveSuspension.gears` | > 0 |
|
|
54
|
+
| `location/city` | `location.city` | as stored |
|
|
55
|
+
| `location/postcode` | `location.zipCode` | STRING; DE 4-digit → leading zero restored (`4103` → `04103`); invalid → left out |
|
|
56
|
+
| `location/region` | postal code table (`DePostalCodesService`) | Bundesland of the postcode (legacy wrote the country code `DE`) |
|
|
57
|
+
| `pictures/picture` | Imbo images | validated clean list, hero first, no cap (20 is a ranking recommendation), never `noimage` |
|
|
58
|
+
| `vin` | `metaData.vin` | only a valid ISO 3779 VIN |
|
|
59
|
+
| `is_new` | `mainData.condition` | `1` new, `0` used |
|
|
60
|
+
| `date` / `time` | `metaData.creationDate` | `DD/MM/YYYY` / `HH:MM` 24 h, Europe/Berlin (legacy `hh:mm:ss`, 12 h, host TZ) |
|
|
61
|
+
|
|
62
|
+
Empty optional elements are left out entirely (all are optional in the XSD); the legacy builder wrote
|
|
63
|
+
`<engine_size/>` / `<power/>` without the required `unit`, which made the file schema-invalid.
|
|
64
|
+
Values are CDATA-wrapped as in the template; `]]>` is split, XML-invalid control characters removed.
|
|
65
|
+
|
|
66
|
+
Excluded cars (counted in `last-run-report.json.excluded`): `duplicate_id`, `filtered` (Cronicle
|
|
67
|
+
rules, no images), `reserved_or_sold`, `no_valid_price`, `no_make`, `no_recognisable_model`,
|
|
68
|
+
`no_valid_year`, `no_valid_image`, `url_too_long`.
|
|
69
|
+
|
|
70
|
+
## Legacy DEV output (last run 09.11.2026 09:01, 1,771 ads) — defects found
|
|
71
|
+
|
|
72
|
+
- `price unit="euro"` on every ad (rejected by the current importer).
|
|
73
|
+
- 5,001 `…/carimage/noimage/…` placeholder pictures (the template forbids "No image available" images).
|
|
74
|
+
- `region` = `DE` (country code, not a region); 415 of 3,094 DEV cars have a 4-digit postcode
|
|
75
|
+
(leading zero lost because CBD stores a number).
|
|
76
|
+
- `euronorm` = `default` on 65 ads; empty `<engine_size/>` / `<power/>` without `unit` → XSD-invalid.
|
|
77
|
+
- `time` as `hh:mm:ss` (12-hour clock); year invented for new cars without a year (`now - 2 months`).
|
|
78
|
+
- Output directory emptied before the run (an outage published an empty / partial file); tar.gz built
|
|
79
|
+
with the external `zip` tooling.
|
|
80
|
+
|
|
81
|
+
DEV dry-run results with 1.26.48: see `RUNBOOK.md` → "DEV runs".
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# OOYYO — open partner questions (ADV-331 / ADV-333)
|
|
2
|
+
|
|
3
|
+
Contact: Aleksandar Ciric (OOYYO). `submission@ooyyo.net` is dead. Draft only — nothing is sent from
|
|
4
|
+
engineering; Marketing sends after review.
|
|
5
|
+
|
|
6
|
+
Already answered (16.10. / 19.10.2026): OOYYO XML only; fixed HTTPS URL, plain or `.gz`; daily pull
|
|
7
|
+
shortly after midnight; full replacement (partly valid file replaces everything, failed download / 0
|
|
8
|
+
readable ads keeps the old inventory); `unit="EUR"` mandatory; photo / year / price / recognisable
|
|
9
|
+
make + model mandatory; URLs ≤ 1,500 chars; UTMs allowed and kept, direct link without redirect; free
|
|
10
|
+
listing has no click reporting and no click id; Featured Vehicles is a separate paid product.
|
|
11
|
+
|
|
12
|
+
Open:
|
|
13
|
+
|
|
14
|
+
1. Time zone of the daily pull ("shortly after midnight") — asked by Marketing on 19.10., no answer yet.
|
|
15
|
+
We need it to schedule our export so the file is fresh but never mid-write (we publish atomically).
|
|
16
|
+
2. How should the 20-car test sample reach the importer for the non-live test: file attachment or a
|
|
17
|
+
temporary URL? If a URL: may it carry a secret token in the path / query (the partner mentioned an
|
|
18
|
+
optional token), and does the importer accept a self-signed / basic-auth protected URL?
|
|
19
|
+
3. Retry behaviour when the URL answers 5xx / times out at the pull time: one retry later the same
|
|
20
|
+
night, or next day only? Any alerting on their side we can subscribe to?
|
|
21
|
+
4. Grace period for deletions: a car missing in one file — removed immediately at that import?
|
|
22
|
+
5. Make / model recognition: is there a reference list (or a report of unmatched values after the
|
|
23
|
+
test import) so we can map our values (e.g. "Mercedes-Benz", "Škoda", sub-brands) up front?
|
|
24
|
+
6. Accepted values for `fuel_type`, `transmission`, `body_type`, `color`: German labels OK (we send
|
|
25
|
+
e.g. `Benzin`, `Elektro`, `Automatik`, `SUV/Geländewagen`), or English keywords preferred?
|
|
26
|
+
7. `euronorm`: are our values `6`, `6d`, `6d-TEMP` OK, or `Euro 6` style?
|
|
27
|
+
8. `date` / `time`: listing creation date in Europe/Berlin — correct meaning ("date of the ad")?
|
|
28
|
+
9. Electric cars: is an ad without `engine_size` fine (we leave it out rather than send 0)?
|
|
29
|
+
10. Crawler: User-Agent and source IPs of the feed fetcher and of any page crawler, so we can classify
|
|
30
|
+
them as automation in our traffic reporting (ADV-333) and allow-list them at Cloudflare.
|
|
31
|
+
11. Does OOYYO ever request our vehicle pages itself (link checking / crawling) after the import?
|
|
32
|
+
12. Rejection report after each production import (counts + reasons) — available beyond the test?
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# OOYYO feed — runbook (ADV-331)
|
|
2
|
+
|
|
3
|
+
## Parameters (Cronicle event, export-cars plugin, `exportType=ooyyo`)
|
|
4
|
+
|
|
5
|
+
| Param | Default | Meaning |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `String` / `Number` / `Date` | — | Cronicle filter rules (DEV 6.0: none) |
|
|
8
|
+
| `ooyyoDryRun` | dry-run | only the exact text `false` makes a live run |
|
|
9
|
+
| `ooyyoMaxImages` | no cut | cut the picture list (OOYYO accepts all; 20 is a ranking recommendation) |
|
|
10
|
+
| `ooyyoImageConcurrency` | 16 | parallel image URL checks |
|
|
11
|
+
| `feedGuardMinSourceItems` | 1000 | DEV snapshot ~3,100 cars |
|
|
12
|
+
| `feedGuardMinPublishedItems` | 1 | set a real minimum from a PROD dry-run before going live |
|
|
13
|
+
| `feedGuardMaxDropPercent` | 20 | max drop of source / ads against the last good run |
|
|
14
|
+
| `feedGuardMaxSourceAgeHours` | 26 | newest base export chunk must be younger |
|
|
15
|
+
| `feedGuardMaxStaleHours` | 48 | last good file reported STALE after this |
|
|
16
|
+
| `feedGuardAcceptDrop` | off | `true` for ONE run after a real, approved stock reduction |
|
|
17
|
+
|
|
18
|
+
No new parameter is needed for a run: the defaults give a safe dry-run.
|
|
19
|
+
|
|
20
|
+
## Files
|
|
21
|
+
|
|
22
|
+
| Mode | Output | State |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| dry-run | `resources/ooyyo-dryrun/ooyyo.xml` + `ooyyo.xml.gz` | `resources/ooyyo-state/dryrun/` |
|
|
25
|
+
| live | `resources/ooyyo-export/ooyyo.xml` + `ooyyo.xml.gz` | `resources/ooyyo-state/export/` |
|
|
26
|
+
|
|
27
|
+
State dir: `last-good.json`, `last-run-report.json` (guard code, exclusions, verify issues, missing
|
|
28
|
+
optional fields, padded postcodes, bytes + sha256, image stats), `run.lock`. A blocked run leaves
|
|
29
|
+
`ooyyo.xml.rejected`; the published files and their `.prev` copies stay.
|
|
30
|
+
|
|
31
|
+
## Run order inside one run
|
|
32
|
+
|
|
33
|
+
source snapshot check → map + partner rules + image check per car → temp XML → read back + verify
|
|
34
|
+
every ad and the count → count guard vs last good → gzip + round-trip check → rename `.xml.gz`, then
|
|
35
|
+
`.xml` (both atomic) → report. Any failure: exit 1, nothing replaced.
|
|
36
|
+
|
|
37
|
+
## Exit codes and the chain
|
|
38
|
+
|
|
39
|
+
Blocked or failed runs exit 1, so the chained `6.1 … part 2` does not run. A dry-run exits 0 and DEV
|
|
40
|
+
`6.1` then uploads `resources/ooyyo-export/autode-ooyyo.tar.gz` — the LEGACY file from 09.11.2026,
|
|
41
|
+
which 1.26.48 no longer writes — to the legacy Jelastic FTP (as `autode-ooyyo-develop.tar.gz`). `6.1`
|
|
42
|
+
is not changed (same decision as meinestadt `5.1`). To run 6.0 alone use the API with `chain: ""`:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
# on the DEV Cronicle host; the key never leaves the host
|
|
46
|
+
K=$(sudo -n node bin/storage-cli.js get global/api_keys/0 | node -e '…print .key…')
|
|
47
|
+
curl -s -X POST 127.0.0.1:<WebServer.http_port>/api/app/run_event/v1 \
|
|
48
|
+
-d '{"api_key":"'$K'","id":"ejtpmrpb002","chain":""}'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Before anything goes live (not done; each step needs explicit OK)
|
|
52
|
+
|
|
53
|
+
1. Marketing approval; 20-car sample (from a PROD dry-run, not DEV data) to Aleksandar for the
|
|
54
|
+
NON-LIVE importer test; accepted / rejected counts and reasons reconciled (target: 0 rejected).
|
|
55
|
+
2. Answers to `PARTNER-QUESTIONS.md` (pull time zone, retry, sample transport, value lists).
|
|
56
|
+
3. Hosting: a fixed HTTPS URL serving `resources/ooyyo-export/ooyyo.xml(.gz)` read-only, optional
|
|
57
|
+
secret token kept out of logs; replace / disable `6.1` (legacy FTP, credentials in the script).
|
|
58
|
+
4. Schedule the export so it finishes well before the partner's midnight pull; monitoring on
|
|
59
|
+
`last-run-report.json` (status, stale) and on the served file's age.
|
|
60
|
+
5. `feedGuardMinPublishedItems` from a PROD dry-run; only then `ooyyoDryRun=false`.
|
|
61
|
+
6. Stop-poll / rollback: serve the `.prev` file or an HTTP 5xx (OOYYO keeps the old inventory on a
|
|
62
|
+
failed download) — never an empty or partial file.
|
|
63
|
+
7. Tracking: `web-funnel-tracking-health` ≥ 1.5.7 (CLASSIFIEDS_OOYYO) on the target Cronicle.
|
|
64
|
+
|
|
65
|
+
## DEV runs
|
|
66
|
+
|
|
67
|
+
(filled in after the DEV run, see below)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# OOYYO — measurement (ADV-333)
|
|
2
|
+
|
|
3
|
+
Status 13.11.2026: DEV only. Implemented: the feed `<url>` scheme (export-cars 1.26.48) and the
|
|
4
|
+
reporting channel `CLASSIFIEDS_OOYYO` (web-funnel-tracking-health 1.5.7). No partner click id, cost,
|
|
5
|
+
postback or pixel exists for the free listing, so none is modelled.
|
|
6
|
+
|
|
7
|
+
## Evidence
|
|
8
|
+
|
|
9
|
+
- Partner, 19.10.2026: OOYYO links DIRECTLY to the `<url>` of the XML, no redirect, query parameters
|
|
10
|
+
(our UTMs) unchanged; URL ≤ 1,500 chars; free listing = no click reporting, no click id.
|
|
11
|
+
- Historic ACF-6139 link: `?source=classifieds&medium=listing&campaign=ooyyo` (Confluence tag
|
|
12
|
+
"ooyyo [organic]"). Before 1.5.7 that resolved to generic classifieds / UTM rules — the partner
|
|
13
|
+
identity was lost, same finding as AutoUncle (ADV-324).
|
|
14
|
+
- ADV-260: fe-main keeps `source/medium/campaign/utm_*` in the marketing cookie (first external
|
|
15
|
+
context wins, internal navigation does not overwrite it) and sends it with every lead.
|
|
16
|
+
|
|
17
|
+
## Path
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
OOYYO result ──(direct link, no redirect)──► https://www.auto.de/search/vehicle/<car _id>
|
|
21
|
+
?source=ooyyo&medium=listing&campaign=listing&utm_source=ooyyo&utm_medium=listing&utm_campaign=listing
|
|
22
|
+
→ fe-main marketing cookie (ADV-260) → visitor session, landing_url keeps the raw query
|
|
23
|
+
→ views: reporting_channel CLASSIFIEDS_OOYYO, family Classifieds, detail OOYYO, publisher OOYYO
|
|
24
|
+
(historic source=classifieds&campaign=ooyyo resolves to the same channel)
|
|
25
|
+
→ form → REQ → Odoo crm_source "ooyyo" (VDP enquiries need the user-actions-contact ADV-327 cookie
|
|
26
|
+
fallback — DEV since 13.11, not on PROD)
|
|
27
|
+
→ rpt_episode_touchpoint → canonical 90-day episode (original episode_start_date) → sale / CM
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- Legacy and utm pairs carry identical values, so fe-main's `getUtmQueryParameter` sees no
|
|
31
|
+
parameter conflict; GA4 sees `ooyyo / listing`.
|
|
32
|
+
- Not PAID_*: the listing is free; it stays out of the paid click-id health rates and gets no cost.
|
|
33
|
+
Spend for OOYYO is "verified no fee" only for the free XML listing; any Featured Vehicles test needs
|
|
34
|
+
its own cost import first.
|
|
35
|
+
- Reconciliation view: platform `ooyyo`, but there are no media rows → sessions only, no exact join.
|
|
36
|
+
|
|
37
|
+
## Crawler / bot filtering
|
|
38
|
+
|
|
39
|
+
Sessions are VISITOR / KNOWN_CRAWLER / INTERNAL by published crawler IP ranges and proven automation
|
|
40
|
+
signatures (ADV-371). OOYYO publishes no ranges; its feed fetch reads the XML file only and creates
|
|
41
|
+
no tracking event. No OOYYO rule is guessed — partner question 10 asks for UA / IPs; once known, add
|
|
42
|
+
them as verified automation with evidence.
|
|
43
|
+
|
|
44
|
+
## Not done (by design)
|
|
45
|
+
|
|
46
|
+
- No `ooyyo_clickid` or similar parameter, no postback, no conversion upload.
|
|
47
|
+
- No retroactive rewrite of historic `crm_source` values.
|
|
48
|
+
- Customer SQL (`source_resolved_v1 = LOWER(crm_source)`) already yields `ooyyo` for the new link; the
|
|
49
|
+
historic `classifieds` + campaign `ooyyo` rows stay `classifieds` until the data owner applies the
|
|
50
|
+
classifieds-campaign rule proposed for ADV-324 / ADV-327 / ADV-330.
|
|
51
|
+
|
|
52
|
+
## QA
|
|
53
|
+
|
|
54
|
+
See `RUNBOOK.md` → "DEV runs" for the DEV test (link → VDP → enquiry → Odoo REQ → views).
|
package/package.json
CHANGED
|
@@ -1,65 +1,66 @@
|
|
|
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/mapping-csv.test.ts && ts-node --transpile-only test/marktde-mapping.test.ts && ts-node --transpile-only test/marktde-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
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"@
|
|
47
|
-
"@types/
|
|
48
|
-
"@types/
|
|
49
|
-
"@types/
|
|
50
|
-
"@
|
|
51
|
-
"@typescript-eslint/
|
|
52
|
-
"eslint": "^8.0
|
|
53
|
-
"eslint
|
|
54
|
-
"eslint-
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@bytebrand/export-cars",
|
|
3
|
+
"version": "1.26.48",
|
|
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/mapping-csv.test.ts && ts-node --transpile-only test/marktde-mapping.test.ts && ts-node --transpile-only test/marktde-feed-run.test.ts && ts-node --transpile-only test/ooyyo-mapping.test.ts && ts-node --transpile-only test/ooyyo-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
|
+
"fast-xml-parser": "4.2.5",
|
|
38
|
+
"google-spreadsheet": "^3.3.0",
|
|
39
|
+
"lodash": "^4.17.21",
|
|
40
|
+
"moment": "^2.29.4",
|
|
41
|
+
"nest-commander": "~3.15.0",
|
|
42
|
+
"reflect-metadata": "^0.1.13",
|
|
43
|
+
"vin-generator": "^1.0.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@nestjs/cli": "^10.1.18",
|
|
47
|
+
"@types/google-spreadsheet": "^3.3.0",
|
|
48
|
+
"@types/inquirer": "^8.2.4",
|
|
49
|
+
"@types/lodash": "^4.14.186",
|
|
50
|
+
"@types/node": "^16.18.58",
|
|
51
|
+
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
|
52
|
+
"@typescript-eslint/parser": "^6.8.0",
|
|
53
|
+
"eslint": "^8.0.1",
|
|
54
|
+
"eslint-config-prettier": "^8.3.0",
|
|
55
|
+
"eslint-plugin-prettier": "^4.0.0",
|
|
56
|
+
"gts": "^3.1.1",
|
|
57
|
+
"prettier": "^2.3.2",
|
|
58
|
+
"ts-node": "^10.0.0",
|
|
59
|
+
"tsconfig-paths": "4.1.0",
|
|
60
|
+
"typescript": "^5.2.2"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=14.0.0"
|
|
64
|
+
},
|
|
65
|
+
"gitHead": "e827f35865032821bf3a081f03a026d3d3f8a540"
|
|
66
|
+
}
|
|
@@ -1,70 +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
|
-
// Guard defaults (same as AutoUncle). The DEV job exports one dealer via the Cronicle rules, so the
|
|
16
|
-
// published count is far below the base snapshot: minPublishedItems only forbids an empty file.
|
|
17
|
-
export const MARKTDE_DEFAULTS = {
|
|
18
|
-
imageConcurrency: 16,
|
|
19
|
-
minSourceItems: 1000,
|
|
20
|
-
minPublishedItems: 1,
|
|
21
|
-
maxDropPercent: 20,
|
|
22
|
-
maxSourceAgeHours: 26,
|
|
23
|
-
maxStaleHours: 48,
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* markt.de vehicle feed (ADV-328): the legacy mobile.de-style CSV (mapping sheet `mobile_yescar`,
|
|
28
|
-
* ACF-4370) plus a ZIP of it, with clean ADV-336 images. There is no current markt.de spec yet, so
|
|
29
|
-
* the format (columns, file names, no header row, `;` separator, UTF-8) stays as it was.
|
|
30
|
-
* Dry-run is the default: the files go to resources/marktde-dryrun, which no Cronicle step reads.
|
|
31
|
-
* Only marktdeDryRun=false writes resources/marktde-export (the directory `9c.1 … part 2` uploads
|
|
32
|
-
* from). Each mode has its own guard state.
|
|
33
|
-
*/
|
|
34
|
-
export class MarktdeConfig extends Config {
|
|
35
|
-
dryRun: boolean;
|
|
36
|
-
imageConcurrency: number;
|
|
37
|
-
// 0 = no limit (no partner limit known)
|
|
38
|
-
maxImages: number;
|
|
39
|
-
stateDir: string;
|
|
40
|
-
feedGuard: FeedGuardOptions;
|
|
41
|
-
|
|
42
|
-
constructor(params: Params) {
|
|
43
|
-
super(params);
|
|
44
|
-
this.campaign = ExportType.MARKTDE;
|
|
45
|
-
this.dryRun = text(params.marktdeDryRun).toLowerCase() !== 'false';
|
|
46
|
-
const mode = this.dryRun ? 'dryrun' : 'export';
|
|
47
|
-
this.pathesConfig.localConvertedDirectory = `resources/${ExportType.MARKTDE}-${mode}`;
|
|
48
|
-
this.stateDir = `resources/${ExportType.MARKTDE}-state/${mode}`;
|
|
49
|
-
this.imageUrl = `https://images.${this.domain}/carimage`;
|
|
50
|
-
this.mappingSpreadsheetTitle = 'mobile_yescar';
|
|
51
|
-
// MobileTransformer still builds its legacy `image` column; marktde.transformer replaces it with
|
|
52
|
-
// the validated clean list.
|
|
53
|
-
this.imageType = ImageType.default;
|
|
54
|
-
this.imageConcurrency = positiveNumber(params.marktdeImageConcurrency, MARKTDE_DEFAULTS.imageConcurrency);
|
|
55
|
-
this.maxImages = Math.floor(positiveNumber(params.marktdeMaxImages, 0));
|
|
56
|
-
this.feedGuard = {
|
|
57
|
-
stateDir: this.stateDir,
|
|
58
|
-
minSourceItems: positiveNumber(params.feedGuardMinSourceItems, MARKTDE_DEFAULTS.minSourceItems),
|
|
59
|
-
minPublishedItems: positiveNumber(params.feedGuardMinPublishedItems, MARKTDE_DEFAULTS.minPublishedItems),
|
|
60
|
-
maxDropPercent: positiveNumber(params.feedGuardMaxDropPercent, MARKTDE_DEFAULTS.maxDropPercent),
|
|
61
|
-
maxSourceAgeHours: positiveNumber(params.feedGuardMaxSourceAgeHours, MARKTDE_DEFAULTS.maxSourceAgeHours),
|
|
62
|
-
maxStaleHours: positiveNumber(params.feedGuardMaxStaleHours, MARKTDE_DEFAULTS.maxStaleHours),
|
|
63
|
-
acceptDrop: text(params.feedGuardAcceptDrop).toLowerCase() === 'true',
|
|
64
|
-
};
|
|
65
|
-
this.outputFiles = [
|
|
66
|
-
`${this.pathesConfig.localConvertedDirectory}/api_export_cars_${ExportType.MARKTDE}.${FileType.CSV}`,
|
|
67
|
-
`${this.pathesConfig.localConvertedDirectory}/api_export_cars_${ExportType.MARKTDE}.${FileType.ZIP}`,
|
|
68
|
-
];
|
|
69
|
-
}
|
|
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
|
+
// Guard defaults (same as AutoUncle). The DEV job exports one dealer via the Cronicle rules, so the
|
|
16
|
+
// published count is far below the base snapshot: minPublishedItems only forbids an empty file.
|
|
17
|
+
export const MARKTDE_DEFAULTS = {
|
|
18
|
+
imageConcurrency: 16,
|
|
19
|
+
minSourceItems: 1000,
|
|
20
|
+
minPublishedItems: 1,
|
|
21
|
+
maxDropPercent: 20,
|
|
22
|
+
maxSourceAgeHours: 26,
|
|
23
|
+
maxStaleHours: 48,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* markt.de vehicle feed (ADV-328): the legacy mobile.de-style CSV (mapping sheet `mobile_yescar`,
|
|
28
|
+
* ACF-4370) plus a ZIP of it, with clean ADV-336 images. There is no current markt.de spec yet, so
|
|
29
|
+
* the format (columns, file names, no header row, `;` separator, UTF-8) stays as it was.
|
|
30
|
+
* Dry-run is the default: the files go to resources/marktde-dryrun, which no Cronicle step reads.
|
|
31
|
+
* Only marktdeDryRun=false writes resources/marktde-export (the directory `9c.1 … part 2` uploads
|
|
32
|
+
* from). Each mode has its own guard state.
|
|
33
|
+
*/
|
|
34
|
+
export class MarktdeConfig extends Config {
|
|
35
|
+
dryRun: boolean;
|
|
36
|
+
imageConcurrency: number;
|
|
37
|
+
// 0 = no limit (no partner limit known)
|
|
38
|
+
maxImages: number;
|
|
39
|
+
stateDir: string;
|
|
40
|
+
feedGuard: FeedGuardOptions;
|
|
41
|
+
|
|
42
|
+
constructor(params: Params) {
|
|
43
|
+
super(params);
|
|
44
|
+
this.campaign = ExportType.MARKTDE;
|
|
45
|
+
this.dryRun = text(params.marktdeDryRun).toLowerCase() !== 'false';
|
|
46
|
+
const mode = this.dryRun ? 'dryrun' : 'export';
|
|
47
|
+
this.pathesConfig.localConvertedDirectory = `resources/${ExportType.MARKTDE}-${mode}`;
|
|
48
|
+
this.stateDir = `resources/${ExportType.MARKTDE}-state/${mode}`;
|
|
49
|
+
this.imageUrl = `https://images.${this.domain}/carimage`;
|
|
50
|
+
this.mappingSpreadsheetTitle = 'mobile_yescar';
|
|
51
|
+
// MobileTransformer still builds its legacy `image` column; marktde.transformer replaces it with
|
|
52
|
+
// the validated clean list.
|
|
53
|
+
this.imageType = ImageType.default;
|
|
54
|
+
this.imageConcurrency = positiveNumber(params.marktdeImageConcurrency, MARKTDE_DEFAULTS.imageConcurrency);
|
|
55
|
+
this.maxImages = Math.floor(positiveNumber(params.marktdeMaxImages, 0));
|
|
56
|
+
this.feedGuard = {
|
|
57
|
+
stateDir: this.stateDir,
|
|
58
|
+
minSourceItems: positiveNumber(params.feedGuardMinSourceItems, MARKTDE_DEFAULTS.minSourceItems),
|
|
59
|
+
minPublishedItems: positiveNumber(params.feedGuardMinPublishedItems, MARKTDE_DEFAULTS.minPublishedItems),
|
|
60
|
+
maxDropPercent: positiveNumber(params.feedGuardMaxDropPercent, MARKTDE_DEFAULTS.maxDropPercent),
|
|
61
|
+
maxSourceAgeHours: positiveNumber(params.feedGuardMaxSourceAgeHours, MARKTDE_DEFAULTS.maxSourceAgeHours),
|
|
62
|
+
maxStaleHours: positiveNumber(params.feedGuardMaxStaleHours, MARKTDE_DEFAULTS.maxStaleHours),
|
|
63
|
+
acceptDrop: text(params.feedGuardAcceptDrop).toLowerCase() === 'true',
|
|
64
|
+
};
|
|
65
|
+
this.outputFiles = [
|
|
66
|
+
`${this.pathesConfig.localConvertedDirectory}/api_export_cars_${ExportType.MARKTDE}.${FileType.CSV}`,
|
|
67
|
+
`${this.pathesConfig.localConvertedDirectory}/api_export_cars_${ExportType.MARKTDE}.${FileType.ZIP}`,
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
}
|