@@ -0,0 +1,103 @@ |
||
| 1 |
+module Agents |
|
| 2 |
+ class GoogleFlightsAgent < Agent |
|
| 3 |
+ include FormConfigurable |
|
| 4 |
+ |
|
| 5 |
+ cannot_receive_events! |
|
| 6 |
+ default_schedule "every_12h" |
|
| 7 |
+ |
|
| 8 |
+ |
|
| 9 |
+ description <<-MD |
|
| 10 |
+ The GoogleFlightsAgent will tell you the minimum airline prices between a pair of cities. The api limit is 50 requests/day. |
|
| 11 |
+ |
|
| 12 |
+ Follow their documentation here (https://developers.google.com/qpx-express/v1/prereqs#get-a-google-account) to retrieve an api key. |
|
| 13 |
+ After you get to the google developer console, created a project, enabled qpx express api then you can choose `api key` credential to be created. |
|
| 14 |
+ |
|
| 15 |
+ The `origin` and `destination` options require an [airport code](http://www.expedia.com/daily/airports/AirportCodes.asp). |
|
| 16 |
+ |
|
| 17 |
+ All the default options must exist. For `infantInSeatCount`, `infantInLapCount`, `seniorCount`, and `childCount`, leave them to the default value of `0` if its not necessary. |
|
| 18 |
+ |
|
| 19 |
+ Make sure `date` is in this type of date format `YYYY-MO-DAY`. |
|
| 20 |
+ |
|
| 21 |
+ You can limit the number of `solutions` returned back. The first solution is the lowest priced ticket. |
|
| 22 |
+ MD |
|
| 23 |
+ |
|
| 24 |
+ event_description <<-MD |
|
| 25 |
+ The event payload will have objects that contains valuable data like this |
|
| 26 |
+ |
|
| 27 |
+ "carrier": [ |
|
| 28 |
+ {
|
|
| 29 |
+ "code": "B6", |
|
| 30 |
+ "name": "Jetblue Airways Corporation" |
|
| 31 |
+ } |
|
| 32 |
+ ] |
|
| 33 |
+ |
|
| 34 |
+ "tripOption": [ |
|
| 35 |
+ "saleTotal": "USD49.10" |
|
| 36 |
+ "slice": [ |
|
| 37 |
+ ... |
|
| 38 |
+ ... |
|
| 39 |
+ "flight": {
|
|
| 40 |
+ "carrier": "B6", |
|
| 41 |
+ "number": "833" |
|
| 42 |
+ } |
|
| 43 |
+ ] |
|
| 44 |
+ ] |
|
| 45 |
+ |
|
| 46 |
+ MD |
|
| 47 |
+ |
|
| 48 |
+ def default_options |
|
| 49 |
+ {
|
|
| 50 |
+ 'qpx_api_key' => '', |
|
| 51 |
+ 'adultCount'=> 1, |
|
| 52 |
+ 'origin' => 'BOS', |
|
| 53 |
+ 'destination' => 'SFO', |
|
| 54 |
+ 'date' => '2016-04-11', |
|
| 55 |
+ 'childCount' => 0, |
|
| 56 |
+ 'infantInSeatCount' => 0, |
|
| 57 |
+ 'infantInLapCount'=> 0, |
|
| 58 |
+ 'seniorCount'=> 0, |
|
| 59 |
+ 'solutions'=> 3 |
|
| 60 |
+ } |
|
| 61 |
+ end |
|
| 62 |
+ |
|
| 63 |
+ form_configurable :qpx_api_key, type: :string |
|
| 64 |
+ form_configurable :adultCount |
|
| 65 |
+ form_configurable :origin, type: :string |
|
| 66 |
+ form_configurable :destination, type: :string |
|
| 67 |
+ form_configurable :date, type: :string |
|
| 68 |
+ form_configurable :childCount |
|
| 69 |
+ form_configurable :infantInSeatCount |
|
| 70 |
+ form_configurable :infantInLapCount |
|
| 71 |
+ form_configurable :seniorCount |
|
| 72 |
+ form_configurable :solutions |
|
| 73 |
+ |
|
| 74 |
+ def validate_options |
|
| 75 |
+ errors.add(:base, "You need a qpx api key") unless options['qpx_api_key'].present? |
|
| 76 |
+ errors.add(:base, "Adult Count must exist") unless options['adultCount'].present? |
|
| 77 |
+ errors.add(:base, "Origin must exist") unless options['origin'].present? |
|
| 78 |
+ errors.add(:base, "Destination must exist") unless options['destination'].present? |
|
| 79 |
+ errors.add(:base, "Date must exist") unless options['date'].present? |
|
| 80 |
+ errors.add(:base, "Child Count") unless options['childCount'].present? |
|
| 81 |
+ errors.add(:base, "Infant In Seat Count must exist") unless options['infantInSeatCount'].present? |
|
| 82 |
+ errors.add(:base, "Infant In Lap Count") unless options['infantInLapCount'].present? |
|
| 83 |
+ errors.add(:base, "Senior Count must exist") unless options['seniorCount'].present? |
|
| 84 |
+ errors.add(:base, "Solutions must exist") unless options['solutions'].present? |
|
| 85 |
+ end |
|
| 86 |
+ |
|
| 87 |
+ def working? |
|
| 88 |
+ !recent_error_logs? |
|
| 89 |
+ end |
|
| 90 |
+ |
|
| 91 |
+ def check |
|
| 92 |
+ post_params = {:request=>{:passengers=>{:kind=>"qpxexpress#passengerCounts", :adultCount=> interpolated["adultCount"], :childCount=> interpolated["childCount"], :infantInLapCount=>interpolated["infantInLapCount"], :infantInSeatCount=>interpolated['infantInSeatCount'], :seniorCount=>interpolated["seniorCount"]}, :slice=>[{:kind=>"qpxexpress#sliceInput", :origin=> interpolated["origin"].to_s , :destination=> interpolated["destination"].to_s , :date=> interpolated["date"].to_s }], :solutions=> interpolated["solutions"]}}
|
|
| 93 |
+ body = JSON.generate(post_params) |
|
| 94 |
+ request = HTTParty.post(event_url, :body => body, :headers => {"Content-Type" => "application/json"})
|
|
| 95 |
+ events = JSON.parse request.body |
|
| 96 |
+ create_event :payload => events |
|
| 97 |
+ end |
|
| 98 |
+ |
|
| 99 |
+ def event_url |
|
| 100 |
+ endpoint = 'https://www.googleapis.com/qpxExpress/v1/trips/search?key=' + "#{URI.encode(interpolated[:qpx_api_key].to_s)}"
|
|
| 101 |
+ end |
|
| 102 |
+ end |
|
| 103 |
+end |
@@ -0,0 +1,184 @@ |
||
| 1 |
+{
|
|
| 2 |
+ "kind": "qpxExpress#tripsSearch", |
|
| 3 |
+ "trips": {
|
|
| 4 |
+ "kind": "qpxexpress#tripOptions", |
|
| 5 |
+ "requestId": "a5KWNgUILMQXl0QjZ0O1Kj", |
|
| 6 |
+ "data": {
|
|
| 7 |
+ "kind": "qpxexpress#data", |
|
| 8 |
+ "airport": [ |
|
| 9 |
+ {
|
|
| 10 |
+ "kind": "qpxexpress#airportData", |
|
| 11 |
+ "code": "BOS", |
|
| 12 |
+ "city": "BOS", |
|
| 13 |
+ "name": "Boston Logan International" |
|
| 14 |
+ }, |
|
| 15 |
+ {
|
|
| 16 |
+ "kind": "qpxexpress#airportData", |
|
| 17 |
+ "code": "SFO", |
|
| 18 |
+ "city": "SFO", |
|
| 19 |
+ "name": "San Francisco International" |
|
| 20 |
+ } |
|
| 21 |
+ ], |
|
| 22 |
+ "city": [ |
|
| 23 |
+ {
|
|
| 24 |
+ "kind": "qpxexpress#cityData", |
|
| 25 |
+ "code": "BOS", |
|
| 26 |
+ "name": "Boston" |
|
| 27 |
+ }, |
|
| 28 |
+ {
|
|
| 29 |
+ "kind": "qpxexpress#cityData", |
|
| 30 |
+ "code": "SFO", |
|
| 31 |
+ "name": "San Francisco" |
|
| 32 |
+ } |
|
| 33 |
+ ], |
|
| 34 |
+ "aircraft": [ |
|
| 35 |
+ {
|
|
| 36 |
+ "kind": "qpxexpress#aircraftData", |
|
| 37 |
+ "code": "320", |
|
| 38 |
+ "name": "Airbus A320" |
|
| 39 |
+ } |
|
| 40 |
+ ], |
|
| 41 |
+ "tax": [ |
|
| 42 |
+ {
|
|
| 43 |
+ "kind": "qpxexpress#taxData", |
|
| 44 |
+ "id": "ZP", |
|
| 45 |
+ "name": "US Flight Segment Tax" |
|
| 46 |
+ }, |
|
| 47 |
+ {
|
|
| 48 |
+ "kind": "qpxexpress#taxData", |
|
| 49 |
+ "id": "AY_001", |
|
| 50 |
+ "name": "US September 11th Security Fee" |
|
| 51 |
+ }, |
|
| 52 |
+ {
|
|
| 53 |
+ "kind": "qpxexpress#taxData", |
|
| 54 |
+ "id": "US_001", |
|
| 55 |
+ "name": "US Transportation Tax" |
|
| 56 |
+ }, |
|
| 57 |
+ {
|
|
| 58 |
+ "kind": "qpxexpress#taxData", |
|
| 59 |
+ "id": "XF", |
|
| 60 |
+ "name": "US Passenger Facility Charge" |
|
| 61 |
+ } |
|
| 62 |
+ ], |
|
| 63 |
+ "carrier": [ |
|
| 64 |
+ {
|
|
| 65 |
+ "kind": "qpxexpress#carrierData", |
|
| 66 |
+ "code": "B6", |
|
| 67 |
+ "name": "Jetblue Airways Corporation" |
|
| 68 |
+ } |
|
| 69 |
+ ] |
|
| 70 |
+ }, |
|
| 71 |
+ "tripOption": [ |
|
| 72 |
+ {
|
|
| 73 |
+ "kind": "qpxexpress#tripOption", |
|
| 74 |
+ "saleTotal": "USD244.10", |
|
| 75 |
+ "id": "SD5atWDaIWFUAOehDmedl8001", |
|
| 76 |
+ "slice": [ |
|
| 77 |
+ {
|
|
| 78 |
+ "kind": "qpxexpress#sliceInfo", |
|
| 79 |
+ "duration": 404, |
|
| 80 |
+ "segment": [ |
|
| 81 |
+ {
|
|
| 82 |
+ "kind": "qpxexpress#segmentInfo", |
|
| 83 |
+ "duration": 404, |
|
| 84 |
+ "flight": {
|
|
| 85 |
+ "carrier": "B6", |
|
| 86 |
+ "number": "833" |
|
| 87 |
+ }, |
|
| 88 |
+ "id": "Gv+0Syg7VaA8NyWp", |
|
| 89 |
+ "cabin": "COACH", |
|
| 90 |
+ "bookingCode": "Z", |
|
| 91 |
+ "bookingCodeCount": 7, |
|
| 92 |
+ "marriedSegmentGroup": "0", |
|
| 93 |
+ "leg": [ |
|
| 94 |
+ {
|
|
| 95 |
+ "kind": "qpxexpress#legInfo", |
|
| 96 |
+ "id": "LXRlPXVU35MvdfE-", |
|
| 97 |
+ "aircraft": "320", |
|
| 98 |
+ "arrivalTime": "2016-04-11T23:10-07:00", |
|
| 99 |
+ "departureTime": "2016-04-11T19:26-04:00", |
|
| 100 |
+ "origin": "BOS", |
|
| 101 |
+ "destination": "SFO", |
|
| 102 |
+ "originTerminal": "C", |
|
| 103 |
+ "destinationTerminal": "I", |
|
| 104 |
+ "duration": 404, |
|
| 105 |
+ "onTimePerformance": 60, |
|
| 106 |
+ "mileage": 2697, |
|
| 107 |
+ "secure": true |
|
| 108 |
+ } |
|
| 109 |
+ ] |
|
| 110 |
+ } |
|
| 111 |
+ ] |
|
| 112 |
+ } |
|
| 113 |
+ ], |
|
| 114 |
+ "pricing": [ |
|
| 115 |
+ {
|
|
| 116 |
+ "kind": "qpxexpress#pricingInfo", |
|
| 117 |
+ "fare": [ |
|
| 118 |
+ {
|
|
| 119 |
+ "kind": "qpxexpress#fareInfo", |
|
| 120 |
+ "id": "Aw9SUGx6XiV/Eeby73yJz5cu1avySnHVCBCW01QV6wk2", |
|
| 121 |
+ "carrier": "B6", |
|
| 122 |
+ "origin": "BOS", |
|
| 123 |
+ "destination": "SFO", |
|
| 124 |
+ "basisCode": "ZH4AUEN" |
|
| 125 |
+ } |
|
| 126 |
+ ], |
|
| 127 |
+ "segmentPricing": [ |
|
| 128 |
+ {
|
|
| 129 |
+ "kind": "qpxexpress#segmentPricing", |
|
| 130 |
+ "fareId": "Aw9SUGx6XiV/Eeby73yJz5cu1avySnHVCBCW01QV6wk2", |
|
| 131 |
+ "segmentId": "Gv+0Syg7VaA8NyWp" |
|
| 132 |
+ } |
|
| 133 |
+ ], |
|
| 134 |
+ "baseFareTotal": "USD213.95", |
|
| 135 |
+ "saleFareTotal": "USD213.95", |
|
| 136 |
+ "saleTaxTotal": "USD30.15", |
|
| 137 |
+ "saleTotal": "USD244.10", |
|
| 138 |
+ "passengers": {
|
|
| 139 |
+ "kind": "qpxexpress#passengerCounts", |
|
| 140 |
+ "adultCount": 1 |
|
| 141 |
+ }, |
|
| 142 |
+ "tax": [ |
|
| 143 |
+ {
|
|
| 144 |
+ "kind": "qpxexpress#taxInfo", |
|
| 145 |
+ "id": "US_001", |
|
| 146 |
+ "chargeType": "GOVERNMENT", |
|
| 147 |
+ "code": "US", |
|
| 148 |
+ "country": "US", |
|
| 149 |
+ "salePrice": "USD16.05" |
|
| 150 |
+ }, |
|
| 151 |
+ {
|
|
| 152 |
+ "kind": "qpxexpress#taxInfo", |
|
| 153 |
+ "id": "AY_001", |
|
| 154 |
+ "chargeType": "GOVERNMENT", |
|
| 155 |
+ "code": "AY", |
|
| 156 |
+ "country": "US", |
|
| 157 |
+ "salePrice": "USD5.60" |
|
| 158 |
+ }, |
|
| 159 |
+ {
|
|
| 160 |
+ "kind": "qpxexpress#taxInfo", |
|
| 161 |
+ "id": "XF", |
|
| 162 |
+ "chargeType": "GOVERNMENT", |
|
| 163 |
+ "code": "XF", |
|
| 164 |
+ "country": "US", |
|
| 165 |
+ "salePrice": "USD4.50" |
|
| 166 |
+ }, |
|
| 167 |
+ {
|
|
| 168 |
+ "kind": "qpxexpress#taxInfo", |
|
| 169 |
+ "id": "ZP", |
|
| 170 |
+ "chargeType": "GOVERNMENT", |
|
| 171 |
+ "code": "ZP", |
|
| 172 |
+ "country": "US", |
|
| 173 |
+ "salePrice": "USD4.00" |
|
| 174 |
+ } |
|
| 175 |
+ ], |
|
| 176 |
+ "fareCalculation": "BOS B6 SFO 213.95ZH4AUEN USD 213.95 END ZP BOS XT 16.05US 4.00ZP 5.60AY 4.50XF BOS4.50", |
|
| 177 |
+ "latestTicketingTime": "2016-03-24T23:59-04:00", |
|
| 178 |
+ "ptc": "ADT" |
|
| 179 |
+ } |
|
| 180 |
+ ] |
|
| 181 |
+ } |
|
| 182 |
+ ] |
|
| 183 |
+ } |
|
| 184 |
+} |
@@ -0,0 +1,98 @@ |
||
| 1 |
+require 'rails_helper' |
|
| 2 |
+ |
|
| 3 |
+describe Agents::GoogleFlightsAgent do |
|
| 4 |
+ before do |
|
| 5 |
+ |
|
| 6 |
+ stub_request(:post, "https://www.googleapis.com/qpxExpress/v1/trips/search?key=800deeaf-e285-9d62-bc90-j999c1973cc9").to_return( |
|
| 7 |
+ :body => File.read(Rails.root.join("spec/data_fixtures/qpx.json")),
|
|
| 8 |
+ :status => 200, |
|
| 9 |
+ :headers => {"Content-Type" => "application/json"}
|
|
| 10 |
+ ) |
|
| 11 |
+ |
|
| 12 |
+ @opts = {
|
|
| 13 |
+ 'qpx_api_key' => '800deeaf-e285-9d62-bc90-j999c1973cc9', |
|
| 14 |
+ 'adultCount' => 1, |
|
| 15 |
+ 'origin' => 'BOS', |
|
| 16 |
+ 'destination' => 'SFO', |
|
| 17 |
+ 'date' => '2016-04-11', |
|
| 18 |
+ 'childCount' => 0, |
|
| 19 |
+ 'infantInSeatCount' => 0, |
|
| 20 |
+ 'infantInLapCount'=> 0, |
|
| 21 |
+ 'seniorCount'=> 0, |
|
| 22 |
+ 'solutions'=> 3 |
|
| 23 |
+ } |
|
| 24 |
+ |
|
| 25 |
+ @checker = Agents::GoogleFlightsAgent.new(:name => "tectonic", :options => @opts) |
|
| 26 |
+ @checker.user = users(:bob) |
|
| 27 |
+ @checker.save! |
|
| 28 |
+ end |
|
| 29 |
+ |
|
| 30 |
+ describe '#helpers' do |
|
| 31 |
+ it "should generate the correct events url" do |
|
| 32 |
+ expect(@checker.send(:event_url)).to eq("https://www.googleapis.com/qpxExpress/v1/trips/search?key=800deeaf-e285-9d62-bc90-j999c1973cc9")
|
|
| 33 |
+ end |
|
| 34 |
+ end |
|
| 35 |
+ |
|
| 36 |
+ describe "#that checker should be valid" do |
|
| 37 |
+ it "should check that the object is valid" do |
|
| 38 |
+ expect(@checker).to be_valid |
|
| 39 |
+ end |
|
| 40 |
+ |
|
| 41 |
+ it "should require credentials" do |
|
| 42 |
+ @checker.options['qpx_api_key'] = nil |
|
| 43 |
+ expect(@checker).not_to be_valid |
|
| 44 |
+ end |
|
| 45 |
+ |
|
| 46 |
+ it "should require adultCount" do |
|
| 47 |
+ @checker.options['adultCount'] = nil |
|
| 48 |
+ expect(@checker).not_to be_valid |
|
| 49 |
+ end |
|
| 50 |
+ |
|
| 51 |
+ it "should require Origin" do |
|
| 52 |
+ @checker.options['origin'] = nil |
|
| 53 |
+ expect(@checker).not_to be_valid |
|
| 54 |
+ end |
|
| 55 |
+ |
|
| 56 |
+ it "should require Destination" do |
|
| 57 |
+ @checker.options['destination'] = nil |
|
| 58 |
+ expect(@checker).not_to be_valid |
|
| 59 |
+ end |
|
| 60 |
+ |
|
| 61 |
+ it "should require Date" do |
|
| 62 |
+ @checker.options['date'] = nil |
|
| 63 |
+ expect(@checker).not_to be_valid |
|
| 64 |
+ end |
|
| 65 |
+ |
|
| 66 |
+ it "should require childCount" do |
|
| 67 |
+ @checker.options['childCount'] = nil |
|
| 68 |
+ expect(@checker).not_to be_valid |
|
| 69 |
+ end |
|
| 70 |
+ |
|
| 71 |
+ it "should require Infant In Seat Count" do |
|
| 72 |
+ @checker.options['infantInSeatCount'] = nil |
|
| 73 |
+ expect(@checker).not_to be_valid |
|
| 74 |
+ end |
|
| 75 |
+ |
|
| 76 |
+ it "should require Infant In Lab Count" do |
|
| 77 |
+ @checker.options['infantInLapCount'] = nil |
|
| 78 |
+ expect(@checker).not_to be_valid |
|
| 79 |
+ end |
|
| 80 |
+ |
|
| 81 |
+ it "should require Senior Count" do |
|
| 82 |
+ @checker.options['seniorCount'] = nil |
|
| 83 |
+ expect(@checker).not_to be_valid |
|
| 84 |
+ end |
|
| 85 |
+ |
|
| 86 |
+ it "should require Solutions" do |
|
| 87 |
+ @checker.options['solutions'] = nil |
|
| 88 |
+ expect(@checker).not_to be_valid |
|
| 89 |
+ end |
|
| 90 |
+ end |
|
| 91 |
+ |
|
| 92 |
+ describe '#check' do |
|
| 93 |
+ it "should check that initial run creates an event" do |
|
| 94 |
+ @checker.memory[:latestTicketingTime] = '2016-03-24T23:59-04:00' |
|
| 95 |
+ expect { @checker.check }.to change { Event.count }.by(1)
|
|
| 96 |
+ end |
|
| 97 |
+ end |
|
| 98 |
+end |