403 lines
14 KiB
Go
403 lines
14 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"gitea.futuresens.co.uk/futuresens/cmstypes"
|
|
"gitea.futuresens.co.uk/futuresens/hardlink/config"
|
|
"gitea.futuresens.co.uk/futuresens/hardlink/dispenser"
|
|
"gitea.futuresens.co.uk/futuresens/hardlink/errorhandlers"
|
|
"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/types"
|
|
"gitea.futuresens.co.uk/futuresens/logging"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type App struct {
|
|
disp *dispenser.Client
|
|
lockserver lockserver.LockServer
|
|
isPayment bool
|
|
db *sql.DB
|
|
cfg *config.ConfigRec
|
|
dbMu sync.Mutex
|
|
cardWellMu sync.RWMutex
|
|
cardWellStatus string
|
|
}
|
|
|
|
func NewApp(disp *dispenser.Client, lockType, encoderAddress, cardWellStatus string, db *sql.DB, cfg *config.ConfigRec) *App {
|
|
app := &App{
|
|
isPayment: cfg.IsPayment,
|
|
disp: disp,
|
|
lockserver: lockserver.NewLockServer(lockType, encoderAddress, errorhandlers.FatalError),
|
|
db: db,
|
|
cfg: cfg,
|
|
}
|
|
app.SetCardWellStatus(cardWellStatus)
|
|
return app
|
|
}
|
|
|
|
func (app *App) RegisterRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("/issuedoorcard", app.issueDoorCard)
|
|
mux.HandleFunc("/printroomticket", app.printRoomTicket)
|
|
mux.HandleFunc("/takepreauth", app.takePreauthorization)
|
|
mux.HandleFunc("/takepayment", app.takePayment)
|
|
mux.HandleFunc("/dispenserstatus", app.reportDispenserStatus)
|
|
}
|
|
|
|
func (app *App) takePreauthorization(w http.ResponseWriter, r *http.Request) {
|
|
const op = logging.Op("takePreauthorization")
|
|
var (
|
|
theResponse cmstypes.ResponseRec
|
|
theRequest cmstypes.TransactionRec
|
|
trResult payment.TransactionResultXML
|
|
result payment.PaymentResult
|
|
save bool
|
|
)
|
|
|
|
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(types.ResultError, "Payment processing is disabled")
|
|
writeTransactionResult(w, http.StatusServiceUnavailable, theResponse)
|
|
return
|
|
}
|
|
|
|
if r.Method == http.MethodOptions {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
log.Println("takePreauthorization called")
|
|
if r.Method != http.MethodPost {
|
|
theResponse.Data = payment.BuildFailureURL(types.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(types.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(types.ResultError, "Invalid XML payload")
|
|
writeTransactionResult(w, http.StatusBadRequest, theResponse)
|
|
return
|
|
}
|
|
log.Printf("Transaction payload: Amount=%s, Type=%s", theRequest.AmountMinorUnits, theRequest.TransactionType)
|
|
|
|
client := &http.Client{Timeout: 300 * time.Second}
|
|
response, err := client.Post(types.LinkTakePreauthorization, "text/xml", bytes.NewBuffer(body))
|
|
if err != nil {
|
|
logging.Error(serviceName, err.Error(), "Payment processing error", string(op), "", "", 0)
|
|
theResponse.Data = payment.BuildFailureURL(types.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(types.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.FillFromTransactionResult(trResult)
|
|
|
|
if err := printer.PrintCardholderReceipt(result.CardholderReceipt); err != nil {
|
|
log.Errorf("PrintCardholderReceipt error: %v", err)
|
|
}
|
|
|
|
theResponse.Status = result.Status
|
|
theResponse.Data, save = payment.BuildPreauthRedirectURL(result.Fields)
|
|
if save {
|
|
go app.persistPreauth(context.Background(), result.Fields, theRequest.CheckoutDate)
|
|
}
|
|
|
|
writeTransactionResult(w, http.StatusOK, theResponse)
|
|
}
|
|
|
|
func (app *App) takePayment(w http.ResponseWriter, r *http.Request) {
|
|
const op = logging.Op("takePayment")
|
|
var (
|
|
theResponse cmstypes.ResponseRec
|
|
theRequest cmstypes.TransactionRec
|
|
trResult payment.TransactionResultXML
|
|
result payment.PaymentResult
|
|
)
|
|
|
|
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(types.ResultError, "Payment processing is disabled")
|
|
writeTransactionResult(w, http.StatusServiceUnavailable, theResponse)
|
|
return
|
|
}
|
|
|
|
if r.Method == http.MethodOptions {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
log.Println("takePayment called")
|
|
if r.Method != http.MethodPost {
|
|
theResponse.Data = payment.BuildFailureURL(types.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(types.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(types.ResultError, "Invalid XML payload")
|
|
writeTransactionResult(w, http.StatusBadRequest, theResponse)
|
|
return
|
|
}
|
|
log.Printf("Transaction payload: Amount=%s, Type=%s", theRequest.AmountMinorUnits, theRequest.TransactionType)
|
|
|
|
client := &http.Client{Timeout: 300 * time.Second}
|
|
response, err := client.Post(types.LinkTakePayment, "text/xml", bytes.NewBuffer(body))
|
|
if err != nil {
|
|
logging.Error(serviceName, err.Error(), "Payment processing error", string(op), "", "", 0)
|
|
theResponse.Data = payment.BuildFailureURL(types.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(types.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.FillFromTransactionResult(trResult)
|
|
|
|
if err := printer.PrintCardholderReceipt(result.CardholderReceipt); err != nil {
|
|
log.Errorf("PrintCardholderReceipt error: %v", err)
|
|
}
|
|
|
|
theResponse.Status = result.Status
|
|
theResponse.Data = payment.BuildPaymentRedirectURL(result.Fields)
|
|
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 {
|
|
errorhandlers.WriteError(w, http.StatusMethodNotAllowed, "Method not allowed; use POST")
|
|
return
|
|
}
|
|
defer r.Body.Close()
|
|
|
|
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
|
|
errorhandlers.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)
|
|
errorhandlers.WriteError(w, http.StatusBadRequest, "Invalid JSON payload: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// parse times
|
|
checkIn, err := time.Parse(types.CustomLayout, doorReq.CheckinTime)
|
|
if err != nil {
|
|
logging.Error(serviceName, err.Error(), "Invalid checkinTime format", string(op), "", "", 0)
|
|
errorhandlers.WriteError(w, http.StatusBadRequest, "Invalid checkinTime format: "+err.Error())
|
|
return
|
|
}
|
|
checkOut, err := time.Parse(types.CustomLayout, doorReq.CheckoutTime)
|
|
if err != nil {
|
|
logging.Error(serviceName, err.Error(), "Invalid checkoutTime format", string(op), "", "", 0)
|
|
errorhandlers.WriteError(w, http.StatusBadRequest, "Invalid checkoutTime format: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Ensure dispenser ready (card at encoder) BEFORE we attempt encoding.
|
|
// With queued dispenser ops, this will not clash with polling.
|
|
status, err := app.disp.DispenserStart(r.Context())
|
|
app.SetCardWellStatus(status)
|
|
if err != nil {
|
|
logging.Error(serviceName, err.Error(), "Dispense error", string(op), "", "", 0)
|
|
errorhandlers.WriteError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Always attempt to finalize after we have moved a card / started an issuance flow.
|
|
// This guarantees we eject and prepare the next card even on lock failures.
|
|
finalize := func() {
|
|
if app.disp == nil {
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
|
defer cancel()
|
|
|
|
status, ferr := app.disp.DispenserFinal(ctx)
|
|
if ferr != nil {
|
|
logging.Error(serviceName, ferr.Error(), "Dispenser final error", string(op), "", "", 0)
|
|
return
|
|
}
|
|
app.SetCardWellStatus(status)
|
|
}
|
|
|
|
// build lock server command
|
|
app.lockserver.BuildCommand(doorReq, checkIn, checkOut)
|
|
|
|
// lock server sequence
|
|
if err := app.lockserver.LockSequence(); err != nil {
|
|
logging.Error(serviceName, err.Error(), "Key encoding", string(op), "", "", 0)
|
|
finalize()
|
|
errorhandlers.WriteError(w, http.StatusBadGateway, err.Error())
|
|
return
|
|
}
|
|
|
|
// final dispenser steps
|
|
finalize()
|
|
|
|
theResponse.Code = http.StatusOK
|
|
theResponse.Message = "Card issued successfully"
|
|
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 {
|
|
errorhandlers.WriteError(w, http.StatusMethodNotAllowed, "Method not allowed; use POST")
|
|
return
|
|
}
|
|
if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "xml") {
|
|
errorhandlers.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)
|
|
errorhandlers.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)
|
|
errorhandlers.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)
|
|
errorhandlers.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",
|
|
})
|
|
}
|
|
|
|
func (app *App) reportDispenserStatus(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
_ = json.NewEncoder(w).Encode(cmstypes.StatusRec{
|
|
Code: http.StatusOK,
|
|
Message: app.CardWellStatus(),
|
|
})
|
|
}
|
|
|
|
func (app *App) SetCardWellStatus(s string) {
|
|
app.cardWellMu.Lock()
|
|
app.cardWellStatus = s
|
|
app.cardWellMu.Unlock()
|
|
}
|
|
|
|
func (app *App) CardWellStatus() string {
|
|
app.cardWellMu.RLock()
|
|
defer app.cardWellMu.RUnlock()
|
|
return app.cardWellStatus
|
|
}
|