233 lines
6.8 KiB
Go
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
|
|
}
|