@bytebrand/export-cars 1.25.0 → 1.26.0
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/constants/fb.constants.d.ts +5 -3
- package/dist/constants/fb.constants.js +6 -4
- package/dist/constants/fb.constants.js.map +1 -1
- package/dist/exporters/facebook-shop.exporter.js +6 -4
- package/dist/exporters/facebook-shop.exporter.js.map +1 -1
- package/dist/formats/facebook-shop.builder.d.ts +3 -3
- package/dist/formats/facebook-shop.builder.js +21 -21
- package/dist/formats/facebook-shop.builder.js.map +1 -1
- package/dist/interfaces/export-item.interface.d.ts +19 -33
- package/dist/services/rules.service.d.ts +1 -1
- package/dist/transformers/facebook-shop.transformer.d.ts +0 -2
- package/dist/transformers/facebook-shop.transformer.js +20 -42
- package/dist/transformers/facebook-shop.transformer.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +1 -1
- package/src/constants/fb.constants.ts +8 -3
- package/src/exporters/facebook-shop.exporter.ts +6 -4
- package/src/formats/facebook-shop.builder.ts +59 -52
- package/src/interfaces/export-item.interface.ts +23 -35
- package/src/transformers/facebook-shop.transformer.ts +24 -46
package/package.json
CHANGED
|
@@ -8,12 +8,17 @@ export const TIMESTAMP_FIRST_YEAR = 1970;
|
|
|
8
8
|
export const META_PUSH_PRIORITY_STANDARD = 'standard';
|
|
9
9
|
export const META_PUSH_PRIORITY_ALLOWED_VALUES = ['dealer_priority_1', 'dealer_priority_2'];
|
|
10
10
|
|
|
11
|
+
// Values must match Meta's Automotive Model Ads offer_type enum exactly (lease/finance/cash) —
|
|
12
|
+
// the old 'financing'/'leasing'/'buy' values are not recognized by the AMA Vehicle Offers Feed.
|
|
11
13
|
export enum OfferType {
|
|
12
|
-
financing = '
|
|
13
|
-
leasing = '
|
|
14
|
-
buy = '
|
|
14
|
+
financing = 'finance',
|
|
15
|
+
leasing = 'lease',
|
|
16
|
+
buy = 'cash',
|
|
15
17
|
}
|
|
16
18
|
|
|
19
|
+
export const AMOUNT_QUALIFIER_APR = 'APR';
|
|
20
|
+
export const TERM_QUALIFIER_MONTHS = 'months';
|
|
21
|
+
|
|
17
22
|
export const OFFER_DISCLAIMER = 'Für Ihre Finanzierungswünsche stellen wir eine Bonitätsanfrage. Bonität ' +
|
|
18
23
|
'vorausgesetzt, ist dies ein repräsentatives Berechnungsbeispiel gem. der Angaben, welches 2/3 aller Kunden, ' +
|
|
19
24
|
'im Sinne des § 17a Abs. 4 PAngV, erhalten. Dieses freibleibende Angebot der Santander Consumer Bank AG, ' +
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Injectable } from '@nestjs/common';
|
|
2
2
|
import { ModuleRef } from '@nestjs/core';
|
|
3
|
+
import { basename } from 'path';
|
|
3
4
|
|
|
4
5
|
import { CronicleUtils } from '@bytebrand/cronicle-helper';
|
|
5
6
|
import { FsChunkInterface } from '@bytebrand/chunkable';
|
|
@@ -24,8 +25,9 @@ export class FacebookShopExporter implements Exporter {
|
|
|
24
25
|
await CronicleUtils.ensureDir(this.configService.config.pathesConfig.localConvertedDirectory);
|
|
25
26
|
await CronicleUtils.emptyDir(this.configService.config.pathesConfig.localConvertedDirectory);
|
|
26
27
|
|
|
27
|
-
const
|
|
28
|
-
|
|
28
|
+
const selfUrl = `https://ftp.${this.configService.config.domain}/feeds/${basename(fileXml)}`;
|
|
29
|
+
const facebookShopXmlBuilder = new FacebookShopXmlBuilder(fileXml);
|
|
30
|
+
await facebookShopXmlBuilder.buildStart(selfUrl);
|
|
29
31
|
|
|
30
32
|
const facebookShopTransformer = await this.moduleRef.create(FacebookShopTransformer);
|
|
31
33
|
|
|
@@ -45,13 +47,13 @@ export class FacebookShopExporter implements Exporter {
|
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
for (const item of items) {
|
|
48
|
-
await
|
|
50
|
+
await facebookShopXmlBuilder.addItem(item);
|
|
49
51
|
}
|
|
50
52
|
this.carService.counter.countCar(data.images);
|
|
51
53
|
}
|
|
52
54
|
});
|
|
53
55
|
|
|
54
|
-
await
|
|
56
|
+
await facebookShopXmlBuilder.buildEnd();
|
|
55
57
|
|
|
56
58
|
console.log(this.carService.counter.getCountedCars());
|
|
57
59
|
}
|
|
@@ -1,52 +1,59 @@
|
|
|
1
|
-
import { escape, isNumber, isString } from 'lodash';
|
|
2
|
-
import { XMLBuilderBase } from '@bytebrand/cronicle-helper';
|
|
3
|
-
import { FacebookShopItem
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
1
|
+
import { escape, isNil, isNumber, isString } from 'lodash';
|
|
2
|
+
import { XMLBuilderBase } from '@bytebrand/cronicle-helper';
|
|
3
|
+
import { FacebookShopItem } from '../interfaces';
|
|
4
|
+
|
|
5
|
+
const MAX_IMAGES = 2;
|
|
6
|
+
|
|
7
|
+
export class FacebookShopXmlBuilder extends XMLBuilderBase {
|
|
8
|
+
// No xmlns:g / g: prefixes here — that namespace belongs to the old Google Merchant /
|
|
9
|
+
// Facebook Shop product-catalog schema. Meta's Automotive Model Ads Vehicle Offers Feed
|
|
10
|
+
// uses plain, undecorated tag names (see facebook-shop.builder field reference above).
|
|
11
|
+
async buildStart(selfUrl?: string): Promise<void> {
|
|
12
|
+
await this.write(
|
|
13
|
+
'<?xml version="1.0" encoding="utf-8"?>\n<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">' +
|
|
14
|
+
'\n<channel>\n<title>auto.de Vehicle Offers Feed</title>' +
|
|
15
|
+
'\n<description>Automotive Model Ads Vehicle Offers Feed für auto.de</description>' +
|
|
16
|
+
'\n<link>https://www.auto.de/</link>' +
|
|
17
|
+
`\n<atom:link href="${escape(selfUrl)}" rel="self" type="application/rss+xml" />\n`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async addItem(car: FacebookShopItem): Promise<void> {
|
|
22
|
+
let item = '\n<item>\n';
|
|
23
|
+
|
|
24
|
+
const { image, ...tags } = car;
|
|
25
|
+
|
|
26
|
+
for (const [tag, value] of Object.entries(tags)) {
|
|
27
|
+
item += this.buildTagElement(tag, value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
item += image
|
|
31
|
+
.slice(0, MAX_IMAGES)
|
|
32
|
+
.map((url: string) => `<image><url>${escape(url)}</url></image>\n`)
|
|
33
|
+
.join('');
|
|
34
|
+
|
|
35
|
+
item += '</item>';
|
|
36
|
+
|
|
37
|
+
await this.write(item);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private buildTagElement(tag: string, value: unknown): string {
|
|
41
|
+
if (isNil(value) || value === '') {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
// getElement() treats falsy values as "missing" — stringify numbers first so a
|
|
45
|
+
// legitimate 0 (e.g. downpayment/term_length) still renders instead of an empty tag.
|
|
46
|
+
if (isString(value)) {
|
|
47
|
+
return this.getElement(tag, escape(value));
|
|
48
|
+
}
|
|
49
|
+
if (isNumber(value)) {
|
|
50
|
+
return this.getElement(tag, String(value));
|
|
51
|
+
}
|
|
52
|
+
return this.getElement(tag, value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async buildEnd(): Promise<void> {
|
|
56
|
+
await this.write('\n</channel>\n</rss>\n');
|
|
57
|
+
await this.end();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -346,45 +346,33 @@ export interface dealerItem {
|
|
|
346
346
|
};
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
// Field set matches Meta's Automotive Model Ads "Vehicle Offers Feed File" reference
|
|
350
|
+
// (https://developers.facebook.com/documentation/ads-commerce/marketing-api/auto-ads/reference#vehicle-offers-feed-file).
|
|
351
|
+
// custom_label_4 is not part of that reference but is kept per ADV-159/161 (dealer push
|
|
352
|
+
// priority) — Meta ignores catalog fields it doesn't recognize rather than rejecting the feed.
|
|
349
353
|
export interface FacebookShopItem {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
354
|
+
vehicle_offer_id: string;
|
|
355
|
+
make: string;
|
|
356
|
+
model: string;
|
|
357
|
+
year: string;
|
|
358
|
+
offer_type: string;
|
|
359
|
+
title: string;
|
|
360
|
+
offer_description: string;
|
|
361
|
+
url: string;
|
|
362
|
+
offer_disclaimer?: string;
|
|
358
363
|
image: string[];
|
|
359
|
-
make: FacebookShopTag;
|
|
360
|
-
model: FacebookShopTag;
|
|
361
|
-
year: FacebookShopTag;
|
|
362
|
-
|
|
363
|
-
user_id: FacebookShopTag;
|
|
364
|
-
custom_label_4: FacebookShopTag;
|
|
365
|
-
dealer_privacy_policy_url: FacebookShopTag;
|
|
366
364
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
term_length?: FacebookShopTag;
|
|
372
|
-
apr?: FacebookShopTag;
|
|
373
|
-
final_payment?: FacebookShopTag;
|
|
374
|
-
total_amount?: FacebookShopTag;
|
|
375
|
-
net_loan_amount?: FacebookShopTag;
|
|
376
|
-
|
|
377
|
-
quantity_to_sell_on_facebook: FacebookShopTag;
|
|
378
|
-
google_product_category: FacebookShopTag;
|
|
379
|
-
sale_price: FacebookShopTag;
|
|
380
|
-
status: FacebookShopTag;
|
|
381
|
-
color: FacebookShopTag;
|
|
382
|
-
product_type: FacebookShopTag;
|
|
383
|
-
}
|
|
365
|
+
price: string;
|
|
366
|
+
availability: string;
|
|
367
|
+
exterior_color: string;
|
|
368
|
+
custom_label_4: string;
|
|
384
369
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
370
|
+
amount_price?: string;
|
|
371
|
+
amount_percentage?: string;
|
|
372
|
+
amount_qualifier?: string;
|
|
373
|
+
term_length?: number;
|
|
374
|
+
term_qualifier?: string;
|
|
375
|
+
downpayment?: string;
|
|
388
376
|
}
|
|
389
377
|
|
|
390
378
|
export interface TrovitXmlItem {
|
|
@@ -9,13 +9,15 @@ import { TranslatorExtended } from '@bytebrand/schema-tools';
|
|
|
9
9
|
import { DataSchemes } from '@bytebrand/car-schema';
|
|
10
10
|
import { getComputedFinancing, normalizeRateObject } from '@bytebrand/fe-financing';
|
|
11
11
|
|
|
12
|
-
import { FacebookShopItem
|
|
12
|
+
import { FacebookShopItem } from '../interfaces';
|
|
13
13
|
import { ConfigService, ImagesService, MappingService } from '../services';
|
|
14
14
|
import {
|
|
15
|
+
AMOUNT_QUALIFIER_APR,
|
|
15
16
|
META_PUSH_PRIORITY_ALLOWED_VALUES,
|
|
16
17
|
META_PUSH_PRIORITY_STANDARD,
|
|
17
18
|
OFFER_DISCLAIMER,
|
|
18
19
|
OfferType,
|
|
20
|
+
TERM_QUALIFIER_MONTHS,
|
|
19
21
|
} from '../constants/fb.constants';
|
|
20
22
|
|
|
21
23
|
interface OfferFinancials {
|
|
@@ -77,35 +79,26 @@ export class FacebookShopTransformer {
|
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
const base = {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
sale_price: this.tag(`${price} EUR`, true),
|
|
85
|
-
link: this.tag(this.getCarLink(car), true),
|
|
82
|
+
make,
|
|
83
|
+
model,
|
|
84
|
+
year: this.getYear(car),
|
|
85
|
+
url: this.getCarLink(car),
|
|
86
86
|
image: imageLinks.slice(0, 2), // main + 2nd image, per ticket
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
custom_label_4: this.tag(customLabel4, false),
|
|
92
|
-
dealer_privacy_policy_url: this.tag('https://www.auto.de/einwilligungserklaerung/', false),
|
|
93
|
-
quantity_to_sell_on_facebook: this.tag(1, true),
|
|
94
|
-
google_product_category: this.tag('916', true),
|
|
95
|
-
status: this.tag('active', true),
|
|
96
|
-
color: this.tag(this.getColor(car), false),
|
|
97
|
-
product_type: this.tag('Vehicles & Parts > Vehicles > Motor Vehicles > Cars, Trucks & Vans', true),
|
|
87
|
+
price: `${price} EUR`,
|
|
88
|
+
availability: this.getAvailability(car),
|
|
89
|
+
exterior_color: this.getColor(car),
|
|
90
|
+
custom_label_4: customLabel4,
|
|
98
91
|
};
|
|
99
92
|
|
|
100
93
|
return offers.map((offer): FacebookShopItem => ({
|
|
101
94
|
...base,
|
|
102
|
-
|
|
103
|
-
title:
|
|
104
|
-
|
|
105
|
-
offer_type:
|
|
106
|
-
...(offer.disclaimer ? { offer_disclaimer:
|
|
95
|
+
vehicle_offer_id: `${vehicleId}_${offer.offerType}`,
|
|
96
|
+
title: offer.title,
|
|
97
|
+
offer_description: offer.description,
|
|
98
|
+
offer_type: offer.offerType,
|
|
99
|
+
...(offer.disclaimer ? { offer_disclaimer: offer.disclaimer } : {}),
|
|
107
100
|
...(offer.financials ? this.financialTags(offer.financials) : {}),
|
|
108
|
-
}
|
|
101
|
+
}));
|
|
109
102
|
}
|
|
110
103
|
|
|
111
104
|
private buildOffers(car: DataSchemes.Car, common: { make: string; model: string; price: number }): Offer[] {
|
|
@@ -212,20 +205,15 @@ export class FacebookShopTransformer {
|
|
|
212
205
|
|
|
213
206
|
private financialTags(financials: OfferFinancials): Partial<FacebookShopItem> {
|
|
214
207
|
return {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
net_loan_amount: this.tag(financials.netLoanAmount, false),
|
|
208
|
+
amount_price: `${financials.monthlyPayment} EUR`,
|
|
209
|
+
amount_percentage: String(financials.apr),
|
|
210
|
+
amount_qualifier: AMOUNT_QUALIFIER_APR,
|
|
211
|
+
term_length: financials.termLength,
|
|
212
|
+
term_qualifier: TERM_QUALIFIER_MONTHS,
|
|
213
|
+
downpayment: `${financials.downPayment} EUR`,
|
|
222
214
|
};
|
|
223
215
|
}
|
|
224
216
|
|
|
225
|
-
private tag(value: any, isGoogle: boolean): FacebookShopTag {
|
|
226
|
-
return { value, isGoogle };
|
|
227
|
-
}
|
|
228
|
-
|
|
229
217
|
// lodash's get() only falls back to the default for an *undefined* path — an explicit
|
|
230
218
|
// `null` stored on the Car document (a common Mongo pattern for "not set") passes
|
|
231
219
|
// straight through and ends up rendering as an empty XML tag. Coalesce it here instead.
|
|
@@ -249,18 +237,8 @@ export class FacebookShopTransformer {
|
|
|
249
237
|
return firstRegistration ? moment(firstRegistration * 1000).format('YYYY') : moment().format('YYYY');
|
|
250
238
|
}
|
|
251
239
|
|
|
252
|
-
private getCondition(car: DataSchemes.Car): string {
|
|
253
|
-
const available = {
|
|
254
|
-
selector_condition_new: 'New',
|
|
255
|
-
selector_condition_used: 'Used',
|
|
256
|
-
selector_unknown: 'Used',
|
|
257
|
-
};
|
|
258
|
-
const condition = get(car, 'mainData.condition', 'selector_unknown');
|
|
259
|
-
return get(available, condition);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
240
|
private getAvailability(car: DataSchemes.Car): string {
|
|
263
|
-
return get(car, 'state.isOnline', false) ? '
|
|
241
|
+
return get(car, 'state.isOnline', false) ? 'AVAILABLE' : 'NOT_AVAILABLE';
|
|
264
242
|
}
|
|
265
243
|
|
|
266
244
|
private getCarLink(car: DataSchemes.Car): string {
|