mirror of
https://github.com/public-transport/db-vendo-client.git
synced 2025-09-17 19:49:23 +03:00
Merge 3e666c0b1b
into d15369406d
This commit is contained in:
commit
9c7cff15d1
11 changed files with 321 additions and 39 deletions
|
@ -634,7 +634,13 @@
|
||||||
"dbbahnhof",
|
"dbbahnhof",
|
||||||
"Deutschlandticket",
|
"Deutschlandticket",
|
||||||
"fahrverguenstigungen",
|
"fahrverguenstigungen",
|
||||||
"cancelation"
|
"cancelation",
|
||||||
|
"Verbund",
|
||||||
|
"Verbundticket",
|
||||||
|
"Verbundtickets",
|
||||||
|
"verbundticket",
|
||||||
|
"Vergangenheit",
|
||||||
|
"reisewunsch"
|
||||||
],
|
],
|
||||||
"ignorePaths": [
|
"ignorePaths": [
|
||||||
"docs/dumps/**",
|
"docs/dumps/**",
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
```
|
20
index.js
20
index.js
|
@ -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);
|
||||||
|
|
||||||
|
|
94
lib/fetch-verbundticket-prices.js
Normal file
94
lib/fetch-verbundticket-prices.js
Normal 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,
|
||||||
|
};
|
|
@ -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]);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
132
parse/tickets.js
132
parse/tickets.js
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Reference in a new issue