refreshJourney with tickets, loyaltyCard/firstClass rest support, accept-lang

This commit is contained in:
Traines 2024-12-11 23:51:58 +00:00
parent f379fba930
commit 760a1bdb54
12 changed files with 10884 additions and 50 deletions

26
api.js
View file

@ -1,6 +1,30 @@
import {createClient} from './index.js';
import {profile as dbProfile} from './p/db/index.js';
import {createHafasRestApi as createApi} from 'hafas-rest-api';
import {loyaltyCardParser} from 'db-rest/lib/loyalty-cards.js';
import {parseBoolean, parseInteger} from 'hafas-rest-api/lib/parse.js';
const mapRouteParsers = (route, parsers) => {
if (!route.includes('journey')) {
return parsers;
}
return {
...parsers,
loyaltyCard: loyaltyCardParser,
firstClass: {
description: 'Search for first-class options?',
type: 'boolean',
default: 'false',
parse: parseBoolean,
},
age: {
description: 'Age of traveller',
type: 'integer',
defaultStr: '*adult*',
parse: parseInteger,
},
};
};
const config = {
hostname: process.env.HOSTNAME || 'localhost',
@ -15,8 +39,10 @@ const config = {
aboutPage: true,
etags: 'strong',
csp: 'default-src \'none\' style-src \'self\' \'unsafe-inline\' img-src https:',
mapRouteParsers,
};
const start = async () => {
const vendo = createClient(dbProfile, 'my-hafas-rest-api');
const api = await createApi(vendo, config);

View file

@ -43,7 +43,7 @@ const createClient = (profile, userAgent, opt = {}) => {
throw new TypeError('userAgent must be a string');
}
if (FORBIDDEN_USER_AGENTS.includes(userAgent.toLowerCase())) {
throw new TypeError(`userAgent should tell the HAFAS API operators how to contact you. If you have copied "${userAgent}" value from the documentation, please adapt it.`);
throw new TypeError(`userAgent should tell the API operators how to contact you. If you have copied "${userAgent}" value from the documentation, please adapt it.`);
}
const _stationBoard = async (station, type, resultsField, parse, opt = {}) => {
@ -241,17 +241,11 @@ const createClient = (profile, userAgent, opt = {}) => {
const req = profile.formatRefreshJourneyReq({profile, opt}, refreshToken);
const {res, common} = await profile.request({profile, opt}, userAgent, req);
if (!Array.isArray(res.outConL) || !res.outConL[0]) {
throw new HafasError('invalid response, expected outConL[0]', null, {});
}
const ctx = {profile, opt, common, res};
return {
journey: profile.parseJourney(ctx, res.outConL[0]),
realtimeDataUpdatedAt: res.planrtTS && res.planrtTS !== '0'
? parseInt(res.planrtTS)
: null,
journey: profile.parseJourney(ctx, res.verbindungen[0]),
realtimeDataUpdatedAt: null, // TODO
};
};

View file

@ -95,7 +95,7 @@ const checkIfResponseIsOk = (_) => {
};
const request = async (ctx, userAgent, reqData) => {
const {profile} = ctx;
const {profile, opt} = ctx;
const endpoint = reqData.endpoint;
delete reqData.endpoint;
@ -110,6 +110,7 @@ const request = async (ctx, userAgent, reqData) => {
'Content-Type': 'application/json',
'Accept-Encoding': 'gzip, br, deflate',
'Accept': 'application/json',
'Accept-Language': opt.language || profile.defaultLanguage || 'en',
'user-agent': profile.randomizeUserAgent
? randomizeUserAgent(userAgent)
: userAgent,
@ -121,7 +122,6 @@ const request = async (ctx, userAgent, reqData) => {
});
const url = endpoint + (reqData.path || '') + '?' + stringify(req.query, {arrayFormat: 'brackets', encodeValuesOnly: true});
console.log(url);
const reqId = randomBytes(3)
.toString('hex');
const fetchReq = new Request(url, req);

View file

@ -1,5 +1,6 @@
{
"journeysEndpoint": "https://int.bahn.de/web/api/angebote/fahrplan",
"refreshJourneysEndpoint": "https://int.bahn.de/web/api/angebote/recon",
"locationsEndpoint": "https://int.bahn.de/web/api/reiseloesung/orte",
"nearbyEndpoint": "https://int.bahn.de/web/api/reiseloesung/orte/nearby",
"boardEndpoint": "https://regio-guide.de/@prd/zupo-travel-information/api/public/ri/board/",

View file

@ -204,36 +204,20 @@ const transformJourneysQuery = ({profile, opt}, query) => {
const formatRefreshJourneyReq = (ctx, refreshToken) => {
const {profile, opt} = ctx;
const req = {
getIST: true,
getPasslist: Boolean(opt.stopovers),
getPolyline: Boolean(opt.polylines),
getTariff: Boolean(opt.tickets),
let query = {
ctxRecon: refreshToken,
deutschlandTicketVorhanden: false,
nurDeutschlandTicketVerbindungen: false,
reservierungsKontingenteVorhanden: false,
};
if (profile.refreshJourneyUseOutReconL) {
req.outReconL = [{ctx: refreshToken}];
} else {
req.ctxRecon = refreshToken;
}
req.trfReq = trfReq(opt, true);
query = Object.assign(query, trfReq(opt, true));
return {
meth: 'Reconstruction',
req,
endpoint: profile.refreshJourneysEndpoint,
body: query,
method: 'post',
};
};
// todo: fix this
// line: {
// type: 'line',
// id: '5-vbbbvb-x9',
// fahrtNr: '52496',
// name: 'X9',
// public: true,
// mode: 'bus',
// product: 'bus',
// operator: {type: 'operator', id: 'nahreisezug', name: 'Nahreisezug'}
// }
const parseLineWithAdditionalName = ({parsed}, l) => {
if (l.nameS && ['bus', 'tram', 'ferry'].includes(l.product)) {
parsed.name = l.nameS;
@ -245,11 +229,8 @@ const parseLineWithAdditionalName = ({parsed}, l) => {
return parsed;
};
// todo: sotRating, conSubscr, isSotCon, showARSLink, sotCtxt
// todo: conSubscr, showARSLink, useableTime
const mutateToAddPrice = (parsed, raw) => {
parsed.price = null;
// TODO find all prices?
if (raw.angebotsPreis?.betrag) {
parsed.price = {
amount: raw.angebotsPreis.betrag,
@ -260,9 +241,49 @@ const mutateToAddPrice = (parsed, raw) => {
return parsed;
};
const mutateToAddTickets = (parsed, opt, j) => {
if (!opt.tickets) {
return;
}
if (j.reiseAngebote && j.reiseAngebote.length > 0) { // if refreshJourney()
parsed.tickets = j.reiseAngebote
.filter(s => s.typ == 'REISEANGEBOT' && !s.angebotsbeziehungList.flatMap(b => b.referenzen)
.find(r => r.referenzAngebotsoption == 'PFLICHT'))
.map((s) => {
return {
name: s.name,
priceObj: {
amount: Math.round(s.preis?.betrag * 100),
currency: s.preis?.waehrung,
},
addData: s.teilpreis ? 'Teilpreis / partial fare' : undefined,
addDataTicketInfo: s.konditionsAnzeigen?.map(a => a.anzeigeUeberschrift)
.join('. '),
addDataTicketDetails: s.konditionsAnzeigen?.map(a => a.textLang)
.join(' '),
addDataTravelInfo: s.leuchtturmInfo?.text,
firstClass: s.klasse == 'KLASSE_1',
partialFare: s.teilpreis,
};
});
if (opt.generateUnreliableTicketUrls) {
// TODO
}
} else if (j.angebotsPreis?.betrag) { // if journeys()
parsed.tickets = [{
name: 'from',
priceObj: {
amount: Math.round(j.angebotsPreis.betrag * 100),
currency: j.angebotsPreis.waehrung,
},
}];
}
};
const parseJourneyWithPriceAndTickets = ({parsed, opt}, raw) => {
mutateToAddPrice(parsed, raw);
// mutateToAddTickets(parsed, opt, raw); TODO
mutateToAddTickets(parsed, opt, raw);
return parsed;
};

View file

@ -1,12 +1,4 @@
const c = {
NONE: Symbol('no loyalty card'),
BAHNCARD: Symbol('Bahncard'),
VORTEILSCARD: Symbol('VorteilsCard'),
HALBTAXABO: Symbol('HalbtaxAbo'),
VOORDEELURENABO: Symbol('Voordeelurenabo'),
SHCARD: Symbol('SH-Card'),
GENERALABONNEMENT: Symbol('General-Abonnement'),
};
import {data as c} from 'hafas-client/p/db/loyalty-cards.js'; // TODO remove hafas-client dep?
// see https://gist.github.com/juliuste/202bb04f450a79f8fa12a2ec3abcd72d
const formatLoyaltyCard = (data) => {

View file

@ -59,6 +59,7 @@
"cross-fetch": "^4.0.0",
"google-polyline": "^1.0.3",
"gps-distance": "0.0.4",
"hafas-client": "^6.3.2",
"https-proxy-agent": "^7.0.0",
"lodash": "^4.17.5",
"luxon": "^3.1.1",
@ -73,6 +74,7 @@
"@pollyjs/core": "^6.0.5",
"@pollyjs/persister-fs": "^6.0.5",
"@stylistic/eslint-plugin": "^1.5.1",
"db-rest": "github:derhuerst/db-rest",
"eslint": "^8.56.0",
"hafas-rest-api": "^5.1.3",
"is-coordinates": "^2.0.2",

View file

@ -2,10 +2,11 @@ import slugg from 'slugg';
const parseLine = (ctx, p) => {
const profile = ctx.profile;
const fahrtNr = p.verkehrsmittel?.nummer || p.transport?.number || p.train?.no;
const res = {
type: 'line',
id: slugg(p.verkehrsmittel?.langText || p.transport?.journeyDescription || p.train?.no), // TODO terrible
fahrtNr: (p.verkehrsmittel?.nummer || p.transport?.number || p.train?.no)+'',
fahrtNr: fahrtNr ? String(fahrtNr) : undefined,
name: p.verkehrsmittel?.name || p.zugName || p.transport?.journeyDescription || p.train && p.train.category + ' ' + p.train.lineName,
public: true,
};

View file

@ -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 res = require('./fixtures/db-refresh-journey.json');
import {dbJourney as expected} from './fixtures/db-refresh-journey.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test');
const {profile} = client;
const opt = {
results: null,
via: null,
stopovers: false,
transfers: -1,
transferTime: 0,
accessibility: 'none',
bike: false,
tickets: true,
polylines: true,
remarks: true,
walkingSpeed: 'normal',
startWithWalking: true,
scheduledDays: false,
departure: '2020-04-10T20:33+02:00',
products: {},
};
tap.test('parses a refresh journey correctly (DB)', (t) => {
const ctx = {profile, opt, common: null, res};
const journey = profile.parseJourney(ctx, res.verbindungen[0]);
t.same(journey, expected.journey);
t.end();
});

View file

@ -283,6 +283,13 @@ const dbJourney = {
],
refreshToken: '¶HKI¶T$A=1@O=Köln Hbf@X=6958730@Y=50943029@L=8000207@a=128@$A=1@O=Köln Messe/Deutz@X=6975000@Y=50940872@L=8003368@a=128@$202504110511$202504110512$S 12$$1$$$$$$§W$A=1@O=Köln Messe/Deutz@X=6975000@Y=50940872@L=8003368@a=128@$A=1@O=Köln Messe/Deutz Gl.11-12@X=6974065@Y=50941717@L=8073368@a=128@$202504110512$202504110519$$$1$$$$$$§T$A=1@O=Köln Messe/Deutz Gl.11-12@X=6974065@Y=50941717@L=8073368@a=128@$A=1@O=Nürnberg Hbf@X=11082989@Y=49445615@L=8000284@a=128@$202504110520$202504110858$ICE 523$$1$$$$$$¶KC¶#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#81#AM2#0#RT#7#¶KCC¶I1ZFIzEjRVJHIzMjSElOIzAjRUNLIzcwNTkxMXw3MDU5MTF8NzA2MTM4fDcwNjEzOHwwfDB8NDg1fDcwNTg5N3wxfDB8MTh8MHwwfC0yMTQ3NDgzNjQ4I0dBTSMxMTA0MjUwNTExIwpaI1ZOIzEjU1QjMTczMzE3MzczMSNQSSMxI1pJIzE2MTQ3MyNUQSMxI0RBIzExMDQyNSMxUyM4MDAwMjA4IzFUIzUwNCNMUyM4MDAyNzUzI0xUIzU0NSNQVSM4MSNSVCMxI0NBI3MjWkUjMTIjWkIjUyAgICAgMTIjUEMjNCNGUiM4MDAwMjA3I0ZUIzUxMSNUTyM4MDAzMzY4I1RUIzUxMiMKRiNWTiMwI1NUIzE3MzMxNzM3MzEjUEkjMSNQVSM4MSNaSSMyMjgzODI4ODkzI0RBIzExMDQyNSNGUiM4MDAzMzY4I1RPIzgwNzMzNjgjRlQjNTEyI1RUIzUxOSNUUyMwI0ZGIyNGViMwIwpaI1ZOIzEjU1QjMTczMzE3MzczMSNQSSMxI1pJIzE1NTA2MyNUQSMwI0RBIzExMDQyNSMxUyM4MDAwMDgwIzFUIzM1OCNMUyM4MDAwMjYxI0xUIzEwMDYjUFUjODEjUlQjMSNDQSNJQ0UjWkUjNTIzI1pCI0lDRSAgNTIzI1BDIzAjRlIjODA3MzM2OCNGVCM1MjAjVE8jODAwMDI4NCNUVCM4NTgj¶KRCC¶#VE#1#¶SC¶1_H4sIAAAAAAACA32P306DMBjFX8X0GpevhUIhIUFGFv8sGzHOaIwXbHQTU2CWskgIz+GbeOXdXswCemE09qLpOT09v68tOnCJPIQnDkMG4q9Kiyic3EYTV2vJX5DXoqLOZ8ijRn8IkQcGKmsVJYrrMAFCwcIYDeZNlvcmUNBLW9uh4RQb6LloZkLJOfIeWqSafR+Lr5eRDuVl2quLxVSLQyLqXmEgJuoeh5mmT7uxWJNTvp+Xm7FGZKlOnvk4WPpXx3dRnJyvt8Gdb7uUOSYE9z4F1zKBuMHKZxDM9QZAwAlC/WbvY8c0scNsmwSZvzq+ATDA1KIs0INUavzgbJgikfJP7OL4IYs1l7svNMbAiMtczbZcy6I2pj/YzPqHTQh2zd/sHVdxKRqRFdpTsuaDdVnWsuBNWNZFWiFvm4hqvIiTqhJZpb6zfFPGiUxyHWq7rvsE0LytQvMBAAA=',
price: {amount: 31.49, currency: 'EUR', hint: null},
tickets: [{
name: 'from',
priceObj: {
amount: 3149,
currency: 'EUR',
},
}],
remarks: [],
};

265
test/fixtures/db-refresh-journey.js vendored Normal file
View file

@ -0,0 +1,265 @@
const dbJourney = {
journey: {
type: 'journey',
legs: [
{
origin: {
type: 'stop',
id: '8011160',
name: 'Berlin Hbf',
location: {
type: 'location',
id: '8011160',
latitude: 52.525589,
longitude: 13.369549,
},
},
destination: {
type: 'stop',
id: '8000080',
name: 'Dortmund Hbf',
location: {
type: 'location',
id: '8000080',
latitude: 51.517899,
longitude: 7.459294,
},
},
departure: '2024-12-18T00:22:00+01:00',
plannedDeparture: '2024-12-18T00:22:00+01:00',
departureDelay: null,
arrival: '2024-12-18T05:21:00+01:00',
plannedArrival: '2024-12-18T05:21:00+01:00',
arrivalDelay: null,
tripId: '2|#VN#1#ST#1733173731#PI#1#ZI#153019#TA#0#DA#181224#1S#8010255#1T#11#LS#8500010#LT#1048#PU#81#RT#1#CA#ICE#ZE#101#ZB#ICE 101#PC#0#FR#8010255#FT#11#TO#8500010#TT#1048#',
line: {
type: 'line',
id: 'ice-101',
fahrtNr: '101',
name: 'ICE 101',
public: true,
productName: 'ICE',
mode: 'train',
product: 'nationalExpress',
operator: {
type: 'operator',
id: 'db-fernverkehr-ag',
name: 'DB Fernverkehr AG',
},
},
direction: 'KÖLN',
arrivalPlatform: '16',
plannedArrivalPlatform: '16',
departurePlatform: '13',
plannedDeparturePlatform: '13',
remarks: [
{
text: 'Bicycles conveyed - subject to reservation',
type: 'hint',
code: 'bicycle-conveyance-reservation',
summary: 'bicycles conveyed, subject to reservation',
},
{
text: 'Number of bicycles conveyed limited',
type: 'hint',
code: 'bicycle-conveyance',
summary: 'bicycles conveyed',
},
{
text: 'Komfort Check-in possible (visit bahn.de/kci for more information)',
type: 'hint',
code: 'komfort-checkin',
summary: 'Komfort-Checkin available',
},
{
text: 'vehicle-mounted access aid',
type: 'hint',
code: 'boarding-ramp',
summary: 'vehicle-mounted boarding ramp available',
},
{
code: 'text.journeystop.product.or.direction.changes.journey.message',
summary: 'From Minden(Westf) as ICE 101 heading towards Basel SBB',
text: 'From Minden(Westf) as ICE 101 heading towards Basel SBB',
type: 'hint',
},
],
},
{
origin: {
type: 'stop',
id: '8000080',
name: 'Dortmund Hbf',
location: {
type: 'location',
id: '8000080',
latitude: 51.517899,
longitude: 7.459294,
},
},
destination: {
type: 'stop',
id: '8000207',
name: 'Köln Hbf',
location: {
type: 'location',
id: '8000207',
latitude: 50.943029,
longitude: 6.95873,
},
},
departure: '2024-12-18T05:36:00+01:00',
plannedDeparture: '2024-12-18T05:36:00+01:00',
departureDelay: null,
arrival: '2024-12-18T06:47:00+01:00',
plannedArrival: '2024-12-18T06:47:00+01:00',
arrivalDelay: null,
tripId: '2|#VN#1#ST#1733173731#PI#1#ZI#154039#TA#0#DA#181224#1S#8000080#1T#536#LS#8000096#LT#1024#PU#81#RT#1#CA#IC#ZE#2040#ZB#IC 2040#PC#1#FR#8000080#FT#536#TO#8000096#TT#1024#',
line: {
type: 'line',
id: 'ic-2040',
fahrtNr: '2040',
name: 'IC 2040',
public: true,
productName: 'IC',
mode: 'train',
product: 'national',
operator: {
type: 'operator',
id: 'db-fernverkehr-ag',
name: 'DB Fernverkehr AG',
},
},
direction: 'Stuttgart Hbf',
arrivalPlatform: '7 D-G',
plannedArrivalPlatform: '7 D-G',
departurePlatform: '11',
plannedDeparturePlatform: '11',
remarks: [
{
text: 'Bicycles conveyed - subject to reservation',
type: 'hint',
code: 'bicycle-conveyance-reservation',
summary: 'bicycles conveyed, subject to reservation',
},
{
text: 'Number of bicycles conveyed limited',
type: 'hint',
code: 'bicycle-conveyance',
summary: 'bicycles conveyed',
},
{
text: 'Komfort Check-in possible (visit bahn.de/kci for more information)',
type: 'hint',
code: 'komfort-checkin',
summary: 'Komfort-Checkin available',
},
{
code: 'RZ',
summary: 'Einstieg mit Rollstuhl stufenfrei',
text: 'Einstieg mit Rollstuhl stufenfrei',
type: 'hint',
},
{
text: 'Intercity 2: visit www.bahn.de/ic2 for more information',
type: 'hint',
code: 'intercity-2',
summary: 'Intercity 2',
},
],
},
],
refreshToken: 'T$A=1@O=Berlin Hbf@X=13369549@Y=52525589@L=8011160@a=128@$A=1@O=Dortmund Hbf@X=7459294@Y=51517899@L=8000080@a=128@$202412180022$202412180521$ICE 101$$1$$$$$$§T$A=1@O=Dortmund Hbf@X=7459294@Y=51517899@L=8000080@a=128@$A=1@O=Köln Hbf@X=6958730@Y=50943029@L=8000207@a=128@$202412180536$202412180647$IC 2040$$1$$$$$$',
remarks: [],
price: {
amount: 27.99,
currency: 'EUR',
hint: null,
},
tickets: [
{
name: 'Super Sparpreis',
priceObj: {
amount: 2799,
currency: 'EUR',
},
addData: undefined,
addDataTicketInfo: 'Train-specific travel. No cancellations',
addDataTicketDetails: 'You can use all trains indicated on your ticket. Your ticket constitutes a continuous contract of carriage in each direction (through ticket). Should you make a passenger rights claim, the ticket will be considered in its entirety. Special rules apply to tickets including City-Ticket, see there. Your ticket cannot be cancelled.',
addDataTravelInfo: undefined,
firstClass: false,
partialFare: false,
},
{
name: 'Super Sparpreis',
priceObj: {
amount: 3599,
currency: 'EUR',
},
addData: undefined,
addDataTicketInfo: 'Train-specific travel. No cancellations. No access to the DB Lounge',
addDataTicketDetails: 'You can use all trains indicated on your ticket. Your ticket constitutes a continuous contract of carriage in each direction (through ticket). Should you make a passenger rights claim, the ticket will be considered in its entirety. Special rules apply to tickets including City-Ticket, see there. Your ticket cannot be cancelled. Your ticket does not entitle you to use the DB Lounge.',
addDataTravelInfo: 'Travel 1st class',
firstClass: true,
partialFare: false,
},
{
name: 'Sparpreis',
priceObj: {
amount: 3499,
currency: 'EUR',
},
addData: undefined,
addDataTicketInfo: 'Train-specific travel. Cancellation subject to a fee before first day of validity',
addDataTicketDetails: 'You can use all trains indicated on your ticket. Your ticket constitutes a continuous contract of carriage in each direction (through ticket). Should you make a passenger rights claim, the ticket will be considered in its entirety. Special rules apply to tickets including City-Ticket, see there. You can cancel your ticket up to and including 17.12.2024 for a fee of EUR 10,00. You will receive a voucher for the remaining amount. No cancellation thereafter.',
addDataTravelInfo: undefined,
firstClass: false,
partialFare: false,
},
{
name: 'Sparpreis',
priceObj: {
amount: 4499,
currency: 'EUR',
},
addData: undefined,
addDataTicketInfo: 'Train-specific travel. Cancellation subject to a fee before first day of validity. No access to the DB Lounge',
addDataTicketDetails: 'You can use all trains indicated on your ticket. Your ticket constitutes a continuous contract of carriage in each direction (through ticket). Should you make a passenger rights claim, the ticket will be considered in its entirety. Special rules apply to tickets including City-Ticket, see there. You can cancel your ticket up to and including 17.12.2024 for a fee of EUR 10,00. You will receive a voucher for the remaining amount. No cancellation thereafter. Your ticket does not entitle you to use the DB Lounge.',
addDataTravelInfo: 'Travel 1st class',
firstClass: true,
partialFare: false,
},
{
name: 'Flexpreis',
priceObj: {
amount: 13280,
currency: 'EUR',
},
addData: undefined,
addDataTicketInfo: 'Unrestricted choice of trains. Cancellation free of charge before first day of validity. City-Ticket',
addDataTicketDetails: 'Your ICE ticket lets you take any type of train. Your ticket constitutes a continuous contract of carriage in each direction (through ticket). Should you make a passenger rights claim, the ticket will be considered in its entirety. Special rules apply to tickets including City-Ticket; see there. You can cancel your ticket free of charge up to and including 17.12.2024. After that, cancellation is available for a fee of EUR 19,00. Your ticket includes a City-Ticket for Berlin, Tarifbereiche A und B and Köln, Stadtgebiet Köln (Tarifgebiet 2100). The City-Ticket is valid in conjunction with your long-distance ticket only when you use it for connecting services in local or regional rail passenger transport (e.g. S-Bahn, RE and RB trains) as part of the through ticket. The City-Ticket is issued together with your Super Sparpreis or Sparpreis ticket, depending on the journey you have booked.',
addDataTravelInfo: undefined,
firstClass: false,
partialFare: false,
},
{
name: 'Flexpreis',
priceObj: {
amount: 23910,
currency: 'EUR',
},
addData: undefined,
addDataTicketInfo: 'Unrestricted choice of trains. Cancellation free of charge before first day of validity. City-Ticket. Access to the DB Lounge. Seat included',
addDataTicketDetails: 'Your ICE ticket lets you take any type of train. Your ticket constitutes a continuous contract of carriage in each direction (through ticket). Should you make a passenger rights claim, the ticket will be considered in its entirety. Special rules apply to tickets including City-Ticket; see there. You can cancel your ticket free of charge up to and including 17.12.2024. After that, cancellation is available for a fee of EUR 19,00. Your ticket includes a City-Ticket for Berlin, Tarifbereiche A und B and Köln, Stadtgebiet Köln (Tarifgebiet 2100). The City-Ticket is valid in conjunction with your long-distance ticket only when you use it for connecting services in local or regional rail passenger transport (e.g. S-Bahn, RE and RB trains) as part of the through ticket. The City-Ticket is issued together with your Super Sparpreis or Sparpreis ticket, depending on the journey you have booked. Your ticket entitles you to use the DB Lounge. Your ticket includes a free seat reservation.',
addDataTravelInfo: 'Travel 1st class',
firstClass: true,
partialFare: false,
},
],
},
realtimeDataUpdatedAt: null,
};
export {
dbJourney,
};

10485
test/fixtures/db-refresh-journey.json vendored Normal file

File diff suppressed because it is too large Load diff