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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@ -20,19 +17,14 @@ import (
|
|||||||
yaml "gopkg.in/yaml.v3"
|
yaml "gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/dispenser"
|
"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/lockserver"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/payment"
|
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/printer"
|
"gitea.futuresens.co.uk/futuresens/hardlink/printer"
|
||||||
|
|
||||||
"gitea.futuresens.co.uk/futuresens/cmstypes"
|
|
||||||
"gitea.futuresens.co.uk/futuresens/logging"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
buildVersion = "1.0.21"
|
buildVersion = "1.0.22"
|
||||||
serviceName = "hardlink"
|
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.
|
// configRec holds values from config.yml.
|
||||||
@ -50,21 +42,6 @@ type configRec struct {
|
|||||||
TestMode bool `yaml:"testMode"`
|
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() {
|
func main() {
|
||||||
// Load config
|
// Load config
|
||||||
config := readConfig()
|
config := readConfig()
|
||||||
@ -87,7 +64,7 @@ func main() {
|
|||||||
dispenser.Address = []byte(config.DispenserAdrr)
|
dispenser.Address = []byte(config.DispenserAdrr)
|
||||||
dispHandle, err = dispenser.InitializeDispenser()
|
dispHandle, err = dispenser.InitializeDispenser()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalError(err)
|
handlers.FatalError(err)
|
||||||
}
|
}
|
||||||
defer dispHandle.Close()
|
defer dispHandle.Close()
|
||||||
|
|
||||||
@ -95,7 +72,7 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if len(status) == 0 {
|
if len(status) == 0 {
|
||||||
err = fmt.Errorf("%s; wrong dispenser address: %s", err, config.DispenserAdrr)
|
err = fmt.Errorf("%s; wrong dispenser address: %s", err, config.DispenserAdrr)
|
||||||
fatalError(err)
|
handlers.FatalError(err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(status)
|
fmt.Println(status)
|
||||||
fmt.Println(err.Error())
|
fmt.Println(err.Error())
|
||||||
@ -130,33 +107,19 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create App and wire routes
|
// Create App and wire routes
|
||||||
app := newApp(dispHandle, config)
|
app := handlers.NewApp(dispHandle, config.LockType, config.EncoderAddress, config.IsPayment)
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
setUpRoutes(app, mux)
|
app.RegisterRoutes(mux)
|
||||||
|
|
||||||
addr := fmt.Sprintf(":%d", config.Port)
|
addr := fmt.Sprintf(":%d", config.Port)
|
||||||
log.Infof("Starting HTTP server on http://localhost%s", addr)
|
log.Infof("Starting HTTP server on http://localhost%s", addr)
|
||||||
fmt.Printf("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 {
|
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.
|
// setupLogging ensures log directory, opens log file, and configures logrus.
|
||||||
// Returns the *os.File so caller can defer its Close().
|
// Returns the *os.File so caller can defer its Close().
|
||||||
func setupLogging(logDir string) (*os.File, error) {
|
func setupLogging(logDir string) (*os.File, error) {
|
||||||
@ -179,265 +142,6 @@ func setupLogging(logDir string) (*os.File, error) {
|
|||||||
return f, nil
|
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.
|
// readConfig reads config.yml and applies defaults.
|
||||||
func readConfig() configRec {
|
func readConfig() configRec {
|
||||||
var cfg configRec
|
var cfg configRec
|
||||||
@ -458,7 +162,7 @@ func readConfig() configRec {
|
|||||||
|
|
||||||
if cfg.LockType == "" {
|
if cfg.LockType == "" {
|
||||||
err = fmt.Errorf("LockType is required in %s", configName)
|
err = fmt.Errorf("LockType is required in %s", configName)
|
||||||
fatalError(err)
|
handlers.FatalError(err)
|
||||||
}
|
}
|
||||||
cfg.LockType = strings.ToLower(cfg.LockType)
|
cfg.LockType = strings.ToLower(cfg.LockType)
|
||||||
|
|
||||||
@ -477,12 +181,12 @@ func readTicketLayout() printer.LayoutOptions {
|
|||||||
// 1) Read the file
|
// 1) Read the file
|
||||||
data, err := os.ReadFile(layoutName)
|
data, err := os.ReadFile(layoutName)
|
||||||
if err != nil {
|
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
|
// 2) Unmarshal into your struct
|
||||||
if err := xml.Unmarshal(data, &layout); err != nil {
|
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
|
return layout
|
||||||
@ -501,7 +205,7 @@ func startChipDnaClient() {
|
|||||||
|
|
||||||
cmd, err := startClient()
|
cmd, err := startClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalError(err)
|
handlers.FatalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart loop
|
// Restart loop
|
||||||
|
|||||||
@ -76,12 +76,11 @@ type (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ParseTransactionResult parses the XML into entries.
|
// ParseTransactionResult parses the XML into entries.
|
||||||
func ParseTransactionResult(data []byte) ([]EntryXML, error) {
|
func (tr *TransactionResultXML) ParseTransactionResult(data []byte) error {
|
||||||
var tr TransactionResultXML
|
|
||||||
if err := xml.Unmarshal(data, &tr); err != nil {
|
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
|
// initMSSQL opens and pings the SQL Server instance localhost\SQLEXPRESS
|
||||||
|
|||||||
@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
builtVersion is a const in main.go
|
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
|
#### 1.0.21 - 20 October 2025
|
||||||
increased timeout for Salto lock server connection to 40 seconds
|
increased timeout for Salto lock server connection to 40 seconds
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user