@bytebrand/export-cars 1.26.46 → 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/mapping.service.d.ts +2 -0
- package/dist/services/mapping.service.js +33 -13
- package/dist/services/mapping.service.js.map +1 -1
- 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 +1 -1
- package/docs/marktde/RUNBOOK.md +3 -3
- package/docs/marktde/TRACKING.md +8 -2
- 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 +3 -2
- package/src/configs/ooyyo.config.ts +65 -15
- package/src/constants/fb.constants.ts +34 -34
- 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/mapping.service.ts +54 -13
- package/src/services/ooyyo-feed-run.ts +265 -0
- package/src/services/ooyyo-feed-verify.ts +131 -0
- 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 -0
- package/test/ooyyo-feed-run.test.ts +293 -0
- package/test/ooyyo-mapping.test.ts +276 -0
package/docs/marktde/AUDIT.md
CHANGED
|
@@ -13,7 +13,7 @@ letters are code evidence, not a contract.
|
|
|
13
13
|
| Row format | `MobileTransformer` + Google sheet `147qz9sT…` tab `mobile_yescar` (165 rules, ACF-4370 reused the YesCar / mobile.de CSV). `;` separated, every cell quoted, no header row, UTF-8, 517–528 columns per row |
|
|
14
14
|
| Files | `api_export_cars_marktde.csv` + `.zip` (was `resources/marktde-export/`) |
|
|
15
15
|
| DEV Cronicle | `9c.0 Export markt.de - part 1` `ekpy4pqg21u` (rule `metaData.ownerId:xo1ag9JooQebXhnfSclw3NhZyG63`) → `9c.1 … part 2` `ekyvib2rm06` (plain `ftp -pinv 185.15.211.90`, uploads `develop_marktde.zip/.csv`, credentials in the script) → `9d.0 facebook shop`. Triggered by the chain after `9b.1 Export Focus`. No run in the job history |
|
|
16
|
-
| DEV runtime defects found | (a) the Google Sheets API key in `9c.0` is **expired** — the mapping sheet
|
|
16
|
+
| DEV runtime defects found | (a) the Google Sheets API key in `9c.0` is **expired** — the mapping sheet could not load (fixed in 1.26.47: public CSV, no key); (b) the filtered dealer has 0 cars in the DEV snapshot; (c) the host has no `zip` binary — the legacy run wrote the CSV, failed the ZIP and still reported `code 0`; (d) `9c.1` uploads names the exporter never writes; (e) 185.15.211.90 is the legacy Jelastic FTP (port 21 closed, see ADV-319 4.1) |
|
|
17
17
|
| PROD | not checked (DEV-only task). Old contract / account belong to the former auto.de Media GmbH — not transferable (Daniel, 16.10) |
|
|
18
18
|
|
|
19
19
|
## 2. Source → target matrix (columns the feed relies on)
|
package/docs/marktde/RUNBOOK.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
| Param | Default | Meaning |
|
|
6
6
|
|---|---|---|
|
|
7
|
-
| `apikey` | — |
|
|
7
|
+
| `apikey` | — | no longer used for the mapping (since 1.26.47 the tab `mobile_yescar` is read from the sheet's public CSV, no key); the sheet must stay readable by everyone with the link. The DEV 9c.0 key is expired (12.11.2026) |
|
|
8
8
|
| `String` / `Number` / `Date` | — | Cronicle filter rules (DEV 9c.0: one dealer, 0 cars today) |
|
|
9
9
|
| `marktdeDryRun` | dry-run | only the exact text `false` makes a live run |
|
|
10
10
|
| `marktdeMaxImages` | no cut | cut the image list (no markt.de limit known) |
|
|
@@ -42,7 +42,7 @@ the API with `chain: ""` (see below) or from the UI with the chain removed for t
|
|
|
42
42
|
D=/home/deploy/export-cars-<ver>-dryrun/<name> # job.json (chmod 600) + credentials + resources/cars-export symlinks
|
|
43
43
|
NODE_PATH=<pkg>/dist:/opt/cronicle/node_modules node <pkg>/dist/main.js < job.json > run.log
|
|
44
44
|
```
|
|
45
|
-
|
|
45
|
+
Before 1.26.47 (Sheets API key expired): `node harness.js <pkg> < job.json` — the harness runs the same
|
|
46
46
|
`main.js` but loads the mapping rules from the public CSV of the same sheet tab (gviz) with the same
|
|
47
47
|
post-processing as `MappingService`.
|
|
48
48
|
|
|
@@ -50,7 +50,7 @@ post-processing as `MappingService`.
|
|
|
50
50
|
|
|
51
51
|
1. Signed contract / account for auto.de GmbH, current markt.de feed spec + test importer
|
|
52
52
|
(`PARTNER-QUESTIONS.md`); written acceptance of a test import of the 20-row sample.
|
|
53
|
-
2.
|
|
53
|
+
2. Keep the mapping sheet shared with everyone who has the link (1.26.47 reads its public CSV; a private sheet fails the run).
|
|
54
54
|
3. Dealer scope (the rule in 9c.0) and `feedGuardMinPublishedItems` from a PROD dry-run.
|
|
55
55
|
4. Replace `9c.1`: transport and file names from the spec, credentials out of the script; never the
|
|
56
56
|
legacy Jelastic host. Only then `marktdeDryRun=false`.
|
package/docs/marktde/TRACKING.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
Status 12.11.2026: DEV only. Implemented: the clickout link (export-cars 1.26.45) and the reporting
|
|
4
4
|
channel `CLASSIFIEDS_MARKTDE` (web-funnel-tracking-health 1.5.6). Not implemented, by design, until
|
|
5
5
|
markt.de confirms a path in writing: partner click ids, hosted-enquiry ingestion, cost import.
|
|
6
|
+
13.11.2026: DEV end-to-end lead test (link → VDP → enquiry → Odoo REQ → session / lead views), see QA.
|
|
7
|
+
Customer-SQL proposal (not applied): `release-runbooks/adv-328/ADV-330-customer-sql-proposal.sql`.
|
|
6
8
|
|
|
7
9
|
## Evidence
|
|
8
10
|
|
|
@@ -23,7 +25,8 @@ markt.de listing ──(structured field OM)──► https://www.auto.de/search
|
|
|
23
25
|
?source=marktde&medium=listing&campaign=listing&utm_source=marktde&utm_medium=listing&utm_campaign=listing
|
|
24
26
|
→ fe-main marketing cookie (ADV-260) → visitor session, landing_url keeps the raw query
|
|
25
27
|
→ views: reporting_channel CLASSIFIEDS_MARKTDE, family Classifieds, detail markt.de, publisher MARKTDE
|
|
26
|
-
→ form → REQ → Odoo crm_source "marktde" (lower-case source, no SQL change needed
|
|
28
|
+
→ form → REQ → Odoo crm_source "marktde" (lower-case source, no SQL change needed; VDP enquiries
|
|
29
|
+
need the user-actions-contact ADV-327 cookie fallback — DEV since 13.11, not on PROD)
|
|
27
30
|
→ rpt_episode_touchpoint → canonical 90-day episode (original episode_start_date) → sale / CM
|
|
28
31
|
```
|
|
29
32
|
|
|
@@ -63,7 +66,10 @@ stay unchanged (live report layout changes need approval).
|
|
|
63
66
|
| `utm_source=markt.de`, other medium (`cpc`) | CLASSIFIEDS_MARKTDE, not PAID_OTHER | unit test ✓ |
|
|
64
67
|
| Google click id + markt.de utm | click id wins (PAID_SEARCH_GOOGLE) | rule order test ✓ |
|
|
65
68
|
| no consent / revoked | no marketing cookie → lead without source (unchanged behaviour) | not run (needs browser) |
|
|
66
|
-
|
|
|
69
|
+
| VDP enquiry → REQ → Odoo source (cookie only, no body marketing) | utm source from the marketing cookie | ✅ 13.11 DEV: REQ584786 `marktde / listing / listing`, REQ584785 `classifieds / listing / marktde` (raw, legacy link) |
|
|
70
|
+
| same visits in the views | session CLASSIFIEDS_MARKTDE / Classifieds / markt.de / MARKTDE, VISITOR, lead_persisted; lead view REQ joined, crm_status CONFIRMED | ✅ 13.11 DEV (`testmk3-new-1794562402`, `testmk3-legacy-1794562402`) |
|
|
71
|
+
| producer label in the lead view (`resolved_channel`) | not the reporting channel (message-schemes knows no partner classifieds; REFERRAL with the markt.de referrer) — report by the session's `reporting_channel` | documented, unchanged (same for meinestadt / AutoUncle) |
|
|
72
|
+
| episode → sale / CM | crm_source marktde → `source_resolved_v1 = marktde`; legacy link needs the proposed D-rule; episode keeps original start | not run: prod customer SQL; DEV `crm_source_resolved_v1` empty (DEV CRM export stale since 29.10) |
|
|
67
73
|
| partner id, hosted leads, invoice import, credits, double import | — | blocked on partner spec |
|
|
68
74
|
|
|
69
75
|
Launch blockers: contract (auto.de GmbH), clickout permission, spec, legal / data QA, feed + image tests.
|
|
@@ -52,3 +52,20 @@ only — we need the CURRENT automotive specification in writing.
|
|
|
52
52
|
20. Update frequency and timing; full file each time or deltas? How do you remove listings that are no longer
|
|
53
53
|
in the file (sold / withdrawn), and how fast?
|
|
54
54
|
21. Can we run a test import first and get the parsed item count and rejection report?
|
|
55
|
+
|
|
56
|
+
## Measurement and reporting (ADV-327)
|
|
57
|
+
|
|
58
|
+
22. Daily click report: please send the latest original `meinestadt-daily_reporting_AUTODE_<date>.csv` and its
|
|
59
|
+
field definitions — separator, encoding, decimal format, timezone of the report date, currency, net/gross.
|
|
60
|
+
23. Which clicks are billable vs non-billable (bots, duplicates, invalid clicks)? How are corrections, credit
|
|
61
|
+
notes and late revisions delivered (new file for the same day, separate file, invoice only)?
|
|
62
|
+
24. Report delivery: e-mail attachment, SFTP, URL or API? Recipient / account? What happens on a day without
|
|
63
|
+
clicks (empty file or no file)?
|
|
64
|
+
25. Is there a budget cap per day / month, and does the report show when it was reached?
|
|
65
|
+
26. Do you append your own click or listing identifier to the `listingUrl` (name and format)? If not, we
|
|
66
|
+
reconcile in aggregate per day only.
|
|
67
|
+
27. Do you offer or require a conversion pixel, postback or server-to-server event? We send nothing back today
|
|
68
|
+
and would only add a partner-confirmed event with a consent basis and a DPA.
|
|
69
|
+
28. Do you redirect through your own tracking host before our page, and does the redirect keep all query
|
|
70
|
+
parameters?
|
|
71
|
+
29. Data retention and consent requirements on your side for the click data in the report.
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebrand/export-cars",
|
|
3
|
-
"version": "1.26.
|
|
3
|
+
"version": "1.26.48",
|
|
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/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/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"
|
|
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
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@bytebrand/autode-request": "^1.19.2",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"bottleneck": "^2.19.5",
|
|
35
35
|
"cache-manager": "^5.2.4",
|
|
36
36
|
"csv-write-stream": "^2.0.0",
|
|
37
|
+
"fast-xml-parser": "4.2.5",
|
|
37
38
|
"google-spreadsheet": "^3.3.0",
|
|
38
39
|
"lodash": "^4.17.21",
|
|
39
40
|
"moment": "^2.29.4",
|
|
@@ -1,15 +1,65 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { ExportType,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
import { toNumber } from 'lodash';
|
|
2
|
+
|
|
3
|
+
import { ExportType, 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 / meinestadt / markt.de). minPublishedItems only forbids an
|
|
16
|
+
// empty file; before a live run set it from a PROD dry-run (see docs/ooyyo/RUNBOOK.md).
|
|
17
|
+
export const OOYYO_DEFAULTS = {
|
|
18
|
+
imageConcurrency: 16,
|
|
19
|
+
minSourceItems: 1000,
|
|
20
|
+
minPublishedItems: 1,
|
|
21
|
+
maxDropPercent: 20,
|
|
22
|
+
maxSourceAgeHours: 26,
|
|
23
|
+
maxStaleHours: 48,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* OOYYO vehicle XML (ADV-331): one full-inventory XML + the same file gzipped (`.xml.gz`), the two
|
|
28
|
+
* forms OOYYO accepts from a fixed HTTPS URL (partner, 16.10.2026; FTP not recommended).
|
|
29
|
+
* Dry-run is the default: the files go to resources/ooyyo-dryrun, which no Cronicle step reads and
|
|
30
|
+
* nothing serves. Only ooyyoDryRun=false writes resources/ooyyo-export. The legacy
|
|
31
|
+
* autode-ooyyo.tar.gz (FTP package) is no longer produced. Each mode has its own guard state.
|
|
32
|
+
*/
|
|
33
|
+
export class OoyyoConfig extends Config {
|
|
34
|
+
dryRun: boolean;
|
|
35
|
+
imageConcurrency: number;
|
|
36
|
+
// 0 = no limit (OOYYO accepts all pictures; 20 is only a ranking recommendation)
|
|
37
|
+
maxImages: number;
|
|
38
|
+
stateDir: string;
|
|
39
|
+
feedGuard: FeedGuardOptions;
|
|
40
|
+
|
|
41
|
+
constructor(params: Params) {
|
|
42
|
+
super(params);
|
|
43
|
+
this.campaign = ExportType.OOYYO;
|
|
44
|
+
this.dryRun = text(params.ooyyoDryRun).toLowerCase() !== 'false';
|
|
45
|
+
const mode = this.dryRun ? 'dryrun' : 'export';
|
|
46
|
+
this.pathesConfig.localConvertedDirectory = `resources/${ExportType.OOYYO}-${mode}`;
|
|
47
|
+
this.stateDir = `resources/${ExportType.OOYYO}-state/${mode}`;
|
|
48
|
+
this.imageType = ImageType.clean;
|
|
49
|
+
this.imageConcurrency = positiveNumber(params.ooyyoImageConcurrency, OOYYO_DEFAULTS.imageConcurrency);
|
|
50
|
+
this.maxImages = Math.floor(positiveNumber(params.ooyyoMaxImages, 0));
|
|
51
|
+
this.feedGuard = {
|
|
52
|
+
stateDir: this.stateDir,
|
|
53
|
+
minSourceItems: positiveNumber(params.feedGuardMinSourceItems, OOYYO_DEFAULTS.minSourceItems),
|
|
54
|
+
minPublishedItems: positiveNumber(params.feedGuardMinPublishedItems, OOYYO_DEFAULTS.minPublishedItems),
|
|
55
|
+
maxDropPercent: positiveNumber(params.feedGuardMaxDropPercent, OOYYO_DEFAULTS.maxDropPercent),
|
|
56
|
+
maxSourceAgeHours: positiveNumber(params.feedGuardMaxSourceAgeHours, OOYYO_DEFAULTS.maxSourceAgeHours),
|
|
57
|
+
maxStaleHours: positiveNumber(params.feedGuardMaxStaleHours, OOYYO_DEFAULTS.maxStaleHours),
|
|
58
|
+
acceptDrop: text(params.feedGuardAcceptDrop).toLowerCase() === 'true',
|
|
59
|
+
};
|
|
60
|
+
this.outputFiles = [
|
|
61
|
+
`${this.pathesConfig.localConvertedDirectory}/${ExportType.OOYYO}.xml`,
|
|
62
|
+
`${this.pathesConfig.localConvertedDirectory}/${ExportType.OOYYO}.xml.gz`,
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
export const FB_MAX_TRIM_LENGTH = 50;
|
|
2
|
-
export const FB_MAX_IMAGE_COUNT_FOR_DEAFULT = 47;
|
|
3
|
-
export const FB_MIN_MILEAGES = 900;
|
|
4
|
-
export const DEFAULT_ZIP = '06796';
|
|
5
|
-
export const DEFAULT_CITY = 'Sandersdorf-Brehna';
|
|
6
|
-
export const DEFAULT_ADDR1 = 'Max-Planck-Str. 19';
|
|
7
|
-
export const TIMESTAMP_FIRST_YEAR = 1970;
|
|
8
|
-
export const META_PUSH_PRIORITY_STANDARD = 'standard';
|
|
9
|
-
export const META_PUSH_PRIORITY_ALLOWED_VALUES = ['dealer_priority_1', 'dealer_priority_2'];
|
|
10
|
-
|
|
11
|
-
// MarketingImageExportConfig channel code for the Meta AIA overlay images (image-generator's
|
|
12
|
-
// MetaAiaImagesService.CHANNEL_CODE) — distinct from this feed's now-disabled legacy "fb" config.
|
|
13
|
-
export const META_AIA_CHANNEL_CODE = 'meta_aia';
|
|
14
|
-
// Only the financing variant exists so far; leasing/other modes will be added once image-generator
|
|
15
|
-
// renders them too (see MetaAiaImagesService's MetaAiaMode).
|
|
16
|
-
export const META_AIA_MODE_FINANCING = 'financing';
|
|
17
|
-
|
|
18
|
-
// Values must match Meta's Automotive Model Ads offer_type enum exactly (lease/finance/cash) —
|
|
19
|
-
// the old 'financing'/'leasing'/'buy' values are not recognized by the AMA Vehicle Offers Feed.
|
|
20
|
-
export enum OfferType {
|
|
21
|
-
financing = 'finance',
|
|
22
|
-
leasing = 'lease',
|
|
23
|
-
buy = 'cash',
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const AMOUNT_QUALIFIER_APR = 'APR';
|
|
27
|
-
export const TERM_QUALIFIER_MONTHS = 'months';
|
|
28
|
-
|
|
29
|
-
export const OFFER_DISCLAIMER = 'Für Ihre Finanzierungswünsche stellen wir eine Bonitätsanfrage. Bonität ' +
|
|
30
|
-
'vorausgesetzt, ist dies ein repräsentatives Berechnungsbeispiel gem. der Angaben, welches 2/3 aller Kunden, ' +
|
|
31
|
-
'im Sinne des § 17a Abs. 4 PAngV, erhalten. Dieses freibleibende Angebot der Openbank Deutschland AG, ' +
|
|
32
|
-
'Santander-Platz 1, 41061 Mönchengladbach wird vermittelt durch die auto.de GmbH, Max-Planck-Str. 19, 06796 ' +
|
|
33
|
-
'Sandersdorf-Brehna, die als ungebundener Vermittler nicht beratend tätig ist. Irrtümer vorbehalten. Preise ' +
|
|
34
|
-
'ggf. inkl. MwSt.';
|
|
1
|
+
export const FB_MAX_TRIM_LENGTH = 50;
|
|
2
|
+
export const FB_MAX_IMAGE_COUNT_FOR_DEAFULT = 47;
|
|
3
|
+
export const FB_MIN_MILEAGES = 900;
|
|
4
|
+
export const DEFAULT_ZIP = '06796';
|
|
5
|
+
export const DEFAULT_CITY = 'Sandersdorf-Brehna';
|
|
6
|
+
export const DEFAULT_ADDR1 = 'Max-Planck-Str. 19';
|
|
7
|
+
export const TIMESTAMP_FIRST_YEAR = 1970;
|
|
8
|
+
export const META_PUSH_PRIORITY_STANDARD = 'standard';
|
|
9
|
+
export const META_PUSH_PRIORITY_ALLOWED_VALUES = ['dealer_priority_1', 'dealer_priority_2'];
|
|
10
|
+
|
|
11
|
+
// MarketingImageExportConfig channel code for the Meta AIA overlay images (image-generator's
|
|
12
|
+
// MetaAiaImagesService.CHANNEL_CODE) — distinct from this feed's now-disabled legacy "fb" config.
|
|
13
|
+
export const META_AIA_CHANNEL_CODE = 'meta_aia';
|
|
14
|
+
// Only the financing variant exists so far; leasing/other modes will be added once image-generator
|
|
15
|
+
// renders them too (see MetaAiaImagesService's MetaAiaMode).
|
|
16
|
+
export const META_AIA_MODE_FINANCING = 'financing';
|
|
17
|
+
|
|
18
|
+
// Values must match Meta's Automotive Model Ads offer_type enum exactly (lease/finance/cash) —
|
|
19
|
+
// the old 'financing'/'leasing'/'buy' values are not recognized by the AMA Vehicle Offers Feed.
|
|
20
|
+
export enum OfferType {
|
|
21
|
+
financing = 'finance',
|
|
22
|
+
leasing = 'lease',
|
|
23
|
+
buy = 'cash',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const AMOUNT_QUALIFIER_APR = 'APR';
|
|
27
|
+
export const TERM_QUALIFIER_MONTHS = 'months';
|
|
28
|
+
|
|
29
|
+
export const OFFER_DISCLAIMER = 'Für Ihre Finanzierungswünsche stellen wir eine Bonitätsanfrage. Bonität ' +
|
|
30
|
+
'vorausgesetzt, ist dies ein repräsentatives Berechnungsbeispiel gem. der Angaben, welches 2/3 aller Kunden, ' +
|
|
31
|
+
'im Sinne des § 17a Abs. 4 PAngV, erhalten. Dieses freibleibende Angebot der Openbank Deutschland AG, ' +
|
|
32
|
+
'Santander-Platz 1, 41061 Mönchengladbach wird vermittelt durch die auto.de GmbH, Max-Planck-Str. 19, 06796 ' +
|
|
33
|
+
'Sandersdorf-Brehna, die als ungebundener Vermittler nicht beratend tätig ist. Irrtümer vorbehalten. Preise ' +
|
|
34
|
+
'ggf. inkl. MwSt.';
|