@bytebrand/export-cars 1.26.26 → 1.26.28
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/README.md +28 -0
- package/dist/configs/config.d.ts +2 -1
- package/dist/configs/config.js +1 -0
- package/dist/configs/config.js.map +1 -1
- package/dist/configs/google.config.js +16 -0
- package/dist/configs/google.config.js.map +1 -1
- package/dist/exporters/google.exporter.js +12 -2
- package/dist/exporters/google.exporter.js.map +1 -1
- package/dist/interfaces/params.interface.d.ts +10 -0
- package/dist/services/publication-gate.d.ts +15 -0
- package/dist/services/publication-gate.js +107 -0
- package/dist/services/publication-gate.js.map +1 -0
- package/dist/transformers/google-feed-envkv.d.ts +2 -0
- package/dist/transformers/google-feed-envkv.js +62 -24
- package/dist/transformers/google-feed-envkv.js.map +1 -1
- package/dist/transformers/google-feed-item-row.d.ts +6 -1
- package/dist/transformers/google-feed-item-row.js +3 -1
- package/dist/transformers/google-feed-item-row.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/schema/feed-items.json +12 -0
- package/src/configs/config.ts +3 -1
- package/src/configs/google.config.ts +41 -26
- package/src/export.command.ts +38 -38
- package/src/exporters/google.exporter.ts +67 -56
- package/src/interfaces/params.interface.ts +43 -29
- package/src/services/feed-items-bigquery.writer.ts +101 -101
- package/src/services/publication-gate.ts +119 -0
- package/src/transformers/google-feed-envkv.ts +81 -25
- package/src/transformers/google-feed-item-row.ts +78 -71
- package/test/google-feed-envkv.test.ts +8 -3
- package/test/google-feed-item-row.test.ts +135 -121
- package/test/publication-gate.test.ts +86 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { get } from 'lodash';
|
|
3
|
+
import { BigQueryLogger } from '@bytebrand/bigquery-logger';
|
|
4
|
+
import { DataSchemes } from '@bytebrand/car-schema';
|
|
5
|
+
import { PublicationGateConfig } from '../interfaces/params.interface';
|
|
6
|
+
import { GoogleEnvkvAttribute, googleEnvkvSourceFields } from '../transformers/google-feed-envkv';
|
|
7
|
+
|
|
8
|
+
// EnVKV stage C8 — publication gate for estimated EnVKV values in the Google feed.
|
|
9
|
+
//
|
|
10
|
+
// No car field says whether a value was delivered or estimated; car-basic-data writes every change it
|
|
11
|
+
// makes to the enrichment audit log (BigQuery). At the start of the export the gate reads the latest
|
|
12
|
+
// log row per car and field; a field counts as estimated when that row came from an estimate source
|
|
13
|
+
// and its final value is still the value stored on the car (a later import with a delivered value
|
|
14
|
+
// replaces it without a log row, so the stored value no longer matches).
|
|
15
|
+
//
|
|
16
|
+
// off no BigQuery call, the feed as before (default)
|
|
17
|
+
// audit the feed as before, the estimated attributes are recorded in the feed items table
|
|
18
|
+
// enforce estimated attributes are left out of the item (and recorded)
|
|
19
|
+
|
|
20
|
+
// finalValueSource values of CBD's enrichment log that are estimates: the CO2 estimator (EEA register /
|
|
21
|
+
// model), the make-model template sheet and the global defaults.
|
|
22
|
+
export const ESTIMATE_SOURCES = ['co2-estimate', 'mmm', 'globalDefault'];
|
|
23
|
+
|
|
24
|
+
// Every car field a Google EnVKV attribute can be taken from (google-feed-envkv picks).
|
|
25
|
+
export const GATED_FIELDS = [
|
|
26
|
+
'consumption.wltpWeightedCombined',
|
|
27
|
+
'consumption.wltpCombined',
|
|
28
|
+
'consumption.consumptionCombined',
|
|
29
|
+
'consumption.wltpWeightedPowerCombined',
|
|
30
|
+
'consumption.wltpPowerCombined',
|
|
31
|
+
'consumption.consumptionPowerCombined',
|
|
32
|
+
'environmentEmissions.wltpCo2',
|
|
33
|
+
'environmentEmissions.co2',
|
|
34
|
+
'battery.wltpTotalRange',
|
|
35
|
+
'battery.wltpRange',
|
|
36
|
+
'battery.batteryRangeElectric',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// '7.10' and 7.1 are the same value; non-numeric values compare as strings.
|
|
40
|
+
const sameValue = (logged: string, stored: unknown): boolean => {
|
|
41
|
+
if (stored === undefined || stored === null) return false;
|
|
42
|
+
const a = Number(logged);
|
|
43
|
+
const b = Number(stored);
|
|
44
|
+
if (Number.isFinite(a) && Number.isFinite(b)) return Math.abs(a - b) < 1e-9;
|
|
45
|
+
return logged === String(stored);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export class PublicationGate {
|
|
49
|
+
// estimates: `${carId}|${field}` -> final value of the latest estimate row
|
|
50
|
+
constructor(readonly mode: 'off' | 'audit' | 'enforce', private estimates: Map<string, string>) {}
|
|
51
|
+
|
|
52
|
+
static off(): PublicationGate {
|
|
53
|
+
return new PublicationGate('off', new Map());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get size(): number {
|
|
57
|
+
return this.estimates.size;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The attributes of this car whose value comes from an estimated field, in a fixed order.
|
|
61
|
+
estimatedAttributes(car: DataSchemes.Car): GoogleEnvkvAttribute[] {
|
|
62
|
+
if (this.mode === 'off') return [];
|
|
63
|
+
const carId = String(get(car, '_id', ''));
|
|
64
|
+
const attributes: GoogleEnvkvAttribute[] = [];
|
|
65
|
+
for (const [attribute, field] of Object.entries(googleEnvkvSourceFields(car))) {
|
|
66
|
+
const logged = this.estimates.get(`${carId}|${field}`);
|
|
67
|
+
if (logged !== undefined && sameValue(logged, get(car, field))) attributes.push(attribute as GoogleEnvkvAttribute);
|
|
68
|
+
}
|
|
69
|
+
return attributes;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// enforce: drops the estimated attributes from the item. Returns the item for chaining.
|
|
73
|
+
apply<T extends Partial<Record<GoogleEnvkvAttribute, string>>>(item: T, attributes: GoogleEnvkvAttribute[]): T {
|
|
74
|
+
if (this.mode !== 'enforce') return item;
|
|
75
|
+
for (const attribute of attributes) delete item[attribute];
|
|
76
|
+
return item;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class EnrichmentLogReader extends BigQueryLogger {
|
|
81
|
+
async latestEstimates(table: string): Promise<Map<string, string>> {
|
|
82
|
+
const query = `
|
|
83
|
+
SELECT carId, fieldName, finalValue
|
|
84
|
+
FROM (
|
|
85
|
+
SELECT carId, fieldName, finalValue, finalValueSource,
|
|
86
|
+
ROW_NUMBER() OVER (PARTITION BY carId, fieldName ORDER BY timestamp DESC) AS rn
|
|
87
|
+
FROM \`${table}\`
|
|
88
|
+
WHERE fieldName IN UNNEST(@fields)
|
|
89
|
+
)
|
|
90
|
+
WHERE rn = 1 AND finalValueSource IN UNNEST(@sources) AND finalValue IS NOT NULL`;
|
|
91
|
+
const [rows] = await this.client.query({ query, params: { fields: GATED_FIELDS, sources: ESTIMATE_SOURCES } });
|
|
92
|
+
const estimates = new Map<string, string>();
|
|
93
|
+
for (const row of rows) estimates.set(`${row.carId}|${row.fieldName}`, String(row.finalValue));
|
|
94
|
+
return estimates;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Reads the estimates once per run. audit: a failure only switches the gate off. enforce: a failure
|
|
99
|
+
// fails the export, so the last published feed stays in place instead of a feed with unchecked estimates.
|
|
100
|
+
export async function loadPublicationGate(config: PublicationGateConfig | undefined): Promise<PublicationGate> {
|
|
101
|
+
if (!config || config.mode === 'off') return PublicationGate.off();
|
|
102
|
+
const table = `${config.project}.${config.dataset}.${config.table}`;
|
|
103
|
+
try {
|
|
104
|
+
const credentials = require(join(process.cwd(), 'credentials/google-cloud-project-id.json'));
|
|
105
|
+
const reader = new EnrichmentLogReader(credentials, {
|
|
106
|
+
BigQueryProject: config.project,
|
|
107
|
+
BigQueryDataset: config.dataset,
|
|
108
|
+
BigQueryLogTable: config.table,
|
|
109
|
+
});
|
|
110
|
+
const estimates = await reader.latestEstimates(table);
|
|
111
|
+
console.log(`Publication gate ${config.mode}: ${estimates.size} estimated car fields from ${table}`);
|
|
112
|
+
return new PublicationGate(config.mode, estimates);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
const message = String(e?.message || e).split('\n')[0];
|
|
115
|
+
if (config.mode === 'enforce') throw new Error(`Publication gate: cannot read ${table}: ${message}`);
|
|
116
|
+
console.error(`Publication gate disabled for this run: cannot read ${table}: ${message}`);
|
|
117
|
+
return PublicationGate.off();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -70,43 +70,99 @@ const isConfirmedPlugIn = (car: DataSchemes.Car): boolean => isPlugIn(car) && !!
|
|
|
70
70
|
|
|
71
71
|
// Missing or 0 is omitted instead of sending "0l/100km". BEVs carry no fuel consumption
|
|
72
72
|
// (CBD mirrors kWh into consumptionCombined for them).
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
//
|
|
74
|
+
// Each attribute is picked from the first positive field of a list; the pick keeps the field so the
|
|
75
|
+
// publication gate (EnVKV C8) can tell whether the sent value was estimated by CBD.
|
|
76
|
+
interface Picked {
|
|
77
|
+
field: string;
|
|
78
|
+
value: number;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
const firstPositive = (car: DataSchemes.Car, fields: string[]): Picked | undefined => {
|
|
82
|
+
for (const field of fields) {
|
|
83
|
+
const value = positive(get(car, field));
|
|
84
|
+
if (value) return { field, value };
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const pickFuelConsumption = (car: DataSchemes.Car): Picked | undefined => {
|
|
90
|
+
if (isBev(car)) return undefined;
|
|
91
|
+
if (isConfirmedPlugIn(car)) return firstPositive(car, ['consumption.wltpWeightedCombined', 'consumption.consumptionCombined']);
|
|
92
|
+
if (isPlugIn(car)) return firstPositive(car, ['consumption.consumptionCombined']); // unconfirmed plug-in: as before
|
|
93
|
+
// ICE / HEV: the WLTP combined value the VDP and EnVKV use (EnVKV stage D3), the legacy field as fallback
|
|
94
|
+
return firstPositive(car, ['consumption.wltpCombined', 'consumption.consumptionCombined']);
|
|
95
|
+
};
|
|
96
|
+
|
|
81
97
|
// Confirmed plug-in: wltpCombined, no fallback (the legacy combined value is close to the weighted
|
|
82
98
|
// one, sending it here would repeat the weighted figure). Unconfirmed plug-in: the stored combined
|
|
83
99
|
// value, as before.
|
|
84
|
-
|
|
100
|
+
const pickFuelConsumptionDischarged = (car: DataSchemes.Car): Picked | undefined => {
|
|
85
101
|
if (!isPlugIn(car)) return undefined;
|
|
86
|
-
|
|
87
|
-
?
|
|
88
|
-
:
|
|
89
|
-
|
|
90
|
-
}
|
|
102
|
+
return isConfirmedPlugIn(car)
|
|
103
|
+
? firstPositive(car, ['consumption.wltpCombined'])
|
|
104
|
+
: firstPositive(car, ['consumption.consumptionCombined', 'consumption.wltpCombined']);
|
|
105
|
+
};
|
|
91
106
|
|
|
92
107
|
// BEV: combined consumption. Confirmed plug-in: weighted combined. Unconfirmed plug-in: the stored
|
|
93
108
|
// electric value, as before. Nothing for other cars.
|
|
109
|
+
const pickEnergyConsumption = (car: DataSchemes.Car): Picked | undefined => {
|
|
110
|
+
if (isConfirmedPlugIn(car)) return firstPositive(car, ['consumption.wltpWeightedPowerCombined']);
|
|
111
|
+
if (isBev(car) || isPlugIn(car)) return firstPositive(car, ['consumption.wltpPowerCombined', 'consumption.consumptionPowerCombined']);
|
|
112
|
+
return undefined;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// A BEV emits 0 g/km by definition (no field behind it); a confirmed plug-in and ICE / HEV send the
|
|
116
|
+
// WLTP value (weighted for the plug-in); an unconfirmed plug-in keeps the stored legacy value, as before.
|
|
117
|
+
const pickCo2 = (car: DataSchemes.Car): Picked | undefined => {
|
|
118
|
+
if (isBev(car)) return undefined;
|
|
119
|
+
return isPlugIn(car) && !isConfirmedPlugIn(car)
|
|
120
|
+
? firstPositive(car, ['environmentEmissions.co2'])
|
|
121
|
+
: firstPositive(car, ['environmentEmissions.wltpCo2', 'environmentEmissions.co2']);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export function googleFuelConsumption(car: DataSchemes.Car): string | undefined {
|
|
125
|
+
const picked = pickFuelConsumption(car);
|
|
126
|
+
return picked ? `${picked.value}l/100km` : undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function googleFuelConsumptionDischarged(car: DataSchemes.Car): string | undefined {
|
|
130
|
+
const picked = pickFuelConsumptionDischarged(car);
|
|
131
|
+
return picked ? `${picked.value}l/100km` : undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
94
134
|
export function googleEnergyConsumption(car: DataSchemes.Car): string | undefined {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
value = positive(get(car, 'consumption.wltpWeightedPowerCombined'));
|
|
98
|
-
} else if (isBev(car) || isPlugIn(car)) {
|
|
99
|
-
value = positive(get(car, 'consumption.wltpPowerCombined')) ?? positive(get(car, 'consumption.consumptionPowerCombined'));
|
|
100
|
-
}
|
|
101
|
-
return value ? `${value}kWh/100km` : undefined;
|
|
135
|
+
const picked = pickEnergyConsumption(car);
|
|
136
|
+
return picked ? `${picked.value}kWh/100km` : undefined;
|
|
102
137
|
}
|
|
103
138
|
|
|
104
|
-
// A
|
|
105
|
-
// the stored value. A missing or 0 value is omitted.
|
|
139
|
+
// A missing or 0 value is omitted.
|
|
106
140
|
export function googleCo2(car: DataSchemes.Car): string | undefined {
|
|
107
141
|
if (isBev(car)) return '0g/km';
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
142
|
+
const picked = pickCo2(car);
|
|
143
|
+
return picked ? `${picked.value}g/km` : undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// The EnVKV attributes of the Google item (keys of GoogleXmlItem).
|
|
147
|
+
export type GoogleEnvkvAttribute =
|
|
148
|
+
| 'fuel_consumption'
|
|
149
|
+
| 'fuel_consumption_discharged_battery'
|
|
150
|
+
| 'energy_consumption'
|
|
151
|
+
| 'electric_range'
|
|
152
|
+
| 'co2_emissions';
|
|
153
|
+
|
|
154
|
+
// For every EnVKV attribute the item sends: the car field its value came from.
|
|
155
|
+
export function googleEnvkvSourceFields(car: DataSchemes.Car): Partial<Record<GoogleEnvkvAttribute, string>> {
|
|
156
|
+
const picks: Record<GoogleEnvkvAttribute, { field: string } | undefined> = {
|
|
157
|
+
fuel_consumption: pickFuelConsumption(car),
|
|
158
|
+
fuel_consumption_discharged_battery: pickFuelConsumptionDischarged(car),
|
|
159
|
+
energy_consumption: pickEnergyConsumption(car),
|
|
160
|
+
electric_range: pickElectricRange(car),
|
|
161
|
+
co2_emissions: pickCo2(car),
|
|
162
|
+
};
|
|
163
|
+
const fields: Partial<Record<GoogleEnvkvAttribute, string>> = {};
|
|
164
|
+
for (const [attribute, picked] of Object.entries(picks)) {
|
|
165
|
+
if (picked) fields[attribute as GoogleEnvkvAttribute] = picked.field;
|
|
166
|
+
}
|
|
167
|
+
return fields;
|
|
112
168
|
}
|
|
@@ -1,71 +1,78 @@
|
|
|
1
|
-
import { get, isBoolean } from 'lodash';
|
|
2
|
-
import { DataSchemes } from '@bytebrand/car-schema';
|
|
3
|
-
import { GoogleXmlItem } from '../interfaces';
|
|
4
|
-
import { googleElectricRangeSource } from './google-feed-envkv';
|
|
5
|
-
|
|
6
|
-
// One BigQuery row per item the Google feed sent (schema/feed-items.json), so a Merchant Center
|
|
7
|
-
// issue can be traced back to the exact values of a run. The attribute strings are the ones in the
|
|
8
|
-
// XML; null means the attribute was omitted.
|
|
9
|
-
export interface FeedItemRow {
|
|
10
|
-
partitionDay: string;
|
|
11
|
-
exportedAt: string;
|
|
12
|
-
feed: string;
|
|
13
|
-
runId: string | null;
|
|
14
|
-
itemId: string;
|
|
15
|
-
vehicleId: string | null;
|
|
16
|
-
make: string | null;
|
|
17
|
-
model: string | null;
|
|
18
|
-
condition: string | null;
|
|
19
|
-
fuel: string | null;
|
|
20
|
-
hybridPlugin: boolean | null;
|
|
21
|
-
engine: string | null;
|
|
22
|
-
electricRange: string | null;
|
|
23
|
-
fuelConsumption: string | null;
|
|
24
|
-
fuelConsumptionDischarged: string | null;
|
|
25
|
-
energyConsumption: string | null;
|
|
26
|
-
co2Emissions: string | null;
|
|
27
|
-
rangeSourceField: string | null;
|
|
28
|
-
price: number | null;
|
|
29
|
-
availability: string | null;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
1
|
+
import { get, isBoolean } from 'lodash';
|
|
2
|
+
import { DataSchemes } from '@bytebrand/car-schema';
|
|
3
|
+
import { GoogleXmlItem } from '../interfaces';
|
|
4
|
+
import { googleElectricRangeSource } from './google-feed-envkv';
|
|
5
|
+
|
|
6
|
+
// One BigQuery row per item the Google feed sent (schema/feed-items.json), so a Merchant Center
|
|
7
|
+
// issue can be traced back to the exact values of a run. The attribute strings are the ones in the
|
|
8
|
+
// XML; null means the attribute was omitted.
|
|
9
|
+
export interface FeedItemRow {
|
|
10
|
+
partitionDay: string;
|
|
11
|
+
exportedAt: string;
|
|
12
|
+
feed: string;
|
|
13
|
+
runId: string | null;
|
|
14
|
+
itemId: string;
|
|
15
|
+
vehicleId: string | null;
|
|
16
|
+
make: string | null;
|
|
17
|
+
model: string | null;
|
|
18
|
+
condition: string | null;
|
|
19
|
+
fuel: string | null;
|
|
20
|
+
hybridPlugin: boolean | null;
|
|
21
|
+
engine: string | null;
|
|
22
|
+
electricRange: string | null;
|
|
23
|
+
fuelConsumption: string | null;
|
|
24
|
+
fuelConsumptionDischarged: string | null;
|
|
25
|
+
energyConsumption: string | null;
|
|
26
|
+
co2Emissions: string | null;
|
|
27
|
+
rangeSourceField: string | null;
|
|
28
|
+
price: number | null;
|
|
29
|
+
availability: string | null;
|
|
30
|
+
// EnVKV C8: gate mode of the run and the attributes whose value CBD estimated (comma-separated);
|
|
31
|
+
// in enforce mode these attributes were left out of the item.
|
|
32
|
+
publicationGate: string | null;
|
|
33
|
+
estimatedAttributes: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const orNull = (value: unknown): string | null =>
|
|
37
|
+
value === undefined || value === null || value === '' ? null : String(value);
|
|
38
|
+
|
|
39
|
+
// "12345.5 EUR" -> 12345.5
|
|
40
|
+
const sentPrice = (price: string | undefined): number | null => {
|
|
41
|
+
const n = parseFloat(price);
|
|
42
|
+
return Number.isFinite(n) ? n : null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// exportedAt is the run's start, shared by all rows of a run; partitionDay is its UTC day.
|
|
46
|
+
export function buildGoogleFeedItemRow(
|
|
47
|
+
car: DataSchemes.Car,
|
|
48
|
+
item: GoogleXmlItem,
|
|
49
|
+
runId: string | null,
|
|
50
|
+
exportedAt: Date,
|
|
51
|
+
gate: { mode: string; estimatedAttributes: string[] } | null = null,
|
|
52
|
+
): FeedItemRow {
|
|
53
|
+
const hybridPlugin = get(car, 'engineData.hybridPlugin');
|
|
54
|
+
return {
|
|
55
|
+
partitionDay: exportedAt.toISOString().slice(0, 10),
|
|
56
|
+
exportedAt: exportedAt.toISOString(),
|
|
57
|
+
feed: 'google',
|
|
58
|
+
runId: orNull(runId),
|
|
59
|
+
itemId: String(item.id),
|
|
60
|
+
vehicleId: orNull(get(car, 'metaData.vehicleId')),
|
|
61
|
+
make: orNull(item.brand),
|
|
62
|
+
model: orNull(item.model),
|
|
63
|
+
condition: orNull(item.condition),
|
|
64
|
+
fuel: orNull(get(car, 'consumption.fuel')),
|
|
65
|
+
hybridPlugin: isBoolean(hybridPlugin) ? hybridPlugin : null,
|
|
66
|
+
engine: orNull(item.engine),
|
|
67
|
+
electricRange: orNull(item.electric_range),
|
|
68
|
+
fuelConsumption: orNull(item.fuel_consumption),
|
|
69
|
+
fuelConsumptionDischarged: orNull(item.fuel_consumption_discharged_battery),
|
|
70
|
+
energyConsumption: orNull(item.energy_consumption),
|
|
71
|
+
co2Emissions: orNull(item.co2_emissions),
|
|
72
|
+
rangeSourceField: item.electric_range ? googleElectricRangeSource(car) : null,
|
|
73
|
+
price: sentPrice(item.price),
|
|
74
|
+
availability: orNull(item.availability),
|
|
75
|
+
publicationGate: gate ? gate.mode : null,
|
|
76
|
+
estimatedAttributes: gate && gate.estimatedAttributes.length ? gate.estimatedAttributes.join(',') : null,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -87,15 +87,20 @@ const cases: Array<[string, () => void]> = [
|
|
|
87
87
|
assert.equal(googleEnergyConsumption(phev), undefined);
|
|
88
88
|
assert.equal(googleCo2(phev), '32g/km');
|
|
89
89
|
}],
|
|
90
|
-
['non-plug-in hybrids and combustion cars
|
|
90
|
+
['non-plug-in hybrids and combustion cars send the WLTP values (legacy as fallback) and no electricity', () => {
|
|
91
91
|
const hev = car('selector_fuel_hybridElectricalPetrol', {
|
|
92
92
|
consumption: { consumptionCombined: 5.1, wltpCombined: 5.3, wltpWeightedCombined: 2.3, wltpWeightedPowerCombined: 21.5 },
|
|
93
93
|
environmentEmissions: { co2: 116, wltpCo2: 119 },
|
|
94
94
|
});
|
|
95
|
-
assert.equal(googleFuelConsumption(hev), '5.
|
|
95
|
+
assert.equal(googleFuelConsumption(hev), '5.3l/100km');
|
|
96
96
|
assert.equal(googleFuelConsumptionDischarged(hev), undefined);
|
|
97
97
|
assert.equal(googleEnergyConsumption(hev), undefined);
|
|
98
|
-
assert.equal(googleCo2(hev), '
|
|
98
|
+
assert.equal(googleCo2(hev), '119g/km');
|
|
99
|
+
// D3: Mercedes GLC 200 sent 146.7 g (legacy) while CBD / VDP / EnVKV hold the dealer's WLTP 138 g
|
|
100
|
+
const glc = car('selector_fuel_diesel', { consumption: { consumptionCombined: 5.6, wltpCombined: 5.2 }, environmentEmissions: { co2: 146.7, wltpCo2: 138 } });
|
|
101
|
+
assert.equal(googleCo2(glc), '138g/km');
|
|
102
|
+
assert.equal(googleFuelConsumption(glc), '5.2l/100km');
|
|
103
|
+
assert.equal(googleCo2(car('selector_fuel_diesel', { environmentEmissions: { co2: 150 } })), '150g/km');
|
|
99
104
|
}],
|
|
100
105
|
['a plug-in flag without an electric range keeps the stored values (ADV-124 flags every hybrid)', () => {
|
|
101
106
|
const flagged = car('selector_fuel_hybridElectricalPetrol', {
|