@bytebrand/export-cars 1.22.31 → 1.23.1
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 +6 -0
- package/dist/constants/fb.constants.js +13 -1
- package/dist/constants/fb.constants.js.map +1 -1
- package/dist/exporters/facebook-shop.exporter.js +8 -2
- package/dist/exporters/facebook-shop.exporter.js.map +1 -1
- package/dist/formats/facebook-shop.builder.d.ts +0 -2
- package/dist/formats/facebook-shop.builder.js +11 -33
- package/dist/formats/facebook-shop.builder.js.map +1 -1
- package/dist/interfaces/export-item.interface.d.ts +17 -6
- package/dist/transformers/facebook-shop.transformer.d.ts +13 -6
- package/dist/transformers/facebook-shop.transformer.js +161 -37
- 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 +2 -1
- package/src/constants/fb.constants.ts +13 -0
- package/src/exporters/facebook-shop.exporter.ts +9 -2
- package/src/formats/facebook-shop.builder.ts +12 -35
- package/src/interfaces/export-item.interface.ts +19 -3
- package/src/transformers/facebook-shop.transformer.ts +265 -93
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebrand/export-cars",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"author": "bytebrand",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"@bytebrand/car-schema": "^3.14.15",
|
|
20
20
|
"@bytebrand/chunkable": "^5.19.8",
|
|
21
21
|
"@bytebrand/cronicle-helper": "^1.21.8",
|
|
22
|
+
"@bytebrand/fe-financing": "^5.14.10",
|
|
22
23
|
"@bytebrand/i18n-dictionaries": "^1.5.0",
|
|
23
24
|
"@bytebrand/import-helper": "^0.45.0",
|
|
24
25
|
"@bytebrand/schema-tools": "^3.18.8",
|
|
@@ -7,3 +7,16 @@ export const DEFAULT_ADDR1 = 'Max-Planck-Str. 19';
|
|
|
7
7
|
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
|
+
|
|
11
|
+
export enum OfferType {
|
|
12
|
+
financing = 'financing',
|
|
13
|
+
leasing = 'leasing',
|
|
14
|
+
buy = 'buy',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const OFFER_DISCLAIMER = 'Für Ihre Finanzierungswünsche stellen wir eine Bonitätsanfrage. Bonität ' +
|
|
18
|
+
'vorausgesetzt, ist dies ein repräsentatives Berechnungsbeispiel gem. der Angaben, welches 2/3 aller Kunden, ' +
|
|
19
|
+
'im Sinne des § 17a Abs. 4 PAngV, erhalten. Dieses freibleibende Angebot der Santander Consumer Bank AG, ' +
|
|
20
|
+
'Santander-Platz 1, 41061 Mönchengladbach wird vermittelt durch die auto.de GmbH, Max-Planck-Str. 19, 06796 ' +
|
|
21
|
+
'Sandersdorf-Brehna, die als ungebundener Vermittler nicht beratend tätig ist. Irrtümer vorbehalten. Preise ' +
|
|
22
|
+
'ggf. inkl. MwSt.';
|
|
@@ -36,10 +36,17 @@ export class FacebookShopExporter implements Exporter {
|
|
|
36
36
|
const data = chunk.data[i];
|
|
37
37
|
|
|
38
38
|
// console.time(`Transform ${data.car._id}`);
|
|
39
|
-
const
|
|
39
|
+
const items = await facebookShopTransformer.transform(data);
|
|
40
40
|
// console.timeEnd(`Transform ${data.car._id}`);
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
if (!items.length) {
|
|
43
|
+
console.log(`Skip car ${data.car._id} because it has no real images`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const item of items) {
|
|
48
|
+
await facebookShopXmkBuilder.addItem(item);
|
|
49
|
+
}
|
|
43
50
|
this.carService.counter.countCar(data.images);
|
|
44
51
|
}
|
|
45
52
|
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
+
import { escape, isNumber, isString } from 'lodash';
|
|
1
2
|
import { XMLBuilderBase } from '@bytebrand/cronicle-helper';
|
|
2
3
|
import { FacebookShopItem, FacebookShopTag } from '../interfaces';
|
|
3
|
-
import { Description, DescriptionValue } from '../interfaces/description.interface';
|
|
4
|
-
import { isNotEmptyArray } from '../utils/validator.utils';
|
|
5
4
|
|
|
6
5
|
export class FacebookShopXmlBuilder extends XMLBuilderBase {
|
|
7
6
|
async buildStart(): Promise<void> {
|
|
@@ -18,11 +17,7 @@ export class FacebookShopXmlBuilder extends XMLBuilderBase {
|
|
|
18
17
|
const tags = Object.keys(car);
|
|
19
18
|
|
|
20
19
|
for (const tag of tags) {
|
|
21
|
-
|
|
22
|
-
item += this.getDescriptionElement(tag, car[tag].value);
|
|
23
|
-
} else {
|
|
24
|
-
item += this.buildTagElement(tag, car[tag]);
|
|
25
|
-
}
|
|
20
|
+
item += this.buildTagElement(tag, car[tag]);
|
|
26
21
|
}
|
|
27
22
|
item += '</item>';
|
|
28
23
|
|
|
@@ -30,36 +25,18 @@ export class FacebookShopXmlBuilder extends XMLBuilderBase {
|
|
|
30
25
|
}
|
|
31
26
|
|
|
32
27
|
buildTagElement(tag: string, params: FacebookShopTag) {
|
|
28
|
+
// getElement() treats falsy values as "missing" — stringify numbers first so a
|
|
29
|
+
// legitimate 0 (e.g. down_payment/apr) still renders instead of an empty tag.
|
|
30
|
+
let value = params.value;
|
|
31
|
+
if (isString(value)) {
|
|
32
|
+
value = escape(value);
|
|
33
|
+
} else if (isNumber(value)) {
|
|
34
|
+
value = String(value);
|
|
35
|
+
}
|
|
33
36
|
if (params.isGoogle) {
|
|
34
|
-
return this.getElement(`g:${tag}`,
|
|
37
|
+
return this.getElement(`g:${tag}`, value);
|
|
35
38
|
}
|
|
36
|
-
return this.getElement(tag,
|
|
37
|
-
}
|
|
38
|
-
private getDescriptionElement(rootTag: string, descriptionBlocks: Description[]): string {
|
|
39
|
-
const description: string = descriptionBlocks
|
|
40
|
-
.map(({ title, values }: Description) => {
|
|
41
|
-
if (values) {
|
|
42
|
-
return `${title}\n${this.buildDescriptionValues(values)} `;
|
|
43
|
-
}
|
|
44
|
-
return '';
|
|
45
|
-
})
|
|
46
|
-
.join('\n');
|
|
47
|
-
return `<${rootTag}>${description}</${rootTag}>`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
private buildDescriptionValues(values: DescriptionValue[]): string {
|
|
51
|
-
const validValues = [];
|
|
52
|
-
|
|
53
|
-
values.forEach(element => {
|
|
54
|
-
const filteredValues = element.value.filter(el => el);
|
|
55
|
-
if (isNotEmptyArray(filteredValues)) validValues.push(element);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
return validValues
|
|
59
|
-
.map(({ title, value }: DescriptionValue) => {
|
|
60
|
-
return `${title}: ${value.join(', ')}`;
|
|
61
|
-
})
|
|
62
|
-
.join(';\n');
|
|
39
|
+
return this.getElement(tag, value);
|
|
63
40
|
}
|
|
64
41
|
|
|
65
42
|
async buildEnd(): Promise<void> {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { DataSchemes } from '@bytebrand/car-schema';
|
|
2
2
|
|
|
3
3
|
import { IAtributeWrapItem } from './atributes-wrap-item.intreface';
|
|
4
|
-
import { Description } from './description.interface';
|
|
5
4
|
|
|
6
5
|
export interface FbXmlItem {
|
|
7
6
|
url: string;
|
|
@@ -348,15 +347,32 @@ export interface dealerItem {
|
|
|
348
347
|
|
|
349
348
|
export interface FacebookShopItem {
|
|
350
349
|
id: FacebookShopTag;
|
|
350
|
+
vehicle_id: FacebookShopTag;
|
|
351
351
|
title: FacebookShopTag;
|
|
352
|
-
description:
|
|
352
|
+
description: FacebookShopTag;
|
|
353
353
|
availability: FacebookShopTag;
|
|
354
354
|
condition: FacebookShopTag;
|
|
355
355
|
price: FacebookShopTag;
|
|
356
356
|
link: FacebookShopTag;
|
|
357
357
|
image_link: FacebookShopTag;
|
|
358
358
|
additional_image_link: FacebookShopTag;
|
|
359
|
-
|
|
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
|
+
|
|
367
|
+
offer_type: FacebookShopTag;
|
|
368
|
+
offer_disclaimer?: FacebookShopTag;
|
|
369
|
+
monthly_payment?: FacebookShopTag;
|
|
370
|
+
down_payment?: FacebookShopTag;
|
|
371
|
+
term_length?: FacebookShopTag;
|
|
372
|
+
apr?: FacebookShopTag;
|
|
373
|
+
final_payment?: FacebookShopTag;
|
|
374
|
+
total_amount?: FacebookShopTag;
|
|
375
|
+
net_loan_amount?: FacebookShopTag;
|
|
360
376
|
|
|
361
377
|
quantity_to_sell_on_facebook: FacebookShopTag;
|
|
362
378
|
google_product_category: FacebookShopTag;
|
|
@@ -1,93 +1,265 @@
|
|
|
1
|
-
import { Injectable } from '@nestjs/common';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import { get, round, toString } from 'lodash';
|
|
4
|
+
import * as moment from 'moment';
|
|
5
|
+
|
|
6
|
+
import { de, deVehicleDetailsPage, deVehicleProps } from '@bytebrand/i18n-dictionaries';
|
|
7
|
+
import { ImboImageResponseInterface } from '@bytebrand/chunkable';
|
|
8
|
+
import { TranslatorExtended } from '@bytebrand/schema-tools';
|
|
9
|
+
import { DataSchemes } from '@bytebrand/car-schema';
|
|
10
|
+
import { getComputedFinancing, normalizeRateObject } from '@bytebrand/fe-financing';
|
|
11
|
+
|
|
12
|
+
import { FacebookShopItem, FacebookShopTag } from '../interfaces';
|
|
13
|
+
import { ConfigService, ImagesService, MappingService } from '../services';
|
|
14
|
+
import {
|
|
15
|
+
META_PUSH_PRIORITY_ALLOWED_VALUES,
|
|
16
|
+
META_PUSH_PRIORITY_STANDARD,
|
|
17
|
+
OFFER_DISCLAIMER,
|
|
18
|
+
OfferType,
|
|
19
|
+
} from '../constants/fb.constants';
|
|
20
|
+
|
|
21
|
+
interface OfferFinancials {
|
|
22
|
+
monthlyPayment: number;
|
|
23
|
+
downPayment: number;
|
|
24
|
+
termLength: number;
|
|
25
|
+
apr: number;
|
|
26
|
+
finalPayment: number;
|
|
27
|
+
totalAmount: number;
|
|
28
|
+
netLoanAmount: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface Offer {
|
|
32
|
+
offerType: OfferType;
|
|
33
|
+
title: string;
|
|
34
|
+
description: string;
|
|
35
|
+
disclaimer?: string;
|
|
36
|
+
financials?: OfferFinancials;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DE_LOCALE = 'de-DE';
|
|
40
|
+
|
|
41
|
+
@Injectable()
|
|
42
|
+
export class FacebookShopTransformer {
|
|
43
|
+
private translator: TranslatorExtended;
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
private readonly configService: ConfigService,
|
|
47
|
+
private readonly imageService: ImagesService,
|
|
48
|
+
private readonly mappingService: MappingService,
|
|
49
|
+
) {
|
|
50
|
+
const dict = { ...de, ...deVehicleProps, ...deVehicleDetailsPage };
|
|
51
|
+
this.translator = new TranslatorExtended(dict);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async transform({
|
|
55
|
+
car,
|
|
56
|
+
images,
|
|
57
|
+
}: {
|
|
58
|
+
car: DataSchemes.Car;
|
|
59
|
+
images: ImboImageResponseInterface[];
|
|
60
|
+
}): Promise<FacebookShopItem[]> {
|
|
61
|
+
// ignoreDefImages=true: cars with no real photos must be excluded, not padded with placeholders.
|
|
62
|
+
const imageLinks = await this.imageService.getImages(car, images, this.configService.config.imageType, true);
|
|
63
|
+
if (!imageLinks.length) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const vehicleId = toString(get(car, 'metaData.vehicleId', car._id));
|
|
68
|
+
const make = toString(get(car, 'mainData.make', ''));
|
|
69
|
+
const model = toString(get(car, 'mainData.model', ''));
|
|
70
|
+
const price = this.getPriceValue(car);
|
|
71
|
+
const ownerId = get(car, 'metaData.ownerId', null);
|
|
72
|
+
const customLabel4 = await this.getMetaPushPriority(ownerId);
|
|
73
|
+
|
|
74
|
+
const offers = this.buildOffers(car, { make, model, price });
|
|
75
|
+
if (!offers.length) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const base = {
|
|
80
|
+
vehicle_id: this.tag(vehicleId, true),
|
|
81
|
+
availability: this.tag('in stock', true),
|
|
82
|
+
condition: this.tag(this.getCondition(car), true),
|
|
83
|
+
price: this.tag(`${price} EUR`, true),
|
|
84
|
+
sale_price: this.tag(`${price} EUR`, true),
|
|
85
|
+
link: this.tag(this.getCarLink(car), true),
|
|
86
|
+
image_link: this.tag(imageLinks[0], true),
|
|
87
|
+
additional_image_link: this.tag(imageLinks[0], false),
|
|
88
|
+
make: this.tag(make, false),
|
|
89
|
+
model: this.tag(model, false),
|
|
90
|
+
year: this.tag(this.getYear(car), false),
|
|
91
|
+
user_id: this.tag(ownerId, false),
|
|
92
|
+
custom_label_4: this.tag(customLabel4, false),
|
|
93
|
+
dealer_privacy_policy_url: this.tag('https://www.auto.de/einwilligungserklaerung/', false),
|
|
94
|
+
quantity_to_sell_on_facebook: this.tag(1, true),
|
|
95
|
+
google_product_category: this.tag('916', true),
|
|
96
|
+
status: this.tag('active', true),
|
|
97
|
+
color: this.tag(this.getColor(car), false),
|
|
98
|
+
product_type: this.tag('Vehicles & Parts > Vehicles > Motor Vehicles > Cars, Trucks & Vans', true),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return offers.map((offer): FacebookShopItem => ({
|
|
102
|
+
...base,
|
|
103
|
+
id: this.tag(`${vehicleId}_${offer.offerType}`, true),
|
|
104
|
+
title: this.tag(offer.title, true),
|
|
105
|
+
description: this.tag(offer.description, true),
|
|
106
|
+
offer_type: this.tag(offer.offerType, false),
|
|
107
|
+
...(offer.disclaimer ? { offer_disclaimer: this.tag(offer.disclaimer, false) } : {}),
|
|
108
|
+
...(offer.financials ? this.financialTags(offer.financials) : {}),
|
|
109
|
+
} as FacebookShopItem));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private buildOffers(car: DataSchemes.Car, common: { make: string; model: string; price: number }): Offer[] {
|
|
113
|
+
const offers: Offer[] = [];
|
|
114
|
+
const { make, model, price } = common;
|
|
115
|
+
|
|
116
|
+
// Read the raw, already-persisted rate/leasing values before normalizeRateObject
|
|
117
|
+
// mutates car.rate — these three fields must come straight from the Car object,
|
|
118
|
+
// matching exactly what the CDP displays (per ticket: data.rate.*/data.leasing.*).
|
|
119
|
+
const offersFinancing = get(car, 'rate.offersFinancing') !== false;
|
|
120
|
+
const rawFinancing = {
|
|
121
|
+
monthlyPayment: get(car, 'rate.monthlyInstallment', 0),
|
|
122
|
+
downPayment: get(car, 'rate.firstInstallment', 0),
|
|
123
|
+
termLength: get(car, 'rate.paybackPeriod', 0),
|
|
124
|
+
};
|
|
125
|
+
const rawLeasing = {
|
|
126
|
+
monthlyPayment: get(car, 'leasing.monthlyInstallment', 0),
|
|
127
|
+
downPayment: get(car, 'leasing.firstInstallment', 0),
|
|
128
|
+
termLength: get(car, 'leasing.paybackPeriod', 0),
|
|
129
|
+
};
|
|
130
|
+
const leasingIsEligible = this.isLeasingEligible(car);
|
|
131
|
+
|
|
132
|
+
const { financing: computedFinancing, leasing: computedLeasing } = this.computeFinancials(car);
|
|
133
|
+
|
|
134
|
+
if (offersFinancing && rawFinancing.monthlyPayment && computedFinancing) {
|
|
135
|
+
offers.push({
|
|
136
|
+
offerType: OfferType.financing,
|
|
137
|
+
title: `${make} ${model} finanzieren – z. B. ab ${this.formatEUR(rawFinancing.monthlyPayment)} €/Monat`,
|
|
138
|
+
description: `${make} ${model} finanzieren – ${rawFinancing.termLength} Monate Laufzeit, ` +
|
|
139
|
+
`${this.formatEUR(rawFinancing.downPayment)} € Anzahlung`,
|
|
140
|
+
disclaimer: OFFER_DISCLAIMER,
|
|
141
|
+
financials: {
|
|
142
|
+
...rawFinancing,
|
|
143
|
+
apr: get(computedFinancing, 'annualPercentageRate', 0),
|
|
144
|
+
finalPayment: get(computedFinancing, 'finalInstallment', 0),
|
|
145
|
+
totalAmount: get(computedFinancing, 'grossLoanAmount', 0),
|
|
146
|
+
netLoanAmount: get(computedFinancing, 'netLoanAmount', 0),
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (leasingIsEligible && rawLeasing.monthlyPayment && computedLeasing) {
|
|
152
|
+
offers.push({
|
|
153
|
+
offerType: OfferType.leasing,
|
|
154
|
+
title: `${make} ${model} leasen – z. B. ab ${this.formatEUR(rawLeasing.monthlyPayment)} €/Monat`,
|
|
155
|
+
description: `${make} ${model} leasen – ${rawLeasing.termLength} Monate Laufzeit, ` +
|
|
156
|
+
`${this.formatEUR(rawLeasing.downPayment)} € Anzahlung`,
|
|
157
|
+
disclaimer: OFFER_DISCLAIMER,
|
|
158
|
+
financials: {
|
|
159
|
+
...rawLeasing,
|
|
160
|
+
apr: get(computedLeasing, 'annualPercentageRate', 0),
|
|
161
|
+
finalPayment: get(computedLeasing, 'finalInstallment', 0),
|
|
162
|
+
totalAmount: get(computedLeasing, 'grossLoanAmount', 0),
|
|
163
|
+
netLoanAmount: get(computedLeasing, 'netLoanAmount', 0),
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
offers.push({
|
|
169
|
+
offerType: OfferType.buy,
|
|
170
|
+
title: `${make} ${model} kaufen – jetzt für ${this.formatEUR(price)} €`,
|
|
171
|
+
description: `${make} ${model} kaufen – jetzt für ${this.formatEUR(price)} €`,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return offers;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Runs the same normalizeRateObject + getComputedFinancing pipeline the CDP uses, so
|
|
178
|
+
// apr/final_payment/total_amount/net_loan_amount (not stored anywhere on the Car object)
|
|
179
|
+
// match exactly what the customer sees when they click through from the ad.
|
|
180
|
+
private computeFinancials(car: DataSchemes.Car): { financing?: Record<string, any>; leasing?: Record<string, any> } {
|
|
181
|
+
try {
|
|
182
|
+
const normalized = normalizeRateObject(car);
|
|
183
|
+
if (!get(normalized, 'rate.financing')) {
|
|
184
|
+
return {};
|
|
185
|
+
}
|
|
186
|
+
const computed = getComputedFinancing(normalized.rate as any);
|
|
187
|
+
return { financing: computed.financing, leasing: computed.leasing };
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error(error);
|
|
190
|
+
return {};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Mirrors getLeasing.ts's isLeasingActive: same car ages out of leasing eligibility once
|
|
195
|
+
// it's older than leasing.maxAge months or past leasing.maxMileage km.
|
|
196
|
+
private isLeasingEligible(car: DataSchemes.Car): boolean {
|
|
197
|
+
const firstRegistration = get(car, 'mainData.firstRegistration', 0) as number;
|
|
198
|
+
const mileage = get(car, 'mainData.mileage', 0) as number;
|
|
199
|
+
const maxAge = get(car, 'leasing.maxAge', 24) as number;
|
|
200
|
+
const maxMileage = get(car, 'leasing.maxMileage', 1000000) as number;
|
|
201
|
+
|
|
202
|
+
const registrationMs = firstRegistration ? firstRegistration * 1000 : Date.now();
|
|
203
|
+
const maxCarAge = new Date().setMonth(new Date().getMonth() - maxAge);
|
|
204
|
+
|
|
205
|
+
return registrationMs > maxCarAge && maxMileage > mileage;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private financialTags(financials: OfferFinancials): Partial<FacebookShopItem> {
|
|
209
|
+
return {
|
|
210
|
+
monthly_payment: this.tag(financials.monthlyPayment, false),
|
|
211
|
+
down_payment: this.tag(financials.downPayment, false),
|
|
212
|
+
term_length: this.tag(financials.termLength, false),
|
|
213
|
+
apr: this.tag(financials.apr, false),
|
|
214
|
+
final_payment: this.tag(financials.finalPayment, false),
|
|
215
|
+
total_amount: this.tag(financials.totalAmount, false),
|
|
216
|
+
net_loan_amount: this.tag(financials.netLoanAmount, false),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private tag(value: any, isGoogle: boolean): FacebookShopTag {
|
|
221
|
+
return { value, isGoogle };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private formatEUR(value: number): string {
|
|
225
|
+
return Math.round(value || 0).toLocaleString(DE_LOCALE);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private async getMetaPushPriority(ownerId: string): Promise<string> {
|
|
229
|
+
const priorities = await this.mappingService.getMetaPushPriorityMapping();
|
|
230
|
+
const entry = priorities.find(priority => priority.dealer_id === ownerId);
|
|
231
|
+
const value = get(entry, 'meta_push_priority', '');
|
|
232
|
+
return META_PUSH_PRIORITY_ALLOWED_VALUES.includes(value) ? value : META_PUSH_PRIORITY_STANDARD;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private getYear(car: DataSchemes.Car): string {
|
|
236
|
+
const firstRegistration = get(car, 'mainData.firstRegistration', 0) as number;
|
|
237
|
+
return firstRegistration ? moment(firstRegistration * 1000).format('YYYY') : moment().format('YYYY');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private getCondition(car: DataSchemes.Car): string {
|
|
241
|
+
const available = {
|
|
242
|
+
selector_condition_new: 'new',
|
|
243
|
+
selector_condition_used: 'used',
|
|
244
|
+
selector_unknown: 'used',
|
|
245
|
+
};
|
|
246
|
+
const condition = get(car, 'mainData.condition', 'selector_unknown');
|
|
247
|
+
return get(available, condition);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private getCarLink(car: DataSchemes.Car): string {
|
|
251
|
+
return `https://${this.configService.config.domain}/search/vehicle/${car._id}`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private getPriceValue(car: DataSchemes.Car): number {
|
|
255
|
+
return round(get(car, 'price.currentSalesPrice', 0), 2);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private getColor(car: DataSchemes.Car): string {
|
|
259
|
+
let colorSelection = get(car, 'exterior.exteriorColor', 'selector_unknown');
|
|
260
|
+
if (!colorSelection || colorSelection === 'selector_unknown') {
|
|
261
|
+
colorSelection = 'selector_unknown';
|
|
262
|
+
}
|
|
263
|
+
return this.translator.translateVal(colorSelection);
|
|
264
|
+
}
|
|
265
|
+
}
|