@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.
Files changed (33) hide show
  1. package/README.md +28 -0
  2. package/dist/configs/config.d.ts +2 -1
  3. package/dist/configs/config.js +1 -0
  4. package/dist/configs/config.js.map +1 -1
  5. package/dist/configs/google.config.js +16 -0
  6. package/dist/configs/google.config.js.map +1 -1
  7. package/dist/exporters/google.exporter.js +12 -2
  8. package/dist/exporters/google.exporter.js.map +1 -1
  9. package/dist/interfaces/params.interface.d.ts +10 -0
  10. package/dist/services/publication-gate.d.ts +15 -0
  11. package/dist/services/publication-gate.js +107 -0
  12. package/dist/services/publication-gate.js.map +1 -0
  13. package/dist/transformers/google-feed-envkv.d.ts +2 -0
  14. package/dist/transformers/google-feed-envkv.js +62 -24
  15. package/dist/transformers/google-feed-envkv.js.map +1 -1
  16. package/dist/transformers/google-feed-item-row.d.ts +6 -1
  17. package/dist/transformers/google-feed-item-row.js +3 -1
  18. package/dist/transformers/google-feed-item-row.js.map +1 -1
  19. package/dist/tsconfig.build.tsbuildinfo +1 -1
  20. package/package.json +2 -2
  21. package/schema/feed-items.json +12 -0
  22. package/src/configs/config.ts +3 -1
  23. package/src/configs/google.config.ts +41 -26
  24. package/src/export.command.ts +38 -38
  25. package/src/exporters/google.exporter.ts +67 -56
  26. package/src/interfaces/params.interface.ts +43 -29
  27. package/src/services/feed-items-bigquery.writer.ts +101 -101
  28. package/src/services/publication-gate.ts +119 -0
  29. package/src/transformers/google-feed-envkv.ts +81 -25
  30. package/src/transformers/google-feed-item-row.ts +78 -71
  31. package/test/google-feed-envkv.test.ts +8 -3
  32. package/test/google-feed-item-row.test.ts +135 -121
  33. package/test/publication-gate.test.ts +86 -0
@@ -1,121 +1,135 @@
1
- // Run: npm test (ts-node, no test framework needed)
2
- import * as assert from 'node:assert/strict';
3
- import { buildGoogleFeedItemRow } from '../src/transformers/google-feed-item-row';
4
- import { googleElectricRange, googleElectricRangeSource } from '../src/transformers/google-feed-envkv';
5
-
6
- const exportedAt = new Date('2026-10-08T23:30:00.000Z');
7
-
8
- const car = (fuel: string, extra: Record<string, any> = {}): any => ({
9
- _id: 'cbd-1',
10
- metaData: { vehicleId: 'veh-1' },
11
- consumption: { fuel },
12
- engineData: 'hybridPlugin' in extra ? { hybridPlugin: extra.hybridPlugin } : {},
13
- battery: extra.battery || {},
14
- });
15
-
16
- // Shape of what GoogleTransformer returns after dropping the empty optional attributes.
17
- const item = (extra: Record<string, any> = {}): any => ({
18
- id: 'cbd-1',
19
- brand: 'VW',
20
- model: 'Golf',
21
- condition: 'Used',
22
- engine: 'Gasoline',
23
- price: '19990.5 EUR',
24
- availability: 'in_stock',
25
- ...extra,
26
- });
27
-
28
- const cases: Array<[string, () => void]> = [
29
- ['combustion car: sent values, omitted attributes are null', () => {
30
- const row = buildGoogleFeedItemRow(
31
- car('selector_fuel_petrol', { hybridPlugin: false }),
32
- item({ fuel_consumption: '5.1l/100km', co2_emissions: '118g/km' }),
33
- '12345',
34
- exportedAt,
35
- );
36
- assert.deepEqual(row, {
37
- partitionDay: '2026-10-08',
38
- exportedAt: '2026-10-08T23:30:00.000Z',
39
- feed: 'google',
40
- runId: '12345',
41
- itemId: 'cbd-1',
42
- vehicleId: 'veh-1',
43
- make: 'VW',
44
- model: 'Golf',
45
- condition: 'Used',
46
- fuel: 'selector_fuel_petrol',
47
- hybridPlugin: false,
48
- engine: 'Gasoline',
49
- electricRange: null,
50
- fuelConsumption: '5.1l/100km',
51
- fuelConsumptionDischarged: null,
52
- energyConsumption: null,
53
- co2Emissions: '118g/km',
54
- rangeSourceField: null,
55
- price: 19990.5,
56
- availability: 'in_stock',
57
- });
58
- }],
59
- ['BEV: range source is the field the range came from', () => {
60
- const bev = car('selector_fuel_electrical', { battery: { batteryRangeElectric: 400 } });
61
- const row = buildGoogleFeedItemRow(
62
- bev,
63
- item({ engine: 'Electric', electric_range: googleElectricRange(bev), energy_consumption: '16.5kWh/100km', co2_emissions: '0g/km' }),
64
- '12345',
65
- exportedAt,
66
- );
67
- assert.equal(row.electricRange, '400km');
68
- assert.equal(row.rangeSourceField, 'battery.batteryRangeElectric');
69
- assert.equal(row.hybridPlugin, null);
70
- assert.equal(row.energyConsumption, '16.5kWh/100km');
71
- assert.equal(row.co2Emissions, '0g/km');
72
- }],
73
- ['plug-in: EAER first, then WLTP range, then delivered range', () => {
74
- const phev = (battery: object) => car('selector_fuel_hybridElectricalPetrol', { hybridPlugin: true, battery });
75
- assert.equal(googleElectricRangeSource(phev({ wltpTotalRange: 62, wltpRange: 59 })), 'battery.wltpTotalRange');
76
- assert.equal(googleElectricRangeSource(phev({ wltpTotalRange: 0, wltpRange: 59 })), 'battery.wltpRange');
77
- assert.equal(googleElectricRangeSource(phev({ batteryRangeElectric: 50 })), 'battery.batteryRangeElectric');
78
- assert.equal(googleElectricRangeSource(phev({ wltpTotalRangeCity: 70 })), null);
79
- }],
80
- ['range source agrees with googleElectricRange', () => {
81
- const cars = [
82
- car('selector_fuel_electrical', { battery: { wltpRange: 451, batteryRangeElectric: 400 } }),
83
- car('selector_fuel_electrical', { battery: { wltpRange: 0 } }),
84
- car('selector_fuel_hybridElectricalPetrol', { hybridPlugin: false, battery: { wltpRange: 50 } }),
85
- car('selector_fuel_petrol', { battery: { wltpRange: 50 } }),
86
- ];
87
- for (const c of cars) {
88
- assert.equal(googleElectricRangeSource(c) === null, googleElectricRange(c) === undefined);
89
- }
90
- assert.equal(googleElectricRangeSource(cars[0]), 'battery.wltpRange');
91
- }],
92
- ['no range source when the item carries no electric_range', () => {
93
- const bev = car('selector_fuel_electrical', { battery: { wltpRange: 451 } });
94
- assert.equal(buildGoogleFeedItemRow(bev, item(), null, exportedAt).rangeSourceField, null);
95
- }],
96
- ['missing run id, vehicle id and unparsable price become null', () => {
97
- const row = buildGoogleFeedItemRow(
98
- { consumption: {} } as any,
99
- item({ price: ' EUR', availability: undefined }),
100
- undefined,
101
- exportedAt,
102
- );
103
- assert.equal(row.runId, null);
104
- assert.equal(row.vehicleId, null);
105
- assert.equal(row.fuel, null);
106
- assert.equal(row.price, null);
107
- assert.equal(row.availability, null);
108
- }],
109
- ];
110
-
111
- let failed = 0;
112
- for (const [name, fn] of cases) {
113
- try {
114
- fn();
115
- console.log(`ok ${name}`);
116
- } catch (e) {
117
- failed++;
118
- console.log(`FAIL ${name}\n ${(e as Error).message}`);
119
- }
120
- }
121
- if (failed) process.exit(1);
1
+ // Run: npm test (ts-node, no test framework needed)
2
+ import * as assert from 'node:assert/strict';
3
+ import { buildGoogleFeedItemRow } from '../src/transformers/google-feed-item-row';
4
+ import { googleElectricRange, googleElectricRangeSource } from '../src/transformers/google-feed-envkv';
5
+
6
+ const exportedAt = new Date('2026-10-08T23:30:00.000Z');
7
+
8
+ const car = (fuel: string, extra: Record<string, any> = {}): any => ({
9
+ _id: 'cbd-1',
10
+ metaData: { vehicleId: 'veh-1' },
11
+ consumption: { fuel },
12
+ engineData: 'hybridPlugin' in extra ? { hybridPlugin: extra.hybridPlugin } : {},
13
+ battery: extra.battery || {},
14
+ });
15
+
16
+ // Shape of what GoogleTransformer returns after dropping the empty optional attributes.
17
+ const item = (extra: Record<string, any> = {}): any => ({
18
+ id: 'cbd-1',
19
+ brand: 'VW',
20
+ model: 'Golf',
21
+ condition: 'Used',
22
+ engine: 'Gasoline',
23
+ price: '19990.5 EUR',
24
+ availability: 'in_stock',
25
+ ...extra,
26
+ });
27
+
28
+ const cases: Array<[string, () => void]> = [
29
+ ['combustion car: sent values, omitted attributes are null', () => {
30
+ const row = buildGoogleFeedItemRow(
31
+ car('selector_fuel_petrol', { hybridPlugin: false }),
32
+ item({ fuel_consumption: '5.1l/100km', co2_emissions: '118g/km' }),
33
+ '12345',
34
+ exportedAt,
35
+ );
36
+ assert.deepEqual(row, {
37
+ partitionDay: '2026-10-08',
38
+ exportedAt: '2026-10-08T23:30:00.000Z',
39
+ feed: 'google',
40
+ runId: '12345',
41
+ itemId: 'cbd-1',
42
+ vehicleId: 'veh-1',
43
+ make: 'VW',
44
+ model: 'Golf',
45
+ condition: 'Used',
46
+ fuel: 'selector_fuel_petrol',
47
+ hybridPlugin: false,
48
+ engine: 'Gasoline',
49
+ electricRange: null,
50
+ fuelConsumption: '5.1l/100km',
51
+ fuelConsumptionDischarged: null,
52
+ energyConsumption: null,
53
+ co2Emissions: '118g/km',
54
+ rangeSourceField: null,
55
+ price: 19990.5,
56
+ availability: 'in_stock',
57
+ publicationGate: null,
58
+ estimatedAttributes: null,
59
+ });
60
+ }],
61
+ ['publication gate: mode and estimated attributes; none estimated is null', () => {
62
+ const petrol = car('selector_fuel_petrol', { hybridPlugin: false });
63
+ const gated = buildGoogleFeedItemRow(petrol, item(), '12345', exportedAt, {
64
+ mode: 'enforce',
65
+ estimatedAttributes: ['fuel_consumption', 'co2_emissions'],
66
+ });
67
+ assert.equal(gated.publicationGate, 'enforce');
68
+ assert.equal(gated.estimatedAttributes, 'fuel_consumption,co2_emissions');
69
+ const clean = buildGoogleFeedItemRow(petrol, item(), '12345', exportedAt, { mode: 'audit', estimatedAttributes: [] });
70
+ assert.equal(clean.publicationGate, 'audit');
71
+ assert.equal(clean.estimatedAttributes, null);
72
+ }],
73
+ ['BEV: range source is the field the range came from', () => {
74
+ const bev = car('selector_fuel_electrical', { battery: { batteryRangeElectric: 400 } });
75
+ const row = buildGoogleFeedItemRow(
76
+ bev,
77
+ item({ engine: 'Electric', electric_range: googleElectricRange(bev), energy_consumption: '16.5kWh/100km', co2_emissions: '0g/km' }),
78
+ '12345',
79
+ exportedAt,
80
+ );
81
+ assert.equal(row.electricRange, '400km');
82
+ assert.equal(row.rangeSourceField, 'battery.batteryRangeElectric');
83
+ assert.equal(row.hybridPlugin, null);
84
+ assert.equal(row.energyConsumption, '16.5kWh/100km');
85
+ assert.equal(row.co2Emissions, '0g/km');
86
+ }],
87
+ ['plug-in: EAER first, then WLTP range, then delivered range', () => {
88
+ const phev = (battery: object) => car('selector_fuel_hybridElectricalPetrol', { hybridPlugin: true, battery });
89
+ assert.equal(googleElectricRangeSource(phev({ wltpTotalRange: 62, wltpRange: 59 })), 'battery.wltpTotalRange');
90
+ assert.equal(googleElectricRangeSource(phev({ wltpTotalRange: 0, wltpRange: 59 })), 'battery.wltpRange');
91
+ assert.equal(googleElectricRangeSource(phev({ batteryRangeElectric: 50 })), 'battery.batteryRangeElectric');
92
+ assert.equal(googleElectricRangeSource(phev({ wltpTotalRangeCity: 70 })), null);
93
+ }],
94
+ ['range source agrees with googleElectricRange', () => {
95
+ const cars = [
96
+ car('selector_fuel_electrical', { battery: { wltpRange: 451, batteryRangeElectric: 400 } }),
97
+ car('selector_fuel_electrical', { battery: { wltpRange: 0 } }),
98
+ car('selector_fuel_hybridElectricalPetrol', { hybridPlugin: false, battery: { wltpRange: 50 } }),
99
+ car('selector_fuel_petrol', { battery: { wltpRange: 50 } }),
100
+ ];
101
+ for (const c of cars) {
102
+ assert.equal(googleElectricRangeSource(c) === null, googleElectricRange(c) === undefined);
103
+ }
104
+ assert.equal(googleElectricRangeSource(cars[0]), 'battery.wltpRange');
105
+ }],
106
+ ['no range source when the item carries no electric_range', () => {
107
+ const bev = car('selector_fuel_electrical', { battery: { wltpRange: 451 } });
108
+ assert.equal(buildGoogleFeedItemRow(bev, item(), null, exportedAt).rangeSourceField, null);
109
+ }],
110
+ ['missing run id, vehicle id and unparsable price become null', () => {
111
+ const row = buildGoogleFeedItemRow(
112
+ { consumption: {} } as any,
113
+ item({ price: ' EUR', availability: undefined }),
114
+ undefined,
115
+ exportedAt,
116
+ );
117
+ assert.equal(row.runId, null);
118
+ assert.equal(row.vehicleId, null);
119
+ assert.equal(row.fuel, null);
120
+ assert.equal(row.price, null);
121
+ assert.equal(row.availability, null);
122
+ }],
123
+ ];
124
+
125
+ let failed = 0;
126
+ for (const [name, fn] of cases) {
127
+ try {
128
+ fn();
129
+ console.log(`ok ${name}`);
130
+ } catch (e) {
131
+ failed++;
132
+ console.log(`FAIL ${name}\n ${(e as Error).message}`);
133
+ }
134
+ }
135
+ if (failed) process.exit(1);
@@ -0,0 +1,86 @@
1
+ // Run: npm test (ts-node, no test framework needed)
2
+ import * as assert from 'node:assert/strict';
3
+ import { PublicationGate } from '../src/services/publication-gate';
4
+
5
+ const icev = (extra: Record<string, unknown> = {}): any => ({
6
+ _id: 'car-1',
7
+ consumption: { fuel: 'selector_fuel_petrol', wltpCombined: 6.1, consumptionCombined: 6.4, ...(extra.consumption as object) },
8
+ engineData: {},
9
+ battery: {},
10
+ environmentEmissions: { wltpCo2: 139, co2: 145, ...(extra.environmentEmissions as object) },
11
+ });
12
+
13
+ const plugIn = (): any => ({
14
+ _id: 'car-2',
15
+ consumption: { fuel: 'selector_fuel_hybridElectricalPetrol', wltpWeightedCombined: 0.8, wltpCombined: 6.6, wltpWeightedPowerCombined: 15.6 },
16
+ engineData: { hybridPlugin: true },
17
+ battery: { wltpRange: 101 },
18
+ environmentEmissions: { wltpCo2: 18 },
19
+ });
20
+
21
+ const gate = (mode: 'audit' | 'enforce', entries: [string, string][]) => new PublicationGate(mode, new Map(entries));
22
+
23
+ const cases: Array<[string, () => void]> = [
24
+ ['off: never estimated, never touches the item', () => {
25
+ const off = PublicationGate.off();
26
+ assert.deepEqual(off.estimatedAttributes(icev()), []);
27
+ const item = { fuel_consumption: '6.1l/100km' };
28
+ assert.deepEqual(off.apply(item, ['fuel_consumption']), { fuel_consumption: '6.1l/100km' });
29
+ }],
30
+ ['the estimate counts only for the field the attribute was taken from', () => {
31
+ // wltpCombined is sent; an estimate on the legacy fallback does not matter
32
+ assert.deepEqual(gate('audit', [['car-1|consumption.consumptionCombined', '6.4']]).estimatedAttributes(icev()), []);
33
+ assert.deepEqual(gate('audit', [['car-1|consumption.wltpCombined', '6.1']]).estimatedAttributes(icev()), ['fuel_consumption']);
34
+ }],
35
+ ['a later delivered value (stored value differs from the logged estimate) is not an estimate', () => {
36
+ assert.deepEqual(gate('audit', [['car-1|environmentEmissions.wltpCo2', '150']]).estimatedAttributes(icev()), []);
37
+ assert.deepEqual(gate('audit', [['car-1|environmentEmissions.wltpCo2', '139.0']]).estimatedAttributes(icev()), ['co2_emissions']);
38
+ }],
39
+ ['fallback field: wltpCombined missing, consumptionCombined sent and estimated', () => {
40
+ const car = icev({ consumption: { wltpCombined: null } });
41
+ assert.deepEqual(gate('audit', [['car-1|consumption.consumptionCombined', '6.4']]).estimatedAttributes(car), ['fuel_consumption']);
42
+ }],
43
+ ['other cars are not affected', () => {
44
+ assert.deepEqual(gate('audit', [['car-9|consumption.wltpCombined', '6.1']]).estimatedAttributes(icev()), []);
45
+ }],
46
+ ['confirmed plug-in: weighted values estimated, delivered range and CO2 kept', () => {
47
+ const g = gate('enforce', [
48
+ ['car-2|consumption.wltpWeightedCombined', '0.8'],
49
+ ['car-2|consumption.wltpWeightedPowerCombined', '15.6'],
50
+ ]);
51
+ const attributes = g.estimatedAttributes(plugIn());
52
+ assert.deepEqual(attributes, ['fuel_consumption', 'energy_consumption']);
53
+ const item = {
54
+ fuel_consumption: '0.8l/100km',
55
+ fuel_consumption_discharged_battery: '6.6l/100km',
56
+ energy_consumption: '15.6kWh/100km',
57
+ electric_range: '101km',
58
+ co2_emissions: '18g/km',
59
+ };
60
+ assert.deepEqual(g.apply(item, attributes), {
61
+ fuel_consumption_discharged_battery: '6.6l/100km',
62
+ electric_range: '101km',
63
+ co2_emissions: '18g/km',
64
+ });
65
+ }],
66
+ ['audit records but does not remove', () => {
67
+ const item = { fuel_consumption: '6.1l/100km' };
68
+ assert.deepEqual(gate('audit', []).apply(item, ['fuel_consumption']), { fuel_consumption: '6.1l/100km' });
69
+ }],
70
+ ['BEV CO2 0 g/km has no field behind it and is never gated', () => {
71
+ const bev: any = { _id: 'car-3', consumption: { fuel: 'selector_fuel_electrical', wltpPowerCombined: 16.8 }, engineData: {}, battery: {}, environmentEmissions: { wltpCo2: 0 } };
72
+ assert.deepEqual(gate('audit', [['car-3|environmentEmissions.wltpCo2', '0'], ['car-3|consumption.wltpPowerCombined', '16.8']]).estimatedAttributes(bev), ['energy_consumption']);
73
+ }],
74
+ ];
75
+
76
+ let failed = 0;
77
+ for (const [name, fn] of cases) {
78
+ try {
79
+ fn();
80
+ console.log(`ok ${name}`);
81
+ } catch (e) {
82
+ failed += 1;
83
+ console.error(`FAIL ${name}\n ${e.message}`);
84
+ }
85
+ }
86
+ if (failed) process.exit(1);