added functionality to commit transactions
This commit is contained in:
parent
1bceb55285
commit
f1dc0ccce4
69
main.go
69
main.go
@ -1,7 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// "database/sql"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -29,7 +29,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
buildVersion = "1.0.15"
|
buildVersion = "1.0.17"
|
||||||
serviceName = "hardlink"
|
serviceName = "hardlink"
|
||||||
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/"
|
transactionUrl = "http://127.0.0.1:18181/start-transaction/"
|
||||||
@ -51,12 +51,14 @@ type configRec struct {
|
|||||||
|
|
||||||
// App holds shared resources.
|
// App holds shared resources.
|
||||||
type App struct {
|
type App struct {
|
||||||
|
configRec configRec
|
||||||
dispPort *serial.Port
|
dispPort *serial.Port
|
||||||
lockserver lockserver.LockServer
|
lockserver lockserver.LockServer
|
||||||
}
|
}
|
||||||
|
|
||||||
func newApp(dispPort *serial.Port, config configRec) *App {
|
func newApp(dispPort *serial.Port, config configRec) *App {
|
||||||
return &App{
|
return &App{
|
||||||
|
configRec: config,
|
||||||
dispPort: dispPort,
|
dispPort: dispPort,
|
||||||
lockserver: lockserver.NewLockServer(config.LockType, config.EncoderAddress, fatalError),
|
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)
|
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) {
|
func (app *App) startTransaction(w http.ResponseWriter, r *http.Request) {
|
||||||
const op = logging.Op("startTransaction")
|
const op = logging.Op("startTransaction")
|
||||||
var (
|
var (
|
||||||
theResponse cmstypes.ResponseRec
|
theResponse cmstypes.ResponseRec
|
||||||
cardholderReceipt string
|
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-Origin", "*")
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||||
w.Header().Set("Content-Type", "application/json")
|
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 {
|
if r.Method == http.MethodOptions {
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
return
|
return
|
||||||
@ -249,29 +269,43 @@ func (app *App) startTransaction(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
log.Println("startTransaction called")
|
log.Println("startTransaction called")
|
||||||
if r.Method != http.MethodPost {
|
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
|
return
|
||||||
}
|
}
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
|
|
||||||
if ct := r.Header.Get("Content-Type"); ct != "text/xml" {
|
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
|
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}
|
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 {
|
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)
|
||||||
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
|
return
|
||||||
}
|
}
|
||||||
defer response.Body.Close()
|
defer response.Body.Close()
|
||||||
|
|
||||||
body, err := io.ReadAll(response.Body)
|
body, err = io.ReadAll(response.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Error(serviceName, err.Error(), "Read response body error", string(op), "", "", 0)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,10 +315,11 @@ func (app *App) startTransaction(w http.ResponseWriter, r *http.Request) {
|
|||||||
result := make(map[string]string)
|
result := make(map[string]string)
|
||||||
for _, e := range responseEntries {
|
for _, e := range responseEntries {
|
||||||
switch e.Key {
|
switch e.Key {
|
||||||
case "RECEIPT_DATA", "RECEIPT_DATA_MERCHANT":
|
case payment.ReceiptData, payment.ReceiptDataMerchant:
|
||||||
case "RECEIPT_DATA_CARDHOLDER":
|
// ignore these
|
||||||
|
case payment.ReceiptDataCardholder:
|
||||||
cardholderReceipt = e.Value
|
cardholderReceipt = e.Value
|
||||||
case "TRANSACTION_RESULT":
|
case payment.TransactionResult:
|
||||||
theResponse.Status.Message = e.Value
|
theResponse.Status.Message = e.Value
|
||||||
theResponse.Status.Code = http.StatusOK
|
theResponse.Status.Code = http.StatusOK
|
||||||
result[e.Key] = e.Value
|
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)
|
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)
|
theResponse.Data = payment.BuildRedirectURL(result)
|
||||||
|
writeTransactionResult(w, http.StatusOK, theResponse)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) {
|
func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -16,6 +16,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// Transaction types
|
||||||
|
SaleTransactionType = "sale"
|
||||||
|
AccountVerificationType = "account verification"
|
||||||
|
|
||||||
|
// Transaction results
|
||||||
ResultApproved = "approved"
|
ResultApproved = "approved"
|
||||||
ResultDeclined = "declined"
|
ResultDeclined = "declined"
|
||||||
ResultCancelled = "cancelled"
|
ResultCancelled = "cancelled"
|
||||||
@ -23,6 +28,24 @@ const (
|
|||||||
ResultError = "error"
|
ResultError = "error"
|
||||||
CheckinSuccessfulEndpoint = "/successful" // Endpoint to send guest to after successful payment
|
CheckinSuccessfulEndpoint = "/successful" // Endpoint to send guest to after successful payment
|
||||||
CheckinUnsuccessfulEndpoint = "/unsuccessful"
|
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
|
// XML parsing structs
|
||||||
@ -42,6 +65,14 @@ type (
|
|||||||
Key string `xml:"Key"`
|
Key string `xml:"Key"`
|
||||||
Value string `xml:"Value"`
|
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.
|
// ParseTransactionResult parses the XML into entries.
|
||||||
@ -217,47 +248,70 @@ func nullableFloatArg(nf sql.NullFloat64) interface{} {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildRedirectURL builds the redirect URL to send the guest to after payment.
|
||||||
func BuildRedirectURL(result map[string]string) string {
|
func BuildRedirectURL(result map[string]string) string {
|
||||||
var msgType, description string
|
res := result[TransactionResult]
|
||||||
q := url.Values{}
|
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 {
|
return buildSuccessURL(result)
|
||||||
q.Set("TxnReference", result["REFERENCE"])
|
|
||||||
q.Set("CardHash", hex.EncodeToString([]byte(result["CARD_HASH"])))
|
// Transaction type Sale?
|
||||||
q.Set("CardReference", hex.EncodeToString([]byte(result["CARD_REFERENCE"])))
|
case strings.EqualFold(tType, SaleTransactionType):
|
||||||
u := url.URL{
|
// Transaction confirmed?
|
||||||
Path: CheckinSuccessfulEndpoint,
|
if strings.EqualFold(result[ConfirmResult], ResultApproved) {
|
||||||
RawQuery: q.Encode(),
|
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
|
// Not approved
|
||||||
if res != "" {
|
return BuildFailureURL(res, result[Errors])
|
||||||
msgType = res
|
}
|
||||||
}
|
|
||||||
|
|
||||||
errors, ok := result["ERRORS"]
|
func buildSuccessURL(result map[string]string) string {
|
||||||
if ok && errors != "" {
|
q := url.Values{}
|
||||||
description = errors
|
q.Set("TxnReference", result[Reference])
|
||||||
}
|
q.Set("CardHash", hex.EncodeToString([]byte(result[CardHash])))
|
||||||
errors, ok = result["ERROR"]
|
q.Set("CardReference", hex.EncodeToString([]byte(result[CardReference])))
|
||||||
if ok && errors != "" {
|
return (&url.URL{
|
||||||
description += " " + errors
|
Path: CheckinSuccessfulEndpoint,
|
||||||
|
RawQuery: q.Encode(),
|
||||||
|
}).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildFailureURL(msgType, description string) string {
|
||||||
|
q := url.Values{}
|
||||||
|
if msgType == "" {
|
||||||
|
msgType = ResultError
|
||||||
}
|
}
|
||||||
if description == "" {
|
if description == "" {
|
||||||
description = "Transaction failed"
|
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("MsgType", msgType)
|
||||||
q.Set("Description", description)
|
q.Set("Description", description)
|
||||||
u := url.URL{
|
return (&url.URL{
|
||||||
Path: CheckinUnsuccessfulEndpoint,
|
Path: CheckinUnsuccessfulEndpoint,
|
||||||
RawQuery: q.Encode(),
|
RawQuery: q.Encode(),
|
||||||
}
|
}).String()
|
||||||
return u.String()
|
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
builtVersion is a const in main.go
|
builtVersion is a const in main.go
|
||||||
|
|
||||||
|
#### 1.0.17 - 30 August 2025
|
||||||
|
added functionality to commit transactions
|
||||||
|
|
||||||
#### 1.0.15 - 27 August 2025
|
#### 1.0.15 - 27 August 2025
|
||||||
fixed TCP/IP connection to the lock server
|
fixed TCP/IP connection to the lock server
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user