Compare commits

...

28 commits
v6.8.0 ... main

Author SHA1 Message Date
Traines
a2e3a36e20 spelling...
Some checks failed
test / lint-and-spellcheck (push) Has been cancelled
test / unit-tests (18.x) (push) Has been cancelled
test / unit-tests (20.x) (push) Has been cancelled
test / unit-tests (22.x) (push) Has been cancelled
test / integration-tests (18.x) (push) Has been cancelled
test / integration-tests (20.x) (push) Has been cancelled
test / integration-tests (22.x) (push) Has been cancelled
test / e2e-tests (18.x) (push) Has been cancelled
2026-01-10 14:01:25 +00:00
Traines
6fa0abbde5 fix cancelled state for exceptional cases 2026-01-10 13:48:33 +00:00
Traines
0c39991e0c bump version
Some checks failed
test / lint-and-spellcheck (push) Has been cancelled
test / unit-tests (18.x) (push) Has been cancelled
test / unit-tests (20.x) (push) Has been cancelled
test / unit-tests (22.x) (push) Has been cancelled
test / integration-tests (18.x) (push) Has been cancelled
test / integration-tests (20.x) (push) Has been cancelled
test / integration-tests (22.x) (push) Has been cancelled
test / e2e-tests (18.x) (push) Has been cancelled
2025-12-15 09:43:05 +00:00
121593
3cfff6eec7
fix: stationBoard entries can be nullish (#41)
When no entries are present an error was thrown when calling `entries.flat()`
2025-12-15 10:41:16 +01:00
Traines
a60cf63b2e dbris: only show trip number if line number is unavailable
Some checks failed
test / lint-and-spellcheck (push) Has been cancelled
test / unit-tests (18.x) (push) Has been cancelled
test / unit-tests (20.x) (push) Has been cancelled
test / unit-tests (22.x) (push) Has been cancelled
test / integration-tests (18.x) (push) Has been cancelled
test / integration-tests (20.x) (push) Has been cancelled
test / integration-tests (22.x) (push) Has been cancelled
test / e2e-tests (18.x) (push) Has been cancelled
2025-11-14 22:45:45 +00:00
Traines
9358c51363 fix dbris 2025-11-14 22:33:01 +00:00
Traines
785f5fcc37 add checks back to release, fix readme
[skip ci]
2025-10-24 20:16:32 +00:00
Traines
ca6ceabbed fix bestprice for db profile 2025-10-24 19:40:50 +00:00
Jannis
d0120439e6
chore: CI: publish to npm using OIDC tokens (#36)
Some checks failed
test / lint-and-spellcheck (push) Has been cancelled
test / unit-tests (18.x) (push) Has been cancelled
test / unit-tests (20.x) (push) Has been cancelled
test / unit-tests (22.x) (push) Has been cancelled
test / integration-tests (18.x) (push) Has been cancelled
test / integration-tests (20.x) (push) Has been cancelled
test / integration-tests (22.x) (push) Has been cancelled
test / e2e-tests (18.x) (push) Has been cancelled
see also https://docs.npmjs.com/trusted-publishers
2025-10-22 20:44:53 +02:00
Traines
c2216120c9 set db profile to old dbnav domain as an alternative, since the new domain
Some checks are pending
test / lint-and-spellcheck (push) Waiting to run
test / unit-tests (18.x) (push) Waiting to run
test / unit-tests (20.x) (push) Waiting to run
test / unit-tests (22.x) (push) Waiting to run
test / integration-tests (18.x) (push) Waiting to run
test / integration-tests (20.x) (push) Waiting to run
test / integration-tests (22.x) (push) Waiting to run
test / e2e-tests (18.x) (push) Blocked by required conditions
might have more agressive blocking
2025-10-21 20:54:11 +00:00
Traines
bdbf4f3761 fix ticket parsing 2025-10-21 20:37:50 +00:00
Traines
a1ab95c249 linting... 2025-10-21 18:33:30 +00:00
Traines
691f07b331 remove arrival time check for refreshJourney since it seems to change for older journeys 2025-10-21 18:30:06 +00:00
Traines
13351e3977 skip weird failing test since dbweb is basically unusable anyways 2025-10-21 18:17:31 +00:00
Traines
2f65f3d05d discontinue dbbahnhof, update deps 2025-10-21 18:05:29 +00:00
Traines
f31f56c00d update dbnav endpoints, fix tests 2025-10-21 17:40:14 +00:00
Traines
31ef3ad56a new dumps 2025-10-21 16:31:22 +00:00
Traines
5ac43bcfba temporarily disable tests on publish
Some checks are pending
test / lint-and-spellcheck (push) Waiting to run
test / unit-tests (18.x) (push) Waiting to run
test / unit-tests (20.x) (push) Waiting to run
test / unit-tests (22.x) (push) Waiting to run
test / integration-tests (18.x) (push) Waiting to run
test / integration-tests (20.x) (push) Waiting to run
test / integration-tests (22.x) (push) Waiting to run
test / e2e-tests (18.x) (push) Blocked by required conditions
2025-10-21 14:16:55 +00:00
Traines
5a2e4f5d13 hotfix for dbnav compat 2025-10-21 14:10:38 +00:00
McToel
2b1e816c7f
Bumped application version (#34) 2025-10-21 15:58:57 +02:00
Traines
b2d9a4e53e linting
Some checks failed
test / lint-and-spellcheck (push) Has been cancelled
test / unit-tests (18.x) (push) Has been cancelled
test / unit-tests (20.x) (push) Has been cancelled
test / unit-tests (22.x) (push) Has been cancelled
test / integration-tests (18.x) (push) Has been cancelled
test / integration-tests (20.x) (push) Has been cancelled
test / integration-tests (22.x) (push) Has been cancelled
test / e2e-tests (18.x) (push) Has been cancelled
2025-08-25 20:18:26 +00:00
Traines
8e9d6ea67a switch db profile to db nav for boards (regioguide deprecation) 2025-08-25 19:55:25 +00:00
Traines
d15369406d v6.9.0
Some checks failed
test / lint-and-spellcheck (push) Has been cancelled
test / unit-tests (18.x) (push) Has been cancelled
test / unit-tests (20.x) (push) Has been cancelled
test / unit-tests (22.x) (push) Has been cancelled
test / integration-tests (18.x) (push) Has been cancelled
test / integration-tests (20.x) (push) Has been cancelled
test / integration-tests (22.x) (push) Has been cancelled
test / e2e-tests (18.x) (push) Has been cancelled
2025-07-14 17:52:52 +00:00
Marius Angelmann
86f2302ad4
Add BMIS number support for business customer rates (#30)
* Add BMIS number support for business customer rates

Introduces a new `bmisNumber` option to journey requests, allowing bahn.business customers to access corporate rates by providing their 7-digit BMIS number. Adds documentation and a `createBusinessClient` helper for convenience. The request payload now includes a `firmenZugehoerigkeit` object when a BMIS number is set.

* Update cspell.config.json

Update cspell.config.json to fix spell check issues in PR #30
2025-07-14 19:48:41 +02:00
Traines
b59d7b3084 fix docs
[skip ci]
2025-04-27 17:13:10 +00:00
Traines
db4c03054a cspell...
Some checks failed
test / lint-and-spellcheck (push) Has been cancelled
test / unit-tests (18.x) (push) Has been cancelled
test / unit-tests (20.x) (push) Has been cancelled
test / unit-tests (22.x) (push) Has been cancelled
test / integration-tests (18.x) (push) Has been cancelled
test / integration-tests (20.x) (push) Has been cancelled
test / integration-tests (22.x) (push) Has been cancelled
test / e2e-tests (18.x) (push) Has been cancelled
2025-04-11 21:05:18 +00:00
Traines
eac21d188b 6.8.1 2025-04-11 20:59:29 +00:00
Traines
ad09f8b1be dticket support 2025-04-11 20:59:06 +00:00
47 changed files with 9159 additions and 1533 deletions

View file

@ -10,6 +10,9 @@ env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }} IMAGE_NAME: ${{ github.repository }}
permissions:
id-token: write # for OIDC-based publishing to npm
jobs: jobs:
build-and-push-docker: build-and-push-docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -53,7 +56,9 @@ jobs:
with: with:
node-version: '20.x' node-version: '20.x'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
# for OIDC-based publishing to npm
- name: setup npm v11
run: npm install -g npm@11
- run: npm ci - run: npm ci
- run: npm publish --provenance --access public - run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View file

@ -95,6 +95,8 @@
"bitmasks", "bitmasks",
"Blaschkoallee", "Blaschkoallee",
"Blissestr", "Blissestr",
"bmis",
"BMIS",
"BNWNZF", "BNWNZF",
"Böhme", "Böhme",
"BONVOYO", "BONVOYO",
@ -195,6 +197,7 @@
"Fehrbelliner", "Fehrbelliner",
"Fernbf", "Fernbf",
"Fernverkehr", "Fernverkehr",
"firmen",
"Flexpreis", "Flexpreis",
"Flix", "Flix",
"Fltr", "Fltr",
@ -263,6 +266,7 @@
"Hohenzollerndamm", "Hohenzollerndamm",
"Hüngheim", "Hüngheim",
"IBNR", "IBNR",
"identifikationsart",
"Ihren", "Ihren",
"Ihrer", "Ihrer",
"Informationen", "Informationen",
@ -610,6 +614,7 @@
"Zoologischer", "Zoologischer",
"Zuege", "Zuege",
"zugart", "zugart",
"Zugehoerigkeit",
"zugattrib", "zugattrib",
"zugattribute", "zugattribute",
"Zuges", "Zuges",
@ -627,7 +632,13 @@
"Intervalle", "Intervalle",
"tagesbest", "tagesbest",
"dbbahnhof", "dbbahnhof",
"cancelation" "Deutschlandticket",
"fahrverguenstigungen",
"cancelation",
"MOTIS",
"motis",
"Berechnung",
"fällt"
], ],
"ignorePaths": [ "ignorePaths": [
"docs/dumps/**", "docs/dumps/**",

View file

@ -48,7 +48,7 @@ Notes:
* routing-search returns polylines (!) * routing-search returns polylines (!)
## Vendo/Movas Navigator API ## Vendo/Movas Navigator API
https://app.vendo.noncd.db.de/mob/ https://app.vendo.noncd.db.de/mob/ and/or https://app.services-bahn.de/
EPs: EPs:
* bahnhofstafel/abfahrt * bahnhofstafel/abfahrt

View file

@ -0,0 +1,17 @@
POST /mob/angebote/fahrplan HTTP/1.1
Accept: application/x.db.vendo.mob.verbindungssuche.v9+json
X-Correlation-ID: 599238b3-d1e0-43a0-9534-ebc0dbf30b72_339465f1-2b99-4c6e-b739-c8ad0efed49a
X-Device-Os-Name: Android
X-Device-Os-Version: 32
X-Device-Model: Google Pixel 3a
X-App-Version: 25.18.2
User-Agent: DBNavigator/Android/25.18.2
Accept-Language: en,de
X-INSTANA-ANDROID: 48cce61a-1b80-4dc4-8818-50bbb38ef54d
Content-Type: application/x.db.vendo.mob.verbindungssuche.v9+json
Content-Length: 822
Host: app.services-bahn.de
Connection: Keep-Alive
Accept-Encoding: gzip
{"autonomeReservierung":false,"einstiegsTypList":["STANDARD"],"fahrverguenstigungen":{"deutschlandTicketVorhanden":true,"nurDeutschlandTicketVerbindungen":false},"klasse":"KLASSE_2","reiseHin":{"wunsch":{"abgangsLocationId":"A\u003d1@O\u003dFrankfurt(Main)Hbf@X\u003d8662833@Y\u003d50106682@U\u003d80@L\u003d8000105@B\u003d1@p\u003d1760568530@i\u003dU×008011068@","alternativeHalteBerechnung":true,"verkehrsmittel":["ALL"],"zeitWunsch":{"reiseDatum":"2025-10-21T17:47:34.688151+02:00","zeitPunktArt":"ABFAHRT"},"zielLocationId":"A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@U\u003d80@L\u003d8011160@B\u003d1@p\u003d1760568530@i\u003dU×008065969@"}},"reisendenProfil":{"reisende":[{"ermaessigungen":["KEINE_ERMAESSIGUNG KLASSENLOS"],"reisendenTyp":"ERWACHSENER"}]},"reservierungsKontingenteVorhanden":false}

Binary file not shown.

View file

@ -0,0 +1,17 @@
POST /mob/angebote/fahrplan HTTP/1.1
Accept: application/x.db.vendo.mob.verbindungssuche.v9+json
X-Correlation-ID: 599238b3-d1e0-43a0-9534-ebc0dbf30b72_339465f1-2b99-4c6e-b739-c8ad0efed49a
X-Device-Os-Name: Android
X-Device-Os-Version: 32
X-Device-Model: Google Pixel 3a
X-App-Version: 25.18.2
User-Agent: DBNavigator/Android/25.18.2
Accept-Language: en,de
X-INSTANA-ANDROID: 1f809128-7b90-411d-8fc4-99a329bc3bf6
Content-Type: application/x.db.vendo.mob.verbindungssuche.v9+json
Content-Length: 821
Host: app.services-bahn.de
Connection: Keep-Alive
Accept-Encoding: gzip
{"autonomeReservierung":false,"einstiegsTypList":["STANDARD"],"fahrverguenstigungen":{"deutschlandTicketVorhanden":true,"nurDeutschlandTicketVerbindungen":true},"klasse":"KLASSE_2","reiseHin":{"wunsch":{"abgangsLocationId":"A\u003d1@O\u003dFrankfurt(Main)Hbf@X\u003d8662833@Y\u003d50106682@U\u003d80@L\u003d8000105@B\u003d1@p\u003d1760568530@i\u003dU×008011068@","alternativeHalteBerechnung":true,"verkehrsmittel":["ALL"],"zeitWunsch":{"reiseDatum":"2025-10-21T17:47:34.688151+02:00","zeitPunktArt":"ABFAHRT"},"zielLocationId":"A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@U\u003d80@L\u003d8011160@B\u003d1@p\u003d1760568530@i\u003dU×008065969@"}},"reisendenProfil":{"reisende":[{"ermaessigungen":["KEINE_ERMAESSIGUNG KLASSENLOS"],"reisendenTyp":"ERWACHSENER"}]},"reservierungsKontingenteVorhanden":false}

Binary file not shown.

View file

@ -0,0 +1,17 @@
POST /mob/location/search HTTP/1.1
Accept: application/x.db.vendo.mob.location.v3+json
X-Correlation-ID: 599238b3-d1e0-43a0-9534-ebc0dbf30b72_339465f1-2b99-4c6e-b739-c8ad0efed49a
X-Device-Os-Name: Android
X-Device-Os-Version: 32
X-Device-Model: Google Pixel 3a
X-App-Version: 25.18.2
User-Agent: DBNavigator/Android/25.18.2
Accept-Language: en,de
X-INSTANA-ANDROID: a0f760ec-0443-4209-b564-e714ea548a16
Content-Type: application/x.db.vendo.mob.location.v3+json
Content-Length: 45
Host: app.services-bahn.de
Connection: Keep-Alive
Accept-Encoding: gzip
{"locationTypes":["ALL"],"searchTerm":"test"}

Binary file not shown.

View file

@ -0,0 +1,17 @@
POST /mob/bahnhofstafel/abfahrt HTTP/1.1
Accept: application/x.db.vendo.mob.bahnhofstafeln.v2+json
X-Correlation-ID: 9c9405f1-4792-4ec0-ba1f-ca231a15ac36_339465f1-2b99-4c6e-b739-c8ad0efed49a
X-Device-Os-Name: Android
X-Device-Os-Version: 32
X-Device-Model: Google Pixel 3a
X-App-Version: 25.18.2
User-Agent: DBNavigator/Android/25.18.2
Accept-Language: en,de
X-INSTANA-ANDROID: a8b79643-ab22-472d-ab6e-40d5b888b078
Content-Type: application/x.db.vendo.mob.bahnhofstafeln.v2+json
Content-Length: 401
Host: app.services-bahn.de
Connection: Keep-Alive
Accept-Encoding: gzip
{"anfragezeit":"17:47","datum":"2025-10-21","ursprungsBahnhofId":"A\u003d1@O\u003dTessin@X\u003d12462618@Y\u003d54032020@U\u003d81@L\u003d8013106@B\u003d1@p\u003d1760557009@i\u003dU×008027109@","verkehrsmittel":["HOCHGESCHWINDIGKEITSZUEGE","INTERCITYUNDEUROCITYZUEGE","INTERREGIOUNDSCHNELLZUEGE","NAHVERKEHRSONSTIGEZUEGE","SBAHNEN","BUSSE","SCHIFFE","UBAHN","STRASSENBAHN","ANRUFPFLICHTIGEVERKEHRE"]}

Binary file not shown.

View file

@ -0,0 +1,15 @@
GET /mob/zuglauf/2%7C%23VN%231%23ST%231760568530%23PI%230%23ZI%23237216%23TA%230%23DA%23211025%231S%238013106%231T%231814%23LS%238010381%23LT%232015%23PU%2380%23RT%231%23CA%23RB%23ZE%2313134%23ZB%23RB%20%20%20%20%20%20%20%20%20%2013134%23PC%233%23FR%238013106%23FT%231814%23TO%238010381%23TT%232015%23 HTTP/1.1
Accept: application/x.db.vendo.mob.zuglauf.v2+json
Content-Type: application/x.db.vendo.mob.zuglauf.v2+json
X-Correlation-ID: 9c9405f1-4792-4ec0-ba1f-ca231a15ac36_339465f1-2b99-4c6e-b739-c8ad0efed49a
X-Device-Os-Name: Android
X-Device-Os-Version: 32
X-Device-Model: Google Pixel 3a
X-App-Version: 25.18.2
User-Agent: DBNavigator/Android/25.18.2
Accept-Language: en,de
X-INSTANA-ANDROID: 1a70c54c-b0a8-4d8a-a23b-d6da23a50423
Host: app.services-bahn.de
Connection: Keep-Alive
Accept-Encoding: gzip

Binary file not shown.

View file

@ -0,0 +1,17 @@
POST /mob/angebote/recon/autonomereservierung HTTP/1.1
Accept: application/x.db.vendo.mob.verbindungssuche.v9+json
X-Correlation-ID: 232a47fb-0827-4e11-aa2e-d141c718f63f_339465f1-2b99-4c6e-b739-c8ad0efed49a
X-Device-Os-Name: Android
X-Device-Os-Version: 32
X-Device-Model: Google Pixel 3a
X-App-Version: 25.18.2
User-Agent: DBNavigator/Android/25.18.2
Accept-Language: en,de
X-INSTANA-ANDROID: 0b0fdc2d-be90-4e00-b5ed-33bf80fe2e23
Content-Type: application/x.db.vendo.mob.verbindungssuche.v9+json
Content-Length: 1862
Host: app.services-bahn.de
Connection: Keep-Alive
Accept-Encoding: gzip
{"einstiegsTypList":["STANDARD"],"klasse":"KLASSE_2","reisendenProfil":{"reisende":[{"ermaessigungen":["KEINE_ERMAESSIGUNG KLASSENLOS"],"reisendenTyp":"ERWACHSENER"}]},"reservierungsKontingenteVorhanden":false,"suchParameter":{"reisewunschHin":{"abgangsLocationId":"A\u003d1@O\u003dFrankfurt(Main)Hbf@X\u003d8662833@Y\u003d50106682@U\u003d80@L\u003d8000105@i\u003dU×008011068@","alternativeHalteBerechnung":true,"verkehrsmittel":["ALL"],"zeitWunsch":{"reiseDatum":"2025-10-21T17:47:34.688151+02:00","zeitPunktArt":"ABFAHRT"},"zielLocationId":"A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@U\u003d80@L\u003d8011160@i\u003dU×008065969@"}},"verbindungHin":{"kontext":"¶HKI¶T$A\u003d1@O\u003dFrankfurt(Main)Hbf@X\u003d8662833@Y\u003d50106682@L\u003d8000105@a\u003d128@$A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@L\u003d8011160@a\u003d128@$202510211802$202510212203$ICE 1032$$1$$$$$$¶KC¶#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#81#AM2#0#RT#7#¶KCC¶I1ZFIzEjRVJHIzEjSElOIzAjRUNLIzQ1MDM2Mnw0NTAzNjJ8NDUwNjAzfDQ1MDYwM3wwfDB8NDg1fDQ1MDM0NHwxfDB8MTA1MHwwfDB8LTIxNDc0ODM2NDgjR0FNIzIxMTAyNTE4MDIjClojVk4jMSNTVCMxNzYwNTY4NTMwI1BJIzAjWkkjMjExNTc4I1RBIzAjREEjMjExMDI1IzFTIzgwMDAxMDUjMVQjMTgwMiNMUyM4MDEwMjU1I0xUIzIyMTUjUFUjODAjUlQjMSNDQSNJQ0UjWkUjMTAzMiNaQiNJQ0UgICAgICAgICAgMTAzMiNQQyMwI0ZSIzgwMDAxMDUjRlQjMTgwMiNUTyM4MDExMTYwI1RUIzIyMDMj¶KRCC¶#VE#1#¶SC¶1_H4sIAAAAAAACA32P4UrDMBSFX0XuL4U6knTJ0kKhdqWoTDfGJor4I67prHbtTFOxlD6HD+SLmbaKgiKBkHM493w3DbxIBS7gER+DBfJVGxEGo6twhIkxlHwGt4G82kXgUqt7BOAiC4pKh0JLkyaIUIwIht5cpbvOxJPxBCFjJX3DMbbgMa+jTKsZuLcN6HrfxRbLeWhCuyLu1Nnl1IgXkVV9BSI2tHf9UtOH7VBsyLHcz4rNUJOlsUmeeNife5ES+VNSKX14IdL86PQ+8a89zhjhtu3feBRhxBgn/trjyJ+ZCxmH+oEZ3nt4whBlnNrIT731+xtCHGEzwH2zUamHn0b9OkKpP/mBVFmaHwxcbNvMoWOnAxNzKHe+wRhjhv4BM+ow5zd4K3W4XIGrVSV7tSiy2iDlD+u8qFQu66Co8rgENxFZ+ZkVZZmlpf7Kyk2xEErsTKhp2/YDhSDWlQgCAAA\u003d"}}

Binary file not shown.

View file

@ -0,0 +1,17 @@
POST /mob/angebote/recon HTTP/1.1
Accept: application/x.db.vendo.mob.verbindungssuche.v9+json
X-Correlation-ID: 232a47fb-0827-4e11-aa2e-d141c718f63f_339465f1-2b99-4c6e-b739-c8ad0efed49a
X-Device-Os-Name: Android
X-Device-Os-Version: 32
X-Device-Model: Google Pixel 3a
X-App-Version: 25.18.2
User-Agent: DBNavigator/Android/25.18.2
Accept-Language: en,de
X-INSTANA-ANDROID: 746aa2dd-bc0c-450c-a444-f6036963caf9
Content-Type: application/x.db.vendo.mob.verbindungssuche.v9+json
Content-Length: 1962
Host: app.services-bahn.de
Connection: Keep-Alive
Accept-Encoding: gzip
{"einstiegsTypList":["STANDARD"],"fahrverguenstigungen":{"deutschlandTicketVorhanden":true,"nurDeutschlandTicketVerbindungen":false},"klasse":"KLASSE_2","reisendenProfil":{"reisende":[{"ermaessigungen":["KEINE_ERMAESSIGUNG KLASSENLOS"],"reisendenTyp":"ERWACHSENER"}]},"reservierungsKontingenteVorhanden":false,"suchParameter":{"reisewunschHin":{"abgangsLocationId":"A\u003d1@O\u003dFrankfurt(Main)Hbf@X\u003d8662833@Y\u003d50106682@U\u003d80@L\u003d8000105@i\u003dU×008011068@","alternativeHalteBerechnung":true,"verkehrsmittel":["ALL"],"zeitWunsch":{"reiseDatum":"2025-10-21T17:47:34.688151+02:00","zeitPunktArt":"ABFAHRT"},"zielLocationId":"A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@U\u003d80@L\u003d8011160@i\u003dU×008065969@"}},"verbindungHin":{"kontext":"¶HKI¶T$A\u003d1@O\u003dFrankfurt(Main)Hbf@X\u003d8662833@Y\u003d50106682@L\u003d8000105@a\u003d128@$A\u003d1@O\u003dBerlin Hbf@X\u003d13369549@Y\u003d52525589@L\u003d8011160@a\u003d128@$202510211802$202510212203$ICE 1032$$1$$$$$$¶KC¶#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#81#AM2#0#RT#7#¶KCC¶I1ZFIzEjRVJHIzEjSElOIzAjRUNLIzQ1MDM2Mnw0NTAzNjJ8NDUwNjAzfDQ1MDYwM3wwfDB8NDg1fDQ1MDM0NHwxfDB8MTA1MHwwfDB8LTIxNDc0ODM2NDgjR0FNIzIxMTAyNTE4MDIjClojVk4jMSNTVCMxNzYwNTY4NTMwI1BJIzAjWkkjMjExNTc4I1RBIzAjREEjMjExMDI1IzFTIzgwMDAxMDUjMVQjMTgwMiNMUyM4MDEwMjU1I0xUIzIyMTUjUFUjODAjUlQjMSNDQSNJQ0UjWkUjMTAzMiNaQiNJQ0UgICAgICAgICAgMTAzMiNQQyMwI0ZSIzgwMDAxMDUjRlQjMTgwMiNUTyM4MDExMTYwI1RUIzIyMDMj¶KRCC¶#VE#1#¶SC¶1_H4sIAAAAAAACA32P4UrDMBSFX0XuL4U6knTJ0kKhdqWoTDfGJor4I67prHbtTFOxlD6HD+SLmbaKgiKBkHM493w3DbxIBS7gER+DBfJVGxEGo6twhIkxlHwGt4G82kXgUqt7BOAiC4pKh0JLkyaIUIwIht5cpbvOxJPxBCFjJX3DMbbgMa+jTKsZuLcN6HrfxRbLeWhCuyLu1Nnl1IgXkVV9BSI2tHf9UtOH7VBsyLHcz4rNUJOlsUmeeNife5ES+VNSKX14IdL86PQ+8a89zhjhtu3feBRhxBgn/trjyJ+ZCxmH+oEZ3nt4whBlnNrIT731+xtCHGEzwH2zUamHn0b9OkKpP/mBVFmaHwxcbNvMoWOnAxNzKHe+wRhjhv4BM+ow5zd4K3W4XIGrVSV7tSiy2iDlD+u8qFQu66Co8rgENxFZ+ZkVZZmlpf7Kyk2xEErsTKhp2/YDhSDWlQgCAAA\u003d"}}

Binary file not shown.

View file

@ -81,6 +81,7 @@ With `opt`, you can override the default options, which look like this:
firstClass: false, // first or second class for tickets firstClass: false, // first or second class for tickets
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
} }
``` ```
@ -316,6 +317,27 @@ hafas.journeys(from, to, {
}) })
``` ```
## Using the `bmisNumber` option
bahn.business customers with a BMIS number can get their corporate rates and corporate tariffs by providing their 7-digit BMIS number:
```js
// Option 1: Using the bmisNumber parameter directly
await client.journeys(from, to, {
bmisNumber: '1234567' // Your 7-digit BMIS number
})
// Option 2: Using the createBusinessClient helper function
import {createBusinessClient} from 'db-vendo-client'
import {profile as dbProfile} from 'db-vendo-client/p/db/index.js'
const businessClient = createBusinessClient(dbProfile, userAgent, '1234567')
// Now all journey searches will automatically include the BMIS number
await businessClient.journeys(from, to)
```
When a BMIS number is provided, the request will include a `firmenZugehoerigkeit` object with the BMIS number and identification type, allowing business customers to access their special rates and conditions.
## 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.

View file

@ -10,7 +10,7 @@ With `opt`, you can override the default options, which look like this:
{ {
results: 8, // maximum number of results results: 8, // maximum number of results
distance: null, // maximum walking distance in meters distance: null, // maximum walking distance in meters
poi: false, // return points of interest? poi: false, // not supported
stops: true, // return stops/stations? stops: true, // return stops/stations?
subStops: true, // not supported subStops: true, // not supported
entrances: true, // not supported entrances: true, // not supported

View file

@ -613,7 +613,7 @@ paths:
default: true default: true
- name: linesOfStops - name: linesOfStops
in: query in: query
description: Parse & return lines of each stop/station? description: not supported
schema: schema:
type: boolean type: boolean
default: false default: false
@ -1887,11 +1887,11 @@ components:
default: false default: false
type: boolean type: boolean
subStops: subStops:
description: parse & expose sub-stops of stations? description: not supported
default: false default: false
type: boolean type: boolean
entrances: entrances:
description: parse & expose entrances of stops/stations? description: not supported
default: true default: true
type: boolean type: boolean
remarks: remarks:
@ -1995,11 +1995,11 @@ components:
default: false default: false
type: boolean type: boolean
subStops: subStops:
description: parse & expose sub-stops of stations? description: not supported
default: true default: true
type: boolean type: boolean
entrances: entrances:
description: parse & expose entrances of stops/stations? description: not supported
default: true default: true
type: boolean type: boolean
remarks: remarks:
@ -2034,15 +2034,15 @@ components:
default: true default: true
type: boolean type: boolean
subStops: subStops:
description: parse & expose sub-stops of stations? description: not supported
default: false default: false
type: boolean type: boolean
entrances: entrances:
description: parse & expose entrances of stops/stations? description: not supported
default: true default: true
type: boolean type: boolean
linesOfStops: linesOfStops:
description: parse & expose lines at each stop/station? description: not supported
default: false default: false
type: boolean type: boolean
language: language:
@ -2061,11 +2061,11 @@ components:
default: false default: false
type: boolean type: boolean
subStops: subStops:
description: parse & expose sub-stops of stations? description: not supported
default: true default: true
type: boolean type: boolean
entrances: entrances:
description: parse & expose entrances of stops/stations? description: not supported
default: true default: true
type: boolean type: boolean
remarks: remarks:
@ -2088,11 +2088,11 @@ components:
default: false default: false
type: boolean type: boolean
subStops: subStops:
description: parse & expose sub-stops of stations? description: not supported
default: true default: true
type: boolean type: boolean
entrances: entrances:
description: parse & expose entrances of stops/stations? description: not supported
default: true default: true
type: boolean type: boolean
remarks: remarks:
@ -2128,11 +2128,11 @@ components:
default: 10 default: 10
type: number type: number
subStops: subStops:
description: parse & expose sub-stops of stations? description: not supported
default: true default: true
type: boolean type: boolean
entrances: entrances:
description: parse & expose entrances of stops/stations? description: not supported
default: true default: true
type: boolean type: boolean
linesOfStops: linesOfStops:
@ -2206,7 +2206,7 @@ components:
default: undefined default: undefined
type: number type: number
poi: poi:
description: return points of interest? description: not supported
default: false default: false
type: boolean type: boolean
stops: stops:
@ -2218,15 +2218,15 @@ components:
description: products description: products
default: undefined default: undefined
subStops: subStops:
description: parse & expose sub-stops of stations? description: not supported
default: true default: true
type: boolean type: boolean
entrances: entrances:
description: parse & expose entrances of stops/stations? description: not supported
default: true default: true
type: boolean type: boolean
linesOfStops: linesOfStops:
description: parse & expose lines at each stop/station? description: not supported
default: false default: false
type: boolean type: boolean
language: language:
@ -2254,11 +2254,11 @@ components:
description: products description: products
default: undefined default: undefined
subStops: subStops:
description: parse & expose sub-stops of stations? description: not supported
default: true default: true
type: boolean type: boolean
entrances: entrances:
description: parse & expose entrances of stops/stations? description: not supported
default: true default: true
type: boolean type: boolean
polylines: polylines:
@ -2296,11 +2296,11 @@ components:
default: 20 default: 20
type: number type: number
subStops: subStops:
description: parse & expose sub-stops of stations? description: not supported
default: true default: true
type: boolean type: boolean
entrances: entrances:
description: parse & expose entrances of stops/stations? description: not supported
default: true default: true
type: boolean type: boolean
polylines: polylines:

View file

@ -111,7 +111,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};
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
if (!opt.includeRelatedStations) { if (!opt.includeRelatedStations) {
@ -181,6 +181,9 @@ const createClient = (profile, userAgent, opt = {}) => {
scheduledDays: false, // parse & expose dates each journey is valid on? scheduledDays: false, // parse & expose dates each journey is valid on?
notOnlyFastRoutes: false, // if true, also show routes that are mathematically non-optimal notOnlyFastRoutes: false, // if true, also show routes that are mathematically non-optimal
bestprice: false, // search for lowest prices across the entire day bestprice: false, // search for lowest prices across the entire day
deutschlandTicketDiscount: false,
deutschlandTicketConnectionsOnly: false,
bmisNumber: null, // 7-digit BMIS number for business customer rates
}, opt); }, opt);
if (opt.when !== undefined) { if (opt.when !== undefined) {
@ -239,6 +242,9 @@ const createClient = (profile, userAgent, opt = {}) => {
entrances: true, // parse & expose entrances of stops/stations? entrances: true, // parse & expose entrances of stops/stations?
remarks: true, // parse & expose hints & warnings? remarks: true, // parse & expose hints & warnings?
scheduledDays: false, // parse & expose dates the journey is valid on? scheduledDays: false, // parse & expose dates the journey is valid on?
deutschlandTicketDiscount: false,
deutschlandTicketConnectionsOnly: false,
bmisNumber: null, // 7-digit BMIS number for business customer rates
}, opt); }, opt);
const req = profile.formatRefreshJourneyReq({profile, opt}, refreshToken); const req = profile.formatRefreshJourneyReq({profile, opt}, refreshToken);
@ -391,7 +397,33 @@ const createClient = (profile, userAgent, opt = {}) => {
return client; return client;
}; };
const createBusinessClient = (profile, userAgent, bmisNumber, opt = {}) => {
if (!bmisNumber || typeof bmisNumber !== 'string') {
throw new TypeError('bmisNumber must be a non-empty string');
}
// Create a client with BMIS number included in all journey requests
const client = createClient(profile, userAgent, opt);
// Wrap journeys method to always include BMIS number
const originalJourneys = client.journeys;
client.journeys = async (from, to, opt = {}) => {
return originalJourneys(from, to, {...opt, bmisNumber});
};
// Wrap refreshJourney method to always include BMIS number
if (client.refreshJourney) {
const originalRefreshJourney = client.refreshJourney;
client.refreshJourney = async (refreshToken, opt = {}) => {
return originalRefreshJourney(refreshToken, {...opt, bmisNumber});
};
}
return client;
};
export { export {
createClient, createClient,
createBusinessClient,
loadEnrichedStationData, loadEnrichedStationData,
}; };

View file

@ -76,6 +76,18 @@ const mapRouteParsers = (route, parsers) => {
default: false, default: false,
parse: parseBoolean, parse: parseBoolean,
}, },
deutschlandTicketDiscount: {
description: 'Calculate ticket prices assuming Deutschlandticket is present',
type: 'boolean',
default: false,
parse: parseBoolean,
},
deutschlandTicketConnectionsOnly: {
description: 'Only return journeys that can be used with the Deutschlandticket',
type: 'boolean',
default: false,
parse: parseBoolean,
},
}; };
} }
if (route.includes('departures') || route.includes('arrivals')) { if (route.includes('departures') || route.includes('arrivals')) {

View file

@ -105,7 +105,6 @@ const request = async (ctx, userAgent, reqData) => {
if (reqOptions.query) { if (reqOptions.query) {
url += '?' + stringify(reqOptions.query, {arrayFormat: 'brackets', encodeValuesOnly: true}); url += '?' + stringify(reqOptions.query, {arrayFormat: 'brackets', encodeValuesOnly: true});
} }
console.log(url);
const reqId = randomBytesHexString(6); const reqId = randomBytesHexString(6);
const fetchReq = new Request(url, reqOptions); const fetchReq = new Request(url, reqOptions);
profile.logRequest(ctx, fetchReq, reqId); profile.logRequest(ctx, fetchReq, reqId);
@ -122,6 +121,7 @@ const request = async (ctx, userAgent, reqData) => {
if (!res.ok) { if (!res.ok) {
// todo [breaking]: make this a FetchError or a HafasClientError? // todo [breaking]: make this a FetchError or a HafasClientError?
console.log(JSON.stringify(res), await res.text());
const err = new Error(res.statusText); const err = new Error(res.statusText);
Object.assign(err, errProps); Object.assign(err, errProps);
throw err; throw err;

12
p/db/base.json Normal file
View file

@ -0,0 +1,12 @@
{
"journeysEndpoint": "https://app.vendo.noncd.db.de/mob/angebote/fahrplan",
"bestpriceEndpoint": "https://app.vendo.noncd.db.de/mob/angebote/tagesbestpreis",
"refreshJourneysEndpointTickets": "https://app.vendo.noncd.db.de/mob/angebote/recon",
"refreshJourneysEndpointPolyline": "https://app.vendo.noncd.db.de/mob/trip/recon",
"locationsEndpoint": "https://app.vendo.noncd.db.de/mob/location/search",
"stopEndpoint": "https://app.vendo.noncd.db.de/mob/location/details/",
"nearbyEndpoint": "https://app.vendo.noncd.db.de/mob/location/nearby",
"tripEndpoint": "https://app.vendo.noncd.db.de/mob/zuglauf/",
"boardEndpoint": "https://app.vendo.noncd.db.de/mob/bahnhofstafel/",
"defaultLanguage": "en"
}

View file

@ -1,37 +1,36 @@
import dbnavBase from '../dbnav/base.json' with { type: 'json' }; import base from './base.json' with { type: 'json' };
import dbregioguideBase from '../dbregioguide/base.json' with { type: 'json' };
import {products} from '../../lib/products.js'; import {products} from '../../lib/products.js';
// journeys() // journeys()
import {formatJourneysReq} from '../dbnav/journeys-req.js'; import {formatJourneysReq} from '../dbnav/journeys-req.js';
const {journeysEndpoint} = dbnavBase; const {journeysEndpoint} = base;
const {bestpriceEndpoint} = base;
// refreshJourneys() // refreshJourneys()
import {formatRefreshJourneyReq} from '../dbnav/journeys-req.js'; import {formatRefreshJourneyReq} from '../dbnav/journeys-req.js';
const {refreshJourneysEndpointTickets, refreshJourneysEndpointPolyline} = dbnavBase; const {refreshJourneysEndpointTickets, refreshJourneysEndpointPolyline} = base;
// locations() // locations()
import {formatLocationsReq} from '../dbnav/locations-req.js'; import {formatLocationsReq} from '../dbnav/locations-req.js';
import {formatLocationFilter} from '../dbnav/location-filter.js'; import {formatLocationFilter} from '../dbnav/location-filter.js';
const {locationsEndpoint} = dbnavBase; const {locationsEndpoint} = base;
// stop() // stop()
import {formatStopReq} from '../dbnav/stop-req.js'; import {formatStopReq} from '../dbnav/stop-req.js';
import {parseStop} from '../dbnav/parse-stop.js'; import {parseStop} from '../dbnav/parse-stop.js';
const {stopEndpoint} = dbnavBase; const {stopEndpoint} = base;
// nearby() // nearby()
import {formatNearbyReq} from '../dbnav/nearby-req.js'; import {formatNearbyReq} from '../dbnav/nearby-req.js';
const {nearbyEndpoint} = dbnavBase; const {nearbyEndpoint} = base;
// trip() // trip()
import {formatTripReq} from './trip-req.js'; import {formatTripReq} from './trip-req.js';
const tripEndpoint_dbnav = dbnavBase.tripEndpoint; const {tripEndpoint} = base;
const tripEndpoint_dbregioguide = dbregioguideBase.tripEndpoint;
// arrivals(), departures() // arrivals(), departures()
import {formatStationBoardReq} from '../dbregioguide/station-board-req.js'; import {formatStationBoardReq} from '../dbnav/station-board-req.js';
const {boardEndpoint} = dbregioguideBase; const {boardEndpoint} = base;
const profile = { const profile = {
locale: 'de-DE', locale: 'de-DE',
@ -41,6 +40,7 @@ const profile = {
formatJourneysReq, formatJourneysReq,
journeysEndpoint, journeysEndpoint,
bestpriceEndpoint,
formatRefreshJourneyReq, formatRefreshJourneyReq,
refreshJourneysEndpointTickets, refreshJourneysEndpointPolyline, refreshJourneysEndpointTickets, refreshJourneysEndpointPolyline,
@ -55,7 +55,7 @@ const profile = {
nearbyEndpoint, nearbyEndpoint,
formatTripReq, formatTripReq,
tripEndpoint_dbnav, tripEndpoint_dbregioguide, tripEndpoint,
formatStationBoardReq, formatStationBoardReq,
boardEndpoint, boardEndpoint,

View file

@ -1,16 +1,9 @@
import {formatTripReq as hafasFormatTripReq} from '../dbnav/trip-req.js'; import {formatTripReq as hafasFormatTripReq} from '../dbnav/trip-req.js';
import {formatTripReq as risTripReq} from '../dbregioguide/trip-req.js';
const formatTripReq = ({profile, opt}, id) => { const formatTripReq = ({profile, opt}, id) => {
const _profile = {...profile}; const _profile = {...profile};
if (id.includes('#')) { _profile['tripEndpoint'] = profile.tripEndpoint;
_profile['tripEndpoint'] = profile.tripEndpoint_dbnav; return hafasFormatTripReq({profile: _profile, opt}, id);
return hafasFormatTripReq({profile: _profile, opt}, id);
}
_profile['tripEndpoint'] = profile.tripEndpoint_dbregioguide;
return risTripReq({profile: _profile, opt}, id);
}; };
export { export {

View file

@ -1,12 +1,12 @@
{ {
"journeysEndpoint": "https://app.vendo.noncd.db.de/mob/angebote/fahrplan", "journeysEndpoint": "https://app.services-bahn.de/mob/angebote/fahrplan",
"bestpriceEndpoint": "https://app.vendo.noncd.db.de/mob/angebote/tagesbestpreis", "bestpriceEndpoint": "https://app.services-bahn.de/mob/angebote/tagesbestpreis",
"refreshJourneysEndpointTickets": "https://app.vendo.noncd.db.de/mob/angebote/recon", "refreshJourneysEndpointTickets": "https://app.services-bahn.de/mob/angebote/recon",
"refreshJourneysEndpointPolyline": "https://app.vendo.noncd.db.de/mob/trip/recon", "refreshJourneysEndpointPolyline": "https://app.services-bahn.de/mob/trip/recon",
"locationsEndpoint": "https://app.vendo.noncd.db.de/mob/location/search", "locationsEndpoint": "https://app.services-bahn.de/mob/location/search",
"stopEndpoint": "https://app.vendo.noncd.db.de/mob/location/details/", "stopEndpoint": "https://app.services-bahn.de/mob/location/details/",
"nearbyEndpoint": "https://app.vendo.noncd.db.de/mob/location/nearby", "nearbyEndpoint": "https://app.services-bahn.de/mob/location/nearby",
"tripEndpoint": "https://app.vendo.noncd.db.de/mob/zuglauf/", "tripEndpoint": "https://app.services-bahn.de/mob/zuglauf/",
"boardEndpoint": "https://app.vendo.noncd.db.de/mob/bahnhofstafel/", "boardEndpoint": "https://app.services-bahn.de/mob/bahnhofstafel/",
"defaultLanguage": "en" "defaultLanguage": "en"
} }

View file

@ -4,11 +4,15 @@ const formatBaseJourneysReq = (ctx) => {
// TODO opt.accessibility // TODO opt.accessibility
// TODO routingMode // TODO routingMode
const travellers = ctx.profile.formatTravellers(ctx); const travellers = ctx.profile.formatTravellers(ctx);
return { const baseReq = {
autonomeReservierung: false, autonomeReservierung: false,
einstiegsTypList: [ einstiegsTypList: [
'STANDARD', 'STANDARD',
], ],
fahrverguenstigungen: {
deutschlandTicketVorhanden: ctx.opt.deutschlandTicketDiscount,
nurDeutschlandTicketVerbindungen: ctx.opt.deutschlandTicketConnectionsOnly,
},
klasse: travellers.klasse, klasse: travellers.klasse,
reisendenProfil: { reisendenProfil: {
reisende: travellers.reisende.map(t => { reisende: travellers.reisende.map(t => {
@ -23,6 +27,16 @@ const formatBaseJourneysReq = (ctx) => {
}, },
reservierungsKontingenteVorhanden: false, reservierungsKontingenteVorhanden: false,
}; };
// Add business customer affiliation if BMIS number is provided
if (ctx.opt.bmisNumber) {
baseReq.firmenZugehoerigkeit = {
bmisNr: ctx.opt.bmisNumber,
identifikationsart: 'BMIS',
};
}
return baseReq;
}; };
const formatJourneysReq = (ctx, from, to, when, outFrwd, journeysRef) => { const formatJourneysReq = (ctx, from, to, when, outFrwd, journeysRef) => {
@ -39,6 +53,7 @@ const formatJourneysReq = (ctx, from, to, when, outFrwd, journeysRef) => {
wunsch: { wunsch: {
abgangsLocationId: from.lid, abgangsLocationId: from.lid,
verkehrsmittel: filters, verkehrsmittel: filters,
alternativeHalteBerechnung: true, // what is this?
zeitWunsch: { zeitWunsch: {
reiseDatum: profile.formatTime(profile, when, true), reiseDatum: profile.formatTime(profile, when, true),
zeitPunktArt: outFrwd ? 'ABFAHRT' : 'ANKUNFT', zeitPunktArt: outFrwd ? 'ABFAHRT' : 'ANKUNFT',
@ -61,7 +76,7 @@ const formatJourneysReq = (ctx, from, to, when, outFrwd, journeysRef) => {
return { return {
endpoint: opt.bestprice ? profile.bestpriceEndpoint : profile.journeysEndpoint, endpoint: opt.bestprice ? profile.bestpriceEndpoint : profile.journeysEndpoint,
body: query, body: query,
headers: getHeaders('application/x.db.vendo.mob.verbindungssuche.v8+json'), headers: getHeaders('application/x.db.vendo.mob.verbindungssuche.v9+json'),
method: 'post', method: 'post',
}; };
}; };
@ -78,7 +93,7 @@ const formatRefreshJourneyReq = (ctx, refreshToken) => {
return { return {
endpoint: opt.tickets ? profile.refreshJourneysEndpointTickets : profile.refreshJourneysEndpointPolyline, endpoint: opt.tickets ? profile.refreshJourneysEndpointTickets : profile.refreshJourneysEndpointPolyline,
body: query, body: query,
headers: getHeaders('application/x.db.vendo.mob.verbindungssuche.v8+json'), headers: getHeaders('application/x.db.vendo.mob.verbindungssuche.v9+json'),
method: 'post', method: 'post',
}; };
}; };

View file

@ -15,7 +15,7 @@ const profile = {
journeysOutFrwd: false, journeysOutFrwd: false,
departuresGetPasslist: false, departuresGetPasslist: false,
departuresStbFltrEquiv: true, departuresStbFltrEquiv: true,
trip: false, trip: true,
radar: false, radar: false,
refreshJourney: false, refreshJourney: false,
journeysFromTrip: false, journeysFromTrip: false,

View file

@ -10,8 +10,8 @@ const formatJourneysReq = (ctx, from, to, when, outFrwd, journeysRef) => {
let query = { let query = {
maxUmstiege: transfers, maxUmstiege: transfers,
minUmstiegszeit: opt.transferTime, minUmstiegszeit: opt.transferTime,
deutschlandTicketVorhanden: false, deutschlandTicketVorhanden: opt.deutschlandTicketDiscount,
nurDeutschlandTicketVerbindungen: false, nurDeutschlandTicketVerbindungen: opt.deutschlandTicketConnectionsOnly,
reservierungsKontingenteVorhanden: false, reservierungsKontingenteVorhanden: false,
schnelleVerbindungen: !opt.notOnlyFastRoutes, schnelleVerbindungen: !opt.notOnlyFastRoutes,
sitzplatzOnly: false, sitzplatzOnly: false,

2900
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{ {
"name": "db-vendo-client", "name": "db-vendo-client",
"description": "Client for bahn.de public transport APIs.", "description": "Client for bahn.de public transport APIs.",
"version": "6.8.0", "version": "6.10.8",
"type": "module", "type": "module",
"main": "index.js", "main": "index.js",
"files": [ "files": [

View file

@ -7,7 +7,7 @@ const parseLine = (ctx, p) => {
type: 'line', type: 'line',
id: slugg(p.verkehrsmittel?.langText || p.verkehrmittel?.langText || p.transport?.journeyDescription || p.risZuglaufId || p.train && p.train.category + ' ' + p.train.lineName + ' ' + p.train.no || p.no && p.name + ' ' + p.no || p.langtext || p.mitteltext || p.zugName || p.lineName), // TODO terrible id: slugg(p.verkehrsmittel?.langText || p.verkehrmittel?.langText || p.transport?.journeyDescription || p.risZuglaufId || p.train && p.train.category + ' ' + p.train.lineName + ' ' + p.train.no || p.no && p.name + ' ' + p.no || p.langtext || p.mitteltext || p.zugName || p.lineName), // TODO terrible
fahrtNr: String(fahrtNr), fahrtNr: String(fahrtNr),
name: p.verkehrsmittel?.name || p.verkehrsmittel?.langText || p.verkehrmittel?.name || p.verkehrmittel?.langText || p.zugName || p.transport && p.transport.category + ' ' + p.transport.line || p.train && p.train.category + ' ' + p.train.lineName || p.name || p.mitteltext || p.langtext || p.lineName, name: p.verkehrsmittel?.name || p.verkehrsmittel?.langText || p.verkehrmittel?.name || p.verkehrmittel?.langText || p.zugName || p.transport && p.transport.category + ' ' + (p.transport.line || p.transport.number) || p.train && p.train.category + ' ' + p.train.lineName || p.name || p.mitteltext || p.langtext || p.lineName,
public: true, public: true,
}; };

View file

@ -60,28 +60,28 @@ const parseRemarks = (ctx, ref) => {
}; };
/* /*
meldungenAsObject meldungenAsObject
{ {
"code": "MDA-AK-MSG-1000", "code": "MDA-AK-MSG-1000",
"nachrichtKurz": "Connection is in the past.", "nachrichtKurz": "Connection is in the past.",
"nachrichtLang": "Selected connection is in the past.", "nachrichtLang": "Selected connection is in the past.",
"fahrtRichtungKennzeichen": "HINFAHRT" "fahrtRichtungKennzeichen": "HINFAHRT"
} }
[ [
{ {
"code": "MDA-AK-MSG-3000", "code": "MDA-AK-MSG-3000",
"nachrichtKurz": "Booking not possible.", "nachrichtKurz": "Booking not possible.",
"nachrichtLang": "Booking is no longer possible for the connection you selected", "nachrichtLang": "Booking is no longer possible for the connection you selected",
"fahrtRichtungKennzeichen": "HINFAHRT" "fahrtRichtungKennzeichen": "HINFAHRT"
} }
] ]
priorisierteMeldungen priorisierteMeldungen
{ {
"prioritaet": "HOCH", "prioritaet": "HOCH",
"text": "ICE 597 departs differently from Mainz Hbf from Platform 1b" "text": "ICE 597 departs differently from Mainz Hbf from Platform 1b"
} }
[ [
{ {
"prioritaet": "NIEDRIG", "prioritaet": "NIEDRIG",
"text": "Advance notice! In the period from 15.12.24 to 17.01.25, construction work will take place between Mainz Hbf and Frankfurt(Main)Hbf. There will be changed run times and partial cancellation. Please inform yourself early on the Internet and at the stations." "text": "Advance notice! In the period from 15.12.24 to 17.01.25, construction work will take place between Mainz Hbf and Frankfurt(Main)Hbf. There will be changed run times and partial cancellation. Please inform yourself early on the Internet and at the stations."
@ -90,14 +90,14 @@ const parseRemarks = (ctx, ref) => {
"prioritaet": "HOCH", "prioritaet": "HOCH",
"text": "The route between Mainz Hbf and Mainz Nord is currently closed. The reason is a repair on the track. At the moment, no train journeys are possible in the affected section of the route. As a result, there are now delays and partial failures. The trains terminates and starts unscheduled in Mainz Hbf. Please check your travel connections shortly before the train departs. This message will be updated as soon as we have more information." "text": "The route between Mainz Hbf and Mainz Nord is currently closed. The reason is a repair on the track. At the moment, no train journeys are possible in the affected section of the route. As a result, there are now delays and partial failures. The trains terminates and starts unscheduled in Mainz Hbf. Please check your travel connections shortly before the train departs. This message will be updated as soon as we have more information."
} }
] ]
[ [
{ {
"prioritaet": "HOCH", "prioritaet": "HOCH",
"text": "Trip is not possible" "text": "Trip is not possible"
} }
] ]
[ [
{ {
"prioritaet": "HOCH", "prioritaet": "HOCH",
"text": "Intervention by authorities" "text": "Intervention by authorities"
@ -106,44 +106,44 @@ const parseRemarks = (ctx, ref) => {
"prioritaet": "HOCH", "prioritaet": "HOCH",
"text": "Switch repairs between Frankfurt(Main)Hbf and Mannheim Hbf delays rail transport. The train is diverted. The stop Mainz Hbf is cancelled. Please allow for a delay of up to 10 minutes. Please check for any changes to your journey prior to departure." "text": "Switch repairs between Frankfurt(Main)Hbf and Mannheim Hbf delays rail transport. The train is diverted. The stop Mainz Hbf is cancelled. Please allow for a delay of up to 10 minutes. Please check for any changes to your journey prior to departure."
} }
] ]
[ [
{ {
"prioritaet": "HOCH", "prioritaet": "HOCH",
"text": "Stop cancelled", "text": "Stop cancelled",
"type": "HALT_AUSFALL" "type": "HALT_AUSFALL"
} }
] ]
risNotizen risNotizen
{ {
"key": "text.realtime.connection.platform.change", "key": "text.realtime.connection.platform.change",
"value": "ICE 597 departs differently from Mainz Hbf from Platform 1b" "value": "ICE 597 departs differently from Mainz Hbf from Platform 1b"
} }
{key: "FT", value: "Staff delayed due to earlier journey", routeIdxFrom: 0, routeIdxTo: 12} {key: "FT", value: "Staff delayed due to earlier journey", routeIdxFrom: 0, routeIdxTo: 12}
[ [
{ {
"key": "text.realtime.connection.cancelled", "key": "text.realtime.connection.cancelled",
"value": "Trip is not possible" "value": "Trip is not possible"
} }
] ]
[ [
{ {
"key": "FT", "key": "FT",
"value": "Intervention by authorities", "value": "Intervention by authorities",
"routeIdxFrom": 9, "routeIdxFrom": 9,
"routeIdxTo": 21 "routeIdxTo": 21
} }
] ]
[ [
{ {
"key": "text.realtime.stop.cancelled", "key": "text.realtime.stop.cancelled",
"value": "Stop cancelled" "value": "Stop cancelled"
} }
] ]
himMeldungen himMeldungen
[ [
{ {
"ueberschrift": "Construction work.", "ueberschrift": "Construction work.",
"text": "Advance notice! In the period from 15.12.24 to 17.01.25, construction work will take place between Mainz Hbf and Frankfurt(Main)Hbf. There will be changed run times and partial cancellation. Please inform yourself early on the Internet and at the stations.", "text": "Advance notice! In the period from 15.12.24 to 17.01.25, construction work will take place between Mainz Hbf and Frankfurt(Main)Hbf. There will be changed run times and partial cancellation. Please inform yourself early on the Internet and at the stations.",
@ -156,14 +156,14 @@ const parseRemarks = (ctx, ref) => {
"prioritaet": "HOCH", "prioritaet": "HOCH",
"modDateTime": "2024-12-06T06:24:35" "modDateTime": "2024-12-06T06:24:35"
} }
[ [
{ {
"ueberschrift": "Disruption.", "ueberschrift": "Disruption.",
"text": "Switch repairs between Frankfurt(Main)Hbf and Mannheim Hbf delays rail transport. The train is diverted. The stop Mainz Hbf is cancelled. Please allow for a delay of up to 10 minutes. Please check for any changes to your journey prior to departure.", "text": "Switch repairs between Frankfurt(Main)Hbf and Mannheim Hbf delays rail transport. The train is diverted. The stop Mainz Hbf is cancelled. Please allow for a delay of up to 10 minutes. Please check for any changes to your journey prior to departure.",
"prioritaet": "HOCH", "prioritaet": "HOCH",
"modDateTime": "2024-12-05T19:01:48" "modDateTime": "2024-12-05T19:01:48"
} }
] ]
zugattribute zugattribute
[ [
@ -213,7 +213,7 @@ const parseCancelled = (ref) => {
(ref.risNotizen || ref.echtzeitNotizen || ref.meldungen).find(r => r.key == 'text.realtime.stop.cancelled' (ref.risNotizen || ref.echtzeitNotizen || ref.meldungen).find(r => r.key == 'text.realtime.stop.cancelled'
|| r.type == 'HALT_AUSFALL' || r.type == 'HALT_AUSFALL'
|| r.text == 'Halt entfällt' || r.text == 'Halt entfällt'
|| r.text == 'Stop cancelled', || r.text?.includes('fällt aus') || r.text?.includes('cancelled'),
), ),
); );
}; };

View file

@ -26,7 +26,9 @@ const parseTickets = (ctx, j) => {
.flatMap(p => [ .flatMap(p => [
p.einfacheFahrt?.standard?.reisePosition, p.einfacheFahrt?.standard?.reisePosition,
p.einfacheFahrt?.upsellEntgelt?.einfacheFahrt?.reisePosition, p.einfacheFahrt?.upsellEntgelt?.einfacheFahrt?.reisePosition,
].filter(p => p) p.einfacheFahrt?.upsellAngebote?.map(a => a.upsellEntgelt?.einfacheFahrt?.reisePosition),
].flatMap(p => p)
.filter(p => p)
.map(p => { .map(p => {
p.reisePosition.teilpreis = Boolean(p.teilpreisInformationen?.length); p.reisePosition.teilpreis = Boolean(p.teilpreisInformationen?.length);
return p.reisePosition; return p.reisePosition;
@ -44,7 +46,7 @@ const parseTickets = (ctx, j) => {
amount: Math.round(s.preis?.betrag * 100), amount: Math.round(s.preis?.betrag * 100),
currency: s.preis?.waehrung, currency: s.preis?.waehrung,
}, },
firstClass: s.klasse == 'KLASSE_1' || s.premium || Boolean(s.nutzungsInformationen?.find(i => i.klasse == 'KLASSE_1')), firstClass: s.klasse == 'KLASSE_1' || Boolean(s.nutzungsInformationen?.find(i => i.klasse == 'KLASSE_1')),
partialFare: s.teilpreis, partialFare: s.teilpreis,
}; };
if (s.teilpreis) { if (s.teilpreis) {

View file

@ -6,7 +6,7 @@ const parseTrip = (ctx, t, id) => { // t = raw trip
trip.id = trip.tripId || id; trip.id = trip.tripId || id;
delete trip.tripId; delete trip.tripId;
delete trip.reachable; delete trip.reachable;
trip.cancelled = Boolean(profile.parseCancelled(t)); trip.cancelled = Boolean(profile.parseCancelled(t) || trip.stopovers?.length && trip.stopovers?.every(s => s.cancelled));
// TODO opt.scheduledDays // TODO opt.scheduledDays
return trip; return trip;

View file

@ -23,25 +23,25 @@ What doesn't work:
Depending on the configured profile, db-vendo-client will use multiple different DB APIs that offer varying functionality, so choose wisely: Depending on the configured profile, db-vendo-client will use multiple different DB APIs that offer varying functionality, so choose wisely:
| Profile | `db` | `dbnav` | `dbweb` | `dbbahnhof` | `dbris` | | Profile | `db` | `dbnav` | `dbweb` | `dbris` |
| ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | | ------------- | ------------- | ------------- | ------------- | ------------- |
| no API key required | ✅ | ✅ | ✅ | ✅ | ❌ | | no API key required | ✅ | ✅ | ✅ | ❌ |
| all above endpoints supported | ✅ | ✅ | except `stop()` | only boards | only boards | | all above endpoints supported | ✅ | ✅ | except `stop()` | only boards |
| duration for boards | up to 12h | always 1h | always 1h | up to 6h, only from current time | up to 12h | | duration for boards | always 1h | always 1h | always 1h | up to 12h |
| remarks | not for boards | for boards only most important remarks | all remarks on boards and journeys | most remarks | all remarks | | remarks | for boards only most important remarks | for boards only most important remarks | all remarks on boards and journeys | all remarks |
| cancelled trips | contained with cancelled flag in journeys, not contained in boards | contained with cancelled flag | contained with cancelled flag | contained with cancelled flag | contained with cancelled flag | | cancelled trips | contained with cancelled flag | contained with cancelled flag | contained with cancelled flag | contained with cancelled flag |
| tickets | only for `refreshJourney()`, mutually exclusive with polylines | only for `refreshJourney()`, mutually exclusive with polylines | only for `refreshJourney()`, mutually exclusive with polylines | ❌ | ❌ | | tickets | only for `refreshJourney()`, mutually exclusive with polylines | only for `refreshJourney()`, mutually exclusive with polylines | only for `refreshJourney()`, mutually exclusive with polylines | ❌ |
| polylines | only for `refreshJourney()` (mutually exclusive with tickets) and for `trip()` (only for HAFAS trip ids) | only for `refreshJourney()/trip()`, mutually exclusive with tickets | only for `refreshJourney()/trip()`, mutually exclusive with tickets | ❌ | ❌ | | polylines | only for `refreshJourney()/trip()`, mutually exclusive with tickets | only for `refreshJourney()/trip()`, mutually exclusive with tickets | only for `refreshJourney()/trip()`, mutually exclusive with tickets | ❌ |
| trip ids used | HAFAS trip ids for journeys, RIS trip ids for boards (static on train splits?) | HAFAS trip ids | HAFAS trip ids | RIS trip ids | RIS trip ids | | trip ids used | HAFAS trip ids | HAFAS trip ids | HAFAS trip ids | RIS trip ids |
| line.id/fahrtNr used | actual fahrtNr | actual fahrtNr for journeys, unreliable/route id for boards and `trip()` | unreliable/route id | unreliable/route id | ✅ | | line.id/fahrtNr used | actual fahrtNr for journeys, unreliable/route id for boards and `trip()` | actual fahrtNr for journeys, unreliable/route id for boards and `trip()` | unreliable/route id | ✅ |
| adminCode/operator | ✅ | only for journeys | only operator | only adminCode | ✅ | | adminCode/operator | only for journeys | only for journeys | only operator | ✅ |
| stopovers | not in boards | not in boards | ✅ | some | ✅ | | stopovers | not in boards | not in boards | ✅ | ✅ |
| assumed backend API stability | less stable | more stable | less stable | less stable | more stable | | assumed backend API stability | less stable | more stable | less stable | more stable |
| quotas | 60 requests per minute for journeys, unknown for boards (IPv4) | 60 requests per minute (IPv4) | aggressive blocking (IPv4/IPv6) | ? | depends on API key | | quotas | 60 requests per minute (IPv4), current backend possibly shut off soon | 60 requests per minute (IPv4), possibly aggressive blocking | aggressive blocking (IPv4/IPv6) | depends on API key |
> [!IMPORTANT] > [!IMPORTANT]
> If you think that for your project, quotas may become an issue, [consider alternative ways to obtain the data you need.](https://github.com/derhuerst/db-rest/blob/6/docs/readme.md#why-not-to-use-this-api). > If you think that for your project, quotas may become an issue, [consider alternative ways to obtain the data you need.](https://github.com/derhuerst/db-rest/blob/6/docs/readme.md#why-not-to-use-this-api), e.g. [motis-fptf-client](https://github.com/motis-project/motis-fptf-client) (a drop-in replacement for db-vendo-client/hafas-client) in conjunction with https://transitous.org (please consider the [usage policy](https://transitous.org/api/) there) or a self-hosted [MOTIS](https://github.com/motis-project/motis) instance.
Feel free to report anything that you stumble upon via Issues or create a PR :) Feel free to report anything that you stumble upon via Issues or create a PR :)
@ -92,6 +92,7 @@ There are [community-maintained TypeScript typings available as `@types/hafas-cl
- [hafas-client](https://github.com/public-transport/hafas-client/) including further related projects - [hafas-client](https://github.com/public-transport/hafas-client/) including further related projects
- [hafas-rest-api](https://github.com/public-transport/hafas-rest-api/) expose a hafas-client or db-vendo-client instance as a REST API - [hafas-rest-api](https://github.com/public-transport/hafas-rest-api/) expose a hafas-client or db-vendo-client instance as a REST API
- [db-rest](https://github.com/derhuerst/db-rest/) for the legacy DB HAFAS endpoint - [db-rest](https://github.com/derhuerst/db-rest/) for the legacy DB HAFAS endpoint
- [motis-fptf-client](https://github.com/motis-project/motis-fptf-client) a drop-in replacement for db-vendo-client/hafas-client wrapping a [MOTIS](https://github.com/motis-project/motis) instance
- [`*.transport.rest`](https://transport.rest/)  Public APIs wrapping some HAFAS endpoints. - [`*.transport.rest`](https://transport.rest/)  Public APIs wrapping some HAFAS endpoints.
## Contributing ## Contributing

View file

@ -23,7 +23,7 @@ import {testJourneysWithDetour} from './lib/journeys-with-detour.js';
const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o); const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o);
const minute = 60 * 1000; const minute = 60 * 1000;
const T_MOCK = 1747040400 * 1000; // 2025-05-12T08:00:00+01:00 const T_MOCK = 1764831628 * 1000; // Thu Dec 04 2025 07:00:28 GMT+0000
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK); const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
const cfg = { const cfg = {
@ -127,7 +127,7 @@ if (!process.env.VCR_OFF) {
}); });
tap.test('refreshJourney valid tickets', async (t) => { tap.test('refreshJourney valid tickets', async (t) => {
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00 const T_MOCK = 1758279600 * 1000;
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK); const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
const journeysRes = await client.journeys(berlinHbf, münchenHbf, { const journeysRes = await client.journeys(berlinHbf, münchenHbf, {
@ -261,7 +261,7 @@ if (!process.env.VCR_MODE) {
} }
tap.test('refreshJourney', async (t) => { tap.test('refreshJourney', async (t) => {
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00 const T_MOCK = 1763542800 * 1000;
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK); const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
const validate = createValidate({...cfg, when}); const validate = createValidate({...cfg, when});

View file

@ -3,7 +3,7 @@ import isRoughlyEqual from 'is-roughly-equal';
import {createWhen} from './lib/util.js'; import {createWhen} from './lib/util.js';
import {createClient} from '../../index.js'; import {createClient} from '../../index.js';
import {profile as dbProfile} from '../../p/dbregioguide/index.js'; import {profile as dbProfile} from '../../p/dbbahnhof/index.js';
import { import {
createValidateStation, createValidateStation,
createValidateTrip, createValidateTrip,
@ -23,7 +23,7 @@ import {testJourneysWithDetour} from './lib/journeys-with-detour.js';
const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o); const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o);
const minute = 60 * 1000; const minute = 60 * 1000;
const T_MOCK = 1747040400 * 1000; // 2025-05-12T08:00:00+01:00 const T_MOCK = 1764831628 * 1000; // Thu Dec 04 2025 07:00:28 GMT+0000
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK); const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
const cfg = { const cfg = {
@ -100,7 +100,7 @@ const berlinSüdkreuz = '8011113';
const kölnHbf = '8000207'; const kölnHbf = '8000207';
tap.test('departures at Berlin Schwedter Str.', async (t) => { tap.skip('departures at Berlin Schwedter Str.', async (t) => {
const res = await client.departures(blnSchwedterStr, { const res = await client.departures(blnSchwedterStr, {
duration: 5, when, duration: 5, when,
}); });
@ -114,7 +114,7 @@ tap.test('departures at Berlin Schwedter Str.', async (t) => {
t.end(); t.end();
}); });
tap.test('departures with station object', async (t) => { tap.skip('departures with station object', async (t) => {
const res = await client.departures({ const res = await client.departures({
type: 'station', type: 'station',
id: jungfernheide, id: jungfernheide,
@ -130,7 +130,7 @@ tap.test('departures with station object', async (t) => {
t.end(); t.end();
}); });
tap.test('arrivals at Berlin Schwedter Str.', async (t) => { tap.skip('arrivals at Berlin Schwedter Str.', async (t) => {
const res = await client.arrivals(blnSchwedterStr, { const res = await client.arrivals(blnSchwedterStr, {
duration: 5, when, duration: 5, when,
}); });

View file

@ -23,7 +23,7 @@ import {testJourneysWithDetour} from './lib/journeys-with-detour.js';
const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o); const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o);
const minute = 60 * 1000; const minute = 60 * 1000;
const T_MOCK = 1747040400 * 1000; // 2025-05-12T08:00:00+01:00 const T_MOCK = 1764831628 * 1000; // Thu Dec 04 2025 07:00:28 GMT+0000
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK); const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
const cfg = { const cfg = {
@ -126,7 +126,7 @@ tap.test('journeys  Berlin Schwedter Str. to München Hbf', async (t) => {
}); });
tap.test('refreshJourney valid tickets', async (t) => { tap.test('refreshJourney valid tickets', async (t) => {
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00 const T_MOCK = 1758279600 * 1000;
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK); const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
const journeysRes = await client.journeys(berlinHbf, münchenHbf, { const journeysRes = await client.journeys(berlinHbf, münchenHbf, {
@ -261,7 +261,7 @@ if (!process.env.VCR_OFF) {
} }
tap.test('refreshJourney', async (t) => { tap.test('refreshJourney', async (t) => {
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00 const T_MOCK = 1763542800 * 1000;
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK); const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
const validate = createValidate({...cfg, when}); const validate = createValidate({...cfg, when});

View file

@ -1,499 +0,0 @@
import tap from 'tap';
import isRoughlyEqual from 'is-roughly-equal';
import {createWhen} from './lib/util.js';
import {createClient} from '../../index.js';
import {profile as dbProfile} from '../../p/dbregioguide/index.js';
import {
createValidateStation,
createValidateTrip,
} from './lib/validators.js';
import {createValidateFptfWith as createValidate} from './lib/validate-fptf-with.js';
import {testJourneysStationToStation} from './lib/journeys-station-to-station.js';
import {testJourneysStationToAddress} from './lib/journeys-station-to-address.js';
import {testJourneysStationToPoi} from './lib/journeys-station-to-poi.js';
import {testEarlierLaterJourneys} from './lib/earlier-later-journeys.js';
import {testLegCycleAlternatives} from './lib/leg-cycle-alternatives.js';
import {testRefreshJourney} from './lib/refresh-journey.js';
import {journeysFailsWithNoProduct} from './lib/journeys-fails-with-no-product.js';
import {testDepartures} from './lib/departures.js';
import {testArrivals} from './lib/arrivals.js';
import {testJourneysWithDetour} from './lib/journeys-with-detour.js';
const isObj = o => o !== null && 'object' === typeof o && !Array.isArray(o);
const minute = 60 * 1000;
const T_MOCK = 1747040400 * 1000; // 2025-05-12T08:00:00+01:00
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
const cfg = {
when,
stationCoordsOptional: true, // TODO
products: dbProfile.products,
minLatitude: 46.673100,
maxLatitude: 55.030671,
minLongitude: 6.896517,
maxLongitude: 16.180237,
};
const validate = createValidate(cfg);
const assertValidPrice = (t, p) => {
t.ok(p);
if (p.amount !== null) {
t.equal(typeof p.amount, 'number');
t.ok(p.amount > 0);
}
if (p.hint !== null) {
t.equal(typeof p.hint, 'string');
t.ok(p.hint);
}
};
const assertValidTickets = (test, tickets) => {
test.ok(Array.isArray(tickets));
for (let fare of tickets) {
test.equal(typeof fare.name, 'string', 'Mandatory field "name" is missing or not a string');
test.ok(fare.name);
test.ok(isObj(fare.priceObj), 'Mandatory field "priceObj" is missing or not an object');
test.equal(typeof fare.priceObj.amount, 'number', 'Mandatory field "amount" in "priceObj" is missing or not a number');
test.ok(fare.priceObj.amount > 0);
if ('currency' in fare.priceObj) {
test.equal(typeof fare.priceObj.currency, 'string');
}
// Check optional fields
if ('addData' in fare) {
test.equal(typeof fare.addData, 'string');
}
if ('addDataTicketInfo' in fare) {
test.equal(typeof fare.addDataTicketInfo, 'string');
}
if ('addDataTicketDetails' in fare) {
test.equal(typeof fare.addDataTicketDetails, 'string');
}
if ('addDataTravelInfo' in fare) {
test.equal(typeof fare.addDataTravelInfo, 'string');
}
if ('addDataTravelDetails' in fare) {
test.equal(typeof fare.firstClass, 'boolean');
}
}
};
const client = createClient(dbProfile, 'public-transport/hafas-client:test', {enrichStations: false});
const berlinHbf = '8011160';
const münchenHbf = '8000261';
const jungfernheide = '8011167';
const blnSchwedterStr = '732652';
const westhafen = '8089116';
const wedding = '8089131';
const württembergallee = '731084';
const regensburgHbf = '8000309';
const blnOstbahnhof = '8010255';
const blnTiergarten = '8089091';
const blnJannowitzbrücke = '8089019';
const potsdamHbf = '8012666';
const berlinSüdkreuz = '8011113';
const kölnHbf = '8000207';
/*
tap.test('journeys Berlin Schwedter Str. to München Hbf', async (t) => {
const res = await client.journeys(blnSchwedterStr, münchenHbf, {
results: 4,
departure: when,
stopovers: true,
});
await testJourneysStationToStation({
test: t,
res,
validate,
fromId: blnSchwedterStr,
toId: münchenHbf,
});
// todo: find a journey where there pricing info is always available
for (let journey of res.journeys) {
if (journey.price) {
assertValidPrice(t, journey.price);
}
if (journey.tickets) {
assertValidTickets(t, journey.tickets);
}
}
t.end();
});
tap.test('refreshJourney valid tickets', async (t) => {
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
const journeysRes = await client.journeys(berlinHbf, münchenHbf, {
results: 4,
departure: when,
stopovers: true,
});
const refreshWithoutTicketsRes = await client.refreshJourney(journeysRes.journeys[0].refreshToken, {
tickets: false,
});
const refreshWithTicketsRes = await client.refreshJourney(journeysRes.journeys[0].refreshToken, {
tickets: true,
});
for (let res of [refreshWithoutTicketsRes, refreshWithTicketsRes]) {
if (res.journey.tickets !== undefined) {
assertValidTickets(t, res.journey.tickets);
}
}
t.end();
});
// todo: journeys, only one product
tap.test('journeys fails with no product', async (t) => {
await journeysFailsWithNoProduct({
test: t,
fetchJourneys: client.journeys,
fromId: blnSchwedterStr,
toId: münchenHbf,
when,
products: dbProfile.products,
});
t.end();
});
tap.test('Berlin Schwedter Str. to Torfstraße 17', async (t) => {
const torfstr = {
type: 'location',
address: 'Torfstraße 17',
latitude: 52.5416823,
longitude: 13.3491223,
};
const res = await client.journeys(blnSchwedterStr, torfstr, {
results: 3,
departure: when,
});
await testJourneysStationToAddress({
test: t,
res,
validate,
fromId: blnSchwedterStr,
to: torfstr,
});
t.end();
});
tap.test('Berlin Schwedter Str. to ATZE Musiktheater', async (t) => {
const atze = {
type: 'location',
id: '991598902',
poi: true,
name: 'Berlin, Atze Musiktheater für Kinder (Kultur und U',
latitude: 52.542417,
longitude: 13.350437,
};
const res = await client.journeys(blnSchwedterStr, atze, {
results: 3,
departure: when,
});
await testJourneysStationToPoi({
test: t,
res,
validate,
fromId: blnSchwedterStr,
to: atze,
});
t.end();
});
tap.test('journeys: via works with detour', async (t) => {
// Going from Westhafen to Wedding via Württembergallee without detour
// is currently impossible. We check if the routing engine computes a detour.
const res = await client.journeys(westhafen, wedding, {
via: württembergallee,
results: 1,
departure: when,
stopovers: true,
});
await testJourneysWithDetour({
test: t,
res,
validate,
detourIds: [württembergallee],
});
t.end();
});
// todo: walkingSpeed "Berlin - Charlottenburg, Hallerstraße" -> jungfernheide
// todo: without detour
// todo: with the DB endpoint, earlierRef/laterRef is missing queries many days in the future
tap.skip('earlier/later journeys, Jungfernheide -> München Hbf', async (t) => {
await testEarlierLaterJourneys({
test: t,
fetchJourneys: client.journeys,
validate,
fromId: jungfernheide,
toId: münchenHbf,
when,
});
t.end();
});
if (!process.env.VCR_MODE) {
tap.test('journeys leg cycle & alternatives', async (t) => {
await testLegCycleAlternatives({
test: t,
fetchJourneys: client.journeys,
fromId: blnTiergarten,
toId: blnJannowitzbrücke,
when,
});
t.end();
});
}
tap.test('refreshJourney', async (t) => {
const T_MOCK = 1710831600 * 1000; // 2024-03-19T08:00:00+01:00
const when = createWhen(dbProfile.timezone, dbProfile.locale, T_MOCK);
const validate = createValidate({...cfg, when});
await testRefreshJourney({
test: t,
fetchJourneys: client.journeys,
refreshJourney: client.refreshJourney,
validate,
fromId: jungfernheide,
toId: münchenHbf,
when,
});
t.end();
});
tap.skip('journeysFromTrip U Mehringdamm to U Naturkundemuseum, reroute to Spittelmarkt.', async (t) => {
const blnMehringdamm = '730939';
const blnStadtmitte = '732541';
const blnNaturkundemuseum = '732539';
const blnSpittelmarkt = '732543';
const isU6Leg = leg => leg.line && leg.line.name
&& leg.line.name.toUpperCase()
.replace(/\s+/g, '') === 'U6';
const sameStopOrStation = (stopA) => (stopB) => {
if (stopA.id && stopB.id && stopA.id === stopB.id) {
return true;
}
const statA = stopA.stat && stopA.stat.id || NaN;
const statB = stopB.stat && stopB.stat.id || NaN;
return statA === statB || stopA.id === statB || stopB.id === statA;
};
const departureOf = st => Number(new Date(st.departure || st.scheduledDeparture));
const arrivalOf = st => Number(new Date(st.arrival || st.scheduledArrival));
// `journeysFromTrip` only supports queries *right now*, so we can't use `when` as in all
// other tests. To make the test less brittle, we pick a connection that is served all night. 🙄
const when = new Date();
const validate = createValidate({...cfg, when});
const findTripBetween = async (stopAId, stopBId, products = {}) => {
const {journeys} = await client.journeys(stopAId, stopBId, {
departure: new Date(when - 10 * minute),
transfers: 0, products,
results: 8, stopovers: false, remarks: false,
});
for (const j of journeys) {
const l = j.legs.find(isU6Leg);
if (!l) {
continue;
}
const t = await client.trip(l.tripId, {
stopovers: true, remarks: false,
});
const pastStopovers = t.stopovers
.filter(st => departureOf(st) < Date.now()); // todo: <= ?
const hasStoppedAtA = pastStopovers
.find(sameStopOrStation({id: stopAId}));
const willStopAtB = t.stopovers
.filter(st => arrivalOf(st) > Date.now()) // todo: >= ?
.find(sameStopOrStation({id: stopBId}));
if (hasStoppedAtA && willStopAtB) {
const prevStopover = maxBy(pastStopovers, departureOf);
return {trip: t, prevStopover};
}
}
return {trip: null, prevStopover: null};
};
// Find a vehicle from U Mehringdamm to U Stadtmitte (to the north) that is currently
// between these two stations.
const {trip, prevStopover} = await findTripBetween(blnMehringdamm, blnStadtmitte, {
regionalExpress: false, regional: false, suburban: false,
});
t.ok(trip, 'precondition failed: trip not found');
t.ok(prevStopover, 'precondition failed: previous stopover missing');
// todo: "Error: Suche aus dem Zug: Vor Abfahrt des Zuges"
const newJourneys = await client.journeysFromTrip(trip.id, prevStopover, blnSpittelmarkt, {
results: 3, stopovers: true, remarks: false,
});
// Validate with fake prices.
const withFakePrice = (j) => {
const clone = Object.assign({}, j);
clone.price = {amount: 123, currency: 'EUR'};
return clone;
};
// todo: there is no such validator!
validate(t, newJourneys.map(withFakePrice), 'journeysFromTrip', 'newJourneys');
for (let i = 0; i < newJourneys.length; i++) {
const j = newJourneys[i];
const n = `newJourneys[${i}]`;
const legOnTrip = j.legs.find(l => l.tripId === trip.id);
t.ok(legOnTrip, n + ': leg with trip ID not found');
t.equal(last(legOnTrip.stopovers).stop.id, blnStadtmitte);
}
});
tap.test('trip details', async (t) => {
const res = await client.journeys(berlinHbf, münchenHbf, {
results: 1, departure: when,
});
const p = res.journeys[0].legs.find(l => !l.walking);
t.ok(p.tripId, 'precondition failed');
t.ok(p.line.name, 'precondition failed');
const tripRes = await client.trip(p.tripId, {when});
const validate = createValidate(cfg, {
trip: (cfg) => {
const validateTrip = createValidateTrip(cfg);
const validateTripWithFakeDirection = (val, trip, name) => {
validateTrip(val, {
...trip,
direction: trip.direction || 'foo', // todo, see #49
}, name);
};
return validateTripWithFakeDirection;
},
});
validate(t, tripRes, 'tripResult', 'tripRes');
t.end();
});
*/
tap.test('departures at Berlin Schwedter Str.', async (t) => {
const res = await client.departures(blnSchwedterStr, {
duration: 5, when,
});
await testDepartures({
test: t,
res,
validate,
id: blnSchwedterStr,
});
t.end();
});
tap.test('departures with station object', async (t) => {
const res = await client.departures({
type: 'station',
id: jungfernheide,
name: 'Berlin Jungfernheide',
location: {
type: 'location',
latitude: 1.23,
longitude: 2.34,
},
}, {when});
validate(t, res, 'departuresResponse', 'res');
t.end();
});
tap.test('arrivals at Berlin Schwedter Str.', async (t) => {
const res = await client.arrivals(blnSchwedterStr, {
duration: 5, when,
});
await testArrivals({
test: t,
res,
validate,
id: blnSchwedterStr,
});
t.end();
});
/*
tap.test('nearby Berlin Jungfernheide', async (t) => {
const nearby = await client.nearby({
type: 'location',
latitude: 52.530273,
longitude: 13.299433,
}, {
results: 2, distance: 400,
});
validate(t, nearby, 'locations', 'nearby');
t.equal(nearby.length, 2);
const s0 = nearby[0];
t.equal(s0.id, jungfernheide);
t.equal(s0.name, 'Berlin Jungfernheide');
t.ok(isRoughlyEqual(0.0005, s0.location.latitude, 52.530408));
t.ok(isRoughlyEqual(0.0005, s0.location.longitude, 13.299424));
t.ok(s0.distance >= 0);
t.ok(s0.distance <= 100);
t.end();
});
tap.test('locations named Jungfernheide', async (t) => {
const locations = await client.locations('Jungfernheide', {
results: 10,
});
validate(t, locations, 'locations', 'locations');
t.ok(locations.length <= 10);
t.ok(locations.some((l) => {
return l.station && l.station.id === jungfernheide || l.id === jungfernheide;
}), 'Jungfernheide not found');
t.end();
});
tap.test('stop', async (t) => {
const s = await client.stop(regensburgHbf);
validate(t, s, ['stop', 'station'], 'stop');
t.equal(s.id, regensburgHbf);
t.end();
});
tap.test('line with additionalName', async (t) => {
const {departures} = await client.departures(potsdamHbf, {
when,
duration: 12 * 60, // 12 minutes
products: {bus: false, suburban: false, tram: false},
});
t.ok(departures.some(d => d.line && d.line.additionalName));
t.end();
});
*/

File diff suppressed because one or more lines are too long

View file

@ -3,7 +3,7 @@ const simplify = j => j.legs.map(l => {
origin: l.origin, origin: l.origin,
destination: l.destination, destination: l.destination,
departure: l.plannedDeparture || l.departure, departure: l.plannedDeparture || l.departure,
arrival: l.plannedArrival || l.arrival, // arrival: l.plannedArrival || l.arrival, // sometimes differs on older journeys
line: l.line, line: l.line,
}; };
}); });

View file

@ -38,10 +38,10 @@ tap.test('db trip(): dynamic request formatting', (t) => {
const reqDbNav = profile.formatTripReq(ctx, tripIdHafas); const reqDbNav = profile.formatTripReq(ctx, tripIdHafas);
delete reqDbNav.headers['X-Correlation-ID']; delete reqDbNav.headers['X-Correlation-ID'];
const reqDbRegioGuide = profile.formatTripReq(ctx, tripIdRis); // const reqDbRegioGuide = profile.formatTripReq(ctx, tripIdRis);
t.same(reqDbNav, reqDbNavExpected); t.same(reqDbNav, reqDbNavExpected);
t.same(reqDbRegioGuide, reqDbRegioGuideExpected); // t.same(reqDbRegioGuide, reqDbRegioGuideExpected);
t.end(); t.end();
}); });

View file

@ -41,6 +41,10 @@ const berlinWienQuery0 = Object.freeze(
einstiegsTypList: [ einstiegsTypList: [
'STANDARD', 'STANDARD',
], ],
fahrverguenstigungen: {
deutschlandTicketVorhanden: undefined,
nurDeutschlandTicketVerbindungen: undefined,
},
klasse: 'KLASSE_2', klasse: 'KLASSE_2',
reiseHin: { reiseHin: {
wunsch: { wunsch: {
@ -48,6 +52,7 @@ const berlinWienQuery0 = Object.freeze(
verkehrsmittel: [ verkehrsmittel: [
'ALL', 'ALL',
], ],
alternativeHalteBerechnung: true,
zeitWunsch: { zeitWunsch: {
reiseDatum: '2024-12-07T23:50:12+01:00', reiseDatum: '2024-12-07T23:50:12+01:00',
zeitPunktArt: 'ABFAHRT', zeitPunktArt: 'ABFAHRT',

View file

@ -57,8 +57,8 @@ const berlinWienQuery0 = Object.freeze(
sitzplatzOnly: false, sitzplatzOnly: false,
bikeCarriage: false, bikeCarriage: false,
reservierungsKontingenteVorhanden: false, reservierungsKontingenteVorhanden: false,
nurDeutschlandTicketVerbindungen: false, nurDeutschlandTicketVerbindungen: undefined,
deutschlandTicketVorhanden: false, deutschlandTicketVorhanden: undefined,
maxUmstiege: null, maxUmstiege: null,
zwischenhalte: null, zwischenhalte: null,
minUmstiegszeit: 0, minUmstiegszeit: 0,