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) { const ( debounceDay = 30 debounceNight = 600 title = "ChipDNA Error" ) key := app.availabilityKey() 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 } debounce := debounceDay hour := time.Now().Hour() if hour < 6 { debounce = debounceNight } log.Printf("PDQ reported unavailable - starting %ds debounce timer", debounce) timer := time.AfterFunc(time.Duration(debounce)*time.Second, func() { mail.SendEmailOnError( app.cfg.Hotel, app.cfg.Kiosk, title, fmt.Sprintf("ChipDNA PDQ unavailable for more than %d seconds", debounce), ) app.availabilityMu.Lock() delete(app.availabilityTimers, key) app.availabilityMu.Unlock() }) app.availabilityTimers[key] = timer } func (app *App) availabilityKey() string { return fmt.Sprintf("hotel=%s|kiosk=%d|app=%p", strings.TrimSpace(app.cfg.Hotel), app.cfg.Kiosk, app, ) }