moved handlers from main.go to handlers/handlers.go
This commit is contained in:
parent
7993a2360d
commit
f4f44da541
289
handlers/handlers.go
Normal file
289
handlers/handlers.go
Normal file
@ -0,0 +1,289 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tarm/serial"
|
||||
|
||||
"gitea.futuresens.co.uk/futuresens/cmstypes"
|
||||
"gitea.futuresens.co.uk/futuresens/hardlink/dispenser"
|
||||
"gitea.futuresens.co.uk/futuresens/hardlink/lockserver"
|
||||
"gitea.futuresens.co.uk/futuresens/hardlink/payment"
|
||||
"gitea.futuresens.co.uk/futuresens/hardlink/printer"
|
||||
"gitea.futuresens.co.uk/futuresens/logging"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
customLayout = "2006-01-02 15:04:05 -0700"
|
||||
transactionUrl = "http://127.0.0.1:18181/start-transaction/"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
dispPort *serial.Port
|
||||
lockserver lockserver.LockServer
|
||||
isPayment bool
|
||||
}
|
||||
|
||||
func NewApp(dispPort *serial.Port, lockType, encoderAddress string, isPayment bool) *App {
|
||||
return &App{
|
||||
isPayment: isPayment,
|
||||
dispPort: dispPort,
|
||||
lockserver: lockserver.NewLockServer(lockType, encoderAddress, FatalError),
|
||||
}
|
||||
}
|
||||
|
||||
func (app *App) RegisterRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/issuedoorcard", app.issueDoorCard)
|
||||
mux.HandleFunc("/printroomticket", app.printRoomTicket)
|
||||
mux.HandleFunc("/starttransaction", app.startTransaction)
|
||||
}
|
||||
|
||||
func (app *App) startTransaction(w http.ResponseWriter, r *http.Request) {
|
||||
const op = logging.Op("startTransaction")
|
||||
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("startTransaction 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("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", 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.BuildRedirectURL(result)
|
||||
writeTransactionResult(w, http.StatusOK, theResponse)
|
||||
}
|
||||
|
||||
func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) {
|
||||
const op = logging.Op("issueDoorCard")
|
||||
var (
|
||||
doorReq lockserver.DoorCardRequest
|
||||
theResponse cmstypes.StatusRec
|
||||
)
|
||||
|
||||
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 r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("issueDoorCard called")
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "Method not allowed; use POST")
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
|
||||
writeError(w, http.StatusUnsupportedMediaType, "Content-Type must be application/json")
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&doorReq); err != nil {
|
||||
logging.Error(serviceName, err.Error(), "ReadJSON", string(op), "", "", 0)
|
||||
writeError(w, http.StatusBadRequest, "Invalid JSON payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// parse times
|
||||
checkIn, err := time.Parse(customLayout, doorReq.CheckinTime)
|
||||
if err != nil {
|
||||
logging.Error(serviceName, err.Error(), "Invalid checkinTime format", string(op), "", "", 0)
|
||||
writeError(w, http.StatusBadRequest, "Invalid checkinTime format: "+err.Error())
|
||||
return
|
||||
}
|
||||
checkOut, err := time.Parse(customLayout, doorReq.CheckoutTime)
|
||||
if err != nil {
|
||||
logging.Error(serviceName, err.Error(), "Invalid checkoutTime format", string(op), "", "", 0)
|
||||
writeError(w, http.StatusBadRequest, "Invalid checkoutTime format: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// dispenser sequence
|
||||
if status, err := dispenser.DispenserSequence(app.dispPort); err != nil {
|
||||
if status != "" {
|
||||
logging.Error(serviceName, status, "Dispense error", string(op), "", "", 0)
|
||||
writeError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error())
|
||||
} else {
|
||||
logging.Error(serviceName, err.Error(), "Dispense error", string(op), "", "", 0)
|
||||
writeError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error()+"; check card stock")
|
||||
}
|
||||
return
|
||||
} else {
|
||||
log.Info(status)
|
||||
}
|
||||
|
||||
// build lock server command
|
||||
app.lockserver.BuildCommand(doorReq, checkIn, checkOut)
|
||||
|
||||
// lock server sequence
|
||||
err = app.lockserver.LockSequence()
|
||||
if err != nil {
|
||||
logging.Error(serviceName, err.Error(), "Key encoding", string(op), "", "", 0)
|
||||
writeError(w, http.StatusBadGateway, err.Error())
|
||||
dispenser.CardOutOfMouth(app.dispPort)
|
||||
return
|
||||
}
|
||||
|
||||
// final dispenser steps
|
||||
if status, err := dispenser.CardOutOfMouth(app.dispPort); err != nil {
|
||||
logging.Error(serviceName, err.Error(), "Dispenser eject error", string(op), "", "", 0)
|
||||
writeError(w, http.StatusServiceUnavailable, "Dispenser eject error: "+err.Error())
|
||||
return
|
||||
} else {
|
||||
log.Info(status)
|
||||
}
|
||||
|
||||
theResponse.Code = http.StatusOK
|
||||
theResponse.Message = "Card issued successfully"
|
||||
// success! return 200 and any data you like
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(theResponse)
|
||||
}
|
||||
|
||||
func (app *App) printRoomTicket(w http.ResponseWriter, r *http.Request) {
|
||||
const op = logging.Op("printRoomTicket")
|
||||
var roomDetails printer.RoomDetailsRec
|
||||
// Allow CORS preflight if needed
|
||||
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")
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
log.Println("printRoomTicket called")
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "Method not allowed; use POST")
|
||||
return
|
||||
}
|
||||
if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "xml") {
|
||||
writeError(w, http.StatusUnsupportedMediaType, "Content-Type must be application/xml")
|
||||
return
|
||||
}
|
||||
|
||||
defer r.Body.Close()
|
||||
if err := xml.NewDecoder(r.Body).Decode(&roomDetails); err != nil {
|
||||
logging.Error(serviceName, err.Error(), "ReadXML", string(op), "", "", 0)
|
||||
writeError(w, http.StatusBadRequest, "Invalid XML payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data, err := printer.BuildRoomTicket(roomDetails)
|
||||
if err != nil {
|
||||
logging.Error(serviceName, err.Error(), "BuildRoomTicket", string(op), "", "", 0)
|
||||
writeError(w, http.StatusInternalServerError, "BuildRoomTicket failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Send to the Windows Epson TM-T82II via the printer package
|
||||
if err := printer.SendToPrinter(data); err != nil {
|
||||
logging.Error(serviceName, err.Error(), "printRoomTicket", "printRoomTicket", "", "", 0)
|
||||
writeError(w, http.StatusInternalServerError, "Print failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Success
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(cmstypes.StatusRec{
|
||||
Code: http.StatusOK,
|
||||
Message: "Print job sent successfully",
|
||||
})
|
||||
}
|
||||
41
handlers/http_helpers.go
Normal file
41
handlers/http_helpers.go
Normal file
@ -0,0 +1,41 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"gitea.futuresens.co.uk/futuresens/cmstypes"
|
||||
"gitea.futuresens.co.uk/futuresens/logging"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const serviceName = "hardlink"
|
||||
|
||||
// writeError is a helper to send a JSON error and HTTP status in one go.
|
||||
func writeError(w http.ResponseWriter, status int, msg string) {
|
||||
theResponse := cmstypes.StatusRec{
|
||||
Code: status,
|
||||
Message: msg,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
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 FatalError(err error) {
|
||||
fmt.Println(err.Error())
|
||||
log.Errorf(err.Error())
|
||||
fmt.Println(". Press Enter to exit...")
|
||||
fmt.Scanln()
|
||||
os.Exit(1)
|
||||
}
|
||||
318
main.go
318
main.go
@ -1,11 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
@ -20,19 +17,14 @@ import (
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
|
||||
"gitea.futuresens.co.uk/futuresens/hardlink/dispenser"
|
||||
"gitea.futuresens.co.uk/futuresens/hardlink/handlers"
|
||||
"gitea.futuresens.co.uk/futuresens/hardlink/lockserver"
|
||||
"gitea.futuresens.co.uk/futuresens/hardlink/payment"
|
||||
"gitea.futuresens.co.uk/futuresens/hardlink/printer"
|
||||
|
||||
"gitea.futuresens.co.uk/futuresens/cmstypes"
|
||||
"gitea.futuresens.co.uk/futuresens/logging"
|
||||
)
|
||||
|
||||
const (
|
||||
buildVersion = "1.0.21"
|
||||
buildVersion = "1.0.22"
|
||||
serviceName = "hardlink"
|
||||
customLayout = "2006-01-02 15:04:05 -0700"
|
||||
transactionUrl = "http://127.0.0.1:18181/start-transaction/"
|
||||
)
|
||||
|
||||
// configRec holds values from config.yml.
|
||||
@ -50,21 +42,6 @@ type configRec struct {
|
||||
TestMode bool `yaml:"testMode"`
|
||||
}
|
||||
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Load config
|
||||
config := readConfig()
|
||||
@ -87,7 +64,7 @@ func main() {
|
||||
dispenser.Address = []byte(config.DispenserAdrr)
|
||||
dispHandle, err = dispenser.InitializeDispenser()
|
||||
if err != nil {
|
||||
fatalError(err)
|
||||
handlers.FatalError(err)
|
||||
}
|
||||
defer dispHandle.Close()
|
||||
|
||||
@ -95,7 +72,7 @@ func main() {
|
||||
if err != nil {
|
||||
if len(status) == 0 {
|
||||
err = fmt.Errorf("%s; wrong dispenser address: %s", err, config.DispenserAdrr)
|
||||
fatalError(err)
|
||||
handlers.FatalError(err)
|
||||
} else {
|
||||
fmt.Println(status)
|
||||
fmt.Println(err.Error())
|
||||
@ -130,33 +107,19 @@ func main() {
|
||||
}
|
||||
|
||||
// Create App and wire routes
|
||||
app := newApp(dispHandle, config)
|
||||
app := handlers.NewApp(dispHandle, config.LockType, config.EncoderAddress, config.IsPayment)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
setUpRoutes(app, mux)
|
||||
app.RegisterRoutes(mux)
|
||||
|
||||
addr := fmt.Sprintf(":%d", config.Port)
|
||||
log.Infof("Starting HTTP server on http://localhost%s", addr)
|
||||
fmt.Printf("Starting HTTP server on http://localhost%s", addr)
|
||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||
fatalError(err)
|
||||
handlers.FatalError(err)
|
||||
}
|
||||
}
|
||||
|
||||
func setUpRoutes(app *App, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/issuedoorcard", app.issueDoorCard)
|
||||
mux.HandleFunc("/printroomticket", app.printRoomTicket)
|
||||
mux.HandleFunc("/starttransaction", app.startTransaction)
|
||||
}
|
||||
|
||||
func fatalError(err error) {
|
||||
fmt.Println(err.Error())
|
||||
log.Errorf(err.Error())
|
||||
fmt.Println(". Press Enter to exit...")
|
||||
fmt.Scanln()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// setupLogging ensures log directory, opens log file, and configures logrus.
|
||||
// Returns the *os.File so caller can defer its Close().
|
||||
func setupLogging(logDir string) (*os.File, error) {
|
||||
@ -179,265 +142,6 @@ func setupLogging(logDir string) (*os.File, error) {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// writeError is a helper to send a JSON error and HTTP status in one go.
|
||||
func writeError(w http.ResponseWriter, status int, msg string) {
|
||||
theResponse := cmstypes.StatusRec{
|
||||
Code: status,
|
||||
Message: msg,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
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
|
||||
}
|
||||
|
||||
log.Println("startTransaction 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("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", 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
|
||||
}
|
||||
|
||||
responseEntries, _ := payment.ParseTransactionResult(body)
|
||||
|
||||
// Compose JSON from responseEntries
|
||||
result := make(map[string]string)
|
||||
for _, e := range responseEntries {
|
||||
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.BuildRedirectURL(result)
|
||||
writeTransactionResult(w, http.StatusOK, theResponse)
|
||||
}
|
||||
|
||||
func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) {
|
||||
const op = logging.Op("issueDoorCard")
|
||||
var (
|
||||
doorReq lockserver.DoorCardRequest
|
||||
theResponse cmstypes.StatusRec
|
||||
)
|
||||
|
||||
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 r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("issueDoorCard called")
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "Method not allowed; use POST")
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
|
||||
writeError(w, http.StatusUnsupportedMediaType, "Content-Type must be application/json")
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&doorReq); err != nil {
|
||||
logging.Error(serviceName, err.Error(), "ReadJSON", string(op), "", "", 0)
|
||||
writeError(w, http.StatusBadRequest, "Invalid JSON payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// parse times
|
||||
checkIn, err := time.Parse(customLayout, doorReq.CheckinTime)
|
||||
if err != nil {
|
||||
logging.Error(serviceName, err.Error(), "Invalid checkinTime format", string(op), "", "", 0)
|
||||
writeError(w, http.StatusBadRequest, "Invalid checkinTime format: "+err.Error())
|
||||
return
|
||||
}
|
||||
checkOut, err := time.Parse(customLayout, doorReq.CheckoutTime)
|
||||
if err != nil {
|
||||
logging.Error(serviceName, err.Error(), "Invalid checkoutTime format", string(op), "", "", 0)
|
||||
writeError(w, http.StatusBadRequest, "Invalid checkoutTime format: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// dispenser sequence
|
||||
if status, err := dispenser.DispenserSequence(app.dispPort); err != nil {
|
||||
if status != "" {
|
||||
logging.Error(serviceName, status, "Dispense error", string(op), "", "", 0)
|
||||
writeError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error())
|
||||
} else {
|
||||
logging.Error(serviceName, err.Error(), "Dispense error", string(op), "", "", 0)
|
||||
writeError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error()+"; check card stock")
|
||||
}
|
||||
return
|
||||
} else {
|
||||
log.Info(status)
|
||||
}
|
||||
|
||||
// build lock server command
|
||||
app.lockserver.BuildCommand(doorReq, checkIn, checkOut)
|
||||
|
||||
// lock server sequence
|
||||
err = app.lockserver.LockSequence()
|
||||
if err != nil {
|
||||
logging.Error(serviceName, err.Error(), "Key encoding", string(op), "", "", 0)
|
||||
writeError(w, http.StatusBadGateway, err.Error())
|
||||
dispenser.CardOutOfMouth(app.dispPort)
|
||||
return
|
||||
}
|
||||
|
||||
// final dispenser steps
|
||||
if status, err := dispenser.CardOutOfMouth(app.dispPort); err != nil {
|
||||
logging.Error(serviceName, err.Error(), "Dispenser eject error", string(op), "", "", 0)
|
||||
writeError(w, http.StatusServiceUnavailable, "Dispenser eject error: "+err.Error())
|
||||
return
|
||||
} else {
|
||||
log.Info(status)
|
||||
}
|
||||
|
||||
theResponse.Code = http.StatusOK
|
||||
theResponse.Message = "Card issued successfully"
|
||||
// success! return 200 and any data you like
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(theResponse)
|
||||
}
|
||||
|
||||
func (app *App) printRoomTicket(w http.ResponseWriter, r *http.Request) {
|
||||
const op = logging.Op("printRoomTicket")
|
||||
var roomDetails printer.RoomDetailsRec
|
||||
// Allow CORS preflight if needed
|
||||
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")
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
log.Println("printRoomTicket called")
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "Method not allowed; use POST")
|
||||
return
|
||||
}
|
||||
if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "xml") {
|
||||
writeError(w, http.StatusUnsupportedMediaType, "Content-Type must be application/xml")
|
||||
return
|
||||
}
|
||||
|
||||
defer r.Body.Close()
|
||||
if err := xml.NewDecoder(r.Body).Decode(&roomDetails); err != nil {
|
||||
logging.Error(serviceName, err.Error(), "ReadXML", string(op), "", "", 0)
|
||||
writeError(w, http.StatusBadRequest, "Invalid XML payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data, err := printer.BuildRoomTicket(roomDetails)
|
||||
if err != nil {
|
||||
logging.Error(serviceName, err.Error(), "BuildRoomTicket", string(op), "", "", 0)
|
||||
writeError(w, http.StatusInternalServerError, "BuildRoomTicket failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Send to the Windows Epson TM-T82II via the printer package
|
||||
if err := printer.SendToPrinter(data); err != nil {
|
||||
logging.Error(serviceName, err.Error(), "printRoomTicket", "printRoomTicket", "", "", 0)
|
||||
writeError(w, http.StatusInternalServerError, "Print failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Success
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(cmstypes.StatusRec{
|
||||
Code: http.StatusOK,
|
||||
Message: "Print job sent successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// readConfig reads config.yml and applies defaults.
|
||||
func readConfig() configRec {
|
||||
var cfg configRec
|
||||
@ -458,7 +162,7 @@ func readConfig() configRec {
|
||||
|
||||
if cfg.LockType == "" {
|
||||
err = fmt.Errorf("LockType is required in %s", configName)
|
||||
fatalError(err)
|
||||
handlers.FatalError(err)
|
||||
}
|
||||
cfg.LockType = strings.ToLower(cfg.LockType)
|
||||
|
||||
@ -477,12 +181,12 @@ func readTicketLayout() printer.LayoutOptions {
|
||||
// 1) Read the file
|
||||
data, err := os.ReadFile(layoutName)
|
||||
if err != nil {
|
||||
fatalError(fmt.Errorf("failed to read %s: %v", layoutName, err))
|
||||
handlers.FatalError(fmt.Errorf("failed to read %s: %v", layoutName, err))
|
||||
}
|
||||
|
||||
// 2) Unmarshal into your struct
|
||||
if err := xml.Unmarshal(data, &layout); err != nil {
|
||||
fatalError(fmt.Errorf("failed to parse %s: %v", layoutName, err))
|
||||
handlers.FatalError(fmt.Errorf("failed to parse %s: %v", layoutName, err))
|
||||
}
|
||||
|
||||
return layout
|
||||
@ -501,7 +205,7 @@ func startChipDnaClient() {
|
||||
|
||||
cmd, err := startClient()
|
||||
if err != nil {
|
||||
fatalError(err)
|
||||
handlers.FatalError(err)
|
||||
}
|
||||
|
||||
// Restart loop
|
||||
|
||||
@ -76,12 +76,11 @@ type (
|
||||
)
|
||||
|
||||
// ParseTransactionResult parses the XML into entries.
|
||||
func ParseTransactionResult(data []byte) ([]EntryXML, error) {
|
||||
var tr TransactionResultXML
|
||||
func (tr *TransactionResultXML) ParseTransactionResult(data []byte) error {
|
||||
if err := xml.Unmarshal(data, &tr); err != nil {
|
||||
return nil, fmt.Errorf("XML unmarshal: %w", err)
|
||||
return fmt.Errorf("XML unmarshal: %w", err)
|
||||
}
|
||||
return tr.Entries, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// initMSSQL opens and pings the SQL Server instance localhost\SQLEXPRESS
|
||||
|
||||
@ -2,6 +2,12 @@
|
||||
|
||||
builtVersion is a const in main.go
|
||||
|
||||
#### 1.0.23 - 20 October 2025
|
||||
moved handlers from main.go to handlers/handlers.go
|
||||
|
||||
#### 1.0.22 - 20 October 2025
|
||||
added test mode into config file to allow testing without connecting to the dispenser
|
||||
|
||||
#### 1.0.21 - 20 October 2025
|
||||
increased timeout for Salto lock server connection to 40 seconds
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user