diff --git a/configurations/test/server.yml.tmp b/configurations/test/server.yml.tmp
index 677eee64c..c24f035fa 100644
--- a/configurations/test/server.yml.tmp
+++ b/configurations/test/server.yml.tmp
@@ -33,7 +33,7 @@ modules:
gtfsapi:
enabled: true
load_on_fetch: false
- # use_extension: mtc
+ use_extension: mtc
# update_frequency: 30 # in seconds
manager:
normalizeFieldTransformation:
@@ -74,8 +74,12 @@ extensions:
enabled: true
rtd_api: http://localhost:9876/
s3_bucket: bucket-name
+ ### Uncomment and edit if the S3 region of the bucket above is different than application.data.s3_region.
+ # s3_region: us-west-2
s3_prefix: waiting/
s3_download_prefix: waiting/
+ ### Uncomment and edit if separate credentials are needed to access the bucket above.
+ # s3_credentials_file: ~/.aws/credentials
transitland:
enabled: true
api: https://transit.land/api/v1/feeds
diff --git a/pom.xml b/pom.xml
index 113e49c11..aa23c463a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -272,7 +272,7 @@
com.github.ibi-group
gtfs-lib
- dbbf3d166cf31fc9dae490ef4b8db42ec436f743
+ 926b910e098fa8aba38ada01ce2f5bb7dd38e917
diff --git a/src/main/java/com/conveyal/datatools/common/utils/aws/S3Utils.java b/src/main/java/com/conveyal/datatools/common/utils/aws/S3Utils.java
index 078781132..ada92fbbf 100644
--- a/src/main/java/com/conveyal/datatools/common/utils/aws/S3Utils.java
+++ b/src/main/java/com/conveyal/datatools/common/utils/aws/S3Utils.java
@@ -22,6 +22,7 @@
import java.io.IOException;
import java.net.URL;
import java.util.Date;
+import java.util.List;
import java.util.UUID;
import static com.conveyal.datatools.common.utils.SparkUtils.logMessageAndHalt;
@@ -40,41 +41,21 @@ public class S3Utils {
public static final String DEFAULT_BUCKET;
public static final String DEFAULT_BUCKET_GTFS_FOLDER = "gtfs/";
+ public static final String APP_DATA_S3_REGION = "application.data.s3_region";
+
+ public static final String APP_DATA_CREDS_FILE = "application.data.s3_credentials_file";
+
static {
// Placeholder variables need to be used before setting the final variable to make sure initialization occurs
- AmazonS3 tempS3Client = null;
AWSCredentialsProvider tempS3CredentialsProvider = null;
String tempGtfsS3Bucket = null;
S3ClientManagerImpl tempS3ClientManager = null;
// Only configure s3 if the config requires doing so
if (DataManager.useS3 || hasConfigProperty("modules.gtfsapi.use_extension")) {
- try {
- AmazonS3ClientBuilder builder = AmazonS3ClientBuilder.standard();
- String credentialsFile = DataManager.getConfigPropertyAsText("application.data.s3_credentials_file");
- tempS3CredentialsProvider = credentialsFile != null ?
- new ProfileCredentialsProvider(credentialsFile, "default") :
- new DefaultAWSCredentialsProviderChain(); // default credentials providers, e.g. IAM role
- builder.withCredentials(tempS3CredentialsProvider);
-
- // If region configuration string is provided, use that.
- // Otherwise defaults to value provided in ~/.aws/config
- String region = DataManager.getConfigPropertyAsText("application.data.s3_region");
- if (region != null) {
- builder.withRegion(region);
- }
- tempS3Client = builder.build();
- } catch (Exception e) {
- LOG.error(
- "S3 client not initialized correctly. Must provide config property application.data.s3_region or specify region in ~/.aws/config",
- e
- );
- }
-
- if (tempS3Client == null) {
- throw new IllegalArgumentException("Fatal error initializing the default s3Client");
- }
- tempS3ClientManager = new S3ClientManagerImpl(tempS3Client);
+ S3Wrapper build = buildS3Wrapper(List.of(APP_DATA_CREDS_FILE), List.of(APP_DATA_S3_REGION));
+ tempS3CredentialsProvider = build.credentials;
+ tempS3ClientManager = new S3ClientManagerImpl(build.s3Client);
// s3 storage
tempGtfsS3Bucket = DataManager.getConfigPropertyAsText("application.data.gtfs_s3_bucket");
@@ -89,6 +70,76 @@ public class S3Utils {
DEFAULT_BUCKET = tempGtfsS3Bucket;
}
+ public static class S3Wrapper {
+ public final AWSCredentialsProvider credentials;
+ public final AmazonS3 s3Client;
+
+ public S3Wrapper(AWSCredentialsProvider credentials, AmazonS3 s3Client) {
+ this.credentials = credentials;
+ this.s3Client = s3Client;
+ }
+ }
+
+ /**
+ * Builds an S3 client from the specified credentials and region, or fallback to defaults
+ * using the ambient IAM role.
+ * @param configCredentials The credentials file to use for the client.
+ * @param configRegions The configuration entries in the order to try to get the AWS region to apply.
+ * @return An S3 client for the provided credentials and region.
+ */
+ public static S3Wrapper buildS3Wrapper(
+ List configCredentials,
+ List configRegions
+ ) throws IllegalArgumentException {
+ AmazonS3 s3client = null;
+ AWSCredentialsProvider credentialsProvider = null;
+ try {
+ AmazonS3ClientBuilder builder = AmazonS3ClientBuilder.standard();
+ // Iterate through the credentials and stop at the first non-null returned.
+ // Default to the ambient IAM credentials, if any.
+ for (String configCred : configCredentials) {
+ String credentialsFile = DataManager.getConfigPropertyAsText(configCred);
+ if (credentialsFile != null) {
+ try {
+ credentialsProvider = new ProfileCredentialsProvider(credentialsFile, "default");
+ } catch (IllegalArgumentException e) {
+ LOG.error("Invalid credentials from {}. Trying the next one.", configCred, e);
+ }
+ if (credentialsProvider != null) break;
+ }
+ }
+ if (credentialsProvider == null) {
+ // default credentials providers, e.g. IAM role
+ credentialsProvider = new DefaultAWSCredentialsProviderChain();
+ }
+ builder.withCredentials(credentialsProvider);
+
+ // Iterate through the regions and stop at the first non-null returned.
+ // Otherwise defaults to value (typically provided in ~/.aws/config)
+ for (String configRegion : configRegions) {
+ String region = DataManager.getConfigPropertyAsText(configRegion);
+ if (region != null) {
+ builder.withRegion(region);
+ break;
+ }
+ }
+
+ s3client = builder.build();
+ } catch (Exception e) {
+ LOG.error(
+ "S3 client not initialized correctly. Must provide config property {} or specify region in ~/.aws/config",
+ configRegions,
+ e
+ );
+ }
+
+ if (s3client == null) {
+ throw new IllegalArgumentException("Fatal error initializing the default s3Client");
+ }
+
+ return new S3Wrapper(credentialsProvider, s3client);
+ }
+
/**
* Makes a key for an object id that is assumed to be in the default bucket's GTFS folder
*/
@@ -105,7 +156,7 @@ public static String getDefaultBucketUriForKey(String key) {
}
public static String getDefaultBucketUrlForKey(String key) {
- return String.format("https://s3.amazonaws.com/%s/%s", DEFAULT_BUCKET, key);
+ return String.format("https://%s.s3.amazonaws.com/%s", DEFAULT_BUCKET, key);
}
/**
diff --git a/src/main/java/com/conveyal/datatools/editor/controllers/api/EditorController.java b/src/main/java/com/conveyal/datatools/editor/controllers/api/EditorController.java
index 671466860..5eec768a6 100644
--- a/src/main/java/com/conveyal/datatools/editor/controllers/api/EditorController.java
+++ b/src/main/java/com/conveyal/datatools/editor/controllers/api/EditorController.java
@@ -313,7 +313,7 @@ private String cascadeDeleteStop(Request req, Response res) {
int stopSequence = patternStop.getValue();
// Begin with the stop prior to the one deleted, unless at the beginning.
int beginWithSequence = (stopSequence != 0) ? stopSequence - 1 : stopSequence;
- tableWriter.normalizeStopTimesForPattern(patternStop.getKey(), beginWithSequence, false);
+ tableWriter.normalizeStopTimesForPattern(patternStop.getKey(), beginWithSequence, false, false);
}
}
}
@@ -407,8 +407,9 @@ private String updateStopTimesFromPatternStops (Request req, Response res) {
try {
int beginStopSequence = Integer.parseInt(req.queryParams("stopSequence"));
boolean interpolateStopTimes = Boolean.parseBoolean(req.queryParams("interpolateStopTimes"));
+ boolean ignoreNonBlankStopTimes = Boolean.parseBoolean(req.queryParams("ignoreNonBlankStopTimes"));
JdbcTableWriter tableWriter = new JdbcTableWriter(table, datasource, namespace);
- int stopTimesUpdated = tableWriter.normalizeStopTimesForPattern(patternId, beginStopSequence, interpolateStopTimes);
+ int stopTimesUpdated = tableWriter.normalizeStopTimesForPattern(patternId, beginStopSequence, interpolateStopTimes, ignoreNonBlankStopTimes);
return SparkUtils.formatJSON("updateResult", stopTimesUpdated + " stop times updated.");
} catch (Exception e) {
logMessageAndHalt(req, 400, "Error normalizing stop times", e);
diff --git a/src/main/java/com/conveyal/datatools/manager/DataManager.java b/src/main/java/com/conveyal/datatools/manager/DataManager.java
index 5d4dc0edc..04629444f 100644
--- a/src/main/java/com/conveyal/datatools/manager/DataManager.java
+++ b/src/main/java/com/conveyal/datatools/manager/DataManager.java
@@ -209,25 +209,36 @@ static void registerRoutes() throws IOException {
SnapshotController.register(EDITOR_API_PREFIX);
EditorLockController.register(EDITOR_API_PREFIX);
+ new EditorControllerImpl(EDITOR_API_PREFIX, Table.AREAS, DataManager.GTFS_DATA_SOURCE);
new EditorControllerImpl(EDITOR_API_PREFIX, Table.AGENCY, DataManager.GTFS_DATA_SOURCE);
new EditorControllerImpl(EDITOR_API_PREFIX, Table.ATTRIBUTIONS, DataManager.GTFS_DATA_SOURCE);
new EditorControllerImpl(EDITOR_API_PREFIX, Table.CALENDAR, DataManager.GTFS_DATA_SOURCE);
// NOTE: fare_attributes controller handles updates to nested table fare_rules.
new EditorControllerImpl(EDITOR_API_PREFIX, Table.FARE_ATTRIBUTES, DataManager.GTFS_DATA_SOURCE);
+ new EditorControllerImpl(EDITOR_API_PREFIX, Table.FARE_LEG_JOIN_RULES, DataManager.GTFS_DATA_SOURCE);
+ new EditorControllerImpl(EDITOR_API_PREFIX, Table.FARE_LEG_RULES, DataManager.GTFS_DATA_SOURCE);
+ new EditorControllerImpl(EDITOR_API_PREFIX, Table.FARE_MEDIAS, DataManager.GTFS_DATA_SOURCE);
+ new EditorControllerImpl(EDITOR_API_PREFIX, Table.FARE_PRODUCTS, DataManager.GTFS_DATA_SOURCE);
+ new EditorControllerImpl(EDITOR_API_PREFIX, Table.FARE_TRANSFER_RULES, DataManager.GTFS_DATA_SOURCE);
new EditorControllerImpl(EDITOR_API_PREFIX, Table.FEED_INFO, DataManager.GTFS_DATA_SOURCE);
+
// NOTE: Booking rules, locations and location shapes are GTFS Flex additions.
new EditorControllerImpl(EDITOR_API_PREFIX, Table.BOOKING_RULES, DataManager.GTFS_DATA_SOURCE);
new EditorControllerImpl(EDITOR_API_PREFIX, Table.LOCATIONS, DataManager.GTFS_DATA_SOURCE);
new EditorControllerImpl(EDITOR_API_PREFIX, Table.LOCATION_GROUP, DataManager.GTFS_DATA_SOURCE);
new EditorControllerImpl(EDITOR_API_PREFIX, Table.LOCATION_GROUP_STOPS, DataManager.GTFS_DATA_SOURCE);
new EditorControllerImpl(EDITOR_API_PREFIX, Table.LOCATION_SHAPES, DataManager.GTFS_DATA_SOURCE);
+ new EditorControllerImpl(EDITOR_API_PREFIX, Table.NETWORKS, DataManager.GTFS_DATA_SOURCE);
+ new EditorControllerImpl(EDITOR_API_PREFIX, Table.RIDER_CATEGORIES, DataManager.GTFS_DATA_SOURCE);
new EditorControllerImpl(EDITOR_API_PREFIX, Table.ROUTES, DataManager.GTFS_DATA_SOURCE);
// NOTE: Patterns controller handles updates to nested tables shapes, pattern stops, and frequencies.
new EditorControllerImpl(EDITOR_API_PREFIX, Table.PATTERNS, DataManager.GTFS_DATA_SOURCE);
new EditorControllerImpl(EDITOR_API_PREFIX, Table.SCHEDULE_EXCEPTIONS, DataManager.GTFS_DATA_SOURCE);
new EditorControllerImpl(EDITOR_API_PREFIX, Table.STOPS, DataManager.GTFS_DATA_SOURCE);
+ new EditorControllerImpl(EDITOR_API_PREFIX, Table.TIME_FRAMES, DataManager.GTFS_DATA_SOURCE);
new EditorControllerImpl(EDITOR_API_PREFIX, Table.TRANSLATIONS, DataManager.GTFS_DATA_SOURCE);
new EditorControllerImpl(EDITOR_API_PREFIX, Table.TRIPS, DataManager.GTFS_DATA_SOURCE);
+
// TODO: Add transfers.txt controller?
}
@@ -248,7 +259,7 @@ static void registerRoutes() throws IOException {
String extensionFeedBucket = getExtensionPropertyAsText(extensionType, "s3_bucket");
String extensionBucketFolder = getExtensionPropertyAsText(extensionType, "s3_download_prefix");
int updateFrequency = getConfigProperty("modules.gtfsapi.update_frequency").asInt();
- if (S3Utils.DEFAULT_BUCKET != null && extensionBucketFolder != null) {
+ if (extensionFeedBucket != null && extensionBucketFolder != null) {
FeedUpdater.schedule(updateFrequency, extensionFeedBucket, extensionBucketFolder);
}
else LOG.warn("FeedUpdater not initialized. S3 bucket and folder not provided.");
diff --git a/src/main/java/com/conveyal/datatools/manager/extensions/mtc/MtcFeedResource.java b/src/main/java/com/conveyal/datatools/manager/extensions/mtc/MtcFeedResource.java
index 4e441fc01..5da20eaed 100644
--- a/src/main/java/com/conveyal/datatools/manager/extensions/mtc/MtcFeedResource.java
+++ b/src/main/java/com/conveyal/datatools/manager/extensions/mtc/MtcFeedResource.java
@@ -1,6 +1,7 @@
package com.conveyal.datatools.manager.extensions.mtc;
import com.amazonaws.AmazonServiceException;
+import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.conveyal.datatools.common.utils.CloseableHttpURLConnection;
import com.conveyal.datatools.common.utils.aws.CheckedAWSException;
@@ -31,6 +32,8 @@
import java.util.List;
import java.util.Map;
+import static com.conveyal.datatools.common.utils.aws.S3Utils.APP_DATA_CREDS_FILE;
+import static com.conveyal.datatools.common.utils.aws.S3Utils.APP_DATA_S3_REGION;
import static com.conveyal.datatools.manager.models.ExternalFeedSourceProperty.constructId;
import static com.mongodb.client.model.Filters.eq;
@@ -54,6 +57,9 @@ public class MtcFeedResource implements ExternalFeedResource {
public static final String RESOURCE_TYPE = "MTC";
public static final String STOP_CODE_PRIMARY_PREFIX_FIELD_NAME = "PrimaryPrefix";
public static final String STOP_CODE_SECONDARY_PREFIXES_FIELD_NAME = "SecondaryPrefixes";
+ public static final String CONFIG_MTC_CREDENTIALS = "extensions.mtc.s3_credentials_file";
+ public static final String CONFIG_MTC_REGION = "extensions.mtc.s3_region";
+ private static S3Utils.S3Wrapper s3Wrapper;
private String rtdApi, s3Bucket, s3Prefix;
@@ -212,7 +218,7 @@ public void feedVersionCreated(
LOG.info("Pushing to MTC S3 Bucket: s3://{}/{}", s3Bucket, keyName);
File file = feedVersion.retrieveGtfsFile();
try {
- S3Utils.getDefaultS3Client().putObject(new PutObjectRequest(s3Bucket, keyName, file));
+ getS3Client().putObject(new PutObjectRequest(s3Bucket, keyName, file));
} catch (Exception e) {
LOG.error("Could not upload feed version to s3.");
e.printStackTrace();
@@ -354,4 +360,26 @@ public static List getSecondaryStopCodePrefixes(Map configRegions = List.of(CONFIG_MTC_REGION, APP_DATA_S3_REGION);
+ List configCredentials = List.of(CONFIG_MTC_CREDENTIALS, APP_DATA_CREDS_FILE);
+ return S3Utils.buildS3Wrapper(configCredentials, configRegions);
+ }
+
+ /**
+ * Get the S3 client from a cached wrapper to access the MTC feed processing bucket.
+ * Note: In S3Utils, an AWSClientManager is used, however in practice, only the default client is invoked
+ * (i.e. with role=null, region=null), so the logical path returns the original s3Client, which we are doing here.
+ */
+ public static AmazonS3 getS3Client() {
+ if (s3Wrapper == null) {
+ s3Wrapper = getS3Wrapper();
+ }
+ return s3Wrapper.s3Client;
+ }
}
diff --git a/src/main/java/com/conveyal/datatools/manager/jobs/FeedUpdater.java b/src/main/java/com/conveyal/datatools/manager/jobs/FeedUpdater.java
index 3800df702..b49eff6fa 100644
--- a/src/main/java/com/conveyal/datatools/manager/jobs/FeedUpdater.java
+++ b/src/main/java/com/conveyal/datatools/manager/jobs/FeedUpdater.java
@@ -5,7 +5,7 @@
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.S3ObjectSummary;
import com.conveyal.datatools.common.utils.aws.CheckedAWSException;
-import com.conveyal.datatools.common.utils.aws.S3Utils;
+import com.conveyal.datatools.manager.extensions.mtc.MtcFeedResource;
import com.conveyal.datatools.manager.models.ExternalFeedSourceProperty;
import com.conveyal.datatools.manager.models.FeedSource;
import com.conveyal.datatools.manager.models.FeedVersion;
@@ -406,7 +406,7 @@ private FeedVersion findMatchingFeedVersion(
) throws AmazonServiceException, IOException, CheckedAWSException {
String filename = keyName.split("/")[1];
String feedId = filename.replace(".zip", "");
- S3Object object = S3Utils.getDefaultS3Client().getObject(feedBucket, keyName);
+ S3Object object = MtcFeedResource.getS3Client().getObject(feedBucket, keyName);
File file = new File(FeedStore.basePath, filename);
try (
InputStream in = object.getObjectContent();
@@ -447,17 +447,14 @@ public interface CompletedFeedRetriever {
/**
* Implements the default behavior for above interface.
+ * This method uses extensions.mtc.s3_credentials_file and s3_region, if populated,
+ * and falls back to the application's S3 credentials and region otherwise.
*/
public class DefaultCompletedFeedRetriever implements CompletedFeedRetriever {
@Override
public List retrieveCompletedFeeds() {
- try {
- ObjectListing gtfsList = S3Utils.getDefaultS3Client().listObjects(feedBucket, bucketFolder);
- return gtfsList.getObjectSummaries();
- } catch (CheckedAWSException e) {
- LOG.error("Failed to list S3 Objects", e);
- return null;
- }
+ ObjectListing gtfsList = MtcFeedResource.getS3Client().listObjects(feedBucket, bucketFolder);
+ return gtfsList.getObjectSummaries();
}
}
}
diff --git a/src/main/java/com/conveyal/datatools/manager/jobs/PublishProjectFeedsJob.java b/src/main/java/com/conveyal/datatools/manager/jobs/PublishProjectFeedsJob.java
index 214790baa..9cf75c8fb 100644
--- a/src/main/java/com/conveyal/datatools/manager/jobs/PublishProjectFeedsJob.java
+++ b/src/main/java/com/conveyal/datatools/manager/jobs/PublishProjectFeedsJob.java
@@ -1,14 +1,13 @@
package com.conveyal.datatools.manager.jobs;
-import com.amazonaws.services.s3.AmazonS3;
-import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.conveyal.datatools.common.status.MonitorableJob;
import com.conveyal.datatools.common.utils.aws.S3Utils;
import com.conveyal.datatools.manager.auth.Auth0UserProfile;
-import com.conveyal.datatools.manager.models.FeedVersion;
+import com.conveyal.datatools.manager.models.FeedSource;
import com.conveyal.datatools.manager.models.Project;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.io.FileUtils;
+import org.apache.commons.math3.util.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -48,53 +47,59 @@ public void jobLogic () {
r.append("\n");
r.append("\n");
r.append("\n");
- r.append("" + title + "\n");
- r.append("");
+ r.append("").append(title).append("\n");
+ r.append("");
r.append("\n");
r.append("\n");
- r.append("" + title + "
\n");
+ r.append("").append(title).append("
\n");
r.append("The following feeds, in GTFS format, are available for download and use.\n");
if (dataSupportEmail != null) {
r.append(String.format("If you have inquiries, please contact us at: %1$s", dataSupportEmail));
}
r.append("\n");
status.update("Ensuring public GTFS files are up-to-date.", 50);
+
+ SimpleDateFormat dateFormat = new SimpleDateFormat("dd MMM yyyy");
project.retrieveProjectFeedSources().stream()
- .filter(fs -> fs.isPublic && fs.retrieveLatest() != null)
- .forEach(fs -> {
- // generate list item for feed source
- String url;
- if (fs.url != null && !hasConfigProperty("modules.enterprise.prefer_s3_links")) {
- url = fs.url.toString();
- }
- else {
- // ensure latest feed is written to the s3 public folder
- try {
- fs.makePublic();
- } catch (Exception e) {
- status.fail("Failed to make GTFS files public on S3", e);
- return;
- }
- url = S3Utils.getDefaultBucketUrlForKey(fs.toPublicKey());
- }
- FeedVersion latest = fs.retrieveLatest();
- r.append("- ");
- r.append("");
- r.append(fs.name);
- r.append("");
- r.append(" (");
- if (fs.url != null && fs.lastFetched != null) {
- r.append("last checked: " + new SimpleDateFormat("dd MMM yyyy").format(fs.lastFetched) + ", ");
- }
- if (fs.lastUpdated() != null) {
- r.append("last updated: " + new SimpleDateFormat("dd MMM yyyy").format(fs.lastUpdated()) + ")");
+ .filter(fs -> fs.isPublic)
+ // Store latest version id, so we don't have to fetch it again in the forEach stage.
+ .map(fs -> new Pair(fs, fs.latestVersionId()) {})
+ .filter(p -> p.getValue() != null)
+ .forEach(p -> {
+ // generate list item for feed source
+ FeedSource fs = p.getKey();
+ String latestVersionId = p.getValue();
+ String url;
+ if (fs.url != null && !hasConfigProperty("modules.enterprise.prefer_s3_links")) {
+ url = fs.url.toString();
+ }
+ else {
+ // ensure latest feed is written to the s3 public folder
+ try {
+ fs.makePublic(latestVersionId);
+ } catch (Exception e) {
+ status.fail("Failed to make GTFS files public on S3", e);
+ return;
}
- r.append("
");
- });
+ url = S3Utils.getDefaultBucketUrlForKey(fs.toPublicKey());
+ }
+ r.append("- ");
+ r.append("");
+ r.append(fs.name);
+ r.append("");
+ r.append(" (");
+ if (fs.url != null && fs.lastFetched != null) {
+ r.append("last checked: ").append(dateFormat.format(fs.lastFetched)).append(", ");
+ }
+ if (fs.lastUpdated() != null) {
+ r.append("last updated: ").append(dateFormat.format(fs.lastUpdated())).append(")");
+ }
+ r.append("
");
+ });
r.append("
");
r.append("");
r.append("");
@@ -111,12 +116,9 @@ public void jobLogic () {
e.printStackTrace();
}
try {
- AmazonS3 defaultS3Client = S3Utils.getDefaultS3Client();
- defaultS3Client.putObject(S3Utils.DEFAULT_BUCKET, folder + fileName, file);
- defaultS3Client.setObjectAcl(S3Utils.DEFAULT_BUCKET, folder + fileName, CannedAccessControlList.PublicRead);
+ S3Utils.uploadObject(folder + fileName, file);
} catch (Exception e) {
status.fail("Failed to perform S3 actions", e);
- return;
}
}
diff --git a/src/main/java/com/conveyal/datatools/manager/models/FeedSource.java b/src/main/java/com/conveyal/datatools/manager/models/FeedSource.java
index fe1cc7948..c90baa53e 100644
--- a/src/main/java/com/conveyal/datatools/manager/models/FeedSource.java
+++ b/src/main/java/com/conveyal/datatools/manager/models/FeedSource.java
@@ -597,13 +597,22 @@ public String toPublicKey() {
* Makes the feed source's latest version have public access on AWS S3.
*/
public void makePublic() throws CheckedAWSException {
+ makePublic(this.latestVersionId());
+ }
+
+ /**
+ * Makes the feed source's specified version have public access on AWS S3.
+ */
+ public void makePublic(String versionId) throws CheckedAWSException {
String sourceKey = S3Utils.DEFAULT_BUCKET_GTFS_FOLDER + this.id + ".zip";
String publicKey = toPublicKey();
- String versionId = this.latestVersionId();
String latestVersionKey = S3Utils.DEFAULT_BUCKET_GTFS_FOLDER + versionId;
- // only deploy to public if storing feeds on s3 (no mechanism for downloading/publishing
- // them otherwise)
+ // Copy, forever, the latest stored version to the public folder, regardless of
+ // whether S3 is used to store individual feed versions (useS3 = true) or not.
+ //
+ // Note: If modules.enterprise.prefer_s3_links is not set and the feed source has a fetch URL,
+ // this method will not be called, and the feed download link will point to the fetch URL instead.
if (DataManager.useS3) {
AmazonS3 defaultS3Client = S3Utils.getDefaultS3Client();
boolean sourceExists = defaultS3Client.doesObjectExist(S3Utils.DEFAULT_BUCKET, sourceKey);
@@ -634,6 +643,9 @@ public void makePublic() throws CheckedAWSException {
defaultS3Client.copyObject(S3Utils.DEFAULT_BUCKET, latestVersionKey, S3Utils.DEFAULT_BUCKET, sourceKey);
}
}
+ } else {
+ LOG.info("copying latest local feed {} to s3 public folder", this);
+ S3Utils.uploadObject(publicKey, retrieveLatest().retrieveGtfsFile());
}
}
diff --git a/src/main/java/com/conveyal/datatools/manager/models/FeedSourceSummary.java b/src/main/java/com/conveyal/datatools/manager/models/FeedSourceSummary.java
index ab5883f2e..90d9943fb 100644
--- a/src/main/java/com/conveyal/datatools/manager/models/FeedSourceSummary.java
+++ b/src/main/java/com/conveyal/datatools/manager/models/FeedSourceSummary.java
@@ -87,6 +87,8 @@ public class FeedSourceSummary {
public FeedValidationResultSummary publishedValidationSummary;
+ public String publishedVersionId;
+
public PublishState publishState;
@JsonInclude(JsonInclude.Include.NON_NULL)
@@ -113,6 +115,7 @@ public FeedSourceSummary(String projectId, String organizationId, Document feedS
// Convert to local date type for consistency.
lastUpdated = getLocalDateFromDate(feedSourceDocument.getDate("lastUpdated"));
url = feedSourceDocument.getString("url");
+ publishedVersionId = feedSourceDocument.getString("publishedVersionId");
// Get optional filename.
filename = feedSourceDocument.getString("filename");
// Optional external properties, if enabled by config.
@@ -180,7 +183,8 @@ public static List getFeedSourceSummaries(String projectId, S
"labelIds",
"url",
"filename",
- "noteIds"
+ "noteIds",
+ "publishedVersionId"
)
),
sort(Sorts.ascending("name"))
@@ -219,7 +223,8 @@ public static Map getLatestFeedVersionForFeedSources
);
// Define the variable passed into the lookup pipeline.
- List> feedSourceId = List.of(new Variable<>("feedSourceId", "$_id"));
+ Variable feedSourceIdVariable = new Variable<>("feedSourceId", "$_id");
+ List> feedSourceId = List.of(feedSourceIdVariable);
// $lookup that uses the above pipeline to produce "latestFeedVersion" (an array with at most one element).
Bson lookupLatestFeedVersion = lookup(
@@ -231,7 +236,14 @@ public static Map getLatestFeedVersionForFeedSources
// Pipeline to find the published FeedVersion by namespace (or identifier stored in publishedVersionId)
List publishedFeedVersionPipeline = Arrays.asList(
- // Match FeedVersion documents where namespace equals the outer document's publishedVersionId.
+ // Match FeedVersion documents where namespace equals the outer document's
+ // feedSourceId (important when dealing with many FeedVersions)
+ // and publishedVersionId.
+ match(
+ expr(
+ new Document("$eq", Arrays.asList("$feedSourceId", "$$feedSourceId"))
+ )
+ ),
match(
expr(
new Document("$eq", Arrays.asList("$namespace", "$$publishedVersionId"))
@@ -242,13 +254,16 @@ public static Map getLatestFeedVersionForFeedSources
project(include("validationResult"))
);
- // Pass publishedVersionId from the local document into the lookup pipeline.
- List> publishedVersionId = List.of(new Variable<>("publishedVersionId", "$publishedVersionId"));
+ // Pass feedSourceId and publishedVersionId from the local document into the lookup pipeline.
+ List> feedSourceIdAndPublishedVersionId = List.of(
+ feedSourceIdVariable,
+ new Variable<>("publishedVersionId", "$publishedVersionId")
+ );
// $lookup that uses the above pipeline to produce "publishedFeedVersion" (an array with at most one element).
Bson lookupPublishedFeedVersion = lookup(
"FeedVersion",
- publishedVersionId,
+ feedSourceIdAndPublishedVersionId,
publishedFeedVersionPipeline,
"publishedFeedVersion"
);
diff --git a/src/main/java/com/conveyal/datatools/manager/models/transform/RemoveNonRevenueTripsTransformation.java b/src/main/java/com/conveyal/datatools/manager/models/transform/RemoveNonRevenueTripsTransformation.java
index 534ff54bd..f68feaf67 100644
--- a/src/main/java/com/conveyal/datatools/manager/models/transform/RemoveNonRevenueTripsTransformation.java
+++ b/src/main/java/com/conveyal/datatools/manager/models/transform/RemoveNonRevenueTripsTransformation.java
@@ -101,21 +101,27 @@ public void transform(FeedTransformZipTarget zipTarget, MonitorableJob.Status st
LOG.warn("No revenue trips! All trips and related stop times will be removed.");
}
- writeToFile(
- zipTarget,
- "stop_times.txt",
- revenueStopTimes.getStopTimeRows().values().stream().flatMap(List::stream).collect(Collectors.toList()),
- headersForStopTime,
- revenueStopTimes.getDeletedRowCount()
- );
+ // Only update tables and report change if rows are removed.
- writeToFile(
- zipTarget,
- "trips.txt",
- revenueTrips.tripRows,
- headersForTrips,
- revenueTrips.getDeletedRowCount()
- );
+ if (revenueStopTimes.getDeletedRowCount() != 0) {
+ writeToFile(
+ zipTarget,
+ "stop_times.txt",
+ revenueStopTimes.getStopTimeRows().values().stream().flatMap(List::stream).collect(Collectors.toList()),
+ headersForStopTime,
+ revenueStopTimes.getDeletedRowCount()
+ );
+ }
+
+ if (revenueTrips.getDeletedRowCount() != 0) {
+ writeToFile(
+ zipTarget,
+ "trips.txt",
+ revenueTrips.tripRows,
+ headersForTrips,
+ revenueTrips.getDeletedRowCount()
+ );
+ }
} catch (Exception e) {
LOG.error("Unknown error encountered while attempting to remove non revenue trip from zip file.", e);
}
@@ -237,7 +243,7 @@ private boolean isRevenueStopTime(
private boolean isRevenuePickupDropOff(CsvReader csvReader, Map fieldIndexes) throws IOException {
String pickup = csvReader.get(fieldIndexes.get(PICKUP_FIELD_NAME));
String dropOff = csvReader.get(fieldIndexes.get(DROP_OFF_FIELD_NAME));
- return !"1".equals(pickup) && !"1".equals(dropOff);
+ return !"1".equals(pickup) || !"1".equals(dropOff);
}
/**
diff --git a/src/main/resources/mongo/getFeedSourceSummaries.js b/src/main/resources/mongo/getFeedSourceSummaries.js
index 3e4db025d..45c6dffe5 100644
--- a/src/main/resources/mongo/getFeedSourceSummaries.js
+++ b/src/main/resources/mongo/getFeedSourceSummaries.js
@@ -15,7 +15,8 @@ db.getCollection('FeedSource').aggregate([
"labelIds": 1,
"url": 1,
"filename": 1,
- "noteIds": 1
+ "noteIds": 1,
+ "publishedVersionId": 1
}
},
{
diff --git a/src/main/resources/mongo/getLatestFeedVersionForFeedSources.js b/src/main/resources/mongo/getLatestFeedVersionForFeedSources.js
index 21a563012..de63cc179 100644
--- a/src/main/resources/mongo/getLatestFeedVersionForFeedSources.js
+++ b/src/main/resources/mongo/getLatestFeedVersionForFeedSources.js
@@ -25,8 +25,10 @@ db.FeedSource.aggregate([
{
$lookup: {
from: "FeedVersion",
- let: { publishedVersionId: "$publishedVersionId" },
+ let: { feedSourceId: "$_id", publishedVersionId: "$publishedVersionId" },
pipeline: [
+ // Filtering by feedSourceId helps by 100x when dealing with large number of feed versions.
+ { $match: { $expr: { $eq: ["$feedSourceId", "$$feedSourceId"] } } },
{ $match: { $expr: { $eq: ["$namespace", "$$publishedVersionId"] } } },
{ $limit: 1 },
{ $project: { validationResult: 1 } }
diff --git a/src/test/java/com/conveyal/datatools/common/utils/aws/S3UtilsTest.java b/src/test/java/com/conveyal/datatools/common/utils/aws/S3UtilsTest.java
new file mode 100644
index 000000000..2064ee237
--- /dev/null
+++ b/src/test/java/com/conveyal/datatools/common/utils/aws/S3UtilsTest.java
@@ -0,0 +1,31 @@
+package com.conveyal.datatools.common.utils.aws;
+
+import com.conveyal.datatools.DatatoolsTest;
+import com.conveyal.datatools.UnitTest;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class S3UtilsTest extends UnitTest {
+ /**
+ * Create feed version for GTFS+ validation test.
+ */
+ @BeforeAll
+ static void setUp() throws IOException {
+ // Start server if it isn't already running.
+ DatatoolsTest.setUp();
+ }
+
+ @Test
+ void canGetDefaultBucketUrlForKey() {
+ final String FILENAME = "public/index.html";
+
+ assertEquals(
+ "https://bucket-name.s3.amazonaws.com/public/index.html",
+ S3Utils.getDefaultBucketUrlForKey(FILENAME)
+ );
+ }
+}
diff --git a/src/test/java/com/conveyal/datatools/editor/controllers/api/EditorControllerTest.java b/src/test/java/com/conveyal/datatools/editor/controllers/api/EditorControllerTest.java
index 45a658b3f..aa0761257 100644
--- a/src/test/java/com/conveyal/datatools/editor/controllers/api/EditorControllerTest.java
+++ b/src/test/java/com/conveyal/datatools/editor/controllers/api/EditorControllerTest.java
@@ -14,6 +14,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import io.restassured.http.Method;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -27,10 +28,13 @@
import java.io.IOException;
import java.sql.SQLException;
import java.util.Date;
+import java.util.Objects;
import java.util.stream.Stream;
import static com.conveyal.datatools.TestUtils.assertThatSqlCountQueryYieldsExpectedCount;
+import static com.conveyal.datatools.TestUtils.createFeedVersion;
import static com.conveyal.datatools.TestUtils.createFeedVersionFromGtfsZip;
+import static com.conveyal.datatools.TestUtils.zipFolderFiles;
import static com.conveyal.datatools.manager.auth.Auth0Users.USERS_API_PATH;
import static com.conveyal.datatools.manager.controllers.api.UserController.TEST_AUTH0_DOMAIN;
import static io.restassured.RestAssured.given;
@@ -45,8 +49,10 @@ public class EditorControllerTest extends UnitTest {
private static Project project;
private static FeedSource feedSource;
private static FeedSource feedSourceCascadeDelete;
+ private static FeedSource faresV2FeedSource;
private static FeedVersion feedVersion;
private static FeedVersion feedVersionCascadeDelete;
+ private static FeedVersion faresV2Version;
private static final ObjectMapper mapper = new ObjectMapper();
/**
@@ -72,12 +78,18 @@ public static void setUp() throws Exception {
feedSourceCascadeDelete.projectId = project.id;
Persistence.feedSources.create(feedSourceCascadeDelete);
+ faresV2FeedSource = new FeedSource("FaresV2");
+ faresV2FeedSource.projectId = project.id;
+ Persistence.feedSources.create(faresV2FeedSource);
+
feedVersion = createFeedVersionFromGtfsZip(feedSource, "bart_old.zip");
feedVersionCascadeDelete = createFeedVersionFromGtfsZip(feedSourceCascadeDelete, "bart_old.zip");
+ faresV2Version = createFeedVersion(faresV2FeedSource, zipFolderFiles("fake-agency-with-fares-v2"));
// Create and run snapshot jobs
- crateAndRunSnapshotJob(feedVersion.name, feedSource.id, feedVersion.namespace);
- crateAndRunSnapshotJob(feedVersionCascadeDelete.name, feedSourceCascadeDelete.id, feedVersionCascadeDelete.namespace);
+ createAndRunSnapshotJob(feedVersion.name, feedSource.id, feedVersion.namespace);
+ createAndRunSnapshotJob(feedVersionCascadeDelete.name, feedSourceCascadeDelete.id, feedVersionCascadeDelete.namespace);
+ createAndRunSnapshotJob(faresV2Version.name, faresV2FeedSource.id, faresV2Version.namespace);
LOG.info("{} setup completed in {} ms", EditorControllerTest.class.getSimpleName(), System.currentTimeMillis() - startTime);
}
@@ -86,15 +98,22 @@ public static void tearDown() {
project.delete();
feedSource.delete();
feedSourceCascadeDelete.delete();
+ faresV2FeedSource.delete();
}
/**
* Create and run a snapshot job in the current thread (so tests do not run until this is complete).
*/
- private static void crateAndRunSnapshotJob(String feedVersionName, String feedSourceId, String namespace) {
+ private static void createAndRunSnapshotJob(String feedVersionName, String feedSourceId, String namespace) {
Snapshot snapshot = new Snapshot("Snapshot of " + feedVersionName, feedSourceId, namespace);
CreateSnapshotJob createSnapshotJob =
- new CreateSnapshotJob(Auth0UserProfile.createTestAdminUser(), snapshot, true, false, false);
+ new CreateSnapshotJob(
+ Auth0UserProfile.createTestAdminUser(),
+ snapshot,
+ true,
+ false,
+ false
+ );
createSnapshotJob.run();
}
@@ -111,14 +130,7 @@ private static Stream createPatchTableTests() {
*/
@ParameterizedTest
@MethodSource("createPatchTableTests")
- public void canPatchTableTests(
- String field,
- String entity,
- int expectedCount,
- String graphQLQueryFile,
- String table
- ) throws IOException {
-
+ void canPatchTableTests(String field, String entity, int expectedCount, String graphQLQueryFile, String table) throws IOException {
LOG.info("Making patch {} request", table);
String value = "NEW";
ObjectNode jsonBody = mapper.createObjectNode();
@@ -138,7 +150,7 @@ public void canPatchTableTests(
* Make sure the patch table endpoint can patch stops conditionally with query.
*/
@Test
- public void canPatchStopsConditionally() throws IOException {
+ void canPatchStopsConditionally() throws IOException {
LOG.info("Making conditional patch stops request");
ObjectNode jsonBody = mapper.createObjectNode();
String field = "stop_desc";
@@ -187,13 +199,7 @@ void canCascadeDeleteStop() throws IOException, SQLException {
stopId,
feedVersionCascadeDelete.feedSourceId
);
- String response = given()
- .port(DataManager.PORT)
- .delete(path)
- .then()
- .extract()
- .response()
- .asString();
+ String response = makeRequest(Method.DELETE, path, "");
JsonNode json = mapper.readTree(response);
assertEquals(OK_200, json.get("code").asInt());
@@ -204,20 +210,64 @@ void canCascadeDeleteStop() throws IOException, SQLException {
}
/**
- * Perform patch table request on the feed source ID with the requested query and patch JSON. A null query will
- * apply the patch JSON to the entire table.
+ * Confirm that a fare product can be created, updated and deleted.
*/
- private static int patchTableRequest(String entity, String feedId, String query, JsonNode oatchJSON) throws IOException {
- String path = String.format("/api/editor/secure/%s?feedId=%s", entity, feedId);
- if (query != null) path += "&" + query;
- String response = given()
+ @Test
+ void canCreateUpdateAndDeleteFareProduct() throws IOException {
+ String fareProductId = "AERIAL_TRAM_ROUND_TRIP";
+ String fareProductIdUpdated = "AERIAL_TRAM_ROUND_TRIP_UPDATED";
+ String urlPrefix = "/api/editor/secure/fareproduct";
+ String urlSuffix = String.format("?feedId=%s&sessionId=test", faresV2Version.feedSourceId);
+ String payload =
+ "{" +
+ "\"fare_product_id\":\"" + fareProductId + "\"," +
+ "\"fare_product_name\":\"Portland Aerial Tram Single Round Trip\"," +
+ "\"rider_category_id\":\"HONORED_CITIZEN\"," +
+ "\"fare_media_id\":\"1\"," +
+ "\"amount\":\"13.5\"," +
+ "\"currency\":\"USD\"" +
+ "}";
+
+ // Create.
+ String response = makeRequest(Method.POST, String.format("%s%s", urlPrefix, urlSuffix), payload);
+ JsonNode json = mapper.readTree(response);
+ String id = json.get("id").asText();
+ assertEquals(fareProductId, json.get("fare_product_id").asText());
+
+ // Update.
+ payload = payload.replace(fareProductId, fareProductIdUpdated);
+ response = makeRequest(Method.PUT, String.format("%s/%s%s", urlPrefix, id, urlSuffix), payload);
+ json = mapper.readTree(response);
+ assertEquals(fareProductIdUpdated, json.get("fare_product_id").asText());
+
+ // Delete.
+ response = makeRequest(Method.DELETE, String.format("%s/%s%s", urlPrefix, id, urlSuffix), "");
+ json = mapper.readTree(response);
+ assertEquals(200, json.get("code").asInt());
+ }
+
+ /**
+ * Make request and return the response.
+ */
+ private static String makeRequest(Method method, String path, Object payload) {
+ return given()
.port(DataManager.PORT)
- .body(oatchJSON)
- .patch(path)
+ .body(payload)
+ .request(method, path)
.then()
.extract()
.response()
.asString();
+ }
+
+ /**
+ * Perform patch table request on the feed source ID with the requested query and patch JSON. A null query will
+ * apply the patch JSON to the entire table.
+ */
+ private static int patchTableRequest(String entity, String feedId, String query, JsonNode patchJSON) throws IOException {
+ String path = String.format("/api/editor/secure/%s?feedId=%s", entity, feedId);
+ if (query != null) path += "&" + query;
+ String response = makeRequest(Method.PATCH, path, patchJSON);
JsonNode json = mapper.readTree(response);
return json.get("count").asInt();
}
@@ -230,16 +280,9 @@ private static JsonNode graphqlQuery (String namespace, String graphQLQueryFile)
ObjectNode variables = mapper.createObjectNode();
variables.put("namespace", namespace);
graphQLBody.set("variables", variables);
- String query = IOUtils.toString(EditorControllerTest.class.getClassLoader().getResourceAsStream(graphQLQueryFile));
+ String query = IOUtils.toString(Objects.requireNonNull(EditorControllerTest.class.getClassLoader().getResourceAsStream(graphQLQueryFile)));
graphQLBody.put("query", query);
- String graphQLString = given()
- .port(DataManager.PORT)
- .body(graphQLBody)
- .post("api/manager/secure/gtfs/graphql")
- .then()
- .extract()
- .response()
- .asString();
+ String graphQLString = makeRequest(Method.POST, "api/manager/secure/gtfs/graphql", graphQLBody);
return mapper.readTree(graphQLString);
}
diff --git a/src/test/java/com/conveyal/datatools/manager/controllers/api/FeedSourceControllerTest.java b/src/test/java/com/conveyal/datatools/manager/controllers/api/FeedSourceControllerTest.java
index b5a9585e3..f8ad6b0ae 100644
--- a/src/test/java/com/conveyal/datatools/manager/controllers/api/FeedSourceControllerTest.java
+++ b/src/test/java/com/conveyal/datatools/manager/controllers/api/FeedSourceControllerTest.java
@@ -11,6 +11,7 @@
import com.conveyal.datatools.manager.models.FeedRetrievalMethod;
import com.conveyal.datatools.manager.models.FeedSource;
import com.conveyal.datatools.manager.models.FeedSourceSummary;
+import com.conveyal.datatools.manager.models.FeedValidationResultSummary;
import com.conveyal.datatools.manager.models.FeedVersion;
import com.conveyal.datatools.manager.models.FeedVersionSummary;
import com.conveyal.datatools.manager.models.FetchFrequency;
@@ -148,8 +149,7 @@ private static void setUpFeedVersionFromLatestDeployment() throws MalformedURLEx
feedVersionPublishedFromLatestDeployment = createFeedVersion(
"published-feed-version-from-latest-deployment",
- // Set to null so the relationship to feed source is via the published version id.
- null,
+ feedSourceWithLatestDeploymentFeedVersion.id,
LocalDate.of(2022, Month.NOVEMBER, 2),
LocalDate.of(2022, Month.NOVEMBER, 3),
feedSourceWithLatestDeploymentFeedVersion.publishedVersionId,
@@ -375,25 +375,28 @@ void canRetrieveDeployedFeedVersionFromLatestDeployment() throws IOException {
);
assertNotNull(feedSourceSummaries);
- assertEquals(feedSourceWithLatestDeploymentFeedVersion.id, feedSourceSummaries.get(0).id);
- assertEquals(feedSourceWithLatestDeploymentFeedVersion.projectId, feedSourceSummaries.get(0).projectId);
- assertEquals(feedSourceWithLatestDeploymentFeedVersion.labelIds, feedSourceSummaries.get(0).labelIds);
- assertEquals(feedSourceWithLatestDeploymentFeedVersion.url.toString(), feedSourceSummaries.get(0).url);
- assertEquals(feedSourceWithLatestDeploymentFeedVersion.noteIds, feedSourceSummaries.get(0).noteIds);
- assertEquals(feedSourceWithLatestDeploymentFeedVersion.organizationId(), feedSourceSummaries.get(0).organizationId);
- assertEquals(feedVersionFromLatestDeploymentVersion2.id, feedSourceSummaries.get(0).deployedFeedVersionId);
- assertEquals(feedVersionFromLatestDeploymentVersion2.validationSummary().startDate, feedSourceSummaries.get(0).deployedFeedVersionStartDate);
- assertEquals(feedVersionFromLatestDeploymentVersion2.validationSummary().endDate, feedSourceSummaries.get(0).deployedFeedVersionEndDate);
- assertEquals(feedVersionFromLatestDeploymentVersion2.validationSummary().errorCount, feedSourceSummaries.get(0).deployedFeedVersionIssues);
- assertEquals(feedVersionFromLatestDeploymentVersion2.id, feedSourceSummaries.get(0).latestValidation.feedVersionId);
- assertEquals(feedVersionFromLatestDeploymentVersion2.validationSummary().startDate, feedSourceSummaries.get(0).latestValidation.startDate);
- assertEquals(feedVersionFromLatestDeploymentVersion2.validationSummary().endDate, feedSourceSummaries.get(0).latestValidation.endDate);
- assertEquals(feedVersionFromLatestDeploymentVersion2.validationSummary().errorCount, feedSourceSummaries.get(0).latestValidation.errorCount);
- assertEquals(feedVersionFromLatestDeploymentVersion2.sentToExternalPublisher, feedSourceSummaries.get(0).latestSentToExternalPublisher);
- assertEquals(PublishState.PUBLISH_BLOCKED, feedSourceSummaries.get(0).publishState);
- assertEquals(feedVersionPublishedFromLatestDeployment.validationResult.errorCount, feedSourceSummaries.get(0).publishedValidationSummary.errorCount);
- assertEquals(feedVersionPublishedFromLatestDeployment.validationResult.firstCalendarDate, feedSourceSummaries.get(0).publishedValidationSummary.startDate);
- assertEquals(feedVersionPublishedFromLatestDeployment.validationResult.lastCalendarDate, feedSourceSummaries.get(0).publishedValidationSummary.endDate);
+ FeedSourceSummary firstSummary = feedSourceSummaries.get(0);
+ FeedValidationResultSummary expectedValidationSummary = feedVersionFromLatestDeploymentVersion2.validationSummary();
+ assertEquals(feedSourceWithLatestDeploymentFeedVersion.id, firstSummary.id);
+ assertEquals(feedSourceWithLatestDeploymentFeedVersion.projectId, firstSummary.projectId);
+ assertEquals(feedSourceWithLatestDeploymentFeedVersion.labelIds, firstSummary.labelIds);
+ assertEquals(feedSourceWithLatestDeploymentFeedVersion.url.toString(), firstSummary.url);
+ assertEquals(feedSourceWithLatestDeploymentFeedVersion.noteIds, firstSummary.noteIds);
+ assertEquals(feedSourceWithLatestDeploymentFeedVersion.organizationId(), firstSummary.organizationId);
+ assertEquals(feedVersionFromLatestDeploymentVersion2.id, firstSummary.deployedFeedVersionId);
+ assertEquals(expectedValidationSummary.startDate, firstSummary.deployedFeedVersionStartDate);
+ assertEquals(expectedValidationSummary.endDate, firstSummary.deployedFeedVersionEndDate);
+ assertEquals(expectedValidationSummary.errorCount, firstSummary.deployedFeedVersionIssues);
+ assertEquals(feedVersionFromLatestDeploymentVersion2.id, firstSummary.latestValidation.feedVersionId);
+ assertEquals(expectedValidationSummary.startDate, firstSummary.latestValidation.startDate);
+ assertEquals(expectedValidationSummary.endDate, firstSummary.latestValidation.endDate);
+ assertEquals(expectedValidationSummary.errorCount, firstSummary.latestValidation.errorCount);
+ assertEquals(feedVersionFromLatestDeploymentVersion2.sentToExternalPublisher, firstSummary.latestSentToExternalPublisher);
+ assertEquals(PublishState.PUBLISH_BLOCKED, firstSummary.publishState);
+ assertEquals(feedSourceWithLatestDeploymentFeedVersion.publishedVersionId, firstSummary.publishedVersionId);
+ assertEquals(feedVersionPublishedFromLatestDeployment.validationResult.errorCount, firstSummary.publishedValidationSummary.errorCount);
+ assertEquals(feedVersionPublishedFromLatestDeployment.validationResult.firstCalendarDate, firstSummary.publishedValidationSummary.startDate);
+ assertEquals(feedVersionPublishedFromLatestDeployment.validationResult.lastCalendarDate, firstSummary.publishedValidationSummary.endDate);
}
@Test
@@ -414,22 +417,26 @@ void canRetrieveDeployedFeedVersionFromPinnedDeployment() throws IOException {
FeedSourceSummary.class
);
assertNotNull(feedSourceSummaries);
- assertEquals(feedSourceWithPinnedDeploymentFeedVersion.id, feedSourceSummaries.get(0).id);
- assertEquals(feedSourceWithPinnedDeploymentFeedVersion.projectId, feedSourceSummaries.get(0).projectId);
- assertEquals(feedSourceWithPinnedDeploymentFeedVersion.labelIds, feedSourceSummaries.get(0).labelIds);
- assertEquals(feedSourceWithPinnedDeploymentFeedVersion.url.toString(), feedSourceSummaries.get(0).url);
- assertEquals(feedSourceWithPinnedDeploymentFeedVersion.noteIds, feedSourceSummaries.get(0).noteIds);
- assertEquals(feedSourceWithPinnedDeploymentFeedVersion.organizationId(), feedSourceSummaries.get(0).organizationId);
- assertEquals(feedVersionFromPinnedDeployment.id, feedSourceSummaries.get(0).deployedFeedVersionId);
- assertEquals(feedVersionFromPinnedDeployment.validationSummary().startDate, feedSourceSummaries.get(0).deployedFeedVersionStartDate);
- assertEquals(feedVersionFromPinnedDeployment.validationSummary().endDate, feedSourceSummaries.get(0).deployedFeedVersionEndDate);
- assertEquals(feedVersionFromPinnedDeployment.validationSummary().errorCount, feedSourceSummaries.get(0).deployedFeedVersionIssues);
- assertEquals(feedVersionFromPinnedDeployment.id, feedSourceSummaries.get(0).latestValidation.feedVersionId);
- assertEquals(feedVersionFromPinnedDeployment.validationSummary().startDate, feedSourceSummaries.get(0).latestValidation.startDate);
- assertEquals(feedVersionFromPinnedDeployment.validationSummary().endDate, feedSourceSummaries.get(0).latestValidation.endDate);
- assertEquals(feedVersionFromPinnedDeployment.validationSummary().errorCount, feedSourceSummaries.get(0).latestValidation.errorCount);
- assertEquals(feedVersionFromPinnedDeployment.sentToExternalPublisher, feedSourceSummaries.get(0).latestSentToExternalPublisher);
- assertEquals(PublishState.PUBLISH_BLOCKED, feedSourceSummaries.get(0).publishState);
+ FeedSourceSummary firstSummary = feedSourceSummaries.get(0);
+ FeedValidationResultSummary expectedValidationSummary = feedVersionFromPinnedDeployment.validationSummary();
+
+ assertEquals(feedSourceWithPinnedDeploymentFeedVersion.id, firstSummary.id);
+ assertEquals(feedSourceWithPinnedDeploymentFeedVersion.projectId, firstSummary.projectId);
+ assertEquals(feedSourceWithPinnedDeploymentFeedVersion.labelIds, firstSummary.labelIds);
+ assertEquals(feedSourceWithPinnedDeploymentFeedVersion.url.toString(), firstSummary.url);
+ assertEquals(feedSourceWithPinnedDeploymentFeedVersion.noteIds, firstSummary.noteIds);
+ assertEquals(feedSourceWithPinnedDeploymentFeedVersion.organizationId(), firstSummary.organizationId);
+ assertEquals(feedVersionFromPinnedDeployment.id, firstSummary.deployedFeedVersionId);
+ assertEquals(expectedValidationSummary.startDate, firstSummary.deployedFeedVersionStartDate);
+ assertEquals(expectedValidationSummary.endDate, firstSummary.deployedFeedVersionEndDate);
+ assertEquals(expectedValidationSummary.errorCount, firstSummary.deployedFeedVersionIssues);
+ assertEquals(feedVersionFromPinnedDeployment.id, firstSummary.latestValidation.feedVersionId);
+ assertEquals(expectedValidationSummary.startDate, firstSummary.latestValidation.startDate);
+ assertEquals(expectedValidationSummary.endDate, firstSummary.latestValidation.endDate);
+ assertEquals(expectedValidationSummary.errorCount, firstSummary.latestValidation.errorCount);
+ assertEquals(feedVersionFromPinnedDeployment.sentToExternalPublisher, firstSummary.latestSentToExternalPublisher);
+ assertEquals(PublishState.PUBLISH_BLOCKED, firstSummary.publishState);
+ assertEquals(feedSourceWithPinnedDeploymentFeedVersion.publishedVersionId, firstSummary.publishedVersionId);
}
@ParameterizedTest
diff --git a/src/test/java/com/conveyal/datatools/manager/extensions/mtc/MtcFeedResourceTest.java b/src/test/java/com/conveyal/datatools/manager/extensions/mtc/MtcFeedResourceTest.java
index 0ab5113e7..edac92893 100644
--- a/src/test/java/com/conveyal/datatools/manager/extensions/mtc/MtcFeedResourceTest.java
+++ b/src/test/java/com/conveyal/datatools/manager/extensions/mtc/MtcFeedResourceTest.java
@@ -1,7 +1,12 @@
package com.conveyal.datatools.manager.extensions.mtc;
+import com.amazonaws.auth.AWSCredentials;
+import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
import com.conveyal.datatools.DatatoolsTest;
+import com.conveyal.datatools.TestUtils;
import com.conveyal.datatools.UnitTest;
+import com.conveyal.datatools.common.utils.aws.S3Utils;
+import com.conveyal.datatools.manager.DataManager;
import com.conveyal.datatools.manager.auth.Auth0Connection;
import com.conveyal.datatools.manager.jobs.ProcessSingleFeedJob;
import com.conveyal.datatools.manager.models.ExternalFeedSourceProperty;
@@ -14,6 +19,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.github.tomakehurst.wiremock.WireMockServer;
import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -26,6 +32,9 @@
import static com.conveyal.datatools.TestUtils.createFeedVersion;
import static com.conveyal.datatools.TestUtils.parseJson;
import static com.conveyal.datatools.TestUtils.zipFolderFiles;
+import static com.conveyal.datatools.manager.DataManager.getConfigPropertyAsText;
+import static com.conveyal.datatools.manager.extensions.mtc.MtcFeedResource.CONFIG_MTC_CREDENTIALS;
+import static com.conveyal.datatools.manager.extensions.mtc.MtcFeedResource.CONFIG_MTC_REGION;
import static com.conveyal.datatools.manager.extensions.mtc.MtcFeedResource.STOP_CODE_PRIMARY_PREFIX_FIELD_NAME;
import static com.conveyal.datatools.manager.extensions.mtc.MtcFeedResource.STOP_CODE_SECONDARY_PREFIXES_FIELD_NAME;
import static com.conveyal.gtfs.error.NewGTFSErrorType.MISSING_STOP_CODE_PREFIX;
@@ -41,12 +50,16 @@
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class MtcFeedResourceTest extends UnitTest {
private static Project project;
private static FeedSource feedSource;
private static WireMockServer wireMockServer;
+ private static String defaultMtcAwsCredentials;
+ private static String defaultMtcAwsRegion;
private static final String AGENCY_CODE = "DE";
@@ -61,6 +74,8 @@ static void setUp() throws IOException {
// Enable MTC extension, but disable the transformations.
ProcessSingleFeedJob.ENABLE_MTC_TRANSFORMATIONS = false;
DatatoolsTest.enableMTCExtension();
+ defaultMtcAwsCredentials = getConfigPropertyAsText(CONFIG_MTC_CREDENTIALS);
+ defaultMtcAwsRegion = getConfigPropertyAsText(CONFIG_MTC_REGION);
Auth0Connection.setAuthDisabled(true);
// Create a project, feed sources.
@@ -91,6 +106,16 @@ static void tearDown() {
ProcessSingleFeedJob.ENABLE_MTC_TRANSFORMATIONS = true;
}
+ @AfterEach
+ void resetState() {
+ DataManager.overrideConfigProperty(CONFIG_MTC_CREDENTIALS, defaultMtcAwsCredentials);
+
+ String currentMtcRegion = getConfigPropertyAsText(CONFIG_MTC_REGION);
+ if (currentMtcRegion != null && !currentMtcRegion.equals(defaultMtcAwsRegion)) {
+ DataManager.overrideConfigProperty(CONFIG_MTC_REGION, defaultMtcAwsRegion);
+ }
+ }
+
@Test
void shouldConvertRtdNullToEmptyString() {
assertThat(MtcFeedResource.convertRtdString("null"), equalTo(""));
@@ -235,4 +260,29 @@ void shouldUpdateRtdCarrierProperty(String address) throws JsonProcessingExcepti
carrier.toJson().contains(String.format(",\"AgencyAddress\":%s,", jsonAddress))
);
}
+
+ @ParameterizedTest
+ @ValueSource(booleans = { false, true })
+ void canUseCorrectMtcCredentials(boolean overwriteDefaults) {
+ if (overwriteDefaults) {
+ DataManager.overrideConfigProperty(CONFIG_MTC_REGION, "us-west-2");
+ String credentialsFile = TestUtils.class.getResource("mock-aws-credentials").getFile();
+ DataManager.overrideConfigProperty(CONFIG_MTC_CREDENTIALS, credentialsFile);
+ }
+
+ S3Utils.S3Wrapper s3wrapper = MtcFeedResource.getS3Wrapper();
+ assertNotNull(s3wrapper);
+
+ // Either application.data.s3_region or extensions.mtc.s3_region from server.yml.tmp.
+ assertEquals(overwriteDefaults ? "us-west-2" : "us-east-1", s3wrapper.s3Client.getRegion().toAWSRegion().getName());
+
+ // The credentials should reflect the content in the mock-aws-credentials file referenced above.
+ if (overwriteDefaults) {
+ AWSCredentials credentials = s3wrapper.credentials.getCredentials();
+ assertEquals("mock-id-123", credentials.getAWSAccessKeyId());
+ assertEquals("mock-secret-123", credentials.getAWSSecretKey());
+ } else {
+ assertEquals(DefaultAWSCredentialsProviderChain.class, s3wrapper.credentials.getClass());
+ }
+ }
}
diff --git a/src/test/java/com/conveyal/datatools/manager/jobs/validation/FaresV2ValidationTest.java b/src/test/java/com/conveyal/datatools/manager/jobs/validation/FaresV2ValidationTest.java
new file mode 100644
index 000000000..a5301d079
--- /dev/null
+++ b/src/test/java/com/conveyal/datatools/manager/jobs/validation/FaresV2ValidationTest.java
@@ -0,0 +1,100 @@
+package com.conveyal.datatools.manager.jobs.validation;
+
+import com.conveyal.datatools.DatatoolsTest;
+import com.conveyal.datatools.manager.auth.Auth0Connection;
+import com.conveyal.datatools.manager.models.FeedSource;
+import com.conveyal.datatools.manager.models.FeedVersion;
+import com.conveyal.datatools.manager.models.Project;
+import com.conveyal.datatools.manager.persistence.Persistence;
+import org.bson.Document;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import static com.conveyal.datatools.TestUtils.createFeedVersion;
+import static com.conveyal.datatools.TestUtils.zipFolderFiles;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class FaresV2ValidationTest {
+
+ private static Project project;
+ private static FeedVersion faresV2Version;
+
+ @BeforeAll
+ public static void setUp() throws IOException {
+ // Start server if it isn't already running.
+ DatatoolsTest.setUp();
+ Auth0Connection.setAuthDisabled(true);
+ // Create a project, feed sources, and feed versions to merge.
+ project = new Project();
+ project.name = String.format("Test %s", new Date());
+ Persistence.projects.create(project);
+
+ FeedSource faresV2FeedSource = new FeedSource("fares-v2");
+ faresV2FeedSource.projectId = project.id;
+ Persistence.feedSources.create(faresV2FeedSource);
+ faresV2Version = createFeedVersion(
+ faresV2FeedSource,
+ zipFolderFiles("fake-agency-with-fares-v2")
+ );
+ Persistence.feedVersions.replace(faresV2Version.id, faresV2Version);
+ }
+
+ @AfterAll
+ static void tearDown() {
+ Auth0Connection.setAuthDisabled(Auth0Connection.getDefaultAuthDisabled());
+ project.delete();
+ }
+
+ @Test
+ void canValidateFareV2Files() {
+ Document summary = (Document) faresV2Version.mobilityDataResult.get("summary");
+ List files = summary.getList("files", String.class);
+ assertTrue(files.contains("areas.txt"));
+ assertTrue(files.contains("fare_leg_rules.txt"));
+ assertTrue(files.contains("fare_leg_join_rules.txt"));
+ assertTrue(files.contains("fare_media.txt"));
+ assertTrue(files.contains("fare_products.txt"));
+ assertTrue(files.contains("fare_transfer_rules.txt"));
+ assertTrue(files.contains("networks.txt"));
+ assertTrue(files.contains("rider_categories.txt"));
+ assertTrue(files.contains("route_networks.txt"));
+ assertTrue(files.contains("stop_areas.txt"));
+ assertTrue(files.contains("timeframes.txt"));
+ }
+
+ /**
+ * This is not an exhaustive test, more of a sanity check that MobilityData can detect errors in fares v2 data.
+ * https://gtfs-validator.mobilitydata.org/rules.html
+ */
+ @ParameterizedTest
+ @MethodSource("createValidationErrorChecks")
+ void canDetectValidationErrors(Set codes, String expectedCode) {
+ assertTrue(codes.contains(expectedCode));
+ }
+
+ private static Stream createValidationErrorChecks() {
+ ArrayList notices = (ArrayList) faresV2Version.mobilityDataResult.get("notices");
+ Set codes = new HashSet<>();
+ for (Document notice : notices) {
+ codes.add(notice.getString("code"));
+ }
+ return Stream.of(
+ Arguments.of(codes, "fare_transfer_rule_duration_limit_type_without_duration_limit"),
+ Arguments.of(codes, "fare_transfer_rule_duration_limit_without_type"),
+ Arguments.of(codes, "fare_transfer_rule_missing_transfer_count"),
+ Arguments.of(codes, "fare_transfer_rule_with_forbidden_transfer_count")
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/conveyal/datatools/manager/models/transform/RemoveNonRevenueTripsTransformationTest.java b/src/test/java/com/conveyal/datatools/manager/models/transform/RemoveNonRevenueTripsTransformationTest.java
index ab738dfb6..cae5496f0 100644
--- a/src/test/java/com/conveyal/datatools/manager/models/transform/RemoveNonRevenueTripsTransformationTest.java
+++ b/src/test/java/com/conveyal/datatools/manager/models/transform/RemoveNonRevenueTripsTransformationTest.java
@@ -107,15 +107,16 @@ static Stream createNonRevenueCases() {
return Stream.of(
Arguments.of(
List.of(
- "non-revenue-trip1,1,",
"non-revenue-trip1,1,1",
"non-revenue-trip1,1,1",
- "non-revenue-trip1,1,"
+ "non-revenue-trip1,1,1",
+ "non-revenue-trip1,1,1"
),
List.of(
"revenue-trip,0,0",
"revenue-trip,1,1",
- "revenue-trip,1,1",
+ "revenue-trip,2,1",
+ "revenue-trip,1,2",
"revenue-trip,0,0"
),
fieldIndexes
@@ -131,8 +132,8 @@ static Stream createNonRevenueCases() {
List.of(
"revenue-trip,0,0,2,2",
"revenue-trip,0,0,1,1",
- "revenue-trip,0,0,2,2",
- "revenue-trip,0,0,2,2"
+ "revenue-trip,0,0,2,1",
+ "revenue-trip,0,0,1,2"
),
allFieldIndexes
)
diff --git a/src/test/java/com/conveyal/datatools/manager/utils/sql/SqlSchemaUpdaterTest.java b/src/test/java/com/conveyal/datatools/manager/utils/sql/SqlSchemaUpdaterTest.java
index c374cc010..f40474ee3 100644
--- a/src/test/java/com/conveyal/datatools/manager/utils/sql/SqlSchemaUpdaterTest.java
+++ b/src/test/java/com/conveyal/datatools/manager/utils/sql/SqlSchemaUpdaterTest.java
@@ -88,7 +88,16 @@ void canCheckAndUpgradeTables(String namespaceType) throws Exception {
// Some tables are missing from the feed and should be flagged.
assertTrue(namespaceCheck.missingTables.containsAll(Lists.newArrayList(
Table.SCHEDULE_EXCEPTIONS,
+ Table.TIME_FRAMES,
+ Table.FARE_MEDIAS,
+ Table.FARE_PRODUCTS,
+ Table.NETWORKS,
+ Table.FARE_LEG_RULES,
+ Table.FARE_TRANSFER_RULES,
Table.FEED_INFO,
+ Table.AREAS,
+ Table.RIDER_CATEGORIES,
+ Table.FARE_LEG_JOIN_RULES,
Table.TRANSFERS,
Table.FREQUENCIES,
Table.ATTRIBUTIONS,
diff --git a/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/agency.txt b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/agency.txt
new file mode 100644
index 000000000..5cb6afa42
--- /dev/null
+++ b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/agency.txt
@@ -0,0 +1,2 @@
+agency_id,agency_name,agency_url,agency_lang,agency_phone,agency_email,agency_timezone,agency_fare_url,agency_branding_url
+1,Fake Transit,http://www.fake-agency.com,,,,America/Los_Angeles,,
diff --git a/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/areas.txt b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/areas.txt
new file mode 100644
index 000000000..73b6734b6
--- /dev/null
+++ b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/areas.txt
@@ -0,0 +1,40 @@
+area_id,area_name
+area_bl,Blue Line
+area_bl_airport,Blue Line - Airport Station
+area_cf_zone_buzzards,CapeFLYER - Wareham/Buzzards Bay/Bourne
+area_cf_zone_hyannis,CapeFLYER - Hyannis
+area_commuter_rail_porter_zone_1a,Commuter Rail Zone 1A
+area_commuter_rail_sumner_tunnel_zone_1a,Commuter Rail Zone 1A
+area_commuter_rail_zone_1,Commuter Rail Zone 1
+area_commuter_rail_zone_10,Commuter Rail Zone 10
+area_commuter_rail_zone_1a,Commuter Rail Zone 1A
+area_commuter_rail_zone_2,Commuter Rail Zone 2
+area_commuter_rail_zone_3,Commuter Rail Zone 3
+area_commuter_rail_zone_4,Commuter Rail Zone 4
+area_commuter_rail_zone_5,Commuter Rail Zone 5
+area_commuter_rail_zone_6,Commuter Rail Zone 6
+area_commuter_rail_zone_7,Commuter Rail Zone 7
+area_commuter_rail_zone_8,Commuter Rail Zone 8
+area_commuter_rail_zone_9,Commuter Rail Zone 9
+area_fairmount_line_zone_1a,Commuter Rail Zone 1A - Fairmount Line
+area_gl_govt_ctr,Green Line - Government Center
+area_green_b_west_of_kenmore,Green Line B - West of Kenmore
+area_green_c_west_of_kenmore,Green Line C - West of Kenmore
+area_green_e_west_of_symphony,Green Line E - West of Symphony
+area_m_ashmont_mattapan,Mattapan Trolley - Ashmont and Mattapan
+area_ol_state,Orange Line - State
+area_red_south_station,Red Line - South Station
+area_route_354_downtown,Route 354 - Downtown
+area_route_354_outside_downtown,Route 354 - Outside Downtown
+area_route_426_downtown,Route 426 - Downtown
+area_route_426_outside_downtown,Route 426 - Outside Downtown
+area_route_450_downtown,Route 450 - Downtown
+area_route_450_outside_downtown,Route 450 - Outside Downtown
+area_sl3_north_of_airport,Silver Line - North of Airport Station
+area_sl_airport,Silver Line - Airport Station
+area_sl_courthouse,Silver Line - Courthouse
+area_sl_logan_terminal,Silver Line - Airport Terminals
+area_sl_silver_line_way,Silver Line - Silver Line Way
+area_sl_south_station,Silver Line - South Station
+area_sl_world_trade_center,Silver Line - World Trade Center
+area_ss_commuter_rail_zone_1a,Commuter Rail Zone 1A - South Station
diff --git a/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/calendar.txt b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/calendar.txt
new file mode 100644
index 000000000..1bf13678e
--- /dev/null
+++ b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/calendar.txt
@@ -0,0 +1,2 @@
+service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date
+04100312-8fe1-46a5-a9f2-556f39478f57,1,1,1,1,1,1,1,20170915,20170917
\ No newline at end of file
diff --git a/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/calendar_dates.txt b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/calendar_dates.txt
new file mode 100644
index 000000000..5d0a31806
--- /dev/null
+++ b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/calendar_dates.txt
@@ -0,0 +1,3 @@
+service_id,date,exception_type
+04100312-8fe1-46a5-a9f2-556f39478f57,20170916,2
+calendar-date-service,20170917,1
\ No newline at end of file
diff --git a/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/fare_leg_join_rules.txt b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/fare_leg_join_rules.txt
new file mode 100644
index 000000000..a05f4b14a
--- /dev/null
+++ b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/fare_leg_join_rules.txt
@@ -0,0 +1,2 @@
+from_network_id,to_network_id,from_stop_id,to_stop_id
+1,2,3,4
\ No newline at end of file
diff --git a/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/fare_leg_rules.txt b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/fare_leg_rules.txt
new file mode 100644
index 000000000..37b60d472
--- /dev/null
+++ b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/fare_leg_rules.txt
@@ -0,0 +1,697 @@
+leg_group_id,network_id,from_area_id,to_area_id,fare_product_id,from_timeframe_group_id,to_timeframe_group_id,transfer_only
+leg_airport_rapid_transit_quick_subway,rapid_transit,area_bl_airport,,prod_rapid_transit_quick_subway,timeframe_regular,,
+leg_cape_buzzards_hyannis_cash,cape_flyer,area_cf_zone_buzzards,area_cf_zone_hyannis,prod_cape_buzzards_hyannis_fare,,,
+leg_cape_buzzards_hyannis_cash,cape_flyer,area_cf_zone_hyannis,area_cf_zone_buzzards,prod_cape_buzzards_hyannis_fare,,,
+leg_cape_buzzards_hyannis_cash,cape_flyer,area_cf_zone_hyannis,area_commuter_rail_zone_8,prod_cape_buzzards_hyannis_fare,,,
+leg_cape_buzzards_hyannis_cash,cape_flyer,area_commuter_rail_zone_8,area_cf_zone_hyannis,prod_cape_buzzards_hyannis_fare,,,
+leg_cape_sbb_buzzards_cash,cape_flyer,area_cf_zone_buzzards,area_commuter_rail_zone_1a,prod_cape_sbb_buzzards_fare,,,
+leg_cape_sbb_buzzards_cash,cape_flyer,area_cf_zone_buzzards,area_commuter_rail_zone_2,prod_cape_sbb_buzzards_fare,,,
+leg_cape_sbb_buzzards_cash,cape_flyer,area_cf_zone_buzzards,area_commuter_rail_zone_4,prod_cape_sbb_buzzards_fare,,,
+leg_cape_sbb_buzzards_cash,cape_flyer,area_commuter_rail_zone_1a,area_cf_zone_buzzards,prod_cape_sbb_buzzards_fare,,,
+leg_cape_sbb_buzzards_cash,cape_flyer,area_commuter_rail_zone_2,area_cf_zone_buzzards,prod_cape_sbb_buzzards_fare,,,
+leg_cape_sbb_buzzards_cash,cape_flyer,area_commuter_rail_zone_4,area_cf_zone_buzzards,prod_cape_sbb_buzzards_fare,,,
+leg_cape_sbb_hyannis_cash,cape_flyer,area_cf_zone_hyannis,area_commuter_rail_zone_1a,prod_cape_sbb_hyannis_fare,,,
+leg_cape_sbb_hyannis_cash,cape_flyer,area_cf_zone_hyannis,area_commuter_rail_zone_2,prod_cape_sbb_hyannis_fare,,,
+leg_cape_sbb_hyannis_cash,cape_flyer,area_cf_zone_hyannis,area_commuter_rail_zone_4,prod_cape_sbb_hyannis_fare,,,
+leg_cape_sbb_hyannis_cash,cape_flyer,area_commuter_rail_zone_1a,area_cf_zone_hyannis,prod_cape_sbb_hyannis_fare,,,
+leg_cape_sbb_hyannis_cash,cape_flyer,area_commuter_rail_zone_2,area_cf_zone_hyannis,prod_cape_sbb_hyannis_fare,,,
+leg_cape_sbb_hyannis_cash,cape_flyer,area_commuter_rail_zone_4,area_cf_zone_hyannis,prod_cape_sbb_hyannis_fare,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_sumner_tunnel_zone_1a,area_commuter_rail_sumner_tunnel_zone_1a,prod_cr_zone_1a,timeframe_sumner_tunnel_closure,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_sumner_tunnel_zone_1a,area_commuter_rail_zone_1a,prod_cr_zone_1a,timeframe_sumner_tunnel_closure,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_1,prod_cr_inter_1,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_10,prod_cr_inter_10,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_1a,prod_cr_zone_1,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_2,prod_cr_inter_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_3,prod_cr_inter_3,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_4,prod_cr_inter_4,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_5,prod_cr_inter_5,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_6,prod_cr_inter_6,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_7,prod_cr_inter_7,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_8,prod_cr_inter_8,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_9,prod_cr_inter_9,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_fairmount_line_zone_1a,prod_cr_zone_1,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_ss_commuter_rail_zone_1a,prod_cr_zone_1,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_1,prod_cr_inter_10,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_10,prod_cr_inter_1,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_1a,prod_cr_zone_10,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_2,prod_cr_inter_9,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_3,prod_cr_inter_8,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_4,prod_cr_inter_7,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_5,prod_cr_inter_6,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_6,prod_cr_inter_5,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_7,prod_cr_inter_4,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_8,prod_cr_inter_3,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_9,prod_cr_inter_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_fairmount_line_zone_1a,prod_cr_zone_10,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_ss_commuter_rail_zone_1a,prod_cr_zone_10,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_sumner_tunnel_zone_1a,prod_cr_zone_1a,timeframe_sumner_tunnel_closure,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_1,prod_cr_zone_1,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_10,prod_cr_zone_10,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_1a,prod_cr_zone_1a,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_2,prod_cr_zone_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_3,prod_cr_zone_3,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_4,prod_cr_zone_4,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_5,prod_cr_zone_5,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_6,prod_cr_zone_6,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_7,prod_cr_zone_7,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_8,prod_cr_zone_8,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_9,prod_cr_zone_9,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_fairmount_line_zone_1a,prod_cr_zone_1a,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_ss_commuter_rail_zone_1a,prod_cr_zone_1a,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_1,prod_cr_inter_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_10,prod_cr_inter_9,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_1a,prod_cr_zone_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_2,prod_cr_inter_1,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_3,prod_cr_inter_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_4,prod_cr_inter_3,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_5,prod_cr_inter_4,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_6,prod_cr_inter_5,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_7,prod_cr_inter_6,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_8,prod_cr_inter_7,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_9,prod_cr_inter_8,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_fairmount_line_zone_1a,prod_cr_zone_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_ss_commuter_rail_zone_1a,prod_cr_zone_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_1,prod_cr_inter_3,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_10,prod_cr_inter_8,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_1a,prod_cr_zone_3,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_2,prod_cr_inter_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_3,prod_cr_inter_1,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_4,prod_cr_inter_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_5,prod_cr_inter_3,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_6,prod_cr_inter_4,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_7,prod_cr_inter_5,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_8,prod_cr_inter_6,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_9,prod_cr_inter_7,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_fairmount_line_zone_1a,prod_cr_zone_3,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_ss_commuter_rail_zone_1a,prod_cr_zone_3,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_1,prod_cr_inter_4,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_10,prod_cr_inter_7,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_1a,prod_cr_zone_4,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_2,prod_cr_inter_3,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_3,prod_cr_inter_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_4,prod_cr_inter_1,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_5,prod_cr_inter_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_6,prod_cr_inter_3,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_7,prod_cr_inter_4,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_8,prod_cr_inter_5,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_9,prod_cr_inter_6,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_fairmount_line_zone_1a,prod_cr_zone_4,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_ss_commuter_rail_zone_1a,prod_cr_zone_4,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_1,prod_cr_inter_5,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_10,prod_cr_inter_6,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_1a,prod_cr_zone_5,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_2,prod_cr_inter_4,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_3,prod_cr_inter_3,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_4,prod_cr_inter_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_5,prod_cr_inter_1,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_6,prod_cr_inter_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_7,prod_cr_inter_3,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_8,prod_cr_inter_4,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_9,prod_cr_inter_5,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_fairmount_line_zone_1a,prod_cr_zone_5,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_ss_commuter_rail_zone_1a,prod_cr_zone_5,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_1,prod_cr_inter_6,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_10,prod_cr_inter_5,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_1a,prod_cr_zone_6,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_2,prod_cr_inter_5,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_3,prod_cr_inter_4,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_4,prod_cr_inter_3,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_5,prod_cr_inter_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_6,prod_cr_inter_1,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_7,prod_cr_inter_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_8,prod_cr_inter_3,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_9,prod_cr_inter_4,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_fairmount_line_zone_1a,prod_cr_zone_6,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_ss_commuter_rail_zone_1a,prod_cr_zone_6,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_1,prod_cr_inter_7,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_10,prod_cr_inter_4,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_1a,prod_cr_zone_7,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_2,prod_cr_inter_6,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_3,prod_cr_inter_5,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_4,prod_cr_inter_4,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_5,prod_cr_inter_3,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_6,prod_cr_inter_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_7,prod_cr_inter_1,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_8,prod_cr_inter_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_9,prod_cr_inter_3,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_fairmount_line_zone_1a,prod_cr_zone_7,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_ss_commuter_rail_zone_1a,prod_cr_zone_7,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_1,prod_cr_inter_8,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_10,prod_cr_inter_3,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_1a,prod_cr_zone_8,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_2,prod_cr_inter_7,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_3,prod_cr_inter_6,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_4,prod_cr_inter_5,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_5,prod_cr_inter_4,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_6,prod_cr_inter_3,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_7,prod_cr_inter_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_8,prod_cr_inter_1,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_9,prod_cr_inter_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_fairmount_line_zone_1a,prod_cr_zone_8,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_ss_commuter_rail_zone_1a,prod_cr_zone_8,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_1,prod_cr_inter_9,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_10,prod_cr_inter_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_1a,prod_cr_zone_9,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_2,prod_cr_inter_8,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_3,prod_cr_inter_7,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_4,prod_cr_inter_6,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_5,prod_cr_inter_5,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_6,prod_cr_inter_4,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_7,prod_cr_inter_3,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_8,prod_cr_inter_2,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_9,prod_cr_inter_1,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_fairmount_line_zone_1a,prod_cr_zone_9,,,
+leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_ss_commuter_rail_zone_1a,prod_cr_zone_9,,,
+leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_1,prod_cr_zone_1,,,
+leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_10,prod_cr_zone_10,,,
+leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_1a,prod_cr_zone_1a,,,
+leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_2,prod_cr_zone_2,,,
+leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_3,prod_cr_zone_3,,,
+leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_4,prod_cr_zone_4,,,
+leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_5,prod_cr_zone_5,,,
+leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_6,prod_cr_zone_6,,,
+leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_7,prod_cr_zone_7,,,
+leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_8,prod_cr_zone_8,,,
+leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_9,prod_cr_zone_9,,,
+leg_commuter_rail_free,commuter_rail,area_commuter_rail_porter_zone_1a,area_commuter_rail_zone_1a,prod_free_fare,timeframe_alewife_kendall_surge,,
+leg_commuter_rail_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_porter_zone_1a,prod_free_fare,timeframe_alewife_kendall_surge,,
+leg_express_bus_cash,express_bus,area_route_354_downtown,area_route_354_outside_downtown,prod_express_bus,,,
+leg_express_bus_cash,express_bus,area_route_354_outside_downtown,area_route_354_downtown,prod_express_bus,,,
+leg_express_bus_cash,express_bus,area_route_426_downtown,area_route_426_outside_downtown,prod_express_bus,,,
+leg_express_bus_cash,express_bus,area_route_426_outside_downtown,area_route_426_downtown,prod_express_bus,,,
+leg_express_bus_cash,express_bus,area_route_450_downtown,area_route_450_outside_downtown,prod_express_bus,,,
+leg_express_bus_cash,express_bus,area_route_450_outside_downtown,area_route_450_downtown,prod_express_bus,,,
+leg_express_bus_cash,express_bus_special,,,prod_express_bus,,,
+leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_1,prod_cr_zone_1,,,
+leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_10,prod_cr_zone_10,,,
+leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_1a,prod_cr_zone_1a,,,
+leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_2,prod_cr_zone_2,,,
+leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_3,prod_cr_zone_3,,,
+leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_4,prod_cr_zone_4,,,
+leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_5,prod_cr_zone_5,,,
+leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_6,prod_cr_zone_6,,,
+leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_7,prod_cr_zone_7,,,
+leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_8,prod_cr_zone_8,,,
+leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_9,prod_cr_zone_9,,,
+leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_fairmount_line_zone_1a,prod_cr_zone_1a,,,
+leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_ss_commuter_rail_zone_1a,prod_cr_zone_1a,,,
+leg_ferry_east_boston_free,ferry_east_boston,,,prod_free_fare,timeframe_sumner_tunnel_closure,,
+leg_ferry_east_boston_free,ferry_east_boston,,,prod_boat_zone_1a,timeframe_regular,,
+leg_ferry_f1_cash,ferry_f1,,,prod_ferry_f1,,,
+leg_ferry_f4_cash,ferry_f4,,,prod_ferry_f4,,,
+leg_ferry_f6_cash,ferry_f6,,,prod_boat_zone_1a,timeframe_sumner_tunnel_closure,,
+leg_ferry_f6_cash,ferry_f6,,,prod_boat_zone_1,timeframe_regular,,
+leg_ferry_lynn_cash,ferry_lynn,,,prod_boat_zone_1a,timeframe_sumner_tunnel_closure,,
+leg_ferry_lynn_cash,ferry_lynn,,,prod_boat_zone_2,timeframe_regular,,
+leg_foxboro_event_cash,cr_foxboro,,,prod_foxboro_event_fare,,,
+leg_local_bus_cash,express_bus,area_route_354_downtown,area_route_354_downtown,prod_local_bus,,,
+leg_local_bus_cash,express_bus,area_route_354_outside_downtown,area_route_354_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,express_bus,area_route_426_downtown,area_route_426_downtown,prod_local_bus,,,
+leg_local_bus_cash,express_bus,area_route_426_outside_downtown,area_route_426_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,express_bus,area_route_450_downtown,area_route_450_downtown,prod_local_bus,,,
+leg_local_bus_cash,express_bus,area_route_450_outside_downtown,area_route_450_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_354_downtown,area_route_354_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_354_downtown,area_route_354_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_354_downtown,area_route_426_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_354_downtown,area_route_426_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_354_downtown,area_route_450_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_354_downtown,area_route_450_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_354_downtown,,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_354_outside_downtown,area_route_354_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_354_outside_downtown,area_route_354_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_354_outside_downtown,area_route_426_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_354_outside_downtown,area_route_426_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_354_outside_downtown,area_route_450_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_354_outside_downtown,area_route_450_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_354_outside_downtown,,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_426_downtown,area_route_354_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_426_downtown,area_route_354_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_426_downtown,area_route_426_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_426_downtown,area_route_426_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_426_downtown,area_route_450_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_426_downtown,area_route_450_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_426_downtown,,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_426_outside_downtown,area_route_354_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_426_outside_downtown,area_route_354_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_426_outside_downtown,area_route_426_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_426_outside_downtown,area_route_426_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_426_outside_downtown,area_route_450_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_426_outside_downtown,area_route_450_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_426_outside_downtown,,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_450_downtown,area_route_354_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_450_downtown,area_route_354_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_450_downtown,area_route_426_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_450_downtown,area_route_426_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_450_downtown,area_route_450_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_450_downtown,area_route_450_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_450_downtown,,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_450_outside_downtown,area_route_354_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_450_outside_downtown,area_route_354_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_450_outside_downtown,area_route_426_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_450_outside_downtown,area_route_426_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_450_outside_downtown,area_route_450_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_450_outside_downtown,area_route_450_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,area_route_450_outside_downtown,,prod_local_bus,,,
+leg_local_bus_cash,local_bus,,area_route_354_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,,area_route_354_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,,area_route_426_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,,area_route_426_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,,area_route_450_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,,area_route_450_outside_downtown,prod_local_bus,,,
+leg_local_bus_cash,local_bus,,,prod_local_bus,,,
+leg_local_bus_free,local_bus_free,area_route_354_downtown,area_route_354_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_354_downtown,area_route_354_outside_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_354_downtown,area_route_426_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_354_downtown,area_route_426_outside_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_354_downtown,area_route_450_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_354_downtown,area_route_450_outside_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_354_downtown,,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_354_outside_downtown,area_route_354_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_354_outside_downtown,area_route_354_outside_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_354_outside_downtown,area_route_426_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_354_outside_downtown,area_route_426_outside_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_354_outside_downtown,area_route_450_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_354_outside_downtown,area_route_450_outside_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_354_outside_downtown,,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_426_downtown,area_route_354_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_426_downtown,area_route_354_outside_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_426_downtown,area_route_426_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_426_downtown,area_route_426_outside_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_426_downtown,area_route_450_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_426_downtown,area_route_450_outside_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_426_downtown,,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_426_outside_downtown,area_route_354_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_426_outside_downtown,area_route_354_outside_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_426_outside_downtown,area_route_426_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_426_outside_downtown,area_route_426_outside_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_426_outside_downtown,area_route_450_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_426_outside_downtown,area_route_450_outside_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_426_outside_downtown,,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_450_downtown,area_route_354_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_450_downtown,area_route_354_outside_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_450_downtown,area_route_426_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_450_downtown,area_route_426_outside_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_450_downtown,area_route_450_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_450_downtown,area_route_450_outside_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_450_downtown,,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_450_outside_downtown,area_route_354_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_450_outside_downtown,area_route_354_outside_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_450_outside_downtown,area_route_426_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_450_outside_downtown,area_route_426_outside_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_450_outside_downtown,area_route_450_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_450_outside_downtown,area_route_450_outside_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,area_route_450_outside_downtown,,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,,area_route_354_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,,area_route_354_outside_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,,area_route_426_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,,area_route_426_outside_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,,area_route_450_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,,area_route_450_outside_downtown,prod_free_fare,,,
+leg_local_bus_free,local_bus_free,,,prod_free_fare,,,
+leg_local_bus_quick_subway,express_bus,area_route_354_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,express_bus,area_route_354_outside_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,express_bus,area_route_426_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,express_bus,area_route_426_outside_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,express_bus,area_route_450_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,express_bus,area_route_450_outside_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_354_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_354_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_354_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_354_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_354_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_354_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_354_downtown,,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_354_outside_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_354_outside_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_354_outside_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_354_outside_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_354_outside_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_354_outside_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_354_outside_downtown,,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_426_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_426_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_426_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_426_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_426_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_426_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_426_downtown,,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_426_outside_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_426_outside_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_426_outside_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_426_outside_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_426_outside_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_426_outside_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_426_outside_downtown,,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_450_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_450_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_450_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_450_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_450_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_450_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_450_downtown,,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_450_outside_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_450_outside_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_450_outside_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_450_outside_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_450_outside_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_450_outside_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,area_route_450_outside_downtown,,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,,area_route_354_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,,area_route_426_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,,area_route_450_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,
+leg_local_bus_quick_subway,local_bus,,,prod_rapid_transit_quick_subway,,,
+leg_local_bus_restricted_cash,local_bus_restricted,,,prod_local_bus,,,
+leg_mattapan_rapid_transit_cash,m_rapid_transit,,,prod_rapid_transit_cash,,,
+leg_mattapan_rapid_transit_quick_subway,m_rapid_transit,area_m_ashmont_mattapan,,prod_rapid_transit_quick_subway,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_354_downtown,area_route_354_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_354_downtown,area_route_354_outside_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_354_downtown,area_route_426_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_354_downtown,area_route_426_outside_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_354_downtown,area_route_450_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_354_downtown,area_route_450_outside_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_354_downtown,,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_354_outside_downtown,area_route_354_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_354_outside_downtown,area_route_354_outside_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_354_outside_downtown,area_route_426_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_354_outside_downtown,area_route_426_outside_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_354_outside_downtown,area_route_450_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_354_outside_downtown,area_route_450_outside_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_354_outside_downtown,,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_426_downtown,area_route_354_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_426_downtown,area_route_354_outside_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_426_downtown,area_route_426_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_426_downtown,area_route_426_outside_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_426_downtown,area_route_450_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_426_downtown,area_route_450_outside_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_426_downtown,,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_426_outside_downtown,area_route_354_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_426_outside_downtown,area_route_354_outside_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_426_outside_downtown,area_route_426_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_426_outside_downtown,area_route_426_outside_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_426_outside_downtown,area_route_450_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_426_outside_downtown,area_route_450_outside_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_426_outside_downtown,,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_450_downtown,area_route_354_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_450_downtown,area_route_354_outside_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_450_downtown,area_route_426_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_450_downtown,area_route_426_outside_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_450_downtown,area_route_450_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_450_downtown,area_route_450_outside_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_450_downtown,,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_450_outside_downtown,area_route_354_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_450_outside_downtown,area_route_354_outside_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_450_outside_downtown,area_route_426_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_450_outside_downtown,area_route_426_outside_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_450_outside_downtown,area_route_450_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_450_outside_downtown,area_route_450_outside_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,area_route_450_outside_downtown,,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,,area_route_354_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,,area_route_354_outside_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,,area_route_426_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,,area_route_426_outside_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,,area_route_450_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,,area_route_450_outside_downtown,prod_free_fare,,,
+leg_rail_replacement_free,rail_replacement_bus,,,prod_free_fare,,,
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_downtown,,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_outside_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_outside_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_outside_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_outside_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_outside_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_outside_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_outside_downtown,,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_downtown,,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_outside_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_outside_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_outside_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_outside_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_outside_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_outside_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_outside_downtown,,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_downtown,,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_outside_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_outside_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_outside_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_outside_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_outside_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_outside_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_outside_downtown,,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,,area_route_354_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,,area_route_426_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,,area_route_450_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,1
+leg_rail_replacement_quick_subway,rail_replacement_bus,,,prod_rapid_transit_quick_subway,,,1
+leg_rapid_transit_cash,rapid_transit,area_green_b_west_of_kenmore,,prod_rapid_transit_cash,,,
+leg_rapid_transit_cash,rapid_transit,area_green_c_west_of_kenmore,,prod_rapid_transit_cash,,,
+leg_rapid_transit_cash,rapid_transit,area_green_e_west_of_symphony,,prod_rapid_transit_cash,,,
+leg_rapid_transit_cash,rapid_transit,,,prod_rapid_transit_cash,,,1
+leg_rapid_transit_free,rapid_transit,area_bl,,prod_free_fare,timeframe_sumner_tunnel_closure,,
+leg_rapid_transit_free,rapid_transit,area_bl_airport,,prod_free_fare,timeframe_sumner_tunnel_closure,,
+leg_rapid_transit_free,rapid_transit,area_gl_govt_ctr,,prod_free_fare,timeframe_sumner_tunnel_closure,,
+leg_rapid_transit_free,rapid_transit,area_ol_state,,prod_free_fare,timeframe_sumner_tunnel_closure,,
+leg_rapid_transit_free,rapid_transit,,,prod_free_fare,,,1
+leg_rapid_transit_quick_subway,rapid_transit,area_bl,,prod_rapid_transit_quick_subway,timeframe_regular,,
+leg_rapid_transit_quick_subway,rapid_transit,area_gl_govt_ctr,,prod_rapid_transit_quick_subway,timeframe_regular,,
+leg_rapid_transit_quick_subway,rapid_transit,area_ol_state,,prod_rapid_transit_quick_subway,timeframe_regular,,
+leg_rapid_transit_quick_subway,rapid_transit,,,prod_rapid_transit_quick_subway,timeframe_regular,,
+leg_sl_rapid_transit_cash,sl_rapid_transit,area_sl3_north_of_airport,area_sl3_north_of_airport,prod_rapid_transit_cash,timeframe_regular,,
+leg_sl_rapid_transit_cash,sl_rapid_transit,area_sl3_north_of_airport,area_sl_airport,prod_rapid_transit_cash,timeframe_regular,,
+leg_sl_rapid_transit_cash,sl_rapid_transit,area_sl3_north_of_airport,,prod_rapid_transit_cash,timeframe_regular,,
+leg_sl_rapid_transit_cash,sl_rapid_transit,area_sl_silver_line_way,area_sl3_north_of_airport,prod_rapid_transit_cash,timeframe_regular,,
+leg_sl_rapid_transit_cash,sl_rapid_transit,area_sl_silver_line_way,area_sl_airport,prod_rapid_transit_cash,timeframe_regular,,
+leg_sl_rapid_transit_cash,sl_rapid_transit,area_sl_silver_line_way,,prod_rapid_transit_cash,,,
+leg_sl_rapid_transit_cash,sl_rapid_transit,area_sl_south_station,area_sl3_north_of_airport,prod_rapid_transit_cash,,,1
+leg_sl_rapid_transit_cash,sl_rapid_transit,area_sl_south_station,area_sl_airport,prod_rapid_transit_cash,,,1
+leg_sl_rapid_transit_cash,sl_rapid_transit,area_sl_south_station,,prod_rapid_transit_cash,,,1
+leg_sl_rapid_transit_cash,sl_rapid_transit,,,prod_rapid_transit_cash,,,
+leg_sl_rapid_transit_free,sl_rapid_transit,area_sl3_north_of_airport,area_sl3_north_of_airport,prod_free_fare,timeframe_sumner_tunnel_closure,,
+leg_sl_rapid_transit_free,sl_rapid_transit,area_sl3_north_of_airport,area_sl_airport,prod_free_fare,timeframe_sumner_tunnel_closure,,
+leg_sl_rapid_transit_free,sl_rapid_transit,area_sl3_north_of_airport,,prod_free_fare,timeframe_sumner_tunnel_closure,,
+leg_sl_rapid_transit_free,sl_rapid_transit,area_sl_airport,area_sl3_north_of_airport,prod_free_fare,timeframe_sumner_tunnel_closure,,
+leg_sl_rapid_transit_free,sl_rapid_transit,area_sl_airport,,prod_free_fare,timeframe_sumner_tunnel_closure,,
+leg_sl_rapid_transit_free,sl_rapid_transit,area_sl_logan_terminal,,prod_free_fare,,,
+leg_sl_rapid_transit_free,sl_rapid_transit,area_sl_silver_line_way,area_sl3_north_of_airport,prod_free_fare,timeframe_sumner_tunnel_closure,,
+leg_sl_rapid_transit_free,sl_rapid_transit,area_sl_silver_line_way,area_sl_airport,prod_free_fare,timeframe_sumner_tunnel_closure,,
+leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_airport,area_sl3_north_of_airport,prod_rapid_transit_quick_subway,timeframe_regular,,
+leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_airport,,prod_rapid_transit_quick_subway,timeframe_regular,,
+leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_courthouse,area_sl3_north_of_airport,prod_rapid_transit_quick_subway,,,
+leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_courthouse,area_sl_airport,prod_rapid_transit_quick_subway,,,
+leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_courthouse,,prod_rapid_transit_quick_subway,,,
+leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_south_station,area_sl3_north_of_airport,prod_rapid_transit_quick_subway,,,
+leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_south_station,area_sl_airport,prod_rapid_transit_quick_subway,,,
+leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_south_station,,prod_rapid_transit_quick_subway,,,
+leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_world_trade_center,area_sl3_north_of_airport,prod_rapid_transit_quick_subway,,,
+leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_world_trade_center,area_sl_airport,prod_rapid_transit_quick_subway,,,
+leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_world_trade_center,,prod_rapid_transit_quick_subway,,,
+leg_ss_fairmount_line_zone_1a_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_fairmount_line_zone_1a,prod_cr_zone_1a,,,
+leg_ss_rapid_transit_cash,rapid_transit,area_red_south_station,,prod_rapid_transit_cash,,,1
+leg_ss_rapid_transit_free,rapid_transit,area_red_south_station,,prod_free_fare,,,1
+leg_ss_rapid_transit_quick_subway,rapid_transit,area_red_south_station,,prod_rapid_transit_quick_subway,,,
+leg_systemwide_free,cape_flyer,area_cf_zone_buzzards,area_cf_zone_hyannis,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,cape_flyer,area_cf_zone_buzzards,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,cape_flyer,area_cf_zone_buzzards,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,cape_flyer,area_cf_zone_buzzards,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,cape_flyer,area_cf_zone_hyannis,area_cf_zone_buzzards,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,cape_flyer,area_cf_zone_hyannis,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,cape_flyer,area_cf_zone_hyannis,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,cape_flyer,area_cf_zone_hyannis,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,cape_flyer,area_cf_zone_hyannis,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,cape_flyer,area_commuter_rail_zone_1a,area_cf_zone_buzzards,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,cape_flyer,area_commuter_rail_zone_1a,area_cf_zone_hyannis,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,cape_flyer,area_commuter_rail_zone_2,area_cf_zone_buzzards,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,cape_flyer,area_commuter_rail_zone_2,area_cf_zone_hyannis,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,cape_flyer,area_commuter_rail_zone_4,area_cf_zone_buzzards,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,cape_flyer,area_commuter_rail_zone_4,area_cf_zone_hyannis,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,cape_flyer,area_commuter_rail_zone_8,area_cf_zone_hyannis,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,,
+leg_systemwide_free,cr_foxboro,,,prod_free_fare,timeframe_systemwide_free,,
diff --git a/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/fare_media.txt b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/fare_media.txt
new file mode 100644
index 000000000..a695de634
--- /dev/null
+++ b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/fare_media.txt
@@ -0,0 +1,5 @@
+fare_media_id,fare_media_name,fare_media_type
+cash,Cash,0
+credit_debit,Credit/debit card,0
+charlieticket,CharlieTicket,1
+mticket,mTicket app,4
diff --git a/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/fare_products.txt b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/fare_products.txt
new file mode 100644
index 000000000..88812f3a4
--- /dev/null
+++ b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/fare_products.txt
@@ -0,0 +1,97 @@
+fare_product_id,fare_product_name,rider_category_id,fare_media_id,amount,currency
+prod_boat_zone_1,Ferry Zone 1 one-way fare,HONORED_CITIZEN,cash,6.50,USD
+prod_boat_zone_1,Ferry Zone 1 one-way fare,,credit_debit,6.50,USD
+prod_boat_zone_1,Ferry Zone 1 one-way fare,,mticket,6.50,USD
+prod_boat_zone_1a,Ferry Zone 1A one-way fare,,cash,2.40,USD
+prod_boat_zone_1a,Ferry Zone 1A one-way fare,,credit_debit,2.40,USD
+prod_boat_zone_1a,Ferry Zone 1A one-way fare,,mticket,2.40,USD
+prod_boat_zone_2,Ferry Zone 2 one-way fare,,cash,7.00,USD
+prod_boat_zone_2,Ferry Zone 2 one-way fare,,credit_debit,7.00,USD
+prod_boat_zone_2,Ferry Zone 2 one-way fare,,mticket,7.00,USD
+prod_cape_buzzards_hyannis_fare,CapeFLYER Middleborough/Lakeville one-way fare,,cash,5.00,USD
+prod_cape_buzzards_hyannis_fare,CapeFLYER Middleborough/Lakeville one-way fare,,credit_debit,5.00,USD
+prod_cape_buzzards_hyannis_fare,CapeFLYER Middleborough/Lakeville one-way fare,,mticket,5.00,USD
+prod_cape_sbb_buzzards_fare,CapeFLYER Bourne one-way fare,,cash,20.00,USD
+prod_cape_sbb_buzzards_fare,CapeFLYER Bourne one-way fare,,credit_debit,20.00,USD
+prod_cape_sbb_buzzards_fare,CapeFLYER Bourne one-way fare,,mticket,20.00,USD
+prod_cape_sbb_hyannis_fare,CapeFLYER Hyannis one-way fare,,cash,22.00,USD
+prod_cape_sbb_hyannis_fare,CapeFLYER Hyannis one-way fare,,credit_debit,22.00,USD
+prod_cape_sbb_hyannis_fare,CapeFLYER Hyannis one-way fare,,mticket,22.00,USD
+prod_cr_inter_1,Commuter Rail Interzone 1 one-way fare,,cash,2.75,USD
+prod_cr_inter_1,Commuter Rail Interzone 1 one-way fare,,credit_debit,2.75,USD
+prod_cr_inter_1,Commuter Rail Interzone 1 one-way fare,,mticket,2.75,USD
+prod_cr_inter_10,Commuter Rail Interzone 10 one-way fare,,cash,7.25,USD
+prod_cr_inter_10,Commuter Rail Interzone 10 one-way fare,,credit_debit,7.25,USD
+prod_cr_inter_10,Commuter Rail Interzone 10 one-way fare,,mticket,7.25,USD
+prod_cr_inter_2,Commuter Rail Interzone 2 one-way fare,,cash,3.25,USD
+prod_cr_inter_2,Commuter Rail Interzone 2 one-way fare,,credit_debit,3.25,USD
+prod_cr_inter_2,Commuter Rail Interzone 2 one-way fare,,mticket,3.25,USD
+prod_cr_inter_3,Commuter Rail Interzone 3 one-way fare,,cash,3.50,USD
+prod_cr_inter_3,Commuter Rail Interzone 3 one-way fare,,credit_debit,3.50,USD
+prod_cr_inter_3,Commuter Rail Interzone 3 one-way fare,,mticket,3.50,USD
+prod_cr_inter_4,Commuter Rail Interzone 4 one-way fare,,cash,4.25,USD
+prod_cr_inter_4,Commuter Rail Interzone 4 one-way fare,,credit_debit,4.25,USD
+prod_cr_inter_4,Commuter Rail Interzone 4 one-way fare,,mticket,4.25,USD
+prod_cr_inter_5,Commuter Rail Interzone 5 one-way fare,,cash,4.75,USD
+prod_cr_inter_5,Commuter Rail Interzone 5 one-way fare,,credit_debit,4.75,USD
+prod_cr_inter_5,Commuter Rail Interzone 5 one-way fare,,mticket,4.75,USD
+prod_cr_inter_6,Commuter Rail Interzone 6 one-way fare,,cash,5.25,USD
+prod_cr_inter_6,Commuter Rail Interzone 6 one-way fare,,credit_debit,5.25,USD
+prod_cr_inter_6,Commuter Rail Interzone 6 one-way fare,,mticket,5.25,USD
+prod_cr_inter_7,Commuter Rail Interzone 7 one-way fare,,cash,5.75,USD
+prod_cr_inter_7,Commuter Rail Interzone 7 one-way fare,,credit_debit,5.75,USD
+prod_cr_inter_7,Commuter Rail Interzone 7 one-way fare,,mticket,5.75,USD
+prod_cr_inter_8,Commuter Rail Interzone 8 one-way fare,,cash,6.25,USD
+prod_cr_inter_8,Commuter Rail Interzone 8 one-way fare,,credit_debit,6.25,USD
+prod_cr_inter_8,Commuter Rail Interzone 8 one-way fare,,mticket,6.25,USD
+prod_cr_inter_9,Commuter Rail Interzone 9 one-way fare,,cash,6.75,USD
+prod_cr_inter_9,Commuter Rail Interzone 9 one-way fare,,credit_debit,6.75,USD
+prod_cr_inter_9,Commuter Rail Interzone 9 one-way fare,,mticket,6.75,USD
+prod_cr_zone_1,Commuter Rail Zone 1 one-way fare,,cash,6.50,USD
+prod_cr_zone_1,Commuter Rail Zone 1 one-way fare,,credit_debit,6.50,USD
+prod_cr_zone_1,Commuter Rail Zone 1 one-way fare,,mticket,6.50,USD
+prod_cr_zone_10,Commuter Rail Zone 10 one-way fare,,cash,13.25,USD
+prod_cr_zone_10,Commuter Rail Zone 10 one-way fare,,credit_debit,13.25,USD
+prod_cr_zone_10,Commuter Rail Zone 10 one-way fare,,mticket,13.25,USD
+prod_cr_zone_1a,Commuter Rail Zone 1A one-way fare,,cash,2.40,USD
+prod_cr_zone_1a,Commuter Rail Zone 1A one-way fare,,credit_debit,2.40,USD
+prod_cr_zone_1a,Commuter Rail Zone 1A one-way fare,,mticket,2.40,USD
+prod_cr_zone_2,Commuter Rail Zone 2 one-way fare,,cash,7.00,USD
+prod_cr_zone_2,Commuter Rail Zone 2 one-way fare,,credit_debit,7.00,USD
+prod_cr_zone_2,Commuter Rail Zone 2 one-way fare,,mticket,7.00,USD
+prod_cr_zone_3,Commuter Rail Zone 3 one-way fare,,cash,8.00,USD
+prod_cr_zone_3,Commuter Rail Zone 3 one-way fare,,credit_debit,8.00,USD
+prod_cr_zone_3,Commuter Rail Zone 3 one-way fare,,mticket,8.00,USD
+prod_cr_zone_4,Commuter Rail Zone 4 one-way fare,,cash,8.75,USD
+prod_cr_zone_4,Commuter Rail Zone 4 one-way fare,,credit_debit,8.75,USD
+prod_cr_zone_4,Commuter Rail Zone 4 one-way fare,,mticket,8.75,USD
+prod_cr_zone_5,Commuter Rail Zone 5 one-way fare,,cash,9.75,USD
+prod_cr_zone_5,Commuter Rail Zone 5 one-way fare,,credit_debit,9.75,USD
+prod_cr_zone_5,Commuter Rail Zone 5 one-way fare,,mticket,9.75,USD
+prod_cr_zone_6,Commuter Rail Zone 6 one-way fare,,cash,10.50,USD
+prod_cr_zone_6,Commuter Rail Zone 6 one-way fare,,credit_debit,10.50,USD
+prod_cr_zone_6,Commuter Rail Zone 6 one-way fare,,mticket,10.50,USD
+prod_cr_zone_7,Commuter Rail Zone 7 one-way fare,,cash,11.00,USD
+prod_cr_zone_7,Commuter Rail Zone 7 one-way fare,,credit_debit,11.00,USD
+prod_cr_zone_7,Commuter Rail Zone 7 one-way fare,,mticket,11.00,USD
+prod_cr_zone_8,Commuter Rail Zone 8 one-way fare,,cash,12.25,USD
+prod_cr_zone_8,Commuter Rail Zone 8 one-way fare,,credit_debit,12.25,USD
+prod_cr_zone_8,Commuter Rail Zone 8 one-way fare,,mticket,12.25,USD
+prod_cr_zone_9,Commuter Rail Zone 9 one-way fare,,cash,12.75,USD
+prod_cr_zone_9,Commuter Rail Zone 9 one-way fare,,credit_debit,12.75,USD
+prod_cr_zone_9,Commuter Rail Zone 9 one-way fare,,mticket,12.75,USD
+prod_express_bus,Express Bus cash fare,,cash,4.25,USD
+prod_ferry_east_boston,East Boston Ferry one-way fare,,mticket,2.40,USD
+prod_ferry_f1,Hingham/Hull Ferry one-way fare,,cash,9.75,USD
+prod_ferry_f1,Hingham/Hull Ferry one-way fare,,credit_debit,9.75,USD
+prod_ferry_f1,Hingham/Hull Ferry one-way fare,,mticket,9.75,USD
+prod_ferry_f4,Charlestown Ferry one-way fare,,cash,3.70,USD
+prod_ferry_f4,Charlestown Ferry one-way fare,,credit_debit,3.70,USD
+prod_ferry_f4,Charlestown Ferry one-way fare,,mticket,3.70,USD
+prod_foxboro_event_fare,Foxboro Event Service round-trip fare,,cash,20.00,USD
+prod_foxboro_event_fare,Foxboro Event Service round-trip fare,,credit_debit,20.00,USD
+prod_foxboro_event_fare,Foxboro Event Service round-trip fare,,mticket,20.00,USD
+prod_free_fare,Free fare,,,0.00,USD
+prod_local_bus,Local Bus cash fare,,cash,1.70,USD
+prod_rapid_transit_cash,Subway cash fare,,cash,2.40,USD
+prod_rapid_transit_quick_subway,Subway Quick Ticket,,charlieticket,2.40,USD
diff --git a/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/fare_transfer_rules.txt b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/fare_transfer_rules.txt
new file mode 100644
index 000000000..bccb48b0f
--- /dev/null
+++ b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/fare_transfer_rules.txt
@@ -0,0 +1,38 @@
+from_leg_group_id,to_leg_group_id,transfer_count,duration_limit,duration_limit_type,fare_transfer_type,fare_product_id,filter_fare_product_id,fare_media_behavior,fare_product_behavior
+leg_airport_rapid_transit_quick_subway,leg_local_bus_quick_subway,99,7200,1,0,prod_rapid_transit_quick_subway,prod_rapid_transit_quick_subway,0,1
+leg_airport_rapid_transit_quick_subway,leg_rapid_transit_quick_subway,,,1,0,prod_rapid_transit_quick_subway,prod_rapid_transit_quick_subway,0,1
+leg_mattapan_rapid_transit_quick_subway,leg_local_bus_quick_subway,,7200,1,0,prod_rapid_transit_quick_subway,prod_rapid_transit_quick_subway,0,1
+leg_mattapan_rapid_transit_quick_subway,leg_rail_replacement_quick_subway,,99,,0,,prod_rapid_transit_quick_subway,0,1
+leg_mattapan_rapid_transit_quick_subway,leg_rapid_transit_quick_subway,99999,7200,1,0,,prod_rapid_transit_quick_subway,0,1
+leg_mattapan_rapid_transit_quick_subway,leg_mattapan_rapid_transit_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1
+leg_rail_replacement_quick_subway,leg_airport_rapid_transit_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1
+leg_rail_replacement_quick_subway,leg_local_bus_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1
+leg_rail_replacement_quick_subway,leg_mattapan_rapid_transit_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1
+leg_rail_replacement_quick_subway,leg_rail_replacement_quick_subway,-1,,,0,,prod_rapid_transit_quick_subway,0,1
+leg_rail_replacement_quick_subway,leg_rapid_transit_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1
+leg_rail_replacement_quick_subway,leg_sl_rapid_transit_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1
+leg_rail_replacement_quick_subway,leg_ss_rapid_transit_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1
+leg_rapid_transit_cash,leg_rapid_transit_cash,-1,,,0,,prod_rapid_transit_cash,0,1
+leg_rapid_transit_cash,leg_sl_rapid_transit_cash,,,,0,,prod_rapid_transit_cash,0,1
+leg_rapid_transit_free,leg_rapid_transit_free,-1,,,0,,prod_free_fare,0,1
+leg_rapid_transit_free,leg_sl_rapid_transit_free,,,,0,,prod_free_fare,0,1
+leg_rapid_transit_quick_subway,leg_airport_rapid_transit_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1
+leg_rapid_transit_quick_subway,leg_local_bus_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1
+leg_rapid_transit_quick_subway,leg_mattapan_rapid_transit_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1
+leg_rapid_transit_quick_subway,leg_rail_replacement_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1
+leg_rapid_transit_quick_subway,leg_rapid_transit_quick_subway,-1,,,0,,prod_rapid_transit_quick_subway,0,1
+leg_rapid_transit_quick_subway,leg_sl_rapid_transit_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1
+leg_rapid_transit_quick_subway,leg_ss_rapid_transit_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1
+leg_sl_rapid_transit_cash,leg_ss_rapid_transit_cash,,,,0,,prod_rapid_transit_cash,0,1
+leg_sl_rapid_transit_free,leg_ss_rapid_transit_free,,,,0,,prod_free_fare,0,1
+leg_sl_rapid_transit_quick_subway,leg_airport_rapid_transit_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1
+leg_sl_rapid_transit_quick_subway,leg_local_bus_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1
+leg_sl_rapid_transit_quick_subway,leg_mattapan_rapid_transit_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1
+leg_sl_rapid_transit_quick_subway,leg_rail_replacement_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1
+leg_sl_rapid_transit_quick_subway,leg_ss_rapid_transit_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1
+leg_ss_rapid_transit_cash,leg_rapid_transit_cash,,,,0,,prod_rapid_transit_cash,0,1
+leg_ss_rapid_transit_free,leg_rapid_transit_free,,,,0,,prod_free_fare,0,1
+leg_ss_rapid_transit_quick_subway,leg_local_bus_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1
+leg_ss_rapid_transit_quick_subway,leg_mattapan_rapid_transit_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1
+leg_ss_rapid_transit_quick_subway,leg_rail_replacement_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1
+leg_ss_rapid_transit_quick_subway,leg_rapid_transit_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1
diff --git a/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/feed_info.txt b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/feed_info.txt
new file mode 100644
index 000000000..ceac60810
--- /dev/null
+++ b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/feed_info.txt
@@ -0,0 +1,2 @@
+feed_id,feed_publisher_name,feed_publisher_url,feed_lang,feed_version
+fake_transit,Conveyal,http://www.conveyal.com,en,1.0
\ No newline at end of file
diff --git a/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/networks.txt b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/networks.txt
new file mode 100644
index 000000000..47a67eaad
--- /dev/null
+++ b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/networks.txt
@@ -0,0 +1,2 @@
+network_id,network_name
+1,Forbidden because network id is defined in routes
\ No newline at end of file
diff --git a/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/rider_categories.txt b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/rider_categories.txt
new file mode 100644
index 000000000..d2bb3107a
--- /dev/null
+++ b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/rider_categories.txt
@@ -0,0 +1,4 @@
+rider_category_id,rider_category_name,min_age,max_age,eligibility_url,is_default_fare_category
+HONORED_CITIZEN,Honored Citizen,,,https://trimet.org/fares/honoredcitizen.htm,0
+ADULT,Adult,,,,1
+YOUTH,Youth,7,17,https://trimet.org/fares/youth.htm,0
diff --git a/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/route_networks.txt b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/route_networks.txt
new file mode 100644
index 000000000..d2f8f9a9d
--- /dev/null
+++ b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/route_networks.txt
@@ -0,0 +1,2 @@
+network_id,route_id
+1,1
\ No newline at end of file
diff --git a/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/routes.txt b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/routes.txt
new file mode 100644
index 000000000..dd6a30eaf
--- /dev/null
+++ b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/routes.txt
@@ -0,0 +1,2 @@
+route_id,agency_id,route_short_name,route_long_name,route_desc,route_type,route_url,route_color,route_text_color,route_sort_order,route_fare_class,line_id,listed_route,network_id
+1,1,,RL,RT,1,https://www.mbta.com/schedules/Red,DA291C,FFFFFF,10010,Rapid Transit,line-Red,,rapid_transit
\ No newline at end of file
diff --git a/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/shapes.txt b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/shapes.txt
new file mode 100644
index 000000000..3f2e3fd13
--- /dev/null
+++ b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/shapes.txt
@@ -0,0 +1,8 @@
+shape_id,shape_pt_lat,shape_pt_lon,shape_pt_sequence,shape_dist_traveled
+5820f377-f947-4728-ac29-ac0102cbc34e,37.0612132,-122.0074332,1,0.0000000
+5820f377-f947-4728-ac29-ac0102cbc34e,37.0611720,-122.0075000,2,7.4997067
+5820f377-f947-4728-ac29-ac0102cbc34e,37.0613590,-122.0076830,3,33.8739075
+5820f377-f947-4728-ac29-ac0102cbc34e,37.0608780,-122.0082780,4,109.0402932
+5820f377-f947-4728-ac29-ac0102cbc34e,37.0603590,-122.0088280,5,184.6078298
+5820f377-f947-4728-ac29-ac0102cbc34e,37.0597610,-122.0093540,6,265.8053023
+5820f377-f947-4728-ac29-ac0102cbc34e,37.0590660,-122.0099190,7,357.8617018
diff --git a/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/stop_areas.txt b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/stop_areas.txt
new file mode 100644
index 000000000..85cbda304
--- /dev/null
+++ b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/stop_areas.txt
@@ -0,0 +1,848 @@
+stop_id,area_id
+4u6g,area_route_426_downtown
+4u6g,area_route_450_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_sl_logan_terminal
+4u6g,area_sl_logan_terminal
+4u6g,area_sl_logan_terminal
+4u6g,area_sl_logan_terminal
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_sl_logan_terminal
+4u6g,area_route_426_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_downtown
+4u6g,area_route_354_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_ol_state
+4u6g,area_ol_state
+4u6g,area_bl
+4u6g,area_bl
+4u6g,area_bl
+4u6g,area_bl
+4u6g,area_bl
+4u6g,area_bl
+4u6g,area_bl
+4u6g,area_bl
+4u6g,area_bl
+4u6g,area_bl_airport
+4u6g,area_bl_airport
+4u6g,area_bl
+4u6g,area_bl
+4u6g,area_bl
+4u6g,area_bl
+4u6g,area_bl
+4u6g,area_bl
+4u6g,area_bl
+4u6g,area_bl
+4u6g,area_bl
+4u6g,area_bl
+4u6g,area_bl
+4u6g,area_bl
+4u6g,area_red_south_station
+4u6g,area_red_south_station
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_green_b_west_of_kenmore
+4u6g,area_gl_govt_ctr
+4u6g,area_gl_govt_ctr
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_c_west_of_kenmore
+4u6g,area_green_e_west_of_symphony
+4u6g,area_green_e_west_of_symphony
+4u6g,area_green_e_west_of_symphony
+4u6g,area_green_e_west_of_symphony
+4u6g,area_green_e_west_of_symphony
+4u6g,area_green_e_west_of_symphony
+4u6g,area_green_e_west_of_symphony
+4u6g,area_green_e_west_of_symphony
+4u6g,area_green_e_west_of_symphony
+4u6g,area_green_e_west_of_symphony
+4u6g,area_green_e_west_of_symphony
+4u6g,area_green_e_west_of_symphony
+4u6g,area_green_e_west_of_symphony
+4u6g,area_green_e_west_of_symphony
+4u6g,area_green_e_west_of_symphony
+4u6g,area_green_e_west_of_symphony
+4u6g,area_green_e_west_of_symphony
+4u6g,area_m_ashmont_mattapan
+4u6g,area_m_ashmont_mattapan
+4u6g,area_m_ashmont_mattapan
+4u6g,area_bl
+4u6g,area_sl_airport
+4u6g,area_sl_airport
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_450_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_426_outside_downtown
+4u6g,area_sl_south_station
+4u6g,area_sl_courthouse
+4u6g,area_sl_world_trade_center
+4u6g,area_sl_silver_line_way
+4u6g,area_sl_world_trade_center
+4u6g,area_sl_courthouse
+4u6g,area_sl_south_station
+4u6g,area_sl_silver_line_way
+4u6g,area_sl3_north_of_airport
+4u6g,area_sl3_north_of_airport
+4u6g,area_sl3_north_of_airport
+4u6g,area_sl3_north_of_airport
+4u6g,area_sl3_north_of_airport
+4u6g,area_sl3_north_of_airport
+4u6g,area_sl3_north_of_airport
+4u6g,area_sl3_north_of_airport
+4u6g,area_route_426_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_route_426_downtown
+4u6g,area_route_450_downtown
+4u6g,area_route_354_outside_downtown
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_cf_zone_buzzards
+4u6g,area_cf_zone_buzzards
+4u6g,area_cf_zone_buzzards
+4u6g,area_cf_zone_hyannis
+4u6g,area_commuter_rail_zone_2
+4u6g,area_fairmount_line_zone_1a
+4u6g,area_fairmount_line_zone_1a
+4u6g,area_fairmount_line_zone_1a
+4u6g,area_fairmount_line_zone_1a
+4u6g,area_fairmount_line_zone_1a
+4u6g,area_fairmount_line_zone_1a
+4u6g,area_fairmount_line_zone_1a
+4u6g,area_fairmount_line_zone_1a
+4u6g,area_fairmount_line_zone_1a
+4u6g,area_fairmount_line_zone_1a
+4u6g,area_fairmount_line_zone_1a
+4u6g,area_fairmount_line_zone_1a
+4u6g,area_fairmount_line_zone_1a
+4u6g,area_fairmount_line_zone_1a
+4u6g,area_fairmount_line_zone_1a
+4u6g,area_fairmount_line_zone_1a
+4u6g,area_fairmount_line_zone_1a
+4u6g,area_fairmount_line_zone_1a
+4u6g,area_fairmount_line_zone_1a
+4u6g,area_fairmount_line_zone_1a
+4u6g,area_fairmount_line_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_5
+4u6g,area_commuter_rail_zone_6
+4u6g,area_commuter_rail_zone_6
+4u6g,area_commuter_rail_porter_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_porter_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_porter_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_5
+4u6g,area_commuter_rail_zone_5
+4u6g,area_commuter_rail_zone_5
+4u6g,area_commuter_rail_zone_5
+4u6g,area_commuter_rail_zone_5
+4u6g,area_commuter_rail_zone_5
+4u6g,area_commuter_rail_zone_6
+4u6g,area_commuter_rail_zone_6
+4u6g,area_commuter_rail_zone_6
+4u6g,area_commuter_rail_zone_7
+4u6g,area_commuter_rail_zone_7
+4u6g,area_commuter_rail_zone_7
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_sumner_tunnel_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_5
+4u6g,area_commuter_rail_zone_6
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_5
+4u6g,area_commuter_rail_zone_6
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_10
+4u6g,area_commuter_rail_zone_9
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_7
+4u6g,area_commuter_rail_zone_7
+4u6g,area_commuter_rail_zone_7
+4u6g,area_commuter_rail_zone_7
+4u6g,area_commuter_rail_zone_7
+4u6g,area_commuter_rail_zone_7
+4u6g,area_commuter_rail_zone_6
+4u6g,area_commuter_rail_zone_6
+4u6g,area_commuter_rail_zone_6
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_ss_commuter_rail_zone_1a
+4u6g,area_ss_commuter_rail_zone_1a
+4u6g,area_ss_commuter_rail_zone_1a
+4u6g,area_ss_commuter_rail_zone_1a
+4u6g,area_ss_commuter_rail_zone_1a
+4u6g,area_ss_commuter_rail_zone_1a
+4u6g,area_ss_commuter_rail_zone_1a
+4u6g,area_ss_commuter_rail_zone_1a
+4u6g,area_ss_commuter_rail_zone_1a
+4u6g,area_ss_commuter_rail_zone_1a
+4u6g,area_ss_commuter_rail_zone_1a
+4u6g,area_ss_commuter_rail_zone_1a
+4u6g,area_ss_commuter_rail_zone_1a
+4u6g,area_ss_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_5
+4u6g,area_commuter_rail_zone_5
+4u6g,area_commuter_rail_zone_5
+4u6g,area_commuter_rail_zone_6
+4u6g,area_commuter_rail_zone_6
+4u6g,area_commuter_rail_zone_6
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_5
+4u6g,area_commuter_rail_zone_6
+4u6g,area_commuter_rail_zone_7
+4u6g,area_commuter_rail_zone_7
+4u6g,area_commuter_rail_zone_7
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_5
+4u6g,area_commuter_rail_zone_5
+4u6g,area_commuter_rail_zone_5
+4u6g,area_commuter_rail_zone_6
+4u6g,area_commuter_rail_zone_6
+4u6g,area_commuter_rail_zone_6
+4u6g,area_commuter_rail_zone_6
+4u6g,area_commuter_rail_zone_6
+4u6g,area_commuter_rail_zone_6
+4u6g,area_commuter_rail_zone_7
+4u6g,area_commuter_rail_zone_7
+4u6g,area_commuter_rail_zone_7
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_8
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1a
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_1
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_2
+4u6g,area_commuter_rail_zone_3
+4u6g,area_commuter_rail_zone_4
+4u6g,area_commuter_rail_zone_5
+4u6g,area_commuter_rail_zone_6
+4u6g,area_commuter_rail_zone_7
+4u6g,area_commuter_rail_zone_7
+4u6g,area_commuter_rail_zone_7
+4u6g,area_commuter_rail_zone_7
+4u6g,area_commuter_rail_zone_7
+4u6g,area_commuter_rail_zone_7
\ No newline at end of file
diff --git a/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/stop_times.txt b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/stop_times.txt
new file mode 100644
index 000000000..1cfefaa49
--- /dev/null
+++ b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/stop_times.txt
@@ -0,0 +1,7 @@
+trip_id,arrival_time,departure_time,stop_id,stop_sequence,stop_headsign,pickup_type,drop_off_type,shape_dist_traveled,timepoint
+a30277f8-e50a-4a85-9141-b1e0da9d429d,07:00:00,07:00:00,4u6g,1,Test stop headsign,0,0,0.0000000,
+a30277f8-e50a-4a85-9141-b1e0da9d429d,07:01:00,07:01:00,johv,2,Test stop headsign 2,0,0,341.4491961,
+frequency-trip,08:00:00,08:00:00,4u6g,1,Test stop headsign frequency trip,0,0,0.0000000,
+frequency-trip,08:29:00,08:29:00,1234,2,Test stop headsign frequency trip 2,0,0,341.4491961,
+calendar-date-trip,08:00:00,08:00:00,4u6g,1,Test stop headsign calendar date trip,0,0,0.0000000,
+calendar-date-trip,08:29:00,08:29:00,1234,2,Test stop headsign calendar date trip 2,0,0,341.4491961,
\ No newline at end of file
diff --git a/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/stops.txt b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/stops.txt
new file mode 100644
index 000000000..ba368047e
--- /dev/null
+++ b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/stops.txt
@@ -0,0 +1,5 @@
+stop_id,stop_code,stop_name,stop_desc,stop_lat,stop_lon,zone_id,stop_url,location_type,parent_station,stop_timezone,wheelchair_boarding
+4u6g,,Butler Ln,,37.0612132,-122.0074332,,,0,,,
+johv,,Scotts Valley Dr & Victor Sq,,37.0590172,-122.0096058,,,0,,,
+123,,Parent Station,,37.0666,-122.0777,,,1,,,
+1234,,Child Stop,,37.06662,-122.07772,,,0,123,,
\ No newline at end of file
diff --git a/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/timeframes.txt b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/timeframes.txt
new file mode 100644
index 000000000..52667abe1
--- /dev/null
+++ b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/timeframes.txt
@@ -0,0 +1,8 @@
+timeframe_group_id,start_time,end_time,service_id
+timeframe_regular,,,04100312-8fe1-46a5-a9f2-556f39478f57
+timeframe_systemwide_free,,,04100312-8fe1-46a5-a9f2-556f39478f57
+timeframe_sumner_tunnel_closure,,,04100312-8fe1-46a5-a9f2-556f39478f57
+timeframe_sumner_tunnel_closure,00:00:00,02:30:00,04100312-8fe1-46a5-a9f2-556f39478f57
+timeframe_regular,02:30:00,24:00:00,04100312-8fe1-46a5-a9f2-556f39478f57
+timeframe_alewife_kendall_surge,,,04100312-8fe1-46a5-a9f2-556f39478f57
+timeframe_alewife_kendall_surge,00:00:00,02:30:00,04100312-8fe1-46a5-a9f2-556f39478f57
diff --git a/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/trips.txt b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/trips.txt
new file mode 100644
index 000000000..982c01e0f
--- /dev/null
+++ b/src/test/resources/com/conveyal/datatools/gtfs/fake-agency-with-fares-v2/trips.txt
@@ -0,0 +1,4 @@
+route_id,trip_id,trip_headsign,trip_short_name,direction_id,block_id,shape_id,bikes_allowed,wheelchair_accessible,service_id
+1,a30277f8-e50a-4a85-9141-b1e0da9d429d,,,0,,5820f377-f947-4728-ac29-ac0102cbc34e,0,0,04100312-8fe1-46a5-a9f2-556f39478f57
+1,frequency-trip,,,0,,5820f377-f947-4728-ac29-ac0102cbc34e,0,0,04100312-8fe1-46a5-a9f2-556f39478f57
+1,calendar-date-trip,,,0,,5820f377-f947-4728-ac29-ac0102cbc34e,0,0,calendar-date-service
\ No newline at end of file
diff --git a/src/test/resources/com/conveyal/datatools/mock-aws-credentials b/src/test/resources/com/conveyal/datatools/mock-aws-credentials
new file mode 100644
index 000000000..6835e25cc
--- /dev/null
+++ b/src/test/resources/com/conveyal/datatools/mock-aws-credentials
@@ -0,0 +1,3 @@
+[default]
+aws_access_key_id = mock-id-123
+aws_secret_access_key = mock-secret-123