diff --git a/main.go b/main.go index 38e6202..47e3cf6 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,7 @@ package main import ( - // "database/sql" + "bytes" "encoding/json" "encoding/xml" "fmt" @@ -29,7 +29,7 @@ import ( ) const ( - buildVersion = "1.0.15" + buildVersion = "1.0.17" serviceName = "hardlink" customLayout = "2006-01-02 15:04:05 -0700" transactionUrl = "http://127.0.0.1:18181/start-transaction/" @@ -51,12 +51,14 @@ type configRec struct { // App holds shared resources. type App struct { + configRec configRec dispPort *serial.Port lockserver lockserver.LockServer } func newApp(dispPort *serial.Port, config configRec) *App { return &App{ + configRec: config, dispPort: dispPort, lockserver: lockserver.NewLockServer(config.LockType, config.EncoderAddress, fatalError), } @@ -230,18 +232,36 @@ func writeError(w http.ResponseWriter, status int, msg string) { json.NewEncoder(w).Encode(theResponse) } +func writeTransactionResult(w http.ResponseWriter, status int, theResponse cmstypes.ResponseRec) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if err := json.NewEncoder(w).Encode(theResponse); err != nil { + logging.Error(serviceName, err.Error(), "JSON encode error", "startTransaction", "", "", 0) + } +} + func (app *App) startTransaction(w http.ResponseWriter, r *http.Request) { const op = logging.Op("startTransaction") var ( theResponse cmstypes.ResponseRec cardholderReceipt string + theRequest cmstypes.TransactionRec ) + 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.configRec.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 @@ -249,29 +269,43 @@ func (app *App) startTransaction(w http.ResponseWriter, r *http.Request) { log.Println("startTransaction called") if r.Method != http.MethodPost { - writeError(w, http.StatusMethodNotAllowed, "Method not allowed; use POST") + 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" { - writeError(w, http.StatusUnsupportedMediaType, "Content-Type must be 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("Start trnasaction payload: Amount=%s, Type=%s", theRequest.AmountMinorUnits, theRequest.TransactionType) + client := &http.Client{Timeout: 300 * time.Second} - response, err := client.Post(transactionUrl, "text/xml", r.Body) + response, err := client.Post(transactionUrl, "text/xml", bytes.NewBuffer(body)) if err != nil { logging.Error(serviceName, err.Error(), "Payment processing error", string(op), "", "", 0) - writeError(w, http.StatusInternalServerError, "Payment processing failed: "+err.Error()) + 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) + body, err = io.ReadAll(response.Body) if err != nil { logging.Error(serviceName, err.Error(), "Read response body error", string(op), "", "", 0) - writeError(w, http.StatusInternalServerError, "Failed to read response body: "+err.Error()) + theResponse.Data = payment.BuildFailureURL(payment.ResultError, "Failed to read response body") + writeTransactionResult(w, http.StatusInternalServerError, theResponse) return } @@ -281,10 +315,11 @@ func (app *App) startTransaction(w http.ResponseWriter, r *http.Request) { result := make(map[string]string) for _, e := range responseEntries { switch e.Key { - case "RECEIPT_DATA", "RECEIPT_DATA_MERCHANT": - case "RECEIPT_DATA_CARDHOLDER": + case payment.ReceiptData, payment.ReceiptDataMerchant: + // ignore these + case payment.ReceiptDataCardholder: cardholderReceipt = e.Value - case "TRANSACTION_RESULT": + case payment.TransactionResult: theResponse.Status.Message = e.Value theResponse.Status.Code = http.StatusOK result[e.Key] = e.Value @@ -297,18 +332,8 @@ func (app *App) startTransaction(w http.ResponseWriter, r *http.Request) { log.Errorf("PrintCardholderReceipt error: %v", err) } - // Insert into DB - // if err := payment.InsertTransactionRecord(r.Context(), app.db, result); err != nil { - // log.Errorf("DB insert error: %v", err) - // } - theResponse.Data = payment.BuildRedirectURL(result) - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(theResponse); err != nil { - logging.Error(serviceName, err.Error(), "JSON encode error", string(op), "", "", 0) - } + writeTransactionResult(w, http.StatusOK, theResponse) } func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) { diff --git a/payment/creditcall.go b/payment/creditcall.go index d742009..bf22561 100644 --- a/payment/creditcall.go +++ b/payment/creditcall.go @@ -16,6 +16,11 @@ import ( ) const ( + // Transaction types + SaleTransactionType = "sale" + AccountVerificationType = "account verification" + + // Transaction results ResultApproved = "approved" ResultDeclined = "declined" ResultCancelled = "cancelled" @@ -23,6 +28,24 @@ const ( ResultError = "error" CheckinSuccessfulEndpoint = "/successful" // Endpoint to send guest to after successful payment CheckinUnsuccessfulEndpoint = "/unsuccessful" + + // Response map keys + CardReference = "CARD_REFERENCE" + CardHash = "CARD_HASH" + Errors = "ERRORS" + ReceiptData = "RECEIPT_DATA" + ReceiptDataMerchant = "RECEIPT_DATA_MERCHANT" + ReceiptDataCardholder = "RECEIPT_DATA_CARDHOLDER" + Reference = "REFERENCE" + TransactionResult = "TRANSACTION_RESULT" + TransactionType = "TRANSACTION_TYPE" + ConfirmResult = "CONFIRM_RESULT" + ConfirmErrors = "CONFIRM_ERRORS" + + // Log field keys + LogFieldError = "error" + LogFieldDescription = "description" + LogResult = "transactionResult" ) // XML parsing structs @@ -42,6 +65,14 @@ type ( Key string `xml:"Key"` Value string `xml:"Value"` } + + TransactionConfirmation struct { + XMLName xml.Name `xml:"TransactionConfirmation"` + Result string `xml:"Result"` + Errors string `xml:"Errors"` + ErrorDescription string `xml:"ErrorDescription"` + ReceiptDataCardholder string `xml:"ReceiptDataCardholder"` + } ) // ParseTransactionResult parses the XML into entries. @@ -217,47 +248,70 @@ func nullableFloatArg(nf sql.NullFloat64) interface{} { return nil } +// BuildRedirectURL builds the redirect URL to send the guest to after payment. func BuildRedirectURL(result map[string]string) string { - var msgType, description string - q := url.Values{} + res := result[TransactionResult] + tType := result[TransactionType] - res := strings.ToLower(result["TRANSACTION_RESULT"]) + // Transaction approved? + if strings.EqualFold(res, ResultApproved) { + switch { + // Transaction type AccountVerification? + case strings.EqualFold(tType, AccountVerificationType): + log.WithField(LogResult, result[TransactionResult]). + Info("Account verification approved") - if res == ResultApproved { - q.Set("TxnReference", result["REFERENCE"]) - q.Set("CardHash", hex.EncodeToString([]byte(result["CARD_HASH"]))) - q.Set("CardReference", hex.EncodeToString([]byte(result["CARD_REFERENCE"]))) - u := url.URL{ - Path: CheckinSuccessfulEndpoint, - RawQuery: q.Encode(), + return buildSuccessURL(result) + + // 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") + + 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 u.String() } - msgType = ResultError - if res != "" { - msgType = res - } + // Not approved + return BuildFailureURL(res, result[Errors]) +} - errors, ok := result["ERRORS"] - if ok && errors != "" { - description = errors - } - errors, ok = result["ERROR"] - if ok && errors != "" { - description += " " + errors +func buildSuccessURL(result map[string]string) string { + q := url.Values{} + q.Set("TxnReference", result[Reference]) + q.Set("CardHash", hex.EncodeToString([]byte(result[CardHash]))) + q.Set("CardReference", hex.EncodeToString([]byte(result[CardReference]))) + return (&url.URL{ + Path: CheckinSuccessfulEndpoint, + RawQuery: q.Encode(), + }).String() +} + +func BuildFailureURL(msgType, description string) string { + q := url.Values{} + if msgType == "" { + msgType = ResultError } if description == "" { description = "Transaction failed" } - log.Errorf("Transaction %s: %s", msgType, description) + log.WithFields(log.Fields{LogFieldError: msgType, LogFieldDescription: description}). + Error("Transaction failed") q.Set("MsgType", msgType) q.Set("Description", description) - u := url.URL{ + return (&url.URL{ Path: CheckinUnsuccessfulEndpoint, RawQuery: q.Encode(), - } - return u.String() + }).String() } diff --git a/release notes.md b/release notes.md index 6c0e7a7..fe0c975 100644 --- a/release notes.md +++ b/release notes.md @@ -2,6 +2,9 @@ builtVersion is a const in main.go +#### 1.0.17 - 30 August 2025 +added functionality to commit transactions + #### 1.0.15 - 27 August 2025 fixed TCP/IP connection to the lock server