package handlers import ( "bytes" "database/sql" "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/db" "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/hardlink/types" "gitea.futuresens.co.uk/futuresens/logging" log "github.com/sirupsen/logrus" ) type App struct { dispPort *serial.Port lockserver lockserver.LockServer isPayment bool db *sql.DB } func NewApp(dispPort *serial.Port, lockType, encoderAddress string, db *sql.DB, isPayment bool) *App { return &App{ isPayment: isPayment, dispPort: dispPort, lockserver: lockserver.NewLockServer(lockType, encoderAddress, FatalError), db: db, } } 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) } 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 { db.InsertPreauth(r.Context(), app.db, 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 { 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(types.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(types.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", }) }