Skip to content

Commit 92712c7

Browse files
committed
add support for BestPriceSearch
1 parent 65096a8 commit 92712c7

File tree

8 files changed

+5002
-0
lines changed

8 files changed

+5002
-0
lines changed

index.js

+86
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {validateProfile} from './lib/validate-profile.js';
77
import {INVALID_REQUEST} from './lib/errors.js';
88
import {sliceLeg} from './lib/slice-leg.js';
99
import {HafasError} from './lib/errors.js';
10+
import {profile as dbProfile} from './p/db/index.js';
1011

1112
// background info: https://github.com/public-transport/hafas-client/issues/286
1213
const FORBIDDEN_USER_AGENTS = [
@@ -268,6 +269,88 @@ const createClient = (profile, userAgent, opt = {}) => {
268269
};
269270
};
270271

272+
const bestPrices = async (from, to, opt = {}) => {
273+
from = profile.formatLocation(profile, from, 'from');
274+
to = profile.formatLocation(profile, to, 'to');
275+
276+
opt = Object.assign({
277+
via: null, // let journeys pass this station?
278+
transfers: -1, // maximum nr of transfers
279+
bike: false, // only bike-friendly journeys
280+
tickets: false, // return tickets?
281+
polylines: false, // return leg shapes?
282+
subStops: false, // parse & expose sub-stops of stations?
283+
entrances: false, // parse & expose entrances of stops/stations?
284+
remarks: true, // parse & expose hints & warnings?
285+
scheduledDays: false, // parse & expose dates each journey is valid on?
286+
}, opt);
287+
if (opt.via) {
288+
opt.via = profile.formatLocation(profile, opt.via, 'opt.via');
289+
}
290+
291+
let when = new Date();
292+
if (opt.departure !== undefined && opt.departure !== null) {
293+
when = new Date(opt.departure);
294+
if (Number.isNaN(Number(when))) {
295+
throw new TypeError('opt.departure is invalid');
296+
}
297+
const now = new Date();
298+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
299+
if (today > when) {
300+
throw new TypeError('opt.departure date older than current date.');
301+
}
302+
}
303+
304+
const filters = [
305+
profile.formatProductsFilter({profile}, opt.products || {}),
306+
];
307+
if (
308+
opt.accessibility
309+
&& profile.filters
310+
&& profile.filters.accessibility
311+
&& profile.filters.accessibility[opt.accessibility]
312+
) {
313+
filters.push(profile.filters.accessibility[opt.accessibility]);
314+
}
315+
316+
const query = {
317+
maxChg: opt.transfers,
318+
depLocL: [from],
319+
viaLocL: opt.via ? [{loc: opt.via}] : [],
320+
arrLocL: [to],
321+
jnyFltrL: filters,
322+
getTariff: Boolean(opt.tickets),
323+
324+
getPolyline: Boolean(opt.polylines),
325+
};
326+
query.outDate = profile.formatDate(profile, when);
327+
328+
if (profile.endpoint !== dbProfile.endpoint) {
329+
throw new Error('db profile expected.');
330+
}
331+
332+
const {res, common} = await profile.request({profile, opt}, userAgent, {
333+
cfg: {polyEnc: 'GPA'},
334+
meth: 'BestPriceSearch',
335+
req: profile.transformJourneysQuery({profile, opt}, query),
336+
});
337+
if (!Array.isArray(res.outConL)) {
338+
return {};
339+
}
340+
// todo: outConGrpL
341+
342+
const ctx = {profile, opt, common, res};
343+
const journeys = res.outConL.map(j => profile.parseJourney(ctx, j));
344+
const bestPrices = res.outDaySegL.map(j => profile.parseBestPrice(ctx, j, journeys));
345+
346+
return {
347+
bestPrices,
348+
realtimeDataUpdatedAt: res.planrtTS && res.planrtTS !== '0'
349+
? parseInt(res.planrtTS)
350+
: null,
351+
};
352+
};
353+
271354
const refreshJourney = async (refreshToken, opt = {}) => {
272355
if ('string' !== typeof refreshToken || !refreshToken) {
273356
throw new TypeError('refreshToken must be a non-empty string.');
@@ -889,6 +972,9 @@ const createClient = (profile, userAgent, opt = {}) => {
889972
if (profile.lines !== false) {
890973
client.lines = lines;
891974
}
975+
if (profile.bestPrices !== false) {
976+
client.bestPrices = bestPrices;
977+
}
892978
Object.defineProperty(client, 'profile', {value: profile});
893979
return client;
894980
};

lib/default-profile.js

+3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {parseOperator} from '../parse/operator.js';
3333
import {parseHint} from '../parse/hint.js';
3434
import {parseWarning} from '../parse/warning.js';
3535
import {parseStopover} from '../parse/stopover.js';
36+
import {parseBestPrice} from '../parse/bestprice.js';
3637

3738
import {formatAddress} from '../format/address.js';
3839
import {formatCoord} from '../format/coord.js';
@@ -91,6 +92,7 @@ const defaultProfile = {
9192
parseTrip,
9293
parseJourneyLeg,
9394
parseJourney,
95+
parseBestPrice,
9496
parseLine,
9597
parseStationName: (_, name) => name,
9698
parseLocation,
@@ -123,6 +125,7 @@ const defaultProfile = {
123125
// `departures()` method: support for `stbFltrEquiv` field?
124126
departuresStbFltrEquiv: false,
125127

128+
bestPrices: false,
126129
trip: false,
127130
radar: false,
128131
refreshJourney: true,

p/db/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,7 @@ const profile = {
692692

693693
generateUnreliableTicketUrls: false,
694694
refreshJourneyUseOutReconL: true,
695+
bestPrices: true,
695696
trip: true,
696697
journeysFromTrip: true,
697698
radar: true,

parse/bestprice.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
2+
const parseBestPrice = (ctx, outDaySeg, journeys) => {
3+
const {profile, res} = ctx;
4+
5+
const bpjourneys = outDaySeg.conRefL
6+
? outDaySeg.conRefL
7+
.map(i => journeys.find(j => j.refreshToken == res.outConL[i].ctxRecon))
8+
.filter(j => Boolean(j))
9+
: [];
10+
11+
const amount = outDaySeg.bestPrice.amount / 100;
12+
const currency = bpjourneys?.[0]?.price?.currency;
13+
14+
const result = {
15+
journeys: bpjourneys,
16+
fromDate: profile.parseDateTime(ctx, outDaySeg.fromDate, outDaySeg.fromTime),
17+
toDate: profile.parseDateTime(ctx, outDaySeg.toDate, outDaySeg.toTime),
18+
bestPrice: amount > 0 && currency ? {amount, currency} : undefined,
19+
};
20+
21+
return result;
22+
};
23+
24+
export {
25+
parseBestPrice,
26+
};

test/db-bestprice.js

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// todo: use import assertions once they're supported by Node.js & ESLint
2+
// https://github.com/tc39/proposal-import-assertions
3+
import {createRequire} from 'module';
4+
const require = createRequire(import.meta.url);
5+
6+
import tap from 'tap';
7+
8+
import {createClient} from '../index.js';
9+
import {profile as rawProfile} from '../p/db/index.js';
10+
const response = require('./fixtures/db-bestprice.json');
11+
import {dbBestPrices as expected} from './fixtures/db-bestprice.js';
12+
13+
const client = createClient(rawProfile, 'public-transport/hafas-client:test');
14+
const {profile} = client;
15+
16+
const opt = {
17+
via: null,
18+
transfers: -1,
19+
transferTime: 0,
20+
accessibility: 'none',
21+
bike: false,
22+
tickets: true,
23+
polylines: true,
24+
remarks: true,
25+
walkingSpeed: 'normal',
26+
startWithWalking: true,
27+
departure: '2023-06-15',
28+
products: {},
29+
};
30+
31+
tap.test('parses a bestprice with a DEVI leg correctly (DB)', (t) => {
32+
const res = response.svcResL[0].res;
33+
const common = profile.parseCommon({profile, opt, res});
34+
const ctx = {profile, opt, common, res};
35+
const journeys = res.outConL.map(j => profile.parseJourney(ctx, j));
36+
const bestPrices = res.outDaySegL.map(j => profile.parseBestPrice(ctx, j, journeys));
37+
38+
t.same(bestPrices, expected.bestPrices);
39+
t.end();
40+
});

0 commit comments

Comments
 (0)