diff --git a/handlers/handlers.go b/handlers/handlers.go index 63c8f6e..e81c56f 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -21,8 +21,9 @@ import ( ) const ( - customLayout = "2006-01-02 15:04:05 -0700" - transactionUrl = "http://127.0.0.1:18181/start-transaction/" + customLayout = "2006-01-02 15:04:05 -0700" + takePreauthorizationUrl = "http://127.0.0.1:18181/start-transaction/" + takePaymentUrl = "http://127.0.0.1:18181/start-and-confirm-transaction/" ) type App struct { @@ -42,11 +43,12 @@ func NewApp(dispPort *serial.Port, lockType, encoderAddress string, isPayment bo func (app *App) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("/issuedoorcard", app.issueDoorCard) mux.HandleFunc("/printroomticket", app.printRoomTicket) - mux.HandleFunc("/starttransaction", app.startTransaction) + mux.HandleFunc("/takepreauth", app.takePreauthorization) + mux.HandleFunc("/takepayment", app.takePayment) } -func (app *App) startTransaction(w http.ResponseWriter, r *http.Request) { - const op = logging.Op("startTransaction") +func (app *App) takePreauthorization(w http.ResponseWriter, r *http.Request) { + const op = logging.Op("takePreauthorization") var ( theResponse cmstypes.ResponseRec cardholderReceipt string @@ -73,7 +75,7 @@ func (app *App) startTransaction(w http.ResponseWriter, r *http.Request) { return } - log.Println("startTransaction called") + log.Println("takePreauthorization called") if r.Method != http.MethodPost { theResponse.Data = payment.BuildFailureURL(payment.ResultError, "Method not allowed; use POST") writeTransactionResult(w, http.StatusMethodNotAllowed, theResponse) @@ -95,10 +97,10 @@ func (app *App) startTransaction(w http.ResponseWriter, r *http.Request) { writeTransactionResult(w, http.StatusBadRequest, theResponse) return } - log.Printf("Start trnasaction payload: Amount=%s, Type=%s", theRequest.AmountMinorUnits, theRequest.TransactionType) + log.Printf("Transaction payload: Amount=%s, Type=%s", theRequest.AmountMinorUnits, theRequest.TransactionType) client := &http.Client{Timeout: 300 * time.Second} - response, err := client.Post(transactionUrl, "text/xml", bytes.NewBuffer(body)) + response, err := client.Post(takePreauthorizationUrl, "text/xml", bytes.NewBuffer(body)) if err != nil { logging.Error(serviceName, err.Error(), "Payment processing error", string(op), "", "", 0) theResponse.Data = payment.BuildFailureURL(payment.ResultError, "No response from payment processor") @@ -140,7 +142,106 @@ func (app *App) startTransaction(w http.ResponseWriter, r *http.Request) { log.Errorf("PrintCardholderReceipt error: %v", err) } - theResponse.Data = payment.BuildRedirectURL(result) + theResponse.Data = payment.BuildPreauthRedirectURL(result) + writeTransactionResult(w, http.StatusOK, theResponse) +} + +func (app *App) takePayment(w http.ResponseWriter, r *http.Request) { + const op = logging.Op("takePayment") + var ( + theResponse cmstypes.ResponseRec + cardholderReceipt string + theRequest cmstypes.TransactionRec + trResult payment.TransactionResultXML + ) + + theResponse.Status.Code = http.StatusInternalServerError + theResponse.Status.Message = "500 Internal server error" + + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + w.Header().Set("Content-Type", "application/json") + + if !app.isPayment { + theResponse.Data = payment.BuildFailureURL(payment.ResultError, "Payment processing is disabled") + writeTransactionResult(w, http.StatusServiceUnavailable, theResponse) + return + } + + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + + log.Println("takePayment called") + if r.Method != http.MethodPost { + theResponse.Data = payment.BuildFailureURL(payment.ResultError, "Method not allowed; use POST") + writeTransactionResult(w, http.StatusMethodNotAllowed, theResponse) + return + } + defer r.Body.Close() + + if ct := r.Header.Get("Content-Type"); ct != "text/xml" { + theResponse.Data = payment.BuildFailureURL(payment.ResultError, "Content-Type must be text/xml") + writeTransactionResult(w, http.StatusUnsupportedMediaType, theResponse) + return + } + + body, _ := io.ReadAll(r.Body) + err := xml.Unmarshal(body, &theRequest) + if err != nil { + logging.Error(serviceName, err.Error(), "ReadXML", string(op), "", "", 0) + theResponse.Data = payment.BuildFailureURL(payment.ResultError, "Invalid XML payload") + writeTransactionResult(w, http.StatusBadRequest, theResponse) + return + } + log.Printf("Transaction payload: Amount=%s, Type=%s", theRequest.AmountMinorUnits, theRequest.TransactionType) + + client := &http.Client{Timeout: 300 * time.Second} + response, err := client.Post(takePaymentUrl, "text/xml", bytes.NewBuffer(body)) + if err != nil { + logging.Error(serviceName, err.Error(), "Payment processing error", string(op), "", "", 0) + theResponse.Data = payment.BuildFailureURL(payment.ResultError, "No response from payment processor") + writeTransactionResult(w, http.StatusBadGateway, theResponse) + return + } + defer response.Body.Close() + + body, err = io.ReadAll(response.Body) + if err != nil { + logging.Error(serviceName, err.Error(), "Read response body error", string(op), "", "", 0) + theResponse.Data = payment.BuildFailureURL(payment.ResultError, "Failed to read response body") + writeTransactionResult(w, http.StatusInternalServerError, theResponse) + return + } + + if err := trResult.ParseTransactionResult(body); err != nil { + logging.Error(serviceName, err.Error(), "Parse transaction result error", string(op), "", "", 0) + } + + // Compose JSON from responseEntries + result := make(map[string]string) + for _, e := range trResult.Entries { + switch e.Key { + case payment.ReceiptData, payment.ReceiptDataMerchant: + // ignore these + case payment.ReceiptDataCardholder: + cardholderReceipt = e.Value + case payment.TransactionResult: + theResponse.Status.Message = e.Value + theResponse.Status.Code = http.StatusOK + result[e.Key] = e.Value + default: + result[e.Key] = e.Value + } + } + + if err := printer.PrintCardholderReceipt(cardholderReceipt); err != nil { + log.Errorf("PrintCardholderReceipt error: %v", err) + } + + theResponse.Data = payment.BuildPaymentRedirectURL(result) writeTransactionResult(w, http.StatusOK, theResponse) } diff --git a/main.go b/main.go index abdb92f..11cbfd3 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ import ( ) const ( - buildVersion = "1.0.25" + buildVersion = "1.0.26" serviceName = "hardlink" ) diff --git a/payment/creditcall.go b/payment/creditcall.go index 0dd5886..704abd4 100644 --- a/payment/creditcall.go +++ b/payment/creditcall.go @@ -250,7 +250,31 @@ func nullableFloatArg(nf sql.NullFloat64) interface{} { } // BuildRedirectURL builds the redirect URL to send the guest to after payment. -func BuildRedirectURL(result map[string]string) string { +func BuildPaymentRedirectURL(result map[string]string) string { + res := result[TransactionResult] + + // Transaction approved? + if strings.EqualFold(res, ResultApproved) { + // Transaction confirmed? + if strings.EqualFold(result[ConfirmResult], ResultApproved) { + log.WithField(LogResult, result[ConfirmResult]). + Info("Transaction approved and confirmed") + + return buildSuccessURL(result) + } + + // Not confirmed + log.WithFields(log.Fields{LogFieldError: result[ConfirmResult], LogFieldDescription: result[ConfirmErrors]}). + Error("Transaction approved but not confirmed") + + return BuildFailureURL(result[ConfirmResult], result[ConfirmErrors]) + } + + // Not approved + return BuildFailureURL(res, result[Errors]) +} + +func BuildPreauthRedirectURL(result map[string]string) string { res := result[TransactionResult] tType := result[TransactionType] @@ -267,18 +291,10 @@ func BuildRedirectURL(result map[string]string) string { // Transaction type Sale? case strings.EqualFold(tType, SaleTransactionType): // Transaction confirmed? - if strings.EqualFold(result[ConfirmResult], ResultApproved) { - log.WithField(LogResult, result[ConfirmResult]). - Info("Transaction approved and confirmed") + log.WithField(LogResult, result[ConfirmResult]). + Info("Amount preauthorized successfully") - return buildSuccessURL(result) - } - - // Not confirmed - log.WithFields(log.Fields{LogFieldError: result[ConfirmResult], LogFieldDescription: result[ConfirmErrors]}). - Error("Transaction approved but not confirmed") - - return BuildFailureURL(result[ConfirmResult], result[ConfirmErrors]) + return buildSuccessURL(result) } } diff --git a/release notes.md b/release notes.md index 7c8c089..de03dac 100644 --- a/release notes.md +++ b/release notes.md @@ -2,6 +2,9 @@ builtVersion is a const in main.go +#### 1.0.26 - 10 December 2025 +added route for taking preauthorization payment + #### 1.0.25 - 08 December 2025 return masked card number and expiry date in the payment success URL