Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Wrote an Air France-KLM data fetcher. #11

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pnr.sh uses normal and public APIs to obtain reservation information that is alr
| Airline | Supported | Notes |
|-------------------------|-----------|----------------------------------------------------------|
| Aeromexico | ✅ | Provides most useful PNR data. |
| Air France-KLM | ✅ | Provides most useful PNR data. |
| Delta Air Lines | ✅ | Provides most useful PNR data. |
| United | ✅ | Provides most useful PNR data. |
| Virgin Atlantic | ✅ | Provides most useful PNR data. |
Expand Down
11 changes: 11 additions & 0 deletions cmd/home.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ func AircanadaHomeHandler(w http.ResponseWriter, r *http.Request) {
})
}

func AirfranceklmHomeHandler(w http.ResponseWriter, r *http.Request) {
t := Parse("airfranceklm-home.html")
t.Execute(w, struct {
Error bool
CommitHash string
}{
r.URL.Query().Get("error") == "t",
commitHash,
})
}

func UnitedHomeHandler(w http.ResponseWriter, r *http.Request) {
t := Parse("united-home.html")
t.Execute(w, struct {
Expand Down
16 changes: 11 additions & 5 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import (
)

const (
aeromexicoEnabled = true
aircanadaEnabled = true
deltaEnabled = true
unitedEnabled = true
virginEnabled = true
aeromexicoEnabled = true
aircanadaEnabled = true
airfranceklmEnabled = true
deltaEnabled = true
unitedEnabled = true
virginEnabled = true
)

var (
Expand Down Expand Up @@ -45,6 +46,11 @@ func main() {
r.HandleFunc("/aircanada", AircanadaRetrieveHandler).Methods("POST")
}

if airfranceklmEnabled {
r.HandleFunc("/airfranceklm", AirfranceklmHomeHandler).Methods("GET")
r.HandleFunc("/airfranceklm", AirfranceklmRetrieveHandler).Methods("POST")
}

if deltaEnabled {
r.HandleFunc("/delta", DeltaHomeHandler).Methods("GET")
r.HandleFunc("/delta", DeltaRetrieveHandler).Methods("POST")
Expand Down
42 changes: 42 additions & 0 deletions cmd/retrieve.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

aeromexico "github.com/pnrsh/pnrsh/pkg/aeromexico/pnr"
aircanada "github.com/pnrsh/pnrsh/pkg/aircanada/pnr"
airfranceklm "github.com/pnrsh/pnrsh/pkg/airfranceklm/pnr"
delta "github.com/pnrsh/pnrsh/pkg/delta/pnr"
united "github.com/pnrsh/pnrsh/pkg/united/pnr"
)
Expand Down Expand Up @@ -118,6 +119,47 @@ func AircanadaRetrieveHandler(w http.ResponseWriter, r *http.Request) {
})
}

func AirfranceklmRetrieveHandler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
w.Header().Add("Location", "/airfranceklm?error=t")
w.WriteHeader(302)
return
}

firstName := r.Form.Get("first_name")
lastName := r.Form.Get("last_name")
confirmationCode := r.Form.Get("confirmation_code")

if len(confirmationCode) != 6 || len(lastName) == 0 {
w.Header().Add("Location", "/airfranceklm?error=t")
w.WriteHeader(302)
return
}

retrievedPNR, err := airfranceklm.Retrieve(confirmationCode, firstName, lastName)
if err != nil {
w.Header().Add("Location", "/airfranceklm?error=t")
w.WriteHeader(302)
return
}

t := Parse("airfranceklm-show.html")

t.Execute(w, struct {
PNR airfranceklm.PNR
FirstName string
LastName string
ConfirmationCode string
CommitHash string
}{
retrievedPNR,
firstName,
lastName,
confirmationCode,
commitHash,
})
}

func UnitedRetrieveHandler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
w.Header().Add("Location", "/united?error=t")
Expand Down
28 changes: 28 additions & 0 deletions cmd/templates/airfranceklm-home.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{{template "base" .}}

{{define "main"}}
{{if .Error}}
<div class="alert alert-danger" role="alert">
<strong>Sorry, your reservation could not be retrieved at this time.</strong> This could happen for a few reasons: your reservation has been fully flown, Air France-KLM has blocked our server, or your reservation hit an edge case with the parser.
</div>
{{end}}

<h2>Air France-KLM</h2>
<p>pnr.sh allows you to dive into your Air France or KLM reservation and see information that is not normally displayed on the website. You can see ticketing information, fare classes, and more useful information!</p>

<form method="POST" action="/airfranceklm">
<div class="mb-3">
<input type="text" class="form-control" id="first_name" name="first_name" placeholder="First name">
</div>

<div class="mb-3">
<input type="text" class="form-control" id="last_name" name="last_name" placeholder="Last name">
</div>

<div class="mb-3">
<input type="text" class="form-control" id="confirmation_code" name="confirmation_code" placeholder="Air France-KLM confirmation number (ex. ABC4EF)">
</div>

<button class="btn btn-lg btn-primary" role="submit">Retrieve Reservation</button>
</form><br />
{{end}}
90 changes: 90 additions & 0 deletions cmd/templates/airfranceklm-show.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{{template "base" .}}

{{define "main"}}
<div class="container">
<h2>Confirmation #{{.ConfirmationCode}}</h2>

<div class="d-flex" style="gap: 0.5em; align-items: center;">
<div class="p-1" style="color: black">Tools</div>
<a class="btn btn-primary" href="{{.PNR.SMCalcLink}}" target="_blank">skymilescalculator.com</a>
<a class="btn btn-secondary" href="{{.PNR.QMCalcLink}}" target="_blank">qualifyingmiles.com</a>
{{ if .PNR.HasDLFlights }}
<span class="px-1">&middot;</span>
<form method="POST" action="/delta">
<input type="hidden" name="first_name" value="{{ .FirstName }}" />
<input type="hidden" name="last_name" value="{{ .LastName }}" />
<input type="hidden" name="confirmation_code" value="{{ .ConfirmationCode }}" />
<button class="btn btn-dark" type="submit">Lookup Delta PNR</button>
</form>
{{ end }}
</div>

<hr />

{{ range .PNR.Tickets }}

<div class="card mb-4" style="background: #e7eefa">
<div class="card-body" style="background: white; border-bottom: #ddd 1px solid; background: white;">
<h5 class="card-title">Ticket {{ .Number }}</h5>
<div><strong>Issued</strong>: {{ .IssueDate }}</div>
<div><strong>Passenger</strong>: {{ .PassengerName }}</div>
<div><strong>FFP</strong>: {{ .Passenger.FrequentFlyerNumber }} &mdash; {{ .Passenger.FrequentFlyerProgram }}</div>
<div><strong>Base Fare</strong>: {{ .Fare.BaseFare }} {{ .Fare.BaseCurrencyCode }}</div>
<div><strong>Carrier-Imposed Surcharges</strong>: {{ .Fare.CarrierImposedSurcharge }} {{ .Fare.CarrierImposedSurchargeCode }}</div>
<div><strong>Taxes</strong>: {{ .Fare.TotalTax }} {{ .Fare.TotalTaxCurrencyCode }}</div>
<div><strong>Total Fare</strong>: {{ .Fare.TotalFare }} {{ .Fare.TotalCurrencyCode }}</div>
</div>

{{ range .Flights }}
<div class="card-body mt-2" style="border-top: #ddd 1px solid; background: white;">
<div class="d-flex" style="align-items: flex-start">
<h5 style="flex-grow: 1">
<strong>{{.MarketingAirlineCode}}{{.FlightNumber}}</strong>
<span class="px-1">&middot;</span>
{{.OriginAirportCode}} &rarr; {{.DestinationAirportCode}}
<span class="px-1">&middot;</span>
<span class="badge bg-primary">{{.Status}}</span>
</h5>
{{ if .SkyPriority }}
<span class="d-block badge bg-danger" style="background: #DF1B2B;">SKYPRIORITY</span>
{{ end }}
</div>
</div>
<table class="table mb-0" style="background: white;">
<tr>
<td><strong>Scheduled Departure</strong></td>
<td>{{.ScheduledDeparture}}</td>
</tr>

<tr>
<td><strong>Scheduled Arrival</strong></td>
<td>{{.ScheduledArrival}}</td>
</tr>

<tr>
<td><strong>Marketing Airline</strong></td>
<td>{{.MarketingAirlineCode}} &mdash; {{ .MarketingAirlineName }}</td>
</tr>

<tr>
<td><strong>Operating Airline</strong></td>
<td>{{.OperatingAirlineCode}} &mdash; {{ .OperatingAirlineName }}</td>
</tr>

<tr>
<td><strong>Aircraft</strong></td>
<td>{{.AircraftType}}</td>
</tr>

<tr>
<td><strong>Class of Service</strong></td>
<td>{{.ClassOfService}} ({{.FareClass}})</td>
</tr>
</tbody>
</table>
{{ end }}
</div>

{{ end }}
</div>
{{end}}
1 change: 1 addition & 0 deletions cmd/templates/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ <h2>Choose an airline</h2>
<div class="list-group">
<a href="/aeromexico" class="list-group-item list-group-item-action">Aeromexico <span class="badge bg-success">Flight Segments</span> <span class="badge bg-success">Action Codes</span> <span class="badge bg-success">Remarks</span> <span class="badge bg-success">Tickets</span></a>
<a href="/aircanada" class="list-group-item list-group-item-action">Air Canada <span class="badge bg-success">Flight Segments</span> <span class="badge bg-success">Action Codes</span> <span class="badge bg-secondary">Remarks</span> <span class="badge bg-success">Tickets</span></a>
<a href="/airfranceklm" class="list-group-item list-group-item-action">Air France / KLM <span class="badge bg-success">Flight Segments</span> <span class="badge bg-secondary">Action Codes</span> <span class="badge bg-secondary">Remarks</span> <span class="badge bg-success">Tickets</span></a>
<a href="/delta" class="list-group-item list-group-item-action">Delta Air Lines <span class="badge bg-success">Flight Segments</span> <span class="badge bg-success">Action Codes</span> <span class="badge bg-secondary">Remarks</span> <span class="badge bg-success">Tickets</span></a>
<a href="/united" class="list-group-item list-group-item-action">United <span class="badge bg-success">Flight Segments</span> <span class="badge bg-success">Action Codes</span> <span class="badge bg-secondary">Remarks</span> <span class="badge bg-success">Tickets</span></a>
<a href="/virgin" class="list-group-item list-group-item-action">Virgin Atlantic <span class="badge bg-success">Flight Segments</span> <span class="badge bg-success">Action Codes</span> <span class="badge bg-success">Remarks</span> <span class="badge bg-success">Tickets</span></a>
Expand Down
146 changes: 146 additions & 0 deletions pkg/airfranceklm/pnr/convert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package pnr

import "strconv"

func convertResponse(res GetPNRResponse) (pnr PNR) {
convertTickets(res, &pnr)

// Look for DL flights.
for _, connection := range res.Itinerary.Connections {
for _, segment := range connection.Segments {
if segment.MarketingFlight.Carrier.Code == "DL" || segment.MarketingFlight.OperatingFlight.Carrier.Code == "DL" {
pnr.HasDLFlights = true
break
}
}
}

pnr.SMCalcLink = generateSmcalcLink(&pnr)
pnr.QMCalcLink = generateQMCalcLink(&pnr)

return pnr
}

func convertTickets(res GetPNRResponse, pnr *PNR) {

for _, pax := range res.Passengers {

var ffpMembership FrequentTravelerMembership
for _, membership := range pax.Memberships {
if membership.FrequentTravelerMembership.Number != "" {
ffpMembership = membership.FrequentTravelerMembership
break
}
}

for _, ticket := range pax.Tickets {
numCoupons := 0

var flights []Flight
for _, booklet := range ticket.TicketBooklets {
numCoupons += len(booklet.TicketCoupons) + len(booklet.InvalidTicketCoupons)

for _, coupon := range booklet.TicketCoupons {
for _, connection := range res.Itinerary.Connections {
for _, flight := range connection.Segments {
if coupon.SegmentID != flight.ID {
continue
}

classOfService := ""
skyPriority := false
for _, detail := range res.Passengers[0].PassengerFlightDetails {
if detail.SegmentID != flight.ID {
continue
}

classOfService = detail.Cabin.Name
skyPriority = detail.SkyPriority
}

flights = append(flights, Flight{
OriginAirportCode: flight.DepartFrom.IATACode,
DestinationAirportCode: flight.ArriveOn.IATACode,
MarketingAirlineCode: flight.MarketingFlight.Carrier.Code,
MarketingAirlineName: flight.MarketingFlight.Carrier.Name,
OperatingAirlineCode: flight.MarketingFlight.OperatingFlight.Carrier.Code,
OperatingAirlineName: flight.MarketingFlight.OperatingFlight.Carrier.Name,
FlightNumber: flight.MarketingFlight.Number,
ClassOfService: classOfService,
ScheduledDeparture: flight.MarketingFlight.OperatingFlight.LocalScheduledDeparture,
ScheduledArrival: flight.MarketingFlight.OperatingFlight.LocalScheduledArrival,
Status: flight.MarketingFlight.Status.Code,
FareClass: flight.MarketingFlight.SellingClass.Code,
AircraftType: flight.MarketingFlight.OperatingFlight.Equipment.Name + " (" + flight.MarketingFlight.OperatingFlight.Equipment.Code + ")",
SkyPriority: skyPriority,
})

break
}
}
}
}

var carrierImposedSurcharge float64 = 0
carrierImposedSurchargeCode := ""

for _, charge := range ticket.AirTransportationCharges.Charges {
if charge.CostType.Code == "AIRLINE_COSTS" {
if carrierImposedSurchargeCode == "" || carrierImposedSurchargeCode == charge.Amount.Currency {
carrierImposedSurcharge += charge.Amount.Amount
carrierImposedSurchargeCode = charge.Amount.Currency
} else {
carrierImposedSurcharge = 0
carrierImposedSurchargeCode = "Mixed Currencies"
break
}
}
}

var taxes []TaxRow
var totalTax float64 = 0
totalTaxCurrencyCode := ""

for _, tax := range ticket.Taxes {
taxes = append(taxes, TaxRow{
TaxType: tax.Code + " - " + tax.Name,
Amount: strconv.FormatFloat(tax.Amount.Amount, 'f', 2, 64),
Currency: tax.Amount.Currency,
CarrierImposedFee: false,
})
if totalTaxCurrencyCode == "" || totalTaxCurrencyCode == tax.Amount.Currency {
totalTax += tax.Amount.Amount
totalTaxCurrencyCode = tax.Amount.Currency
} else {
totalTax = 0
totalTaxCurrencyCode = "Mixed Currencies"
break
}
}

pnr.Tickets = append(pnr.Tickets, Ticket{
Number: ticket.Number,
IssueDate: ticket.TicketingDate,
PassengerName: pax.FirstName + " " + pax.LastName,
NumCoupons: numCoupons,
ValidatedAgainstCoupon: false,
Passenger: Passenger{
Name: pax.FirstName + " " + pax.LastName,
FrequentFlyerNumber: ffpMembership.Number,
FrequentFlyerProgram: ffpMembership.LoyaltyProgram.Name,
},
Flights: flights,
Fare: Fare{
BaseFare: strconv.FormatFloat(ticket.BaseFare.Amount, 'f', 2, 64),
BaseCurrencyCode: ticket.BaseFare.Currency,
CarrierImposedSurcharge: strconv.FormatFloat(float64(carrierImposedSurcharge), 'f', 2, 64),
CarrierImposedSurchargeCode: carrierImposedSurchargeCode,
TotalTax: strconv.FormatFloat(float64(totalTax), 'f', 2, 64),
TotalTaxCurrencyCode: totalTaxCurrencyCode,
TotalFare: strconv.FormatFloat(ticket.TotalFare.Amount, 'f', 2, 64),
TotalCurrencyCode: ticket.TotalFare.Currency,
},
})
}
}
}
Loading