Skip to content

Commit

Permalink
add support for BestPriceSearch
Browse files Browse the repository at this point in the history
  • Loading branch information
bergmannjg committed Mar 23, 2024
1 parent 65096a8 commit 92712c7
Show file tree
Hide file tree
Showing 8 changed files with 5,002 additions and 0 deletions.
86 changes: 86 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {validateProfile} from './lib/validate-profile.js';
import {INVALID_REQUEST} from './lib/errors.js';
import {sliceLeg} from './lib/slice-leg.js';
import {HafasError} from './lib/errors.js';
import {profile as dbProfile} from './p/db/index.js';

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

const bestPrices = async (from, to, opt = {}) => {
from = profile.formatLocation(profile, from, 'from');
to = profile.formatLocation(profile, to, 'to');

opt = Object.assign({
via: null, // let journeys pass this station?
transfers: -1, // maximum nr of transfers
bike: false, // only bike-friendly journeys
tickets: false, // return tickets?
polylines: false, // return leg shapes?
subStops: false, // parse & expose sub-stops of stations?
entrances: false, // parse & expose entrances of stops/stations?
remarks: true, // parse & expose hints & warnings?
scheduledDays: false, // parse & expose dates each journey is valid on?
}, opt);
if (opt.via) {
opt.via = profile.formatLocation(profile, opt.via, 'opt.via');
}

let when = new Date();
if (opt.departure !== undefined && opt.departure !== null) {
when = new Date(opt.departure);
if (Number.isNaN(Number(when))) {
throw new TypeError('opt.departure is invalid');
}
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
if (today > when) {
throw new TypeError('opt.departure date older than current date.');
}
}

const filters = [
profile.formatProductsFilter({profile}, opt.products || {}),
];
if (
opt.accessibility
&& profile.filters
&& profile.filters.accessibility
&& profile.filters.accessibility[opt.accessibility]
) {
filters.push(profile.filters.accessibility[opt.accessibility]);
}

const query = {
maxChg: opt.transfers,
depLocL: [from],
viaLocL: opt.via ? [{loc: opt.via}] : [],
arrLocL: [to],
jnyFltrL: filters,
getTariff: Boolean(opt.tickets),

getPolyline: Boolean(opt.polylines),
};
query.outDate = profile.formatDate(profile, when);

if (profile.endpoint !== dbProfile.endpoint) {
throw new Error('db profile expected.');
}

const {res, common} = await profile.request({profile, opt}, userAgent, {
cfg: {polyEnc: 'GPA'},
meth: 'BestPriceSearch',
req: profile.transformJourneysQuery({profile, opt}, query),
});
if (!Array.isArray(res.outConL)) {
return {};
}
// todo: outConGrpL

const ctx = {profile, opt, common, res};
const journeys = res.outConL.map(j => profile.parseJourney(ctx, j));
const bestPrices = res.outDaySegL.map(j => profile.parseBestPrice(ctx, j, journeys));

return {
bestPrices,
realtimeDataUpdatedAt: res.planrtTS && res.planrtTS !== '0'
? parseInt(res.planrtTS)
: null,
};
};

const refreshJourney = async (refreshToken, opt = {}) => {
if ('string' !== typeof refreshToken || !refreshToken) {
throw new TypeError('refreshToken must be a non-empty string.');
Expand Down Expand Up @@ -889,6 +972,9 @@ const createClient = (profile, userAgent, opt = {}) => {
if (profile.lines !== false) {
client.lines = lines;
}
if (profile.bestPrices !== false) {
client.bestPrices = bestPrices;
}
Object.defineProperty(client, 'profile', {value: profile});
return client;
};
Expand Down
3 changes: 3 additions & 0 deletions lib/default-profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {parseOperator} from '../parse/operator.js';
import {parseHint} from '../parse/hint.js';
import {parseWarning} from '../parse/warning.js';
import {parseStopover} from '../parse/stopover.js';
import {parseBestPrice} from '../parse/bestprice.js';

import {formatAddress} from '../format/address.js';
import {formatCoord} from '../format/coord.js';
Expand Down Expand Up @@ -91,6 +92,7 @@ const defaultProfile = {
parseTrip,
parseJourneyLeg,
parseJourney,
parseBestPrice,
parseLine,
parseStationName: (_, name) => name,
parseLocation,
Expand Down Expand Up @@ -123,6 +125,7 @@ const defaultProfile = {
// `departures()` method: support for `stbFltrEquiv` field?
departuresStbFltrEquiv: false,

bestPrices: false,
trip: false,
radar: false,
refreshJourney: true,
Expand Down
1 change: 1 addition & 0 deletions p/db/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,7 @@ const profile = {

generateUnreliableTicketUrls: false,
refreshJourneyUseOutReconL: true,
bestPrices: true,
trip: true,
journeysFromTrip: true,
radar: true,
Expand Down
26 changes: 26 additions & 0 deletions parse/bestprice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

const parseBestPrice = (ctx, outDaySeg, journeys) => {
const {profile, res} = ctx;

const bpjourneys = outDaySeg.conRefL
? outDaySeg.conRefL
.map(i => journeys.find(j => j.refreshToken == res.outConL[i].ctxRecon))
.filter(j => Boolean(j))
: [];

const amount = outDaySeg.bestPrice.amount / 100;
const currency = bpjourneys?.[0]?.price?.currency;

const result = {
journeys: bpjourneys,
fromDate: profile.parseDateTime(ctx, outDaySeg.fromDate, outDaySeg.fromTime),
toDate: profile.parseDateTime(ctx, outDaySeg.toDate, outDaySeg.toTime),
bestPrice: amount > 0 && currency ? {amount, currency} : undefined,
};

return result;
};

export {
parseBestPrice,
};
40 changes: 40 additions & 0 deletions test/db-bestprice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);

import tap from 'tap';

import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/db/index.js';
const response = require('./fixtures/db-bestprice.json');
import {dbBestPrices as expected} from './fixtures/db-bestprice.js';

const client = createClient(rawProfile, 'public-transport/hafas-client:test');
const {profile} = client;

const opt = {
via: null,
transfers: -1,
transferTime: 0,
accessibility: 'none',
bike: false,
tickets: true,
polylines: true,
remarks: true,
walkingSpeed: 'normal',
startWithWalking: true,
departure: '2023-06-15',
products: {},
};

tap.test('parses a bestprice with a DEVI leg correctly (DB)', (t) => {
const res = response.svcResL[0].res;
const common = profile.parseCommon({profile, opt, res});
const ctx = {profile, opt, common, res};
const journeys = res.outConL.map(j => profile.parseJourney(ctx, j));
const bestPrices = res.outDaySegL.map(j => profile.parseBestPrice(ctx, j, journeys));

t.same(bestPrices, expected.bestPrices);
t.end();
});
Loading

0 comments on commit 92712c7

Please sign in to comment.