This commit is contained in:
Marius Angelmann 2025-07-29 21:52:52 +00:00 committed by GitHub
commit 9c7cff15d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 321 additions and 39 deletions

View file

@ -634,7 +634,13 @@
"dbbahnhof", "dbbahnhof",
"Deutschlandticket", "Deutschlandticket",
"fahrverguenstigungen", "fahrverguenstigungen",
"cancelation" "cancelation",
"Verbund",
"Verbundticket",
"Verbundtickets",
"verbundticket",
"Vergangenheit",
"reisewunsch"
], ],
"ignorePaths": [ "ignorePaths": [
"docs/dumps/**", "docs/dumps/**",

View file

@ -82,6 +82,7 @@ With `opt`, you can override the default options, which look like this:
loyaltyCard: null, // BahnCards etc., see below loyaltyCard: null, // BahnCards etc., see below
language: 'en', // language to get results in language: 'en', // language to get results in
bmisNumber: null, // 7-digit BMIS number for business customer rates bmisNumber: null, // 7-digit BMIS number for business customer rates
autoFetchVerbundtickets: false, // automatically fetch Verbundticket prices via recon API (see below)
} }
``` ```
@ -340,4 +341,32 @@ When a BMIS number is provided, the request will include a `firmenZugehoerigkeit
## The `routingMode` option ## The `routingMode` option
The `routingMode` option is not supported by db-vendo-client. The behavior will be the same as the [`HYBRID` mode of hafas-client](https://github.com/public-transport/hafas-client/blob/main/p/db/readme.md#using-the-routingmode-option), i.e. cancelled trains/infeasible journeys will also be contained for informational purpose. The `routingMode` option is not supported by db-vendo-client. The behavior will be the same as the [`HYBRID` mode of hafas-client](https://github.com/public-transport/hafas-client/blob/main/p/db/readme.md#using-the-routingmode-option), i.e. cancelled trains/infeasible journeys will also be contained for informational purpose.
## Verbundtickets
Verbundtickets (local transport network tickets) require a two-step API process to fetch prices. By default, journeys with Verbundtickets will not include ticket prices. To automatically fetch these prices, use the `autoFetchVerbundtickets` option:
```js
const journeys = await client.journeys(from, to, {
tickets: true,
autoFetchVerbundtickets: true // enable automatic Verbundticket price fetching
})
```
**Note:** This option will make an additional API request for each journey that contains Verbundtickets, which may impact performance. Use it only when you need ticket prices for local transport networks.
### Manual Verbundticket price fetching
If you prefer to fetch Verbundticket prices manually (or if automatic detection doesn't work), you can use the `refreshJourney` method:
```js
const journeys = await client.journeys(from, to, { tickets: true })
const journey = journeys.journeys[0]
// Check if journey has empty ticket prices but a refresh token
if (!journey.price && journey.refreshToken) {
const refreshed = await client.refreshJourney(journey.refreshToken, { tickets: true })
// refreshed.journey will have the Verbundticket prices
}
```

View file

@ -110,7 +110,7 @@ const createClient = (profile, userAgent, opt = {}) => {
const {res} = await profile.request({profile, opt}, userAgent, req); 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()) let results = (res[resultsField] || res.items || res.bahnhofstafelAbfahrtPositionen || res.bahnhofstafelAnkunftPositionen || res.entries.flat())
.map(res => parse(ctx, res)); // TODO sort?, slice .map(res => parse(ctx, res)); // TODO sort?, slice
@ -184,6 +184,7 @@ const createClient = (profile, userAgent, opt = {}) => {
deutschlandTicketDiscount: false, deutschlandTicketDiscount: false,
deutschlandTicketConnectionsOnly: false, deutschlandTicketConnectionsOnly: false,
bmisNumber: null, // 7-digit BMIS number for business customer rates bmisNumber: null, // 7-digit BMIS number for business customer rates
autoFetchVerbundtickets: false, // automatically fetch Verbundticket prices via recon API
}, opt); }, opt);
if (opt.when !== undefined) { if (opt.when !== undefined) {
@ -208,13 +209,13 @@ const createClient = (profile, userAgent, opt = {}) => {
const req = profile.formatJourneysReq({profile, opt}, from, to, when, outFrwd, journeysRef); const req = profile.formatJourneysReq({profile, opt}, from, to, when, outFrwd, journeysRef);
const {res} = await profile.request({profile, opt}, userAgent, req); 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) { if (opt.bestprice) {
res.verbindungen = (res.intervalle || res.tagesbestPreisIntervalle).flatMap(i => i.verbindungen.map(v => ({...v, ...v.verbindung}))); 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 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 const journeys = await Promise.all(verbindungen
.map(j => profile.parseJourney(ctx, j)); .map(j => profile.parseJourney(ctx, j)));
if (opt.bestprice) { if (opt.bestprice) {
journeys.sort((a, b) => a.price?.amount - b.price?.amount); journeys.sort((a, b) => a.price?.amount - b.price?.amount);
} }
@ -245,15 +246,16 @@ const createClient = (profile, userAgent, opt = {}) => {
deutschlandTicketDiscount: false, deutschlandTicketDiscount: false,
deutschlandTicketConnectionsOnly: false, deutschlandTicketConnectionsOnly: false,
bmisNumber: null, // 7-digit BMIS number for business customer rates bmisNumber: null, // 7-digit BMIS number for business customer rates
autoFetchVerbundtickets: false, // automatically fetch Verbundticket prices via recon API
}, opt); }, opt);
const req = profile.formatRefreshJourneyReq({profile, opt}, refreshToken); const req = profile.formatRefreshJourneyReq({profile, opt}, refreshToken);
const {res} = await profile.request({profile, opt}, userAgent, req); const {res} = await profile.request({profile, opt}, userAgent, req);
const ctx = {profile, opt, common, res}; const ctx = {profile, opt, common, res, userAgent};
return { 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 realtimeDataUpdatedAt: null, // TODO
}; };
}; };
@ -278,7 +280,7 @@ const createClient = (profile, userAgent, opt = {}) => {
const {res} = await profile.request({profile, opt}, userAgent, req); 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)); const results = res.map(loc => profile.parseLocation(ctx, loc));
return Number.isInteger(opt.results) return Number.isInteger(opt.results)
@ -327,7 +329,7 @@ const createClient = (profile, userAgent, opt = {}) => {
const req = profile.formatNearbyReq({profile, opt}, location); const req = profile.formatNearbyReq({profile, opt}, location);
const {res} = await profile.request({profile, opt}, userAgent, req); 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 results = res.map(loc => {
const res = profile.parseLocation(ctx, loc); const res = profile.parseLocation(ctx, loc);
if (res.latitude || res.location?.latitude) { if (res.latitude || res.location?.latitude) {
@ -359,7 +361,7 @@ const createClient = (profile, userAgent, opt = {}) => {
const req = profile.formatTripReq({profile, opt}, id); const req = profile.formatTripReq({profile, opt}, id);
const {res} = await profile.request({profile, opt}, userAgent, req); 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); const trip = profile.parseTrip(ctx, res, id);

View file

@ -0,0 +1,94 @@
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',
method: 'post',
headers: getHeaders('application/x.db.vendo.mob.verbindungssuche.v9+json'),
body: {
fahrverguenstigungen: {
nurDeutschlandTicketVerbindungen: ctx.opt.deutschlandTicketConnectionsOnly || false,
deutschlandTicketVorhanden: ctx.opt.deutschlandTicketDiscount || false,
},
reservierungsKontingenteVorhanden: false,
suchParameter: {
reisewunschHin: {
economic: false,
abgangsLocationId: abgangsOrt?.locationId || extractLocationFromKontext(kontext, 'from'),
viaLocations: [],
fahrradmitnahme: false,
zielLocationId: ankunftsOrt?.locationId || extractLocationFromKontext(kontext, 'to'),
zeitWunsch: {
reiseDatum: abgangsDatum || new Date()
.toISOString(),
zeitPunktArt: 'ABFAHRT',
},
verkehrsmittel: ['ALL'],
},
},
einstiegsTypList: ['STANDARD'],
klasse: 'KLASSE_2',
verbindungHin: {
kontext: kontext,
},
reisendenProfil: {
reisende: [{
reisendenTyp: 'ERWACHSENER',
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',
};
}
try {
const {res} = await profile.request(ctx, userAgent, reconRequest);
return res;
} catch (err) {
// Log error but don't fail the entire journey request
if (opt.debug || profile.DEBUG) {
console.error('Failed to fetch Verbundticket prices:', err);
}
return null;
}
};
// Helper to extract location from kontext string
const extractLocationFromKontext = (kontext, type) => {
// Kontext format: "¶HKI¶T$A=1@O=From@...@$A=1@O=To@...@$..."
const parts = kontext.split('$');
if (type === 'from' && parts[1]) {
return parts[1];
} else if (type === 'to' && parts[2]) {
return parts[2];
}
return null;
};
export {
fetchVerbundticketPrices,
};

View file

@ -1,6 +1,6 @@
import {parseJourney as parseJourneyDefault} from '../../parse/journey.js'; import {parseJourney as parseJourneyDefault} from '../../parse/journey.js';
const parseJourney = (ctx, jj) => { const parseJourney = async (ctx, jj) => {
const legs = (jj.verbindung || jj).verbindungsAbschnitte; const legs = (jj.verbindung || jj).verbindungsAbschnitte;
if (legs.length > 0) { if (legs.length > 0) {
legs[0] = preprocessJourneyLeg(legs[0]); legs[0] = preprocessJourneyLeg(legs[0]);

View file

@ -1,4 +1,5 @@
import {parseRemarks} from './remarks.js'; import {parseRemarks} from './remarks.js';
import {fetchVerbundticketPrices} from '../lib/fetch-verbundticket-prices.js';
const createFakeWalkingLeg = (prevLeg, leg) => { const createFakeWalkingLeg = (prevLeg, leg) => {
const fakeWalkingLeg = { const fakeWalkingLeg = {
@ -40,7 +41,7 @@ const trimJourneyId = (journeyId) => {
return journeyId; return journeyId;
}; };
const parseJourney = (ctx, jj) => { // j = raw journey const parseJourney = async (ctx, jj) => { // j = raw journey
const {profile, opt} = ctx; const {profile, opt} = ctx;
const j = jj.verbindung || jj; const j = jj.verbindung || jj;
const fallbackLocations = parseLocationsFromCtxRecon(ctx, j); const fallbackLocations = parseLocationsFromCtxRecon(ctx, j);
@ -80,10 +81,48 @@ const parseJourney = (ctx, jj) => { // j = raw journey
})); }));
} }
res.price = profile.parsePrice(ctx, jj); // Check if this is a Verbundticket that needs price fetching
const tickets = profile.parseTickets(ctx, jj); // DB Navigator mobile API: Verbundtickets have an 'angebote' section with 'verbundCode' but empty 'angebotsCluster'
if (tickets) { const angebote = jj.angebote || j.angebote;
res.tickets = tickets; 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'),
);
if ((hasVerbundCode && hasEmptyAngebotsCluster || hasVerbundWarning) && hasKontext && ctx.userAgent && opt.tickets && opt.autoFetchVerbundtickets) {
// Fetch Verbundticket prices via recon API
const reconResult = await fetchVerbundticketPrices(ctx, ctx.userAgent, j);
if (reconResult) {
// Use the recon result for price and ticket parsing
if (reconResult.angebote) {
// Store the fetched angebote data in the original response for parsing
jj.angebote = reconResult.angebote;
}
res.price = profile.parsePrice(ctx, jj);
const tickets = profile.parseTickets(ctx, jj);
if (tickets) {
res.tickets = tickets;
}
} else {
// Fallback to original parsing
res.price = profile.parsePrice(ctx, jj);
const tickets = profile.parseTickets(ctx, jj);
if (tickets) {
res.tickets = tickets;
}
}
} else {
// Regular journey parsing
res.price = profile.parsePrice(ctx, jj);
const tickets = profile.parseTickets(ctx, jj);
if (tickets) {
res.tickets = tickets;
}
} }
return res; return res;

View file

@ -11,6 +11,39 @@ const parsePrice = (ctx, raw) => {
partialFare: partialFare, partialFare: partialFare,
}; };
} }
// For Verbundtickets, try to get the lowest price from reiseAngebote
if (raw.reiseAngebote && raw.reiseAngebote.length > 0) {
let lowestPrice = null;
for (const angebot of raw.reiseAngebote) {
const fahrtAngebote = angebot.hinfahrt?.fahrtAngebote;
if (fahrtAngebote && fahrtAngebote.length > 0) {
for (const fahrt of fahrtAngebote) {
if (fahrt.preis?.betrag && (!lowestPrice || fahrt.preis.betrag < lowestPrice.amount)) {
lowestPrice = {
amount: fahrt.preis.betrag,
currency: fahrt.preis.waehrung,
hint: null,
partialFare: fahrt.teilpreis || false,
};
}
}
}
// Also check direct price on reiseAngebot
if (angebot.preis?.betrag && (!lowestPrice || angebot.preis.betrag < lowestPrice.amount)) {
lowestPrice = {
amount: angebot.preis.betrag,
currency: angebot.preis.waehrung,
hint: null,
partialFare: angebot.teilpreis || false,
};
}
}
if (lowestPrice) {
return lowestPrice;
}
}
return undefined; return undefined;
}; };
@ -20,19 +53,43 @@ const parseTickets = (ctx, j) => {
} }
let tickets = undefined; let tickets = undefined;
let price = parsePrice(ctx, j); let price = parsePrice(ctx, j);
let ang = j.reiseAngebote // Handle DB Navigator mobile API format
|| j.angebote?.angebotsCluster?.flatMap(c => c.angebotsSubCluster 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
.flatMap(c => c.angebotsPositionen .flatMap(c => c.angebotsPositionen
.flatMap(p => [ .flatMap(p => {
p.einfacheFahrt?.standard?.reisePosition, // Extract all possible ticket types from DB Navigator format
p.einfacheFahrt?.upsellEntgelt?.einfacheFahrt?.reisePosition, const positions = [];
].filter(p => p)
.map(p => { // Handle verbundAngebot
p.reisePosition.teilpreis = Boolean(p.teilpreisInformationen?.length); if (p.verbundAngebot?.reisePosition?.reisePosition) {
return p.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.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.reisePosition || p.einfacheFahrt.upsellEntgelt.einfacheFahrt.reisePosition;
rp.teilpreis = Boolean(p.einfacheFahrt.upsellEntgelt.einfacheFahrt.teilpreisInformationen?.length);
positions.push(rp);
}
return positions;
}),
), ),
); );
}
if (ang && ang.length > 0) { // if refreshJourney() if (ang && ang.length > 0) { // if refreshJourney()
tickets = ang tickets = ang
.filter(s => s.typ == 'REISEANGEBOT' && !s.angebotsbeziehungList?.flatMap(b => b.referenzen) .filter(s => s.typ == 'REISEANGEBOT' && !s.angebotsbeziehungList?.flatMap(b => b.referenzen)
@ -74,6 +131,61 @@ const parseTickets = (ctx, j) => {
currency: price.currency, currency: price.currency,
}, },
}]; }];
} else if (j.reiseAngebote && j.reiseAngebote.length > 0) {
// Handle Verbundtickets in initial journey response
tickets = [];
for (const angebot of j.reiseAngebote) {
// Extract tickets from fahrtAngebote
const fahrtAngebote = angebot.hinfahrt?.fahrtAngebote;
if (fahrtAngebote && fahrtAngebote.length > 0) {
for (const fahrt of fahrtAngebote) {
if (fahrt.preis?.betrag && fahrt.name) {
const ticket = {
name: fahrt.name,
priceObj: {
amount: Math.round(fahrt.preis.betrag * 100),
currency: fahrt.preis.waehrung,
},
firstClass: fahrt.klasse === 'KLASSE_1',
partialFare: fahrt.teilpreis || false,
};
// Add additional info if available
const conds = fahrt.konditionsAnzeigen || fahrt.konditionen;
if (conds) {
ticket.addDataTicketInfo = conds.map(a => a.anzeigeUeberschrift || a.bezeichnung)
.join('. ');
ticket.addDataTicketDetails = conds.map(a => a.textLang || a.details)
.join(' ');
}
tickets.push(ticket);
}
}
}
// Also check if reiseAngebot has direct price/name
if (angebot.preis?.betrag && angebot.name) {
const ticket = {
name: angebot.name,
priceObj: {
amount: Math.round(angebot.preis.betrag * 100),
currency: angebot.preis.waehrung,
},
firstClass: angebot.klasse === 'KLASSE_1',
partialFare: angebot.teilpreis || false,
};
const conds = angebot.konditionsAnzeigen || angebot.konditionen;
if (conds) {
ticket.addDataTicketInfo = conds.map(a => a.anzeigeUeberschrift || a.bezeichnung)
.join('. ');
ticket.addDataTicketDetails = conds.map(a => a.textLang || a.details)
.join(' ');
}
tickets.push(ticket);
}
}
// Sort tickets by price (lowest first)
if (tickets.length > 0) {
tickets.sort((a, b) => a.priceObj.amount - b.priceObj.amount);
}
} }
return tickets; return tickets;
}; };

View file

@ -26,9 +26,9 @@ const opt = {
products: {}, products: {},
}; };
tap.test('parses a refresh journey correctly (DB)', (t) => { tap.test('parses a refresh journey correctly (DB)', async (t) => {
const ctx = {profile, opt, common: null, res}; const ctx = {profile, opt, common: null, res, userAgent: 'test'};
const journey = profile.parseJourney(ctx, res); const journey = await profile.parseJourney(ctx, res);
t.same(journey, expected.journey); t.same(journey, expected.journey);
t.end(); t.end();

View file

@ -26,9 +26,9 @@ const opt = {
products: {}, 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 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.same(journey, expected);
t.end(); t.end();

View file

@ -26,9 +26,9 @@ const opt = {
products: {}, 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 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.same(journey, expected.journey);
t.end(); t.end();

View file

@ -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 tap.test('dbnav profile fixes time zone bug', async (t) => { // see https://github.com/public-transport/db-vendo-client/issues/24
const parsedJourney = parseJourneyDbnav(ctx, structuredClone(input)); const parsedJourney = await parseJourneyDbnav(ctx, structuredClone(input));
const expectedDeparture = '2025-03-06T14:52:00+01:00'; const expectedDeparture = '2025-03-06T14:52:00+01:00';
t.equal(parsedJourney.legs[0].departure, expectedDeparture) 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(); 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); const _input = structuredClone(input);
// fix bug by hand // fix bug by hand
_input.verbindungsAbschnitte[0].ezAbgangsDatum = '2025-03-06T14:52:00+01:00'; _input.verbindungsAbschnitte[0].ezAbgangsDatum = '2025-03-06T14:52:00+01:00';
_input.verbindungsAbschnitte[0].ezAnkunftsDatum = '2025-03-06T14:54: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(); t.end();
}) })