added route for taking preauthorization payment

This commit is contained in:
yurii 2025-12-10 11:07:52 +00:00
parent 89dfa28e6f
commit a4885be458
4 changed files with 142 additions and 22 deletions

View File

@ -22,7 +22,8 @@ import (
const ( const (
customLayout = "2006-01-02 15:04:05 -0700" customLayout = "2006-01-02 15:04:05 -0700"
transactionUrl = "http://127.0.0.1:18181/start-transaction/" takePreauthorizationUrl = "http://127.0.0.1:18181/start-transaction/"
takePaymentUrl = "http://127.0.0.1:18181/start-and-confirm-transaction/"
) )
type App struct { type App struct {
@ -42,11 +43,12 @@ func NewApp(dispPort *serial.Port, lockType, encoderAddress string, isPayment bo
func (app *App) RegisterRoutes(mux *http.ServeMux) { func (app *App) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/issuedoorcard", app.issueDoorCard) mux.HandleFunc("/issuedoorcard", app.issueDoorCard)
mux.HandleFunc("/printroomticket", app.printRoomTicket) 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) { func (app *App) takePreauthorization(w http.ResponseWriter, r *http.Request) {
const op = logging.Op("startTransaction") const op = logging.Op("takePreauthorization")
var ( var (
theResponse cmstypes.ResponseRec theResponse cmstypes.ResponseRec
cardholderReceipt string cardholderReceipt string
@ -73,7 +75,7 @@ func (app *App) startTransaction(w http.ResponseWriter, r *http.Request) {
return return
} }
log.Println("startTransaction called") log.Println("takePreauthorization called")
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
theResponse.Data = payment.BuildFailureURL(payment.ResultError, "Method not allowed; use POST") theResponse.Data = payment.BuildFailureURL(payment.ResultError, "Method not allowed; use POST")
writeTransactionResult(w, http.StatusMethodNotAllowed, theResponse) writeTransactionResult(w, http.StatusMethodNotAllowed, theResponse)
@ -95,10 +97,10 @@ func (app *App) startTransaction(w http.ResponseWriter, r *http.Request) {
writeTransactionResult(w, http.StatusBadRequest, theResponse) writeTransactionResult(w, http.StatusBadRequest, theResponse)
return 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} 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 { if err != nil {
logging.Error(serviceName, err.Error(), "Payment processing error", string(op), "", "", 0) logging.Error(serviceName, err.Error(), "Payment processing error", string(op), "", "", 0)
theResponse.Data = payment.BuildFailureURL(payment.ResultError, "No response from payment processor") 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) 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) writeTransactionResult(w, http.StatusOK, theResponse)
} }

View File

@ -23,7 +23,7 @@ import (
) )
const ( const (
buildVersion = "1.0.25" buildVersion = "1.0.26"
serviceName = "hardlink" serviceName = "hardlink"
) )

View File

@ -250,7 +250,31 @@ func nullableFloatArg(nf sql.NullFloat64) interface{} {
} }
// BuildRedirectURL builds the redirect URL to send the guest to after payment. // 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] res := result[TransactionResult]
tType := result[TransactionType] tType := result[TransactionType]
@ -267,19 +291,11 @@ func BuildRedirectURL(result map[string]string) string {
// Transaction type Sale? // Transaction type Sale?
case strings.EqualFold(tType, SaleTransactionType): case strings.EqualFold(tType, SaleTransactionType):
// Transaction confirmed? // Transaction confirmed?
if strings.EqualFold(result[ConfirmResult], ResultApproved) {
log.WithField(LogResult, result[ConfirmResult]). log.WithField(LogResult, result[ConfirmResult]).
Info("Transaction approved and confirmed") Info("Amount preauthorized successfully")
return buildSuccessURL(result) 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 // Not approved

View File

@ -2,6 +2,9 @@
builtVersion is a const in main.go 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 #### 1.0.25 - 08 December 2025
return masked card number and expiry date in the payment success URL return masked card number and expiry date in the payment success URL