moved handlers from main.go to handlers/handlers.go

This commit is contained in:
yurii 2025-11-13 15:16:06 +00:00
parent 7993a2360d
commit f4f44da541
5 changed files with 351 additions and 312 deletions

289
handlers/handlers.go Normal file
View 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
View 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)
}

320
main.go
View File

@ -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"
serviceName = "hardlink"
customLayout = "2006-01-02 15:04:05 -0700"
transactionUrl = "http://127.0.0.1:18181/start-transaction/"
buildVersion = "1.0.22"
serviceName = "hardlink"
)
// 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

View File

@ -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

View File

@ -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