Skip to content

feat(fares-v2): add support for editing feeds with fares v2 data #1025

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 17 commits into
base: dev
Choose a base branch
from
Draft
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
130 changes: 130 additions & 0 deletions gtfs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1017,3 +1017,133 @@
required: false
- name: added_service
required: false

# Fares v2

- id: fareproduct
name: fare_products.txt
helpContent: Used to describe the range of fares available for purchase by riders or taken into account when computing the total fare for journeys with multiple legs, such as transfer costs.
fields:
- name: fare_product_id
required: true
inputType: GTFS_ID
- name: fare_product_name
required: false
inputType: TEXT
# FARE-TODO: Add rider_category_id?
- name: fare_media_id
required: false
inputType: GTFS_FARE_MEDIA
- name: amount
inputType: NUMBER
required: true
- name: currency
required: true
inputType: DROPDOWN
bulkEditEnabled: true
options:
- value: USD
text: US dollar (USD)
- value: AUD
text: Australian dollar (AUD)
- value: CAD
text: Canadian dollar (CAD)
- value: CHF
text: Swiss franc (CHF)
- value: CNH
text: Chinese renminbi (CNH)
- value: EUR
text: Euro (EUR)
- value: GBP
text: Pound sterling (GBP)
- value: JPY
text: Japanese yen (JPY)
- value: MXN
text: Mexican peso (MXN)
- value: NZD
text: New Zealand dollar (NZD)
- value: SEK
text: Swedish krona (SEK)
- id: faremedia
name: fare_media.txt
helpContent: To describe the different fare media that can be employed to use fare products. Fare media are physical or virtual holders used for the representation and/or validation of a fare product.
fields:
- name: fare_media_id
required: true
inputType: GTFS_ID
- name: fare_media_name
inputType: TEXT
- name: fare_media_type
required: true
inputType: DROPDOWN
options:
- value: '0'
text: None (0)
- value: '1'
text: Physical paper ticket (1)
- value: '2'
text: Physical transit card (2)
- value: '3'
text: cEMV (3)
- value: '4'
text: Mobile app (4)
- id: faretransferrule
name: fare_transfer_rules.txt
helpContent: Fare rules for transfers between legs of travel defined in fare_leg_rules.txt.
fields:
- name: from_leg_group_id
required: false
inputType: TEXT # FARE-TODO: Needs to reference fare_leg_rules
- name: to_leg_group_id
required: false
inputType: TEXT # FARE-TODO: Needs to reference fare_leg_rules
- name: transfer_count
inputType: NUMBER # FARE-TODO: NON-ZERO INT
- name: duration_limit
inputType: POSITIVE_INT
- name: duration_limit_type
inputType: DROPDOWN
# FARE-TODO: Some help content?
options:
- value: '0'
- value: '1'
- value: '2'
- value: '3'
- name: fare_transfer_type
inputType: DROPDOWN
# FARE-TODO: Some help content?
options:
- value: '0'
- value: '1'
- value: '2'
- name: fare_product_id
required: false
inputType: GTFS_FARE_PRODUCT
- id: farelegrule
name: fare_leg_rules.txt
# FARE-TODO: Some help content?
fields:
- name: leg_group_id
required: false
inputType: TEXT # FARE-TODO: should this be text or id?
- name: network_id
required: false
inputType: TEXT # FARE-TODO: Needs to reference networks.network_id or routes.network_id
- name: from_area_id
required: false
inputType: TEXT # FARE-TODO: Needs to reference areas.area_id
- name: to_area_id
required: false
inputType: TEXT # FARE-TODO: Needs to reference areas.area_id
- name: from_timeframe_group_id
required: false
inputType: TEXT # FARE-TODO: Needs to reference timeframes.timeframe_group_id
- name: to_timeframe_group_id
required: false
inputType: TEXT # FARE-TODO: Needs to reference timeframes.timeframe_group_id
- name: fare_product_id
required: true
inputType: TEXT # FARE-TODO: Needs to reference fare_products.fare_product_id
- name: rule_priority
required: false
inputType: POSITIVE_INT
20 changes: 20 additions & 0 deletions lib/editor/actions/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,26 @@ export function fetchBaseGtfs ({
id
fare_id
}
fare_product (limit: -1) {
id
fare_product_id
fare_media_id
}
fare_media (limit: -1) {
id
fare_media_id
}
fare_transfer_rule (limit: -1) {
id
from_leg_group_id
to_leg_group_id
fare_product_id
}
fare_leg_rule (limit: -1) {
fare_product_id
leg_group_id
id
}
routes (limit: -1) {
id
route_id
Expand Down
31 changes: 31 additions & 0 deletions lib/editor/components/EditorInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {Entity, Feed, GtfsSpecField, GtfsAgency, GtfsStop} from '../../type
import type {EditorTables} from '../../types/reducers'

import ColorField from './ColorField'
import FareProductSelect from './FareProductSelect'
import RouteTypeSelect from './RouteTypeSelect'
import VirtualizedEntitySelect from './VirtualizedEntitySelect'
import ZoneSelect from './ZoneSelect'
Expand Down Expand Up @@ -386,6 +387,36 @@ export default class EditorInput extends React.Component<Props> {
</FormGroup>
)
}
case 'GTFS_FARE_PRODUCT':
const fareProducts = getTableById(tableData, 'fareproduct').map(fareProduct => ({
value: fareProduct.fare_product_id,
label: `${fareProduct.fare_product_id} (${fareProduct.fare_media_id})`
}))
return (
<FormGroup {...formProps}>
{basicLabel}
<FareProductSelect
addCreateOption
onChange={this._onSelectChange}
value={currentValue}
options={fareProducts} />
</FormGroup>
)
case 'GTFS_FARE_MEDIA':
const fareMedia = getTableById(tableData, 'faremedia').map(fareMedia => ({
value: fareMedia.fare_media_id,
label: `${fareMedia.fare_media_name}`
}))
return (
<FormGroup {...formProps}>
{basicLabel}
<Select
clearable
onChange={this._onSelectChange}
value={currentValue}
options={fareMedia} />
</FormGroup>
)
default:
return null
}
Expand Down
51 changes: 51 additions & 0 deletions lib/editor/components/FareProductSelect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// @flow

import React, {Component} from 'react'
import Select from 'react-select'

import type {ZoneOption} from '../../types'

type Props = {
addCreateOption?: boolean,
onChange: ?ZoneOption => void,
options: Array<ZoneOption>,
placeholder: string,
value: ?ZoneOption
}

type State = {
value: any
}

export default class FareProductSelect extends Component<Props, State> {
static defaultProps = {
placeholder: 'Select fare product ID...'
}

state = {
value: null
}

_onChange = (option: ZoneOption) => {
const value = option ? option.value : null
this.setState({value})
}

render () {
const {onChange, placeholder, value, options} = this.props
if (value && typeof value === 'string' && !options.find(option => option.value === value)) {
console.warn(`${value} not found in fare product options. Adding to options.`)
options.push({label: value, value})
}
return (
<Select
clearable
data-test-id='fare-product-selector'
noResultsText={`No fare products found.`}
onChange={onChange || this._onChange}
options={options}
placeholder={placeholder}
value={value || this.state.value} />
)
}
}
4 changes: 4 additions & 0 deletions lib/editor/reducers/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export const defaultState = {
routes: [],
schedule_exceptions: [],
stops: [],
fare_leg_rule: [],
fare_media: [],
fare_product: [],
fare_transfer_rule: [],
trip_counts: {
pattern_id: [],
route_id: [],
Expand Down
29 changes: 27 additions & 2 deletions lib/editor/util/gtfs.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ export const COMPONENT_LIST = [
// FIXME: table name for calendar, fare, and schedule exception
{ id: 'calendar', tableName: 'calendar' },
{ id: 'scheduleexception', tableName: 'schedule_exceptions' },
{ id: 'agency', tableName: 'agency' }
{ id: 'agency', tableName: 'agency' },
{ id: 'fareproduct', tableName: 'fare_product' },
{ id: 'faremedia', tableName: 'fare_media' },
{ id: 'faretransferrule', tableName: 'fare_transfer_rule' },
{ id: 'farelegrule', tableName: 'fare_leg_rule' }
]

export function getTableById (tableData: any, id?: string, emptyArrayOnNull: boolean = true): any {
Expand Down Expand Up @@ -240,7 +244,19 @@ export function getEntityName (entity: any): string {
nameKey = 'description'
} else if (typeof entity.fare_id !== 'undefined') {
nameKey = 'fare_id'
} else if (typeof entity.exemplar !== 'undefined') {
} else if (typeof entity.fare_product_id !== 'undefined' && typeof entity.fare_media_id !== 'undefined') {
nameKey = 'fare_product_id'
} else if (typeof entity.fare_media_id !== 'undefined') {
nameKey = 'fare_media_id'
} else if (typeof entity.from_leg_group_id !== 'undefined' && typeof entity.to_leg_group_id !== 'undefined' && entity.fare_product_id !== 'undefined') {
nameKey = 'fare_transfer_rule_id' // FARE-TODO: fare_media_name? Some combo?
} else if (typeof entity.fare_product_id !== 'undefined' && typeof entity.leg_group_id !== 'undefined') {
nameKey = 'fare_leg_rule_id' // FARE-TODO: fare_media_name? Some combo?
}

// FARE-TODO: what to do with fare_transfer_rules.txt?
// FARE-TODO: what to do with fare_leg_rules.txt?
if (typeof entity.exemplar !== 'undefined') {
nameKey = 'name'
}

Expand Down Expand Up @@ -274,6 +290,15 @@ export function getEntityName (entity: any): string {
return `${serviceCalendar.service_id} ${serviceCalendar.description
? `(${serviceCalendar.description})`
: ''}`
case 'fare_product_id':
// FARE-TODO: type!
return `${entity.fare_product_id} (${entity.fare_media_id})`
case 'fare_transfer_rule_id':
// FARE-TODO: type!
return `${entity.from_leg_group_id} ➡️ ${entity.to_leg_group_id} (${entity.fare_product_id})`
case 'fare_leg_rule_id':
// FARE-TODO: type!
return `${entity.fare_product_id} (${entity.leg_group_id})`
default:
const otherEntityType: any = entity
return otherEntityType[nameKey] || NO_NAME
Expand Down
9 changes: 8 additions & 1 deletion lib/editor/util/objects.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
// @flow
import {ENTITY} from '../constants'
import {camelCaseKeys, snakeCaseKeys} from '../../common/util/map-keys'

import type {Entity} from '../../types'

export function componentToText (component: string): string {
switch (component) {
case 'scheduleexception':
return 'exception'
case 'fareproduct':
return 'product'
case 'faremedia':
return 'media'
case 'faretransferrule':
return 'transfer rule'
case 'farelegrule':
return 'leg rule'
default:
return component
}
Expand Down
32 changes: 32 additions & 0 deletions lib/editor/util/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,38 @@ export const GTFS_ICONS = [
addable: true,
title: messages('fare.title'),
label: messages('fare.label')
},
{
id: 'fareproduct',
tableName: 'fareproduct',
icon: 'shopping-cart',
addable: true,
title: 'FARE PRODUCT TEST TITLE',
label: 'V2: Products'
},
{
id: 'faremedia',
tableName: 'faremedia',
icon: 'id-card',
addable: true,
title: 'FARE MEDIA TEST TITLE',
label: 'V2: Media'
},
{
id: 'faretransferrule',
tableName: 'faretransferrule',
icon: 'link',
addable: true,
title: 'FARE TRANSFER RULE TEST TITLE',
label: 'V2: Transfer Rules'
},
{
id: 'farelegrule',
tableName: 'farelegrule',
icon: 'book',
addable: true,
title: 'FARE LEG RULE TEST TITLE',
label: 'V2: Leg Rules'
}
]

Expand Down
Loading
Loading