Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,19 +160,41 @@ They are used as "TC-<the value>". The value needs to be transferred to nova for

# Usage notes
- The TripResult used in the OJP fare service should not be based on short-term real-time information. So the TripRequest should usually contain a UseRealtime set to false.
- The price is only in one direction. If the full price in both directions is needed and artificial trip must be constructed, that contains all necessary legs in both direction (and works from the time view): Search A to B, some delay, search B to A, concatenate the trips into one. This IS necessary as sometimes the trip in both direction is cheaper than two single trips.
- The price is only for A to B. If the full price in both directions is needed and artificial trip must be constructed, that contains all necessary legs in both direction (and works from the time view): Search A to B, some delay, search B to A, concatenate the trips into one. This IS necessary as sometimes the trip in both direction is cheaper than two single trips.
- We base on the commercial stops (as BPUIC). The calls are more and more based on the SLOID. It is important only provide the commerical stops to NOVA. In some cases the commercial stop is no longer directly based on the other one (e.g. Europaplatz). The right one must be obtained from the PlaceContext (done in `sloid2didok` function).

- While OJP generelly supports multiple request. The current version of the software only processes the first request.

# Testing
In the folder `input` there are possible xml files. Some work, some are problematic
The selection of files to use in `test_network_flow.py` is done by `test_configuration.py` which basically contains an array
of the files to use. `test_configuration.py` contains the explanaition on what works and what not.
The selection of files to use in `test_network_flow.py` is done by `test_configuration.json`.

Be aware: For some discount tickets the trip needs to be several days in the future. Currently this needs to be set manually in the respective
Be aware: For some discount tickets the trip needs to be several days in the future. Currently, this needs to be set manually in the respective
request file in `input`. We don't use the `<ojp:DepArrTime>2025-10-10T15:30:40</ojp:DepArrTime>` in many cases, as trips in the past don't have prives.
If it is omitted, then `now` is used. but we keep it in the files commented out, so that you just can put in the time.

test_configuration.json contains an array of test cases:
- id: the identifier
- description: The description of the test case
- file: the file to be loaded (contains an OJPTripRequest)
- travellers: the information about the travelers
- age
- entitlements: the list of entitlement products (there is only a short list available. Most important HTA)
- typ: PERSON, DOG, xxx
- tkid: not supported yet
- birthday: not supported yet
- gender: not supported yet
- subscription: true or false. one ticket or a subscription
- relationship: a list of relevant relationships between the travelers. not supported yet
- result: pass or fail. Should there be a price.
- assert: if set, then the value provided should be found in the answer. not supported yet
- active: saying if this test is active when running the whole file
- future: if set indicates when in the future should be searched (days). The system then uses a random time between 8:00 and 12:00 to start
- start_time: HH:MM:SS, used to test a night bus.

`test_network_flow.py` can be used with at most one parameter:
- `--all`: tests all tests in the test file
- `--id=<xx>`: tests the test with the given id
- Without parameter all tests that are set to active are tested.

# Changelog

Expand All @@ -182,6 +204,7 @@ If it is omitted, then `now` is used. but we keep it in the files commented out,

## 1.2 prepared for dockerization
- Make sure that we can run this in our new environment.
- Fixed more bugs.

## 1.1 Bug fixes, better documentation, better testing
- OJP 2.0 support (first version)
Expand Down
2 changes: 1 addition & 1 deletion input/input_Bern_Chur_SOB_Zukunft.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<ojp:Text>Bern</ojp:Text>
</ojp:LocationName>
</ojp:PlaceRef>
<ojp:DepArrTime>2024-12-08T16:30:40</ojp:DepArrTime> -->
<!-- <ojp:DepArrTime>2024-12-08T16:30:40</ojp:DepArrTime> -->
</ojp:Origin>
<ojp:Destination>
<ojp:PlaceRef>
Expand Down
3 changes: 1 addition & 2 deletions input/input_Bern_Zweisimmen_BLS_Zukunft.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
<ojp:Text>Bern</ojp:Text>
</ojp:LocationName>
</ojp:PlaceRef>
<ojp:DepArrTime>2025-11-24T15:30:40</ojp:DepArrTime>

<ojp:DepArrTime>2026-03-10T15:30:40</ojp:DepArrTime>
</ojp:Origin>
<ojp:Destination>
<ojp:PlaceRef>
Expand Down
39 changes: 39 additions & 0 deletions input/input_Mattelift.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<OJP xmlns="http://www.vdv.de/ojp" xmlns:siri="http://www.siri.org.uk/siri" version="2.0">
<OJPRequest>
<siri:ServiceRequest>
<siri:ServiceRequestContext>
<siri:Language>de</siri:Language>
</siri:ServiceRequestContext>
<siri:RequestTimestamp>2026-03-03T12:36:55.154Z</siri:RequestTimestamp>
<siri:RequestorRef>OJP_DemoApp_Beta_OJP2.0</siri:RequestorRef>
<OJPTripRequest>
<siri:RequestTimestamp>2026-03-03T12:36:55.154Z</siri:RequestTimestamp>
<Origin>
<PlaceRef>
<StopPlaceRef>8500258</StopPlaceRef>
<Name>
<Text>n/a</Text>
</Name>
</PlaceRef>
</Origin>
<Destination>
<PlaceRef>
<StopPlaceRef>8500249</StopPlaceRef>
<Name>
<Text>n/a</Text>
</Name>
</PlaceRef>
</Destination>
<Params>
<NumberOfResults>1</NumberOfResults>
<UseRealtimeData>explanatory</UseRealtimeData>
<IncludeAllRestrictedLines>true</IncludeAllRestrictedLines>
<IncludeTrackSections>false</IncludeTrackSections>
<IncludeLegProjection>false</IncludeLegProjection>
<IncludeIntermediateStops>true</IncludeIntermediateStops>
</Params>
</OJPTripRequest>
</siri:ServiceRequest>
</OJPRequest>
</OJP>
42 changes: 42 additions & 0 deletions input/input_Monatsabo_test.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<OJP xmlns="http://www.vdv.de/ojp" xmlns:siri="http://www.siri.org.uk/siri" version="2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.vdv.de/ojp file:///C:/Users/ue71603/MG_Daten/github/OJP4/OJP.xsd">
<OJPRequest>
<siri:ServiceRequest>
<siri:ServiceRequestContext>
<siri:Language>de</siri:Language>
</siri:ServiceRequestContext>
<siri:RequestTimestamp>2026-02-17T22:00:01.474Z</siri:RequestTimestamp>
<siri:RequestorRef>OJP_DemoApp_Beta_OJP2.0</siri:RequestorRef>
<OJPTripRequest>
<siri:RequestTimestamp>2026-02-17T22:00:01.474Z</siri:RequestTimestamp>
<Origin>
<PlaceRef>
<GeoPosition>
<siri:Longitude>7.33995</siri:Longitude>
<siri:Latitude>46.81392</siri:Latitude>
</GeoPosition>
<Name>
<Text>origin</Text>
</Name>
</PlaceRef>
</Origin>
<Destination>
<PlaceRef>
<StopPlaceRef>8571359</StopPlaceRef>
<Name>
<Text>destination</Text>
</Name>
</PlaceRef>
</Destination>
<Params>
<NumberOfResults>5</NumberOfResults>
<UseRealtimeData>explanatory</UseRealtimeData>
<IncludeAllRestrictedLines>true</IncludeAllRestrictedLines>
<IncludeTrackSections>false</IncludeTrackSections>
<IncludeLegProjection>false</IncludeLegProjection>
<IncludeIntermediateStops>true</IncludeIntermediateStops>
</Params>
</OJPTripRequest>
</siri:ServiceRequest>
</OJPRequest>
</OJP>
8 changes: 4 additions & 4 deletions input/input_coordinates.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@
</ojp:PlaceRef>
</ojp:Destination>
<ojp:Params>
<ojp:NumberOfResultsAfter>5</ojp:NumberOfResultsAfter>
<ojp:IncludeTrackSections>true</ojp:IncludeTrackSections>
<ojp:IncludeLegProjection>true</ojp:IncludeLegProjection>
<ojp:IncludeTurnDescription>true</ojp:IncludeTurnDescription>
<ojp:NumberOfResultsAfter>2</ojp:NumberOfResultsAfter>
<ojp:IncludeTrackSections>false</ojp:IncludeTrackSections>
<ojp:IncludeLegProjection>false</ojp:IncludeLegProjection>
<ojp:IncludeTurnDescription>false</ojp:IncludeTurnDescription>
<ojp:IncludeIntermediateStops>true</ojp:IncludeIntermediateStops>
</ojp:Params>
</ojp:OJPTripRequest>
Expand Down
47 changes: 47 additions & 0 deletions input/input_coordinates_ojp2.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<OJP xmlns="http://www.vdv.de/ojp" xmlns:siri="http://www.siri.org.uk/siri" version="2.0">
<OJPRequest>
<siri:ServiceRequest>
<siri:ServiceRequestContext>
<siri:Language>de</siri:Language>
</siri:ServiceRequestContext>
<siri:RequestTimestamp>2026-03-03T15:00:17.446Z</siri:RequestTimestamp>
<siri:RequestorRef>OJP_DemoApp_Beta_OJP2.0</siri:RequestorRef>
<OJPTripRequest>
<siri:RequestTimestamp>2026-03-03T15:00:17.446Z</siri:RequestTimestamp>
<Origin>
<PlaceRef>
<GeoPosition>
<siri:Longitude>7.449772</siri:Longitude>
<siri:Latitude>46.962961</siri:Latitude>
<Properties/>
</GeoPosition>
<Name>
<Text>46.962961,7.449772</Text>
</Name>
</PlaceRef>
</Origin>
<Destination>
<PlaceRef>
<GeoPosition>
<siri:Longitude>7.489687</siri:Longitude>
<siri:Latitude>46.930422</siri:Latitude>
<Properties/>
</GeoPosition>
<Name>
<Text>46.930422,7.489687</Text>
</Name>
</PlaceRef>
</Destination>
<Params>
<NumberOfResults>5</NumberOfResults>
<UseRealtimeData>explanatory</UseRealtimeData>
<IncludeAllRestrictedLines>true</IncludeAllRestrictedLines>
<IncludeTrackSections>true</IncludeTrackSections>
<IncludeLegProjection>true</IncludeLegProjection>
<IncludeIntermediateStops>true</IncludeIntermediateStops>
</Params>
</OJPTripRequest>
</siri:ServiceRequest>
</OJPRequest>
</OJP>
39 changes: 39 additions & 0 deletions input/input_menusio.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<OJP xmlns="http://www.vdv.de/ojp" xmlns:siri="http://www.siri.org.uk/siri" version="2.0">
<OJPRequest>
<siri:ServiceRequest>
<siri:ServiceRequestContext>
<siri:Language>de</siri:Language>
</siri:ServiceRequestContext>
<siri:RequestTimestamp>2026-03-05T15:57:19.971Z</siri:RequestTimestamp>
<siri:RequestorRef>OJP_DemoApp_Beta_OJP2.0</siri:RequestorRef>
<OJPTripRequest>
<siri:RequestTimestamp>2026-03-05T15:57:19.971Z</siri:RequestTimestamp>
<Origin>
<PlaceRef>
<StopPlaceRef>8505000</StopPlaceRef>
<Name>
<Text>n/a</Text>
</Name>
</PlaceRef>
<DepArrTime>2026-03-10T21:00:00.000Z</DepArrTime>
</Origin>
<Destination>
<PlaceRef>
<StopPlaceRef>8505417</StopPlaceRef>
<Name>
<Text>n/a</Text>
</Name>
</PlaceRef>
</Destination>
<Params>
<NumberOfResults>5</NumberOfResults>
<UseRealtimeData>explanatory</UseRealtimeData>
<IncludeAllRestrictedLines>true</IncludeAllRestrictedLines>
<IncludeTrackSections>true</IncludeTrackSections>
<IncludeLegProjection>true</IncludeLegProjection>
<IncludeIntermediateStops>true</IncludeIntermediateStops>
</Params>
</OJPTripRequest>
</siri:ServiceRequest>
</OJPRequest>
</OJP>
2 changes: 1 addition & 1 deletion input/input_nachtbus.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<ojp:Text>Bern, Bahnhof (Bern)</ojp:Text>
</ojp:LocationName>
</ojp:PlaceRef>
<ojp:DepArrTime>2025-11-01T00:00:45.343Z</ojp:DepArrTime>
<!--<ojp:DepArrTime>2025-11-01T00:00:45.343Z</ojp:DepArrTime>-->
</ojp:Origin>
<ojp:Destination>
<ojp:PlaceRef>
Expand Down
38 changes: 38 additions & 0 deletions input/input_test_dornbir.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<OJP xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.0" xmlns="http://www.vdv.de/ojp" xmlns:siri="http://www.siri.org.uk/siri">
<OJPRequest>
<siri:ServiceRequest>
<siri:ServiceRequestContext>
<siri:Language>de</siri:Language>
</siri:ServiceRequestContext>
<siri:RequestTimestamp>2026-02-25T13:01:28Z</siri:RequestTimestamp>
<siri:RequestorRef>BLS_IOS_SDK_1.3.3</siri:RequestorRef>
<OJPTripRequest>
<siri:RequestTimestamp>2026-02-25T13:01:28Z</siri:RequestTimestamp>
<Origin>
<PlaceRef>
<StopPlaceRef>8102329</StopPlaceRef>
<Name>
<Text>Dornbirn</Text>
</Name>
</PlaceRef>
</Origin>
<Destination>
<PlaceRef>
<StopPlaceRef>8506314</StopPlaceRef>
<Name>
<Text>St. Margrethen SG</Text>
</Name>
</PlaceRef>
</Destination>
<Params>
<NumberOfResults>6</NumberOfResults>
<IncludeLegProjection>false</IncludeLegProjection>
<IncludeIntermediateStops>true</IncludeIntermediateStops>
<IncludeAllRestrictedLines>true</IncludeAllRestrictedLines>
<UseRealtimeData>explanatory</UseRealtimeData>
</Params>
</OJPTripRequest>
</siri:ServiceRequest>
</OJPRequest>
</OJP>
5 changes: 3 additions & 2 deletions map_nova_to_ojp.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python3

import datetime
from decimal import Decimal
from typing import List, Optional, Dict

from xsdata.models.datatype import XmlDateTime
Expand Down Expand Up @@ -42,10 +43,10 @@ def map_preis_auspraegung_to_trip_fare_result(preis_auspraegungen: List[PreisAus
fare_authority_ref='ch:1:sboid:101704',
fare_authority_text='Alliance SwissPass',
price=preis_auspraegung.preis.betrag,
net_price=round(float(preis_auspraegung.preis.betrag)*(1.0-VATRATE/100),2),
net_price=round(float(preis_auspraegung.preis.betrag)*(1.0- VATRATE/100),2),
currency=preis_auspraegung.preis.waehrung,
required_card=required_card,
vat_rate=VATRATE,
vat_rate=Decimal(VATRATE).quantize(Decimal("0.1")),
travel_class=map_klasse_to_fareclass(preis_auspraegung.produkt_einfluss_faktoren.klasse))]))

return FareResultStructure(result_id=id, trip_fare_result=tripfareresults)
Expand Down
2 changes: 1 addition & 1 deletion map_nova_to_ojp2.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def map_preis_auspraegung_to_trip_fare_result(preis_auspraegungen: List[PreisAus
net_price=round(float(preis_auspraegung.preis.betrag)*(1.0-VATRATE/100),2),
currency=preis_auspraegung.preis.waehrung,
required_card=required_card,
vat_rate=Decimal(VATRATE),
vat_rate=Decimal(VATRATE).quantize(Decimal("0.1")),
fare_class=map_klasse_to_fareclass(preis_auspraegung.produkt_einfluss_faktoren.klasse))]))

return FareResultStructure(id=id, trip_fare_result=tripfareresults)
Expand Down
24 changes: 17 additions & 7 deletions map_ojp2_to_nova.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,17 @@ def map_timed_leg_to_segment(timed_leg: TimedLegStructure) -> FahrplanVerbindung
operator_ref = timed_leg.service.operator_ref # needs to be processed afterwards to get the verwaltungs_code
gattungs_code = timed_leg.service.mode.short_name.text[0].value # is correct, but a bit of a hack

# in OJP 2.' the number is in the TrainNumber
verkehrs_mittel_nummer=timed_leg.service.train_number
# unfortunately it is not in line_ref, but in Extension/ojp:PublishedJourneyNumber
_, verkehrs_mittel_nummer, _ = line_ref.split(':')
#_, verkehrs_mittel_nummer, _ = line_ref.split(':')
# This is an other hack.
verkehrs_mittel_nummer = ''.join(filter(lambda x: x.isdigit(), verkehrs_mittel_nummer))
try:
#verkehrs_mittel_nummer = ''.join(filter(lambda x: x.isdigit(), verkehrs_mittel_nummer))
#try:
# Set verkehrs_mittel_nummer to timed_leg.extension.publishedjourneynumber?
verkehrs_mittel_nummer = [x.children[0].text for x in timed_leg.extension.childen if x.qname == '{http://www.vdv.de/ojp}PublishedJourneyNumber'][0]
except:
pass
# verkehrs_mittel_nummer = [x.children[0].text for x in timed_leg.extension.childen if x.qname == '{http://www.vdv.de/ojp}PublishedJourneyNumber'][0]
#except:
# pass

verwaltungs_code= process_operating_ref_ojp2(operator_ref)

Expand Down Expand Up @@ -79,6 +81,14 @@ def map_fare_request_to_nova_request(ojp: Ojp, age: int=30) -> Optional[PreisAus
and len(ojp.ojprequest.service_request.ojpfare_request[0].trip_fare_request.trip.leg) > 0):
return None

#handling of abos (monthly) otherwise the product_taxonomie is set to the standard
produkt_taxonomie = "SBB Preisauskunft"
try:
val = ojp.ojprequest.service_request.ojpfare_request[0].params.fare_authority_filter[0]
if "NOVA-Subscription" in val.value:
produkt_taxonomie="SBB Abonnemente"
except:
pass
try:
if ojp.ojprequest.service_request.ojpfare_request[0].params.traveller is None:
travellers = []
Expand Down Expand Up @@ -171,7 +181,7 @@ def map_fare_request_to_nova_request(ojp: Ojp, age: int=30) -> Optional[PreisAus
correlation_id=str(uuid.uuid1()),
geschaefts_prozess_id="1781786f-57ba-4e9a-bc29-287e2aa97f9a"),
angebots_filter=[TaxonomieFilter(
produkt_taxonomie="SBB Preisauskunft",
produkt_taxonomie=produkt_taxonomie,
taxonomie_klasse_pfad=[TaxonomieKlassePfad(EmptyType())])],
reisender=reisende,
verbindung=verbindungen
Expand Down
Loading