diff --git a/index.js b/index.js index 29a1d9b9..5c231a9e 100644 --- a/index.js +++ b/index.js @@ -110,7 +110,7 @@ const createClient = (profile, userAgent, opt = {}) => { const {res} = await profile.request({profile, opt}, userAgent, req); - const ctx = {profile, opt, common, res}; + const ctx = {profile, opt, common, res, userAgent}; let results = (res[resultsField] || res.items || res.bahnhofstafelAbfahrtPositionen || res.bahnhofstafelAnkunftPositionen || res.entries.flat()) .map(res => parse(ctx, res)); // TODO sort?, slice @@ -209,13 +209,13 @@ const createClient = (profile, userAgent, opt = {}) => { const req = profile.formatJourneysReq({profile, opt}, from, to, when, outFrwd, journeysRef); const {res} = await profile.request({profile, opt}, userAgent, req); - const ctx = {profile, opt, common, res}; + const ctx = {profile, opt, common, res, userAgent}; if (opt.bestprice) { res.verbindungen = (res.intervalle || res.tagesbestPreisIntervalle).flatMap(i => i.verbindungen.map(v => ({...v, ...v.verbindung}))); } const verbindungen = Number.isInteger(opt.results) && opt.results != 3 ? res.verbindungen.slice(0, opt.results) : res.verbindungen; // TODO remove default from hafas-rest-api - const journeys = verbindungen - .map(j => profile.parseJourney(ctx, j)); + const journeys = await Promise.all(verbindungen + .map(j => profile.parseJourney(ctx, j))); if (opt.bestprice) { journeys.sort((a, b) => a.price?.amount - b.price?.amount); } @@ -252,10 +252,10 @@ const createClient = (profile, userAgent, opt = {}) => { const req = profile.formatRefreshJourneyReq({profile, opt}, refreshToken); const {res} = await profile.request({profile, opt}, userAgent, req); - const ctx = {profile, opt, common, res}; + const ctx = {profile, opt, common, res, userAgent}; return { - journey: profile.parseJourney(ctx, res.verbindungen && res.verbindungen[0] || res), + journey: await profile.parseJourney(ctx, res.verbindungen && res.verbindungen[0] || res), realtimeDataUpdatedAt: null, // TODO }; }; @@ -280,7 +280,7 @@ const createClient = (profile, userAgent, opt = {}) => { const {res} = await profile.request({profile, opt}, userAgent, req); - const ctx = {profile, opt, common, res}; + const ctx = {profile, opt, common, res, userAgent}; const results = res.map(loc => profile.parseLocation(ctx, loc)); return Number.isInteger(opt.results) @@ -329,7 +329,7 @@ const createClient = (profile, userAgent, opt = {}) => { const req = profile.formatNearbyReq({profile, opt}, location); const {res} = await profile.request({profile, opt}, userAgent, req); - const ctx = {profile, opt, common, res}; + const ctx = {profile, opt, common, res, userAgent}; const results = res.map(loc => { const res = profile.parseLocation(ctx, loc); if (res.latitude || res.location?.latitude) { @@ -361,7 +361,7 @@ const createClient = (profile, userAgent, opt = {}) => { const req = profile.formatTripReq({profile, opt}, id); const {res} = await profile.request({profile, opt}, userAgent, req); - const ctx = {profile, opt, common, res}; + const ctx = {profile, opt, common, res, userAgent}; const trip = profile.parseTrip(ctx, res, id); diff --git a/lib/fetch-verbundticket-prices.js b/lib/fetch-verbundticket-prices.js index a10c03e1..ed5dcbf8 100644 --- a/lib/fetch-verbundticket-prices.js +++ b/lib/fetch-verbundticket-prices.js @@ -1,23 +1,22 @@ -import {formatBaseJourneysReq} from '../p/dbnav/journeys-req.js'; import {getHeaders} from '../p/dbnav/header.js'; // Helper to fetch Verbundticket prices via recon API const fetchVerbundticketPrices = async (ctx, userAgent, journey) => { const {profile, opt} = ctx; - + // Verbundtickets require a kontext token to fetch prices const kontext = journey.kontext || journey.ctxRecon; if (!kontext) { return null; } - + // Extract journey details const firstLeg = journey.verbindungsAbschnitte?.[0]; const lastLeg = journey.verbindungsAbschnitte?.[journey.verbindungsAbschnitte.length - 1]; const abgangsOrt = firstLeg?.abgangsOrt; const ankunftsOrt = lastLeg?.ankunftsOrt; const abgangsDatum = firstLeg?.abgangsDatum; - + // Build recon request exactly matching DB Navigator mobile API format const reconRequest = { endpoint: profile.refreshJourneysEndpointTickets || 'https://app.vendo.noncd.db.de/mob/angebote/recon', @@ -26,7 +25,7 @@ const fetchVerbundticketPrices = async (ctx, userAgent, journey) => { body: { fahrverguenstigungen: { nurDeutschlandTicketVerbindungen: ctx.opt.deutschlandTicketConnectionsOnly || false, - deutschlandTicketVorhanden: ctx.opt.deutschlandTicketDiscount || false + deutschlandTicketVorhanden: ctx.opt.deutschlandTicketDiscount || false, }, reservierungsKontingenteVorhanden: false, suchParameter: { @@ -37,34 +36,35 @@ const fetchVerbundticketPrices = async (ctx, userAgent, journey) => { fahrradmitnahme: false, zielLocationId: ankunftsOrt?.locationId || extractLocationFromKontext(kontext, 'to'), zeitWunsch: { - reiseDatum: abgangsDatum || new Date().toISOString(), - zeitPunktArt: 'ABFAHRT' + reiseDatum: abgangsDatum || new Date() + .toISOString(), + zeitPunktArt: 'ABFAHRT', }, - verkehrsmittel: ['ALL'] - } + verkehrsmittel: ['ALL'], + }, }, einstiegsTypList: ['STANDARD'], klasse: 'KLASSE_2', verbindungHin: { - kontext: kontext + kontext: kontext, }, reisendenProfil: { reisende: [{ reisendenTyp: 'ERWACHSENER', - ermaessigungen: ['KEINE_ERMAESSIGUNG KLASSENLOS'] - }] - } - } + ermaessigungen: ['KEINE_ERMAESSIGUNG KLASSENLOS'], + }], + }, + }, }; - + // Add business customer affiliation if BMIS number is provided if (ctx.opt.bmisNumber) { reconRequest.body.firmenZugehoerigkeit = { bmisNr: ctx.opt.bmisNumber, - identifikationsart: 'BMIS' + identifikationsart: 'BMIS', }; } - + try { const {res} = await profile.request(ctx, userAgent, reconRequest); return res; @@ -91,4 +91,4 @@ const extractLocationFromKontext = (kontext, type) => { export { fetchVerbundticketPrices, -}; \ No newline at end of file +}; diff --git a/p/dbnav/parse-journey.js b/p/dbnav/parse-journey.js index ddc46d29..bc98ca6a 100644 --- a/p/dbnav/parse-journey.js +++ b/p/dbnav/parse-journey.js @@ -1,6 +1,6 @@ import {parseJourney as parseJourneyDefault} from '../../parse/journey.js'; -const parseJourney = (ctx, jj) => { +const parseJourney = async (ctx, jj) => { const legs = (jj.verbindung || jj).verbindungsAbschnitte; if (legs.length > 0) { legs[0] = preprocessJourneyLeg(legs[0]); diff --git a/parse/journey.js b/parse/journey.js index 71388878..13393460 100644 --- a/parse/journey.js +++ b/parse/journey.js @@ -41,7 +41,7 @@ const trimJourneyId = (journeyId) => { return journeyId; }; -const parseJourney = (ctx, jj) => { // j = raw journey +const parseJourney = async (ctx, jj) => { // j = raw journey const {profile, opt} = ctx; const j = jj.verbindung || jj; const fallbackLocations = parseLocationsFromCtxRecon(ctx, j); @@ -87,17 +87,16 @@ const parseJourney = (ctx, jj) => { // j = raw journey const hasVerbundCode = angebote?.verbundCode; const hasEmptyAngebotsCluster = !angebote?.angebotsCluster || angebote.angebotsCluster.length === 0; const hasKontext = j.kontext || j.ctxRecon; - + // Also check for the warning message that prices need to be fetched - const hasVerbundWarning = angebote?.angebotsMeldungen?.some(msg => - msg.includes('Verbindung liegt in der Vergangenheit') || - msg.includes('Preis') || - msg.includes('Verbund') + const hasVerbundWarning = angebote?.angebotsMeldungen?.some(msg => msg.includes('Verbindung liegt in der Vergangenheit') + || msg.includes('Preis') + || msg.includes('Verbund'), ); - - if ((hasVerbundCode && hasEmptyAngebotsCluster || hasVerbundWarning) && hasKontext && userAgent && opt.tickets && opt.autoFetchVerbundtickets) { + + if ((hasVerbundCode && hasEmptyAngebotsCluster || hasVerbundWarning) && hasKontext && ctx.userAgent && opt.tickets && opt.autoFetchVerbundtickets) { // Fetch Verbundticket prices via recon API - const reconResult = await fetchVerbundticketPrices(ctx, userAgent, j); + const reconResult = await fetchVerbundticketPrices(ctx, ctx.userAgent, j); if (reconResult) { // Use the recon result for price and ticket parsing if (reconResult.angebote) { diff --git a/parse/tickets.js b/parse/tickets.js index 3c233667..3ab00779 100644 --- a/parse/tickets.js +++ b/parse/tickets.js @@ -11,7 +11,7 @@ const parsePrice = (ctx, raw) => { partialFare: partialFare, }; } - + // For Verbundtickets, try to get the lowest price from reiseAngebote if (raw.reiseAngebote && raw.reiseAngebote.length > 0) { let lowestPrice = null; @@ -43,7 +43,7 @@ const parsePrice = (ctx, raw) => { return lowestPrice; } } - + return undefined; }; @@ -55,7 +55,7 @@ const parseTickets = (ctx, j) => { let price = parsePrice(ctx, j); // Handle DB Navigator mobile API format let ang = j.reiseAngebote; - + // If no reiseAngebote, check for angebote.angebotsCluster (DB Navigator mobile format) if (!ang && j.angebote?.angebotsCluster) { ang = j.angebote.angebotsCluster.flatMap(c => c.angebotsSubCluster @@ -63,28 +63,28 @@ const parseTickets = (ctx, j) => { .flatMap(p => { // Extract all possible ticket types from DB Navigator format const positions = []; - + // Handle verbundAngebot if (p.verbundAngebot?.reisePosition?.reisePosition) { const rp = p.verbundAngebot.reisePosition.reisePosition; rp.teilpreis = Boolean(p.verbundAngebot.reisePosition.teilpreisInformationen?.length); positions.push(rp); } - + // Handle regular einfacheFahrt if (p.einfacheFahrt?.standard?.reisePosition) { - const rp = p.einfacheFahrt.standard.reisePosition; + const rp = p.einfacheFahrt.standard.reisePosition.reisePosition || p.einfacheFahrt.standard.reisePosition; rp.teilpreis = Boolean(p.einfacheFahrt.standard.teilpreisInformationen?.length); positions.push(rp); } - + // Handle upsell if (p.einfacheFahrt?.upsellEntgelt?.einfacheFahrt?.reisePosition) { - const rp = p.einfacheFahrt.upsellEntgelt.einfacheFahrt.reisePosition; + const rp = p.einfacheFahrt.upsellEntgelt.einfacheFahrt.reisePosition.reisePosition || p.einfacheFahrt.upsellEntgelt.einfacheFahrt.reisePosition; rp.teilpreis = Boolean(p.einfacheFahrt.upsellEntgelt.einfacheFahrt.teilpreisInformationen?.length); positions.push(rp); } - + return positions; }), ), diff --git a/test/dbnav-refresh-journey.js b/test/dbnav-refresh-journey.js index 50b9f7df..7e9f990d 100644 --- a/test/dbnav-refresh-journey.js +++ b/test/dbnav-refresh-journey.js @@ -26,9 +26,9 @@ const opt = { products: {}, }; -tap.test('parses a refresh journey correctly (DB)', (t) => { - const ctx = {profile, opt, common: null, res}; - const journey = profile.parseJourney(ctx, res); +tap.test('parses a refresh journey correctly (DB)', async (t) => { + const ctx = {profile, opt, common: null, res, userAgent: 'test'}; + const journey = await profile.parseJourney(ctx, res); t.same(journey, expected.journey); t.end(); diff --git a/test/dbweb-journey.js b/test/dbweb-journey.js index 29d7a88b..c61ae9e2 100644 --- a/test/dbweb-journey.js +++ b/test/dbweb-journey.js @@ -26,9 +26,9 @@ const opt = { products: {}, }; -tap.test('parses a dbweb journey correctly', (t) => { // TODO DEVI leg +tap.test('parses a dbweb journey correctly', async (t) => { // TODO DEVI leg const ctx = {profile, opt, common: null, res}; - const journey = profile.parseJourney(ctx, res.verbindungen[0]); + const journey = await profile.parseJourney(ctx, res.verbindungen[0]); t.same(journey, expected); t.end(); diff --git a/test/dbweb-refresh-journey.js b/test/dbweb-refresh-journey.js index 0278f115..55e9470f 100644 --- a/test/dbweb-refresh-journey.js +++ b/test/dbweb-refresh-journey.js @@ -26,9 +26,9 @@ const opt = { products: {}, }; -tap.test('parses a refresh journey correctly (dbweb)', (t) => { +tap.test('parses a refresh journey correctly (dbweb)', async (t) => { const ctx = {profile, opt, common: null, res}; - const journey = profile.parseJourney(ctx, res.verbindungen[0]); + const journey = await profile.parseJourney(ctx, res.verbindungen[0]); t.same(journey, expected.journey); t.end(); diff --git a/test/parse/dbnav-journey.js b/test/parse/dbnav-journey.js index baedbd4d..844a11f2 100644 --- a/test/parse/dbnav-journey.js +++ b/test/parse/dbnav-journey.js @@ -469,8 +469,8 @@ const input = { ], } -tap.test('dbnav profile fixes time zone bug', (t) => { // see https://github.com/public-transport/db-vendo-client/issues/24 - const parsedJourney = parseJourneyDbnav(ctx, structuredClone(input)); +tap.test('dbnav profile fixes time zone bug', async (t) => { // see https://github.com/public-transport/db-vendo-client/issues/24 + const parsedJourney = await parseJourneyDbnav(ctx, structuredClone(input)); const expectedDeparture = '2025-03-06T14:52:00+01:00'; t.equal(parsedJourney.legs[0].departure, expectedDeparture) @@ -484,14 +484,14 @@ tap.test('dbnav profile fixes time zone bug', (t) => { // see https://github.com t.end(); }) -tap.test('dbnav profile parses journey without time zone bug like other profiles', (t) => { +tap.test('dbnav profile parses journey without time zone bug like other profiles', async (t) => { const _input = structuredClone(input); // fix bug by hand _input.verbindungsAbschnitte[0].ezAbgangsDatum = '2025-03-06T14:52:00+01:00'; _input.verbindungsAbschnitte[0].ezAnkunftsDatum = '2025-03-06T14:54:00+01:00'; - const expected = parseJourneyDefault(ctx, _input) + const expected = await parseJourneyDefault(ctx, _input) - t.same(parseJourneyDbnav(ctx, _input), expected); + t.same(await parseJourneyDbnav(ctx, _input), expected); t.end(); })