@bytebrand/export-cars 1.26.1 → 1.26.3

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.
@@ -1,259 +1,259 @@
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 } from '../interfaces';
13
- import { ConfigService, ImagesService, MappingService } from '../services';
14
- import {
15
- AMOUNT_QUALIFIER_APR,
16
- META_PUSH_PRIORITY_ALLOWED_VALUES,
17
- META_PUSH_PRIORITY_STANDARD,
18
- OFFER_DISCLAIMER,
19
- OfferType,
20
- TERM_QUALIFIER_MONTHS,
21
- } from '../constants/fb.constants';
22
-
23
- interface OfferFinancials {
24
- monthlyPayment: number;
25
- downPayment: number;
26
- termLength: number;
27
- apr: number;
28
- finalPayment: number;
29
- totalAmount: number;
30
- netLoanAmount: number;
31
- }
32
-
33
- interface Offer {
34
- offerType: OfferType;
35
- title: string;
36
- description: string;
37
- disclaimer?: string;
38
- financials?: OfferFinancials;
39
- }
40
-
41
- const DE_LOCALE = 'de-DE';
42
-
43
- @Injectable()
44
- export class FacebookShopTransformer {
45
- private translator: TranslatorExtended;
46
-
47
- constructor(
48
- private readonly configService: ConfigService,
49
- private readonly imageService: ImagesService,
50
- private readonly mappingService: MappingService,
51
- ) {
52
- const dict = { ...de, ...deVehicleProps, ...deVehicleDetailsPage };
53
- this.translator = new TranslatorExtended(dict);
54
- }
55
-
56
- async transform({
57
- car,
58
- images,
59
- }: {
60
- car: DataSchemes.Car;
61
- images: ImboImageResponseInterface[];
62
- }): Promise<FacebookShopItem[]> {
63
- // ignoreDefImages=true: cars with no real photos must be excluded, not padded with placeholders.
64
- const imageLinks = await this.imageService.getImages(car, images, this.configService.config.imageType, true);
65
- if (!imageLinks.length) {
66
- return [];
67
- }
68
-
69
- const vehicleId = toString(get(car, 'metaData.vehicleId', car._id));
70
- const make = toString(get(car, 'mainData.make', ''));
71
- const model = toString(get(car, 'mainData.model', ''));
72
- const price = this.getPriceValue(car);
73
- const ownerId = get(car, 'metaData.ownerId', null);
74
- const customLabel4 = await this.getMetaPushPriority(ownerId);
75
-
76
- const offers = this.buildOffers(car, { make, model, price });
77
- if (!offers.length) {
78
- return [];
79
- }
80
-
81
- const base = {
82
- make,
83
- model,
84
- year: this.getYear(car),
85
- url: this.getCarLink(car),
86
- image: imageLinks.slice(0, 2), // main + 2nd image, per ticket
87
- price: `${price} EUR`,
88
- availability: this.getAvailability(car),
89
- exterior_color: this.getColor(car),
90
- custom_label_4: customLabel4,
91
- };
92
-
93
- return offers.map((offer): FacebookShopItem => ({
94
- ...base,
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 } : {}),
100
- ...(offer.financials ? this.financialTags(offer.financials) : {}),
101
- }));
102
- }
103
-
104
- private buildOffers(car: DataSchemes.Car, common: { make: string; model: string; price: number }): Offer[] {
105
- const offers: Offer[] = [];
106
- const { make, model, price } = common;
107
-
108
- // Read the raw, already-persisted rate/leasing values before normalizeRateObject
109
- // mutates car.rate — these three fields must come straight from the Car object,
110
- // matching exactly what the CDP displays (per ticket: data.rate.*/data.leasing.*).
111
- const offersFinancing = get(car, 'rate.offersFinancing') !== false;
112
- const rawFinancing = {
113
- monthlyPayment: this.getNum(car, 'rate.monthlyInstallment'),
114
- downPayment: this.getNum(car, 'rate.firstInstallment'),
115
- termLength: this.getNum(car, 'rate.paybackPeriod'),
116
- };
117
- const leasingIsEligible = this.isLeasingEligible(car);
118
-
119
- const { financing: computedFinancing, leasing: computedLeasing } = this.computeFinancials(car);
120
-
121
- // Unlike financing, the CDP never reads a pre-stored leasing.monthlyInstallment — it
122
- // always computes it live from RWG + price (see fe-main SearchResultsStore/NewMainPage,
123
- // same isLeasingEligible formula). The raw car.leasing.monthlyInstallment field can be
124
- // stale/unset even when the CDP shows an active leasing tab, so the computed value is the
125
- // one that actually matches what the customer sees.
126
- const computedLeasingValues = {
127
- monthlyPayment: this.getNum(computedLeasing, 'monthlyInstallment'),
128
- downPayment: this.getNum(computedLeasing, 'firstInstallment'),
129
- termLength: this.getNum(computedLeasing, 'paybackPeriod'),
130
- };
131
-
132
- if (offersFinancing && rawFinancing.monthlyPayment && computedFinancing) {
133
- offers.push({
134
- offerType: OfferType.financing,
135
- title: `${make} ${model} finanzieren – z. B. ab ${this.formatEUR(rawFinancing.monthlyPayment)} €/Monat`,
136
- description: `${make} ${model} finanzieren – ${rawFinancing.termLength} Monate Laufzeit, ` +
137
- `${this.formatEUR(rawFinancing.downPayment)} € Anzahlung`,
138
- disclaimer: OFFER_DISCLAIMER,
139
- financials: {
140
- ...rawFinancing,
141
- apr: this.getNum(computedFinancing, 'annualPercentageRate'),
142
- finalPayment: this.getNum(computedFinancing, 'finalInstallment'),
143
- totalAmount: this.getNum(computedFinancing, 'grossLoanAmount'),
144
- netLoanAmount: this.getNum(computedFinancing, 'netLoanAmount'),
145
- },
146
- });
147
- }
148
-
149
- if (leasingIsEligible && computedLeasingValues.monthlyPayment && computedLeasing) {
150
- offers.push({
151
- offerType: OfferType.leasing,
152
- title: `${make} ${model} leasen – z. B. ab ${this.formatEUR(computedLeasingValues.monthlyPayment)} €/Monat`,
153
- description: `${make} ${model} leasen – ${computedLeasingValues.termLength} Monate Laufzeit, ` +
154
- `${this.formatEUR(computedLeasingValues.downPayment)} € Anzahlung`,
155
- disclaimer: OFFER_DISCLAIMER,
156
- financials: {
157
- ...computedLeasingValues,
158
- apr: this.getNum(computedLeasing, 'annualPercentageRate'),
159
- finalPayment: this.getNum(computedLeasing, 'finalInstallment'),
160
- totalAmount: this.getNum(computedLeasing, 'grossLoanAmount'),
161
- netLoanAmount: this.getNum(computedLeasing, 'netLoanAmount'),
162
- },
163
- });
164
- }
165
-
166
- offers.push({
167
- offerType: OfferType.buy,
168
- title: `${make} ${model} kaufen – jetzt für ${this.formatEUR(price)} €`,
169
- description: `${make} ${model} kaufen – jetzt für ${this.formatEUR(price)} €`,
170
- });
171
-
172
- return offers;
173
- }
174
-
175
- // Runs the same normalizeRateObject + getComputedFinancing pipeline the CDP uses, so
176
- // apr/final_payment/total_amount/net_loan_amount (not stored anywhere on the Car object)
177
- // match exactly what the customer sees when they click through from the ad.
178
- private computeFinancials(car: DataSchemes.Car): { financing?: Record<string, any>; leasing?: Record<string, any> } {
179
- try {
180
- const normalized = normalizeRateObject(car);
181
- if (!get(normalized, 'rate.financing')) {
182
- return {};
183
- }
184
- const computed = getComputedFinancing(normalized.rate as any);
185
- return { financing: computed.financing, leasing: computed.leasing };
186
- } catch (error) {
187
- console.error(error);
188
- return {};
189
- }
190
- }
191
-
192
- // Mirrors getLeasing.ts's isLeasingActive: same car ages out of leasing eligibility once
193
- // it's older than leasing.maxAge months or past leasing.maxMileage km.
194
- private isLeasingEligible(car: DataSchemes.Car): boolean {
195
- const firstRegistration = this.getNum(car, 'mainData.firstRegistration');
196
- const mileage = this.getNum(car, 'mainData.mileage');
197
- const maxAge = this.getNum(car, 'leasing.maxAge', 24);
198
- const maxMileage = this.getNum(car, 'leasing.maxMileage', 1000000);
199
-
200
- const registrationMs = firstRegistration ? firstRegistration * 1000 : Date.now();
201
- const maxCarAge = new Date().setMonth(new Date().getMonth() - maxAge);
202
-
203
- return registrationMs > maxCarAge && maxMileage > mileage;
204
- }
205
-
206
- private financialTags(financials: OfferFinancials): Partial<FacebookShopItem> {
207
- return {
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`,
214
- };
215
- }
216
-
217
- // lodash's get() only falls back to the default for an *undefined* path — an explicit
218
- // `null` stored on the Car document (a common Mongo pattern for "not set") passes
219
- // straight through and ends up rendering as an empty XML tag. Coalesce it here instead.
220
- private getNum(source: any, path: string, fallback = 0): number {
221
- return get(source, path) ?? fallback;
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 = this.getNum(car, 'mainData.firstRegistration');
237
- return firstRegistration ? moment(firstRegistration * 1000).format('YYYY') : moment().format('YYYY');
238
- }
239
-
240
- private getAvailability(car: DataSchemes.Car): string {
241
- return get(car, 'state.isOnline', false) ? 'AVAILABLE' : 'NOT_AVAILABLE';
242
- }
243
-
244
- private getCarLink(car: DataSchemes.Car): string {
245
- return `https://${this.configService.config.domain}/search/vehicle/${car._id}`;
246
- }
247
-
248
- private getPriceValue(car: DataSchemes.Car): number {
249
- return round(this.getNum(car, 'price.currentSalesPrice'), 2);
250
- }
251
-
252
- private getColor(car: DataSchemes.Car): string {
253
- let colorSelection = get(car, 'exterior.exteriorColor', 'selector_unknown');
254
- if (!colorSelection || colorSelection === 'selector_unknown') {
255
- colorSelection = 'selector_unknown';
256
- }
257
- return this.translator.translateVal(colorSelection);
258
- }
259
- }
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 } from '../interfaces';
13
+ import { ConfigService, ImagesService, MappingService } from '../services';
14
+ import {
15
+ AMOUNT_QUALIFIER_APR,
16
+ META_PUSH_PRIORITY_ALLOWED_VALUES,
17
+ META_PUSH_PRIORITY_STANDARD,
18
+ OFFER_DISCLAIMER,
19
+ OfferType,
20
+ TERM_QUALIFIER_MONTHS,
21
+ } from '../constants/fb.constants';
22
+
23
+ interface OfferFinancials {
24
+ monthlyPayment: number;
25
+ downPayment: number;
26
+ termLength: number;
27
+ apr: number;
28
+ finalPayment: number;
29
+ totalAmount: number;
30
+ netLoanAmount: number;
31
+ }
32
+
33
+ interface Offer {
34
+ offerType: OfferType;
35
+ title: string;
36
+ description: string;
37
+ disclaimer?: string;
38
+ financials?: OfferFinancials;
39
+ }
40
+
41
+ const DE_LOCALE = 'de-DE';
42
+
43
+ @Injectable()
44
+ export class FacebookShopTransformer {
45
+ private translator: TranslatorExtended;
46
+
47
+ constructor(
48
+ private readonly configService: ConfigService,
49
+ private readonly imageService: ImagesService,
50
+ private readonly mappingService: MappingService,
51
+ ) {
52
+ const dict = { ...de, ...deVehicleProps, ...deVehicleDetailsPage };
53
+ this.translator = new TranslatorExtended(dict);
54
+ }
55
+
56
+ async transform({
57
+ car,
58
+ images,
59
+ }: {
60
+ car: DataSchemes.Car;
61
+ images: ImboImageResponseInterface[];
62
+ }): Promise<FacebookShopItem[]> {
63
+ // ignoreDefImages=true: cars with no real photos must be excluded, not padded with placeholders.
64
+ const imageLinks = await this.imageService.getImages(car, images, this.configService.config.imageType, true);
65
+ if (!imageLinks.length) {
66
+ return [];
67
+ }
68
+
69
+ const vehicleId = toString(get(car, 'metaData.vehicleId', car._id));
70
+ const make = toString(get(car, 'mainData.make', ''));
71
+ const model = toString(get(car, 'mainData.model', ''));
72
+ const price = this.getPriceValue(car);
73
+ const ownerId = get(car, 'metaData.ownerId', null);
74
+ const customLabel4 = await this.getMetaPushPriority(ownerId);
75
+
76
+ const offers = this.buildOffers(car, { make, model, price });
77
+ if (!offers.length) {
78
+ return [];
79
+ }
80
+
81
+ const base = {
82
+ make,
83
+ model,
84
+ year: this.getYear(car),
85
+ url: this.getCarLink(car),
86
+ image: imageLinks.slice(0, 2), // main + 2nd image, per ticket
87
+ price: `${price} EUR`,
88
+ availability: this.getAvailability(car),
89
+ exterior_color: this.getColor(car),
90
+ custom_label_4: customLabel4,
91
+ };
92
+
93
+ return offers.map((offer): FacebookShopItem => ({
94
+ ...base,
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 } : {}),
100
+ ...(offer.financials ? this.financialTags(offer.financials) : {}),
101
+ }));
102
+ }
103
+
104
+ private buildOffers(car: DataSchemes.Car, common: { make: string; model: string; price: number }): Offer[] {
105
+ const offers: Offer[] = [];
106
+ const { make, model, price } = common;
107
+
108
+ // Read the raw, already-persisted rate/leasing values before normalizeRateObject
109
+ // mutates car.rate — these three fields must come straight from the Car object,
110
+ // matching exactly what the CDP displays (per ticket: data.rate.*/data.leasing.*).
111
+ const offersFinancing = get(car, 'rate.offersFinancing') !== false;
112
+ const rawFinancing = {
113
+ monthlyPayment: this.getNum(car, 'rate.monthlyInstallment'),
114
+ downPayment: this.getNum(car, 'rate.firstInstallment'),
115
+ termLength: this.getNum(car, 'rate.paybackPeriod'),
116
+ };
117
+ const leasingIsEligible = this.isLeasingEligible(car);
118
+
119
+ const { financing: computedFinancing, leasing: computedLeasing } = this.computeFinancials(car);
120
+
121
+ // Unlike financing, the CDP never reads a pre-stored leasing.monthlyInstallment — it
122
+ // always computes it live from RWG + price (see fe-main SearchResultsStore/NewMainPage,
123
+ // same isLeasingEligible formula). The raw car.leasing.monthlyInstallment field can be
124
+ // stale/unset even when the CDP shows an active leasing tab, so the computed value is the
125
+ // one that actually matches what the customer sees.
126
+ const computedLeasingValues = {
127
+ monthlyPayment: this.getNum(computedLeasing, 'monthlyInstallment'),
128
+ downPayment: this.getNum(computedLeasing, 'firstInstallment'),
129
+ termLength: this.getNum(computedLeasing, 'paybackPeriod'),
130
+ };
131
+
132
+ if (offersFinancing && rawFinancing.monthlyPayment && computedFinancing) {
133
+ offers.push({
134
+ offerType: OfferType.financing,
135
+ title: `${make} ${model} finanzieren – z. B. ab ${this.formatEUR(rawFinancing.monthlyPayment)} €/Monat`,
136
+ description: `${make} ${model} finanzieren – ${rawFinancing.termLength} Monate Laufzeit, ` +
137
+ `${this.formatEUR(rawFinancing.downPayment)} € Anzahlung`,
138
+ disclaimer: OFFER_DISCLAIMER,
139
+ financials: {
140
+ ...rawFinancing,
141
+ apr: this.getNum(computedFinancing, 'annualPercentageRate'),
142
+ finalPayment: this.getNum(computedFinancing, 'finalInstallment'),
143
+ totalAmount: this.getNum(computedFinancing, 'grossLoanAmount'),
144
+ netLoanAmount: this.getNum(computedFinancing, 'netLoanAmount'),
145
+ },
146
+ });
147
+ }
148
+
149
+ if (leasingIsEligible && computedLeasingValues.monthlyPayment && computedLeasing) {
150
+ offers.push({
151
+ offerType: OfferType.leasing,
152
+ title: `${make} ${model} leasen – z. B. ab ${this.formatEUR(computedLeasingValues.monthlyPayment)} €/Monat`,
153
+ description: `${make} ${model} leasen – ${computedLeasingValues.termLength} Monate Laufzeit, ` +
154
+ `${this.formatEUR(computedLeasingValues.downPayment)} € Anzahlung`,
155
+ disclaimer: OFFER_DISCLAIMER,
156
+ financials: {
157
+ ...computedLeasingValues,
158
+ apr: this.getNum(computedLeasing, 'annualPercentageRate'),
159
+ finalPayment: this.getNum(computedLeasing, 'finalInstallment'),
160
+ totalAmount: this.getNum(computedLeasing, 'grossLoanAmount'),
161
+ netLoanAmount: this.getNum(computedLeasing, 'netLoanAmount'),
162
+ },
163
+ });
164
+ }
165
+
166
+ offers.push({
167
+ offerType: OfferType.buy,
168
+ title: `${make} ${model} kaufen – jetzt für ${this.formatEUR(price)} €`,
169
+ description: `${make} ${model} kaufen – jetzt für ${this.formatEUR(price)} €`,
170
+ });
171
+
172
+ return offers;
173
+ }
174
+
175
+ // Runs the same normalizeRateObject + getComputedFinancing pipeline the CDP uses, so
176
+ // apr/final_payment/total_amount/net_loan_amount (not stored anywhere on the Car object)
177
+ // match exactly what the customer sees when they click through from the ad.
178
+ private computeFinancials(car: DataSchemes.Car): { financing?: Record<string, any>; leasing?: Record<string, any> } {
179
+ try {
180
+ const normalized = normalizeRateObject(car);
181
+ if (!get(normalized, 'rate.financing')) {
182
+ return {};
183
+ }
184
+ const computed = getComputedFinancing(normalized.rate as any);
185
+ return { financing: computed.financing, leasing: computed.leasing };
186
+ } catch (error) {
187
+ console.error(error);
188
+ return {};
189
+ }
190
+ }
191
+
192
+ // Mirrors getLeasing.ts's isLeasingActive: same car ages out of leasing eligibility once
193
+ // it's older than leasing.maxAge months or past leasing.maxMileage km.
194
+ private isLeasingEligible(car: DataSchemes.Car): boolean {
195
+ const firstRegistration = this.getNum(car, 'mainData.firstRegistration');
196
+ const mileage = this.getNum(car, 'mainData.mileage');
197
+ const maxAge = this.getNum(car, 'leasing.maxAge', 24);
198
+ const maxMileage = this.getNum(car, 'leasing.maxMileage', 1000000);
199
+
200
+ const registrationMs = firstRegistration ? firstRegistration * 1000 : Date.now();
201
+ const maxCarAge = new Date().setMonth(new Date().getMonth() - maxAge);
202
+
203
+ return registrationMs > maxCarAge && maxMileage > mileage;
204
+ }
205
+
206
+ private financialTags(financials: OfferFinancials): Partial<FacebookShopItem> {
207
+ return {
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`,
214
+ };
215
+ }
216
+
217
+ // lodash's get() only falls back to the default for an *undefined* path — an explicit
218
+ // `null` stored on the Car document (a common Mongo pattern for "not set") passes
219
+ // straight through and ends up rendering as an empty XML tag. Coalesce it here instead.
220
+ private getNum(source: any, path: string, fallback = 0): number {
221
+ return get(source, path) ?? fallback;
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 = this.getNum(car, 'mainData.firstRegistration');
237
+ return firstRegistration ? moment(firstRegistration * 1000).format('YYYY') : moment().format('YYYY');
238
+ }
239
+
240
+ private getAvailability(car: DataSchemes.Car): string {
241
+ return get(car, 'state.isOnline', false) ? 'AVAILABLE' : 'NOT_AVAILABLE';
242
+ }
243
+
244
+ private getCarLink(car: DataSchemes.Car): string {
245
+ return `https://${this.configService.config.domain}/search/vehicle/${car._id}`;
246
+ }
247
+
248
+ private getPriceValue(car: DataSchemes.Car): number {
249
+ return round(this.getNum(car, 'price.currentSalesPrice'), 2);
250
+ }
251
+
252
+ private getColor(car: DataSchemes.Car): string {
253
+ let colorSelection = get(car, 'exterior.exteriorColor', 'selector_unknown');
254
+ if (!colorSelection || colorSelection === 'selector_unknown') {
255
+ colorSelection = 'selector_unknown';
256
+ }
257
+ return this.translator.translateVal(colorSelection);
258
+ }
259
+ }
@@ -32,7 +32,7 @@ import { ConfigService, ImagesService, MappingService } from '../services';
32
32
  import { FbEquipment } from './fb-equipment.transformer';
33
33
  import { isValidVin } from '../utils';
34
34
  import { UserService } from 'src/services';
35
- import { ImageType } from '../enums';
35
+ import { Environment, ImageType } from '../enums';
36
36
  import {
37
37
  DEFAULT_ADDR1,
38
38
  DEFAULT_CITY,
@@ -84,6 +84,17 @@ export class FbTransformer {
84
84
  const autodeId = toString(get(car, '_id', ''));
85
85
  const year = this.getYear(car);
86
86
  const ownerId = get(car, 'metaData.ownerId', null);
87
+
88
+ // ADV-161-follow-up: on prod, only publish dealers listed in the meta push priority sheet
89
+ // (metaPushPriorityCsvUrl). Dev dealer ids don't match that prod sheet, so this stays a
90
+ // no-op there — every car passes through, same as before this filter existed.
91
+ if (this.configService.config.environment === Environment.PRODUCTION) {
92
+ const priorities = await this.mappingService.getMetaPushPriorityMapping();
93
+ if (!priorities.some(priority => priority.dealer_id === ownerId)) {
94
+ return null;
95
+ }
96
+ }
97
+
87
98
  const dealerInfo = this.getDealerItems(car);
88
99
  // console.time(`Address ${car._id}`);
89
100
  const address = await this.getAddress(car, owner);