hardlink/handlers/testhandlers.go
2026-03-09 16:31:52 +00:00

233 lines
6.8 KiB
Go

package handlers
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"gitea.futuresens.co.uk/futuresens/cmstypes"
"gitea.futuresens.co.uk/futuresens/hardlink/errorhandlers"
"gitea.futuresens.co.uk/futuresens/hardlink/lockserver"
"gitea.futuresens.co.uk/futuresens/hardlink/mail"
"gitea.futuresens.co.uk/futuresens/hardlink/payment"
"gitea.futuresens.co.uk/futuresens/hardlink/types"
"gitea.futuresens.co.uk/futuresens/logging"
log "github.com/sirupsen/logrus"
)
func (app *App) testIssueDoorCard(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(types.ServiceName, err.Error(), "ReadJSON", string(op), "", "", 0)
errorhandlers.WriteError(w, http.StatusBadRequest, "Invalid JSON payload: "+err.Error())
return
}
now := time.Now()
checkIn := time.Date(now.Year(), now.Month(), now.Day(), 23, 0, 0, 0, now.Location())
checkOut := checkIn.Add(2 * time.Hour)
// 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(types.ServiceName, err.Error(), "Dispense error", string(op), "", "", 0)
errorhandlers.WriteError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error())
return
}
// build lock server command
app.lockserver.BuildCommand(doorReq, checkIn, checkOut)
// lock server sequence
if err := app.lockserver.LockSequence(); err != nil {
logging.Error(types.ServiceName, err.Error(), "Key encoding", string(op), "", "", 0)
errorhandlers.WriteError(w, http.StatusBadGateway, err.Error())
return
}
theResponse.Code = http.StatusOK
theResponse.Message = "Card issued successfully"
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(theResponse)
}
func (app *App) fetchChipDNAStatus(w http.ResponseWriter, r *http.Request) {
const op = logging.Op("fetchChipDNAStatus")
var 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")
status, err := payment.ReadPdqStatus(app.cfg.Hotel, app.cfg.Kiosk)
if err != nil {
logging.Error(types.ServiceName, err.Error(), "fetchChipDNAStatus", string(op), "", app.cfg.Hotel, app.cfg.Kiosk)
errorhandlers.WriteError(w, http.StatusServiceUnavailable, err.Error())
return
}
b, err := json.MarshalIndent(status, "", " ")
if err != nil {
logging.Error(types.ServiceName, err.Error(), "MarshalIndent", string(op), "", "", 0)
errorhandlers.WriteError(w, http.StatusInternalServerError, "Failed to marshal status data")
return
}
theResponse.Code = http.StatusOK
theResponse.Message = string(b)
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(theResponse)
}
func (app *App) onChipDNAError(w http.ResponseWriter, r *http.Request) {
const op = logging.Op("onChipDNAError")
var tr payment.TransactionResultXML
title := "ChipDNA Error"
message := ""
log.Println("onChipDNAError called")
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
}
if r.Method != http.MethodPost {
errorhandlers.WriteError(w, http.StatusMethodNotAllowed, "Method not allowed; use POST")
return
}
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
if err != nil {
message = "Failed to read request body: " + err.Error()
mail.SendEmailOnError(app.cfg.Hotel, app.cfg.Kiosk, title, message)
errorhandlers.WriteError(w, http.StatusBadRequest, "Unable to read request body")
return
}
if len(body) == 0 {
message = "Received empty request body"
mail.SendEmailOnError(app.cfg.Hotel, app.cfg.Kiosk, title, message)
errorhandlers.WriteError(w, http.StatusBadRequest, "Empty body")
return
}
if err := tr.ParseTransactionResult(body); err != nil {
logging.Error(
types.ServiceName,
err.Error(),
"Parse transaction result error",
string(op),
"",
app.cfg.Hotel,
app.cfg.Kiosk,
)
message = "Failed to parse transaction result: " + err.Error()
mail.SendEmailOnError(app.cfg.Hotel, app.cfg.Kiosk, title, message)
errorhandlers.WriteError(w, http.StatusBadRequest, "Invalid XML")
return
}
for _, e := range tr.Entries {
switch e.Key {
case payment.KeyErrors:
mail.SendEmailOnError(app.cfg.Hotel, app.cfg.Kiosk, title, e.Value)
case payment.KeyIsAvailable:
isAvailable := strings.EqualFold(e.Value, "true")
app.handleAvailabilityDebounced(isAvailable)
}
logging.Error(
types.ServiceName,
e.Value,
e.Key,
string(op),
"",
app.cfg.Hotel,
app.cfg.Kiosk,
)
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"received"}`))
}
func (app *App) handleAvailabilityDebounced(isAvailable bool) {
title := "ChipDNA Error"
key := fmt.Sprintf("%s-%d", app.cfg.Hotel, app.cfg.Kiosk)
app.availabilityMu.Lock()
defer app.availabilityMu.Unlock()
// If device becomes available -> cancel pending timer
if isAvailable {
if t, exists := app.availabilityTimers[key]; exists {
t.Stop()
delete(app.availabilityTimers, key)
log.Println("PDQ availability restored - debounce timer cancelled")
}
return
}
// Device became unavailable -> start 10s debounce if not already started
if _, exists := app.availabilityTimers[key]; exists {
return
}
log.Println("PDQ reported unavailable - starting 10s debounce timer")
timer := time.AfterFunc(5*time.Second, func() {
mail.SendEmailOnError(
app.cfg.Hotel,
app.cfg.Kiosk,
title,
"ChipDNA PDQ unavailable for more than 5 seconds",
)
app.availabilityMu.Lock()
delete(app.availabilityTimers, key)
app.availabilityMu.Unlock()
})
app.availabilityTimers[key] = timer
}