Skip to content

Commit ce4b5b5

Browse files
committed
add support for BestPriceSearch
1 parent 4cb7062 commit ce4b5b5

File tree

8 files changed

+4835
-1
lines changed

8 files changed

+4835
-1
lines changed

index.js

+74-1
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,79 @@ const createClient = (profile, userAgent, opt = {}) => {
245245
}
246246
}
247247

248-
const refreshJourney = async (refreshToken, opt = {}) => {
248+
const bestPrices = async (from, to, opt = {}) => {
249+
from = profile.formatLocation(profile, from, 'from')
250+
to = profile.formatLocation(profile, to, 'to')
251+
252+
opt = Object.assign({
253+
via: null, // let journeys pass this station?
254+
transfers: -1, // maximum nr of transfers
255+
bike: false, // only bike-friendly journeys
256+
tickets: false, // return tickets?
257+
polylines: false, // return leg shapes?
258+
subStops: false, // parse & expose sub-stops of stations?
259+
entrances: false, // parse & expose entrances of stops/stations?
260+
remarks: true, // parse & expose hints & warnings?
261+
scheduledDays: false, // parse & expose dates each journey is valid on?
262+
}, opt)
263+
if (opt.via) opt.via = profile.formatLocation(profile, opt.via, 'opt.via')
264+
265+
let when = new Date();
266+
if (opt.departure !== undefined && opt.departure !== null) {
267+
when = new Date(opt.departure)
268+
if (Number.isNaN(+when)) throw new TypeError('opt.departure is invalid')
269+
const now = new Date();
270+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
271+
if (today > when) {
272+
throw new TypeError('opt.departure date older than current date.')
273+
}
274+
}
275+
276+
const filters = [
277+
profile.formatProductsFilter({profile}, opt.products || {})
278+
]
279+
if (
280+
opt.accessibility &&
281+
profile.filters &&
282+
profile.filters.accessibility &&
283+
profile.filters.accessibility[opt.accessibility]
284+
) {
285+
filters.push(profile.filters.accessibility[opt.accessibility])
286+
}
287+
288+
const query = {
289+
maxChg: opt.transfers,
290+
depLocL: [from],
291+
viaLocL: opt.via ? [{loc: opt.via}] : [],
292+
arrLocL: [to],
293+
jnyFltrL: filters,
294+
getTariff: !!opt.tickets,
295+
296+
getPolyline: !!opt.polylines
297+
}
298+
query.outDate = profile.formatDate(profile, when)
299+
300+
const {res, common} = await profile.request({profile, opt}, userAgent, {
301+
cfg: {polyEnc: 'GPA'},
302+
meth: 'BestPriceSearch',
303+
req: profile.transformJourneysQuery({profile, opt}, query)
304+
})
305+
if (!Array.isArray(res.outConL)) return {}
306+
// todo: outConGrpL
307+
308+
const ctx = {profile, opt, common, res}
309+
const journeys = res.outConL.map(j => profile.parseJourney(ctx, j))
310+
const bestPrices = res.outDaySegL.map(j => profile.parseBestPrice(ctx, j, journeys))
311+
312+
return {
313+
bestPrices,
314+
realtimeDataUpdatedAt: res.planrtTS && res.planrtTS !== '0'
315+
? parseInt(res.planrtTS)
316+
: null,
317+
}
318+
}
319+
320+
const refreshJourney = async (refreshToken, opt = {}) => {
249321
if ('string' !== typeof refreshToken || !refreshToken) {
250322
throw new TypeError('refreshToken must be a non-empty string.')
251323
}
@@ -791,6 +863,7 @@ const createClient = (profile, userAgent, opt = {}) => {
791863
nearby,
792864
serverInfo,
793865
}
866+
if (profile.bestPrices) client.bestPrices = bestPrices
794867
if (profile.trip) client.trip = trip
795868
if (profile.radar) client.radar = radar
796869
if (profile.refreshJourney) client.refreshJourney = refreshJourney

lib/default-profile.js

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {parseArrival} from '../parse/arrival.js'
2323
import {parseTrip} from '../parse/trip.js'
2424
import {parseJourneyLeg} from '../parse/journey-leg.js'
2525
import {parseJourney} from '../parse/journey.js'
26+
import {parseBestPrice} from '../parse/bestprice.js'
2627
import {parseLine} from '../parse/line.js'
2728
import {parseLocation} from '../parse/location.js'
2829
import {parseCommonData as parseCommon} from '../parse/common.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
@@ -545,6 +545,7 @@ const profile = {
545545
formatStation,
546546

547547
refreshJourneyUseOutReconL: true,
548+
bestPrices: true,
548549
trip: true,
549550
journeysFromTrip: true,
550551
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 => !!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)