diff --git a/handlers/handlers.go b/handlers/handlers.go new file mode 100644 index 0000000..63c8f6e --- /dev/null +++ b/handlers/handlers.go @@ -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", + }) +} diff --git a/handlers/http_helpers.go b/handlers/http_helpers.go new file mode 100644 index 0000000..640cb73 --- /dev/null +++ b/handlers/http_helpers.go @@ -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) +} diff --git a/main.go b/main.go index e8c072e..4cb3771 100644 --- a/main.go +++ b/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" - 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 diff --git a/payment/creditcall.go b/payment/creditcall.go index bf22561..c0f3089 100644 --- a/payment/creditcall.go +++ b/payment/creditcall.go @@ -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 diff --git a/release notes.md b/release notes.md index 796a4f7..507f8de 100644 --- a/release notes.md +++ b/release notes.md @@ -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