From 4421b934dc664f090a62b08338efe3b5e4dd1110 Mon Sep 17 00:00:00 2001 From: 0x9ef <0x9ef@tutanota.com> Date: Thu, 2 May 2024 15:07:31 +0300 Subject: [PATCH] feat: add Batch Offer Requests (#12) * fix(duffel): fix panic when req.Body == nil * feat: add Batch Offer Requests --- .gitignore | 1 + batchofferrequests.go | 45 +++ batchofferrequests_test.go | 76 +++++ duffel.go | 12 +- fixtures/200-create-batch-offer-request.json | 10 + fixtures/200-get-batch-offer-request.json | 322 +++++++++++++++++++ request.go | 2 +- 7 files changed, 456 insertions(+), 12 deletions(-) create mode 100644 batchofferrequests.go create mode 100644 batchofferrequests_test.go create mode 100644 fixtures/200-create-batch-offer-request.json create mode 100644 fixtures/200-get-batch-offer-request.json diff --git a/.gitignore b/.gitignore index a438378..662d36b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,6 @@ # Dependency directories (remove the comment below to include it) # vendor/ +*.DS_store examples/.DS_Store .DS_Store \ No newline at end of file diff --git a/batchofferrequests.go b/batchofferrequests.go new file mode 100644 index 0000000..2f47f11 --- /dev/null +++ b/batchofferrequests.go @@ -0,0 +1,45 @@ +package duffel + +import ( + "context" +) + +type ( + BatchOfferRequestClient interface { + CreateBatchOfferRequest(ctx context.Context, requestInput CreateBatchOfferRequestInput) (*BatchOfferRequest, error) + GetBatchOfferRequest(ctx context.Context, id string) (*BatchOfferRequest, error) + } + + CreateBatchOfferRequestInput struct { + // The passengers who want to travel. If you specify an age for a passenger, the type may differ for the same passenger in different offers due to airline's different rules. e.g. one airline may treat a 14 year old as an adult, and another as a young adult. You may only specify an age or a type – not both. + Passengers []OfferRequestPassenger `json:"passengers" url:"-"` + // The slices that make up this offer request. One-way journeys can be expressed using one slice, whereas return trips will need two. + Slices []OfferRequestSlice `json:"slices" url:"-"` + // The cabin that the passengers want to travel in + CabinClass CabinClass `json:"cabin_class" url:"-"` + // The maximum number of connections within any slice of the offer. For example 0 means a direct flight which will have a single segment within each slice and 1 means a maximum of two segments within each slice of the offer. + MaxConnections *int `json:"max_connections,omitempty" url:"-"` + // The maximum amount of time in milliseconds to wait for each airline to respond + SupplierTimeout int `json:"-" url:"supplier_timeout,omitempty"` + } + + BatchOfferRequest struct { + TotalBatches int `json:"total_batches"` + RemainingBatches int `json:"remaining_batches"` + ID string `json:"id"` + Offers []Offer `json:"offers,omitempty"` + CreatedAt DateTime `json:"created_at"` + } +) + +func (a *API) CreateBatchOfferRequest(ctx context.Context, requestInput CreateBatchOfferRequestInput) (*BatchOfferRequest, error) { + return newRequestWithAPI[CreateBatchOfferRequestInput, BatchOfferRequest](a). + Post("/air/batch_offer_requests", &requestInput). + Single(ctx) +} + +func (a *API) GetBatchOfferRequest(ctx context.Context, id string) (*BatchOfferRequest, error) { + return newRequestWithAPI[EmptyPayload, BatchOfferRequest](a). + Getf("/air/batch_offer_requests/%s", id). + Single(ctx) +} diff --git a/batchofferrequests_test.go b/batchofferrequests_test.go new file mode 100644 index 0000000..fdd892e --- /dev/null +++ b/batchofferrequests_test.go @@ -0,0 +1,76 @@ +package duffel + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "gopkg.in/h2non/gock.v1" +) + +func TestCreateBatchOfferRequest(t *testing.T) { + defer gock.Off() + a := assert.New(t) + gock.New("https://api.duffel.com"). + Post("/air/batch_offer_requests"). + Reply(200). + SetHeader("Ratelimit-Limit", "5"). + SetHeader("Ratelimit-Remaining", "5"). + SetHeader("Ratelimit-Reset", time.Now().Format(time.RFC1123)). + SetHeader("Date", time.Now().Format(time.RFC1123)). + File("fixtures/200-create-batch-offer-request.json") + + ctx := context.TODO() + + client := New("duffel_test_123") + data, err := client.CreateBatchOfferRequest(ctx, CreateBatchOfferRequestInput{ + Passengers: []OfferRequestPassenger{ + { + FamilyName: "Earhardt", + GivenName: "Amelia", + Type: PassengerTypeAdult, + }, + { + Age: 14, + }, + }, + CabinClass: CabinClassEconomy, + Slices: []OfferRequestSlice{ + { + DepartureDate: Date(time.Now().AddDate(0, 0, 7)), + Origin: "JFK", + Destination: "AUS", + }, + }, + }) + a.NoError(err) + a.NotNil(data) + + a.Equal(7, data.RemainingBatches) + a.Equal(7, data.TotalBatches) +} + +func TestGetBatchOfferRequest(t *testing.T) { + defer gock.Off() + a := assert.New(t) + gock.New("https://api.duffel.com"). + Get("/air/batch_offer_requests/orq_0000AhTmH2Thpl6RrM97qK"). + Reply(200). + SetHeader("Ratelimit-Limit", "5"). + SetHeader("Ratelimit-Remaining", "5"). + SetHeader("Ratelimit-Reset", time.Now().Format(time.RFC1123)). + SetHeader("Date", time.Now().Format(time.RFC1123)). + File("fixtures/200-get-batch-offer-request.json") + + ctx := context.TODO() + + client := New("duffel_test_123") + + data, err := client.GetBatchOfferRequest(ctx, "orq_0000AhTmH2Thpl6RrM97qK") + a.NoError(err) + a.NotNil(data) + + a.Equal(2, data.TotalBatches) + a.Equal(2, data.RemainingBatches) +} diff --git a/duffel.go b/duffel.go index d05a458..dace8ba 100644 --- a/duffel.go +++ b/duffel.go @@ -17,6 +17,7 @@ const defaultHost = "https://api.duffel.com/" type ( Duffel interface { OfferRequestClient + BatchOfferRequestClient PartialOfferRequestClient OfferClient OrderClient @@ -30,8 +31,6 @@ type ( PlacesClient LastRequestID() (string, bool) - LastResponse() *http.Response - GetClient() *http.Client } Gender string @@ -275,7 +274,6 @@ type ( APIToken string options *Options lastRequestID string - lastResponse *http.Response } ) @@ -330,14 +328,6 @@ func (a *API) LastRequestID() (string, bool) { return a.lastRequestID, a.lastRequestID != "" } -func (a *API) LastResponse() *http.Response { - return a.lastResponse -} - -func (a *API) GetClient() *http.Client { - return a.httpDoer -} - // Assert that our interface matches var ( _ Duffel = (*API)(nil) diff --git a/fixtures/200-create-batch-offer-request.json b/fixtures/200-create-batch-offer-request.json new file mode 100644 index 0000000..e9400a0 --- /dev/null +++ b/fixtures/200-create-batch-offer-request.json @@ -0,0 +1,10 @@ +{ + "data": { + "remaining_batches": 7, + "total_batches": 7, + "client_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTQ3MzU4NDAsImxpdmVfbW9kZSI6ZmFsc2UsIm9yZ2FuaXNhdGlvbl9pZCI6Im9yZ18wMDAwQU4xdXZobHNaQlF6RDhPSTFZIn0.1dOy3eTaUskIgfphTgAlW0aStrwklPModabSf-Znam0", + "created_at": "2024-05-02T11:30:40.589820Z", + "live_mode": false, + "id": "orq_0000AhTmH2Thpl6RrM97qK" + } +} \ No newline at end of file diff --git a/fixtures/200-get-batch-offer-request.json b/fixtures/200-get-batch-offer-request.json new file mode 100644 index 0000000..963ecab --- /dev/null +++ b/fixtures/200-get-batch-offer-request.json @@ -0,0 +1,322 @@ +{ + "data": { + "total_batches": 2, + "remaining_batches": 2, + "offers": [ + { + "total_emissions_kg": "460", + "total_currency": "GBP", + "total_amount": "45.00", + "tax_currency": "GBP", + "tax_amount": "40.80", + "supported_passenger_identity_document_types": [ + "passport" + ], + "slices": [ + { + "segments": [ + { + "stops": [ + { + "id": "sto_00009htYpSCXrwaB9Dn456", + "duration": "PT02H26M", + "departing_at": "2020-06-13T16:38:02", + "arriving_at": "2020-06-13T16:38:02", + "airport": { + "type": "airport", + "time_zone": "Europe/London", + "name": "Heathrow", + "longitude": -141.951519, + "latitude": 64.068865, + "id": "arp_lhr_gb", + "icao_code": "EGLL", + "iata_country_code": "GB", + "iata_code": "LHR", + "iata_city_code": "LON", + "city_name": "London", + "city": { + "name": "London", + "id": "cit_lon_gb", + "iata_country_code": "GB", + "iata_code": "LON" + }, + "airports": [ + { + "time_zone": "Europe/London", + "name": "Heathrow", + "longitude": -141.951519, + "latitude": 64.068865, + "id": "arp_lhr_gb", + "icao_code": "EGLL", + "iata_country_code": "GB", + "iata_code": "LHR", + "iata_city_code": "LON", + "city_name": "London", + "city": { + "name": "London", + "id": "cit_lon_gb", + "iata_country_code": "GB", + "iata_code": "LON" + } + } + ] + } + } + ], + "passengers": [ + { + "passenger_id": "passenger_0", + "fare_basis_code": "OXZ0RO", + "cabin_class_marketing_name": "Economy Basic", + "cabin_class": "economy", + "cabin": { + "name": "economy", + "marketing_name": "Economy Basic", + "amenities": { + "wifi": { + "cost": "free", + "available": "true" + }, + "seat": { + "pitch": "32", + "legroom": "standard" + }, + "power": { + "available": "true" + } + } + }, + "baggages": [ + { + "type": "checked", + "quantity": 1 + } + ] + } + ], + "origin_terminal": "B", + "origin": { + "time_zone": "Europe/London", + "name": "Heathrow", + "longitude": -141.951519, + "latitude": 64.068865, + "id": "arp_lhr_gb", + "icao_code": "EGLL", + "iata_country_code": "GB", + "iata_code": "LHR", + "iata_city_code": "LON", + "city_name": "London", + "city": { + "name": "London", + "id": "cit_lon_gb", + "iata_country_code": "GB", + "iata_code": "LON" + } + }, + "operating_carrier_flight_number": "4321", + "operating_carrier": { + "name": "British Airways", + "logo_symbol_url": "https://assets.duffel.com/img/airlines/for-light-background/full-color-logo/BA.svg", + "logo_lockup_url": "https://assets.duffel.com/img/airlines/for-light-background/full-color-lockup/BA.svg", + "id": "arl_00001876aqC8c5umZmrRds", + "iata_code": "BA", + "conditions_of_carriage_url": "https://www.britishairways.com/en-gb/information/legal/british-airways/general-conditions-of-carriage" + }, + "marketing_carrier_flight_number": "1234", + "marketing_carrier": { + "name": "British Airways", + "logo_symbol_url": "https://assets.duffel.com/img/airlines/for-light-background/full-color-logo/BA.svg", + "logo_lockup_url": "https://assets.duffel.com/img/airlines/for-light-background/full-color-lockup/BA.svg", + "id": "arl_00001876aqC8c5umZmrRds", + "iata_code": "BA", + "conditions_of_carriage_url": "https://www.britishairways.com/en-gb/information/legal/british-airways/general-conditions-of-carriage" + }, + "id": "seg_00009htYpSCXrwaB9Dn456", + "duration": "PT02H26M", + "distance": "424.2", + "destination_terminal": "5", + "destination": { + "time_zone": "America/New_York", + "name": "John F. Kennedy International Airport", + "longitude": -73.778519, + "latitude": 40.640556, + "id": "arp_jfk_us", + "icao_code": "KJFK", + "iata_country_code": "US", + "iata_code": "JFK", + "iata_city_code": "NYC", + "city_name": "New York", + "city": { + "name": "New York", + "id": "cit_nyc_us", + "iata_country_code": "US", + "iata_code": "NYC" + } + }, + "departing_at": "2020-06-13T16:38:02", + "arriving_at": "2020-06-13T16:38:02", + "aircraft": { + "name": "Airbus Industries A380", + "id": "arc_00009UhD4ongolulWd91Ky", + "iata_code": "380" + } + } + ], + "origin_type": "airport", + "origin": { + "type": "airport", + "time_zone": "Europe/London", + "name": "Heathrow", + "longitude": -141.951519, + "latitude": 64.068865, + "id": "arp_lhr_gb", + "icao_code": "EGLL", + "iata_country_code": "GB", + "iata_code": "LHR", + "iata_city_code": "LON", + "city_name": "London", + "city": { + "name": "London", + "id": "cit_lon_gb", + "iata_country_code": "GB", + "iata_code": "LON" + }, + "airports": [ + { + "time_zone": "Europe/London", + "name": "Heathrow", + "longitude": -141.951519, + "latitude": 64.068865, + "id": "arp_lhr_gb", + "icao_code": "EGLL", + "iata_country_code": "GB", + "iata_code": "LHR", + "iata_city_code": "LON", + "city_name": "London", + "city": { + "name": "London", + "id": "cit_lon_gb", + "iata_country_code": "GB", + "iata_code": "LON" + } + } + ] + }, + "id": "sli_00009htYpSCXrwaB9Dn123", + "fare_brand_name": "Basic", + "duration": "PT02H26M", + "destination_type": "airport", + "destination": { + "type": "airport", + "time_zone": "Europe/London", + "name": "Heathrow", + "longitude": -141.951519, + "latitude": 64.068865, + "id": "arp_lhr_gb", + "icao_code": "EGLL", + "iata_country_code": "GB", + "iata_code": "LHR", + "iata_city_code": "LON", + "city_name": "London", + "city": { + "name": "London", + "id": "cit_lon_gb", + "iata_country_code": "GB", + "iata_code": "LON" + }, + "airports": [ + { + "time_zone": "Europe/London", + "name": "Heathrow", + "longitude": -141.951519, + "latitude": 64.068865, + "id": "arp_lhr_gb", + "icao_code": "EGLL", + "iata_country_code": "GB", + "iata_code": "LHR", + "iata_city_code": "LON", + "city_name": "London", + "city": { + "name": "London", + "id": "cit_lon_gb", + "iata_country_code": "GB", + "iata_code": "LON" + } + } + ] + }, + "conditions": { + "change_before_departure": { + "penalty_currency": "GBP", + "penalty_amount": "100.00", + "allowed": true + } + }, + "comparison_key": "BmlZDw==" + } + ], + "private_fares": [ + { + "type": "corporate", + "tracking_reference": "ABN:2345678", + "tour_code": "578DFL", + "corporate_code": "FLX53" + } + ], + "payment_requirements": { + "requires_instant_payment": false, + "price_guarantee_expires_at": "2020-01-17T10:42:14Z", + "payment_required_by": "2020-01-17T10:42:14Z" + }, + "passengers": [ + { + "type": "adult", + "loyalty_programme_accounts": [ + { + "airline_iata_code": "BA", + "account_number": "12901014" + } + ], + "id": "pas_00009hj8USM7Ncg31cBCL", + "given_name": "Amelia", + "fare_type": "contract_bulk", + "family_name": "Earhart", + "age": 14 + } + ], + "passenger_identity_documents_required": false, + "partial": true, + "owner": { + "name": "British Airways", + "logo_symbol_url": "https://assets.duffel.com/img/airlines/for-light-background/full-color-logo/BA.svg", + "logo_lockup_url": "https://assets.duffel.com/img/airlines/for-light-background/full-color-lockup/BA.svg", + "id": "arl_00001876aqC8c5umZmrRds", + "iata_code": "BA", + "conditions_of_carriage_url": "https://www.britishairways.com/en-gb/information/legal/british-airways/general-conditions-of-carriage" + }, + "live_mode": true, + "id": "off_00009htYpSCXrwaB9DnUm0", + "expires_at": "2020-01-17T10:42:14.545Z", + "created_at": "2020-01-17T10:12:14.545Z", + "conditions": { + "refund_before_departure": { + "penalty_currency": "GBP", + "penalty_amount": "100.00", + "allowed": true + }, + "change_before_departure": { + "penalty_currency": "GBP", + "penalty_amount": "100.00", + "allowed": true + } + }, + "base_currency": "GBP", + "base_amount": "30.20" + } + ], + "live_mode": false, + "id": "orq_00009hjdomFOCJyxHG7k7k", + "created_at": "2020-02-12T15:21:01.927Z", + "client_key": "SFMyNTY.g2gDdAAAAANkAAlsaXZlX21vZGVkAAVmYWxzZWQAD29yZ2FuaXNhdGlvbl9pZG0AAAAab3JnXzAwMDA5VWhGY29ERGk5TTFTRjhiS2FkAAtyZXNvdXJjZV9pZG0AAAAab3JxXzAwMDBBVkZWZnFJUXFBWXpYeVRRVlVuBgDpOCvdhwFiAAFRgA.df1RmLeBFUR7r1WFHHiEksilfSZNLhmPX0nj5VOKWJ4" + } + } \ No newline at end of file diff --git a/request.go b/request.go index 14323ae..27cbd55 100644 --- a/request.go +++ b/request.go @@ -22,7 +22,7 @@ func newInternalClient[Req any, Resp any](a *API) *client[Req, Resp] { afterResponse: []func(resp *http.Response){ func(resp *http.Response) { if resp != nil { - a.lastRequestID, a.lastResponse = resp.Header.Get(RequestIDHeader), resp + a.lastRequestID = resp.Header.Get(RequestIDHeader) } }, },