Compare commits
6 Commits
1.1.3
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| c6c2b40f37 | |||
| 8f093159cd | |||
| e1549dda2f | |||
| 163ac0e808 | |||
| 19cfbf185b | |||
| 48e2b6f568 |
@ -12,21 +12,24 @@ import (
|
|||||||
|
|
||||||
// configRec holds values from config.yml.
|
// configRec holds values from config.yml.
|
||||||
type ConfigRec struct {
|
type ConfigRec struct {
|
||||||
Port int `yaml:"port"`
|
Port int `yaml:"port"`
|
||||||
LockserverUrl string `yaml:"lockservUrl"`
|
LockserverUrl string `yaml:"lockservUrl"`
|
||||||
LockType string `yaml:"lockType"`
|
LockType string `yaml:"lockType"`
|
||||||
EncoderAddress string `yaml:"encoderAddr"`
|
EncoderAddress string `yaml:"encoderAddr"`
|
||||||
Cert string `yaml:"cert"`
|
Cert string `yaml:"cert"`
|
||||||
DispenserPort string `yaml:"dispensPort"`
|
DispenserPort string `yaml:"dispensPort"`
|
||||||
DispenserAdrr string `yaml:"dispensAddr"`
|
DispenserAdrr string `yaml:"dispensAddr"`
|
||||||
PrinterName string `yaml:"printerName"`
|
PrinterName string `yaml:"printerName"`
|
||||||
LogDir string `yaml:"logdir"`
|
LogDir string `yaml:"logdir"`
|
||||||
Dbport int `yaml:"dbport"` // Port for the database connection
|
Dbport int `yaml:"dbport"` // Port for the database connection
|
||||||
Dbname string `yaml:"dbname"` // Database name for the connection
|
Dbname string `yaml:"dbname"` // Database name for the connection
|
||||||
Dbuser string `yaml:"dbuser"` // User for the database connection
|
Dbuser string `yaml:"dbuser"` // User for the database connection
|
||||||
Dbpassword string `yaml:"dbpassword"` // Password for the database connection
|
Dbpassword string `yaml:"dbpassword"` // Password for the database connection
|
||||||
IsPayment bool `yaml:"isPayment"`
|
IsPayment bool `yaml:"isPayment"`
|
||||||
TestMode bool `yaml:"testMode"`
|
TestMode bool `yaml:"testMode"`
|
||||||
|
Hotel string `yaml:"hotel"`
|
||||||
|
Kiosk int `yaml:"kiosk"`
|
||||||
|
SendErrorEmails []string `yaml:"senderroremails"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadConfig reads config.yml and applies defaults.
|
// ReadConfig reads config.yml and applies defaults.
|
||||||
|
|||||||
@ -37,6 +37,7 @@ var (
|
|||||||
0x32: "Preparing card fails",
|
0x32: "Preparing card fails",
|
||||||
0x31: "Preparing card",
|
0x31: "Preparing card",
|
||||||
0x30: "Normal",
|
0x30: "Normal",
|
||||||
|
0x36: "Command cannot execute; Preparing card fails",
|
||||||
}
|
}
|
||||||
statusPos1 = map[byte]string{
|
statusPos1 = map[byte]string{
|
||||||
0x38: "Dispensing card",
|
0x38: "Dispensing card",
|
||||||
@ -105,6 +106,9 @@ func stockTake(statusBytes []byte) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
status := ""
|
status := ""
|
||||||
|
if statusBytes[0] == 0x32 || statusBytes[0] == 0x36 {
|
||||||
|
status = statusPos0[statusBytes[0]]
|
||||||
|
}
|
||||||
if statusBytes[2] != 0x30 {
|
if statusBytes[2] != 0x30 {
|
||||||
status = statusPos2[statusBytes[2]]
|
status = statusPos2[statusBytes[2]]
|
||||||
}
|
}
|
||||||
@ -178,7 +182,7 @@ func InitializeDispenser() (*serial.Port, error) {
|
|||||||
const (
|
const (
|
||||||
funcName = "InitializeDispenser"
|
funcName = "InitializeDispenser"
|
||||||
maxRetries = 3
|
maxRetries = 3
|
||||||
retryDelay = 2 * time.Second
|
retryDelay = 4 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
if SerialPort == "" {
|
if SerialPort == "" {
|
||||||
|
|||||||
@ -279,7 +279,9 @@ func (c *Client) DispenserStart(ctx context.Context) (string, error) {
|
|||||||
return stockStatus, fmt.Errorf("[%s] check status: %w", funcName, err)
|
return stockStatus, fmt.Errorf("[%s] check status: %w", funcName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logStatus(status)
|
defer func() {
|
||||||
|
logStatus(status)
|
||||||
|
}()
|
||||||
stockStatus = stockTake(status)
|
stockStatus = stockTake(status)
|
||||||
c.setStock(status)
|
c.setStock(status)
|
||||||
|
|
||||||
@ -294,48 +296,75 @@ func (c *Client) DispenserStart(ctx context.Context) (string, error) {
|
|||||||
return stockStatus, fmt.Errorf("[%s] to encoder: %w", funcName, err)
|
return stockStatus, fmt.Errorf("[%s] to encoder: %w", funcName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(delay)
|
deadline := time.Now().Add(6 * time.Second)
|
||||||
status, err = c.CheckStatus(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return stockStatus, fmt.Errorf("[%s] re-check status: %w", funcName, err)
|
|
||||||
}
|
|
||||||
logStatus(status)
|
|
||||||
stockStatus = stockTake(status)
|
|
||||||
c.setStock(status)
|
|
||||||
|
|
||||||
return stockStatus, nil
|
for {
|
||||||
|
time.Sleep(delay * 2)
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
return stockStatus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
status, _ = c.do(ctx, cmdStatus)
|
||||||
|
|
||||||
|
stockStatus = stockTake(status)
|
||||||
|
c.setStock(status)
|
||||||
|
logStatus(status)
|
||||||
|
|
||||||
|
// error states first
|
||||||
|
if isCardWellEmpty(status) {
|
||||||
|
return stockStatus, fmt.Errorf(stockStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isAtEncoderPosition(status) {
|
||||||
|
return stockStatus, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) DispenserFinal(ctx context.Context) (string, error) {
|
func (c *Client) DispenserFinal(ctx context.Context) (string, error) {
|
||||||
const funcName = "DispenserFinal"
|
const funcName = "DispenserFinal"
|
||||||
stockStatus := ""
|
stockStatus := ""
|
||||||
|
var status []byte
|
||||||
|
|
||||||
if err := c.OutOfMouth(ctx); err != nil {
|
if err := c.OutOfMouth(ctx); err != nil {
|
||||||
return stockStatus, fmt.Errorf("[%s] out of mouth: %w", funcName, err)
|
return stockStatus, fmt.Errorf("[%s] out of mouth: %w", funcName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(delay)
|
time.Sleep(delay)
|
||||||
status, err := c.CheckStatus(ctx)
|
status, err := c.do(ctx, cmdStatus)
|
||||||
if err != nil {
|
if err == nil && len(status) >= 4 {
|
||||||
return stockStatus, fmt.Errorf("[%s] check status: %w", funcName, err)
|
c.setStock(status)
|
||||||
}
|
}
|
||||||
logStatus(status)
|
|
||||||
stockStatus = stockTake(status)
|
|
||||||
c.setStock(status)
|
|
||||||
|
|
||||||
time.Sleep(delay)
|
time.Sleep(delay)
|
||||||
if err := c.ToEncoder(ctx); err != nil {
|
if err := c.ToEncoder(ctx); err != nil {
|
||||||
return stockStatus, fmt.Errorf("[%s] to encoder: %w", funcName, err)
|
return stockStatus, fmt.Errorf("[%s] to encoder: %w", funcName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(delay)
|
defer func() {
|
||||||
status, err = c.CheckStatus(ctx)
|
logStatus(status)
|
||||||
if err != nil {
|
}()
|
||||||
return stockStatus, fmt.Errorf("[%s] re-check status: %w", funcName, err)
|
|
||||||
}
|
|
||||||
logStatus(status)
|
|
||||||
stockStatus = stockTake(status)
|
|
||||||
c.setStock(status)
|
|
||||||
|
|
||||||
return stockStatus, nil
|
deadline := time.Now().Add(6 * time.Second)
|
||||||
|
|
||||||
|
for {
|
||||||
|
time.Sleep(delay * 2)
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
return stockStatus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
status, _ = c.do(ctx, cmdStatus)
|
||||||
|
|
||||||
|
stockStatus = stockTake(status)
|
||||||
|
c.setStock(status)
|
||||||
|
logStatus(status)
|
||||||
|
|
||||||
|
if isCardWellEmpty(status) {
|
||||||
|
return stockStatus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if isAtEncoderPosition(status) {
|
||||||
|
return stockStatus, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,8 +10,6 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
const serviceName = "hardlink"
|
|
||||||
|
|
||||||
// writeError is a helper to send a JSON error and HTTP status in one go.
|
// writeError is a helper to send a JSON error and HTTP status in one go.
|
||||||
func WriteError(w http.ResponseWriter, status int, msg string) {
|
func WriteError(w http.ResponseWriter, status int, msg string) {
|
||||||
theResponse := cmstypes.StatusRec{
|
theResponse := cmstypes.StatusRec{
|
||||||
|
|||||||
1
go.mod
1
go.mod
@ -7,6 +7,7 @@ require (
|
|||||||
gitea.futuresens.co.uk/futuresens/logging v1.0.9
|
gitea.futuresens.co.uk/futuresens/logging v1.0.9
|
||||||
github.com/alexbrainman/printer v0.0.0-20200912035444-f40f26f0bdeb
|
github.com/alexbrainman/printer v0.0.0-20200912035444-f40f26f0bdeb
|
||||||
github.com/denisenkom/go-mssqldb v0.12.3
|
github.com/denisenkom/go-mssqldb v0.12.3
|
||||||
|
github.com/mailjet/mailjet-apiv3-go v0.0.0-20201009050126-c24bc15a9394
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
|
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
|
||||||
golang.org/x/image v0.27.0
|
golang.org/x/image v0.27.0
|
||||||
|
|||||||
2
go.sum
2
go.sum
@ -19,6 +19,8 @@ github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZ
|
|||||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||||
|
github.com/mailjet/mailjet-apiv3-go v0.0.0-20201009050126-c24bc15a9394 h1:+6kiV40vfmh17TDlZG15C2uGje1/XBGT32j6xKmUkqM=
|
||||||
|
github.com/mailjet/mailjet-apiv3-go v0.0.0-20201009050126-c24bc15a9394/go.mod h1:ogN8Sxy3n5VKLhQxbtSBM3ICG/VgjXS/akQJIoDSrgA=
|
||||||
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
||||||
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
|
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@ -17,6 +16,7 @@ import (
|
|||||||
"gitea.futuresens.co.uk/futuresens/hardlink/dispenser"
|
"gitea.futuresens.co.uk/futuresens/hardlink/dispenser"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/errorhandlers"
|
"gitea.futuresens.co.uk/futuresens/hardlink/errorhandlers"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/lockserver"
|
"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/payment"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/printer"
|
"gitea.futuresens.co.uk/futuresens/hardlink/printer"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/types"
|
"gitea.futuresens.co.uk/futuresens/hardlink/types"
|
||||||
@ -25,23 +25,26 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
disp *dispenser.Client
|
disp *dispenser.Client
|
||||||
lockserver lockserver.LockServer
|
lockserver lockserver.LockServer
|
||||||
isPayment bool
|
isPayment bool
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
cfg *config.ConfigRec
|
cfg *config.ConfigRec
|
||||||
dbMu sync.Mutex
|
dbMu sync.Mutex
|
||||||
cardWellMu sync.RWMutex
|
cardWellMu sync.RWMutex
|
||||||
cardWellStatus string
|
cardWellStatus string
|
||||||
|
availabilityMu sync.Mutex
|
||||||
|
availabilityTimers map[string]*time.Timer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApp(disp *dispenser.Client, lockType, encoderAddress, cardWellStatus string, db *sql.DB, cfg *config.ConfigRec) *App {
|
func NewApp(disp *dispenser.Client, lockType, encoderAddress, cardWellStatus string, db *sql.DB, cfg *config.ConfigRec) *App {
|
||||||
app := &App{
|
app := &App{
|
||||||
isPayment: cfg.IsPayment,
|
isPayment: cfg.IsPayment,
|
||||||
disp: disp,
|
disp: disp,
|
||||||
lockserver: lockserver.NewLockServer(lockType, encoderAddress, errorhandlers.FatalError),
|
lockserver: lockserver.NewLockServer(lockType, encoderAddress, errorhandlers.FatalError),
|
||||||
db: db,
|
db: db,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
availabilityTimers: make(map[string]*time.Timer),
|
||||||
}
|
}
|
||||||
app.SetCardWellStatus(cardWellStatus)
|
app.SetCardWellStatus(cardWellStatus)
|
||||||
return app
|
return app
|
||||||
@ -53,10 +56,14 @@ func (app *App) RegisterRoutes(mux *http.ServeMux) {
|
|||||||
mux.HandleFunc("/takepreauth", app.takePreauthorization)
|
mux.HandleFunc("/takepreauth", app.takePreauthorization)
|
||||||
mux.HandleFunc("/takepayment", app.takePayment)
|
mux.HandleFunc("/takepayment", app.takePayment)
|
||||||
mux.HandleFunc("/dispenserstatus", app.reportDispenserStatus)
|
mux.HandleFunc("/dispenserstatus", app.reportDispenserStatus)
|
||||||
|
mux.HandleFunc("/testissuedoorcard", app.testIssueDoorCard)
|
||||||
|
mux.HandleFunc("/ping-pdq", app.fetchChipDNAStatus)
|
||||||
|
mux.HandleFunc("/logerror", app.onChipDNAError)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) takePreauthorization(w http.ResponseWriter, r *http.Request) {
|
func (app *App) takePreauthorization(w http.ResponseWriter, r *http.Request) {
|
||||||
const op = logging.Op("takePreauthorization")
|
const op = logging.Op("takePreauthorization")
|
||||||
|
|
||||||
var (
|
var (
|
||||||
theResponse cmstypes.ResponseRec
|
theResponse cmstypes.ResponseRec
|
||||||
theRequest cmstypes.TransactionRec
|
theRequest cmstypes.TransactionRec
|
||||||
@ -74,9 +81,12 @@ func (app *App) takePreauthorization(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
if !app.isPayment {
|
if !app.isPayment {
|
||||||
theResponse.Data = payment.BuildFailureURL(types.ResultError, "Payment processing is disabled")
|
if !app.cfg.TestMode {
|
||||||
writeTransactionResult(w, http.StatusServiceUnavailable, theResponse)
|
mail.SendEmailOnError(app.cfg.Hotel, app.cfg.Kiosk, "Payment Error", "Attempted preauthorization while payment processing is disabled")
|
||||||
return
|
theResponse.Data = payment.BuildFailureURL(types.ResultError, "Payment processing is disabled")
|
||||||
|
writeTransactionResult(w, http.StatusServiceUnavailable, theResponse)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Method == http.MethodOptions {
|
if r.Method == http.MethodOptions {
|
||||||
@ -90,55 +100,64 @@ func (app *App) takePreauthorization(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeTransactionResult(w, http.StatusMethodNotAllowed, theResponse)
|
writeTransactionResult(w, http.StatusMethodNotAllowed, theResponse)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
if ct := r.Header.Get("Content-Type"); ct != "text/xml" {
|
if r.Header.Get("Content-Type") != "text/xml" {
|
||||||
theResponse.Data = payment.BuildFailureURL(types.ResultError, "Content-Type must be text/xml")
|
theResponse.Data = payment.BuildFailureURL(types.ResultError, "Content-Type must be text/xml")
|
||||||
writeTransactionResult(w, http.StatusUnsupportedMediaType, theResponse)
|
writeTransactionResult(w, http.StatusUnsupportedMediaType, theResponse)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
body, _ := io.ReadAll(r.Body)
|
defer r.Body.Close()
|
||||||
err := xml.Unmarshal(body, &theRequest)
|
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Error(serviceName, err.Error(), "ReadXML", string(op), "", "", 0)
|
logging.Error(types.ServiceName, err.Error(), "Read body error", string(op), "", "", 0)
|
||||||
|
theResponse.Data = payment.BuildFailureURL(types.ResultError, "Failed to read request body")
|
||||||
|
writeTransactionResult(w, http.StatusBadRequest, theResponse)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := xml.Unmarshal(body, &theRequest); err != nil {
|
||||||
|
logging.Error(types.ServiceName, err.Error(), "ReadXML", string(op), "", "", 0)
|
||||||
theResponse.Data = payment.BuildFailureURL(types.ResultError, "Invalid XML payload")
|
theResponse.Data = payment.BuildFailureURL(types.ResultError, "Invalid XML payload")
|
||||||
writeTransactionResult(w, http.StatusBadRequest, theResponse)
|
writeTransactionResult(w, http.StatusBadRequest, theResponse)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Transaction payload: Amount=%s, Type=%s", theRequest.AmountMinorUnits, theRequest.TransactionType)
|
|
||||||
|
log.Printf(
|
||||||
|
"Preauthorization payload: Amount=%s, Type=%s",
|
||||||
|
theRequest.AmountMinorUnits,
|
||||||
|
theRequest.TransactionType,
|
||||||
|
)
|
||||||
|
|
||||||
client := &http.Client{Timeout: 300 * time.Second}
|
client := &http.Client{Timeout: 300 * time.Second}
|
||||||
response, err := client.Post(types.LinkTakePreauthorization, "text/xml", bytes.NewBuffer(body))
|
|
||||||
|
// ---- START TRANSACTION ----
|
||||||
|
|
||||||
|
body, err = callChipDNA(client, types.LinkStartTransaction, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Error(serviceName, err.Error(), "Payment processing error", string(op), "", "", 0)
|
logging.Error(types.ServiceName, err.Error(), "Preauth processing error", string(op), "", "", 0)
|
||||||
|
|
||||||
theResponse.Data = payment.BuildFailureURL(types.ResultError, "No response from payment processor")
|
theResponse.Data = payment.BuildFailureURL(types.ResultError, "No response from payment processor")
|
||||||
writeTransactionResult(w, http.StatusBadGateway, theResponse)
|
writeTransactionResult(w, http.StatusBadGateway, theResponse)
|
||||||
return
|
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 {
|
if err := trResult.ParseTransactionResult(body); err != nil {
|
||||||
logging.Error(serviceName, err.Error(), "Parse transaction result error", string(op), "", "", 0)
|
logging.Error(types.ServiceName, err.Error(), "Parse transaction result error", string(op), "", "", 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compose JSON from responseEntries
|
|
||||||
result.FillFromTransactionResult(trResult)
|
result.FillFromTransactionResult(trResult)
|
||||||
|
|
||||||
if err := printer.PrintCardholderReceipt(result.CardholderReceipt); err != nil {
|
// ---- PRINT RECEIPT ----
|
||||||
log.Errorf("PrintCardholderReceipt error: %v", err)
|
|
||||||
}
|
printer.PrintReceipt(result.CardholderReceipt)
|
||||||
|
|
||||||
|
// ---- REDIRECT ----
|
||||||
|
|
||||||
theResponse.Status = result.Status
|
theResponse.Status = result.Status
|
||||||
theResponse.Data, save = payment.BuildPreauthRedirectURL(result.Fields)
|
theResponse.Data, save = payment.BuildPreauthRedirectURL(result.Fields)
|
||||||
|
|
||||||
if save {
|
if save {
|
||||||
go app.persistPreauth(context.Background(), result.Fields, theRequest.CheckoutDate)
|
go app.persistPreauth(context.Background(), result.Fields, theRequest.CheckoutDate)
|
||||||
}
|
}
|
||||||
@ -148,6 +167,7 @@ func (app *App) takePreauthorization(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (app *App) takePayment(w http.ResponseWriter, r *http.Request) {
|
func (app *App) takePayment(w http.ResponseWriter, r *http.Request) {
|
||||||
const op = logging.Op("takePayment")
|
const op = logging.Op("takePayment")
|
||||||
|
|
||||||
var (
|
var (
|
||||||
theResponse cmstypes.ResponseRec
|
theResponse cmstypes.ResponseRec
|
||||||
theRequest cmstypes.TransactionRec
|
theRequest cmstypes.TransactionRec
|
||||||
@ -164,9 +184,14 @@ func (app *App) takePayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
if !app.isPayment {
|
if !app.isPayment {
|
||||||
theResponse.Data = payment.BuildFailureURL(types.ResultError, "Payment processing is disabled")
|
if !app.cfg.TestMode {
|
||||||
writeTransactionResult(w, http.StatusServiceUnavailable, theResponse)
|
mail.SendEmailOnError(app.cfg.Hotel, app.cfg.Kiosk, "Payment Error", "Attempted payment while payment processing is disabled")
|
||||||
return
|
theResponse.Status.Code = http.StatusServiceUnavailable
|
||||||
|
theResponse.Data = payment.BuildFailureURL(types.ResultError, "Payment processing is disabled")
|
||||||
|
writeTransactionResult(w, http.StatusServiceUnavailable, theResponse)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Method == http.MethodOptions {
|
if r.Method == http.MethodOptions {
|
||||||
@ -180,55 +205,118 @@ func (app *App) takePayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeTransactionResult(w, http.StatusMethodNotAllowed, theResponse)
|
writeTransactionResult(w, http.StatusMethodNotAllowed, theResponse)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
if ct := r.Header.Get("Content-Type"); ct != "text/xml" {
|
if r.Header.Get("Content-Type") != "text/xml" {
|
||||||
theResponse.Data = payment.BuildFailureURL(types.ResultError, "Content-Type must be text/xml")
|
theResponse.Data = payment.BuildFailureURL(types.ResultError, "Content-Type must be text/xml")
|
||||||
writeTransactionResult(w, http.StatusUnsupportedMediaType, theResponse)
|
writeTransactionResult(w, http.StatusUnsupportedMediaType, theResponse)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
body, _ := io.ReadAll(r.Body)
|
defer r.Body.Close()
|
||||||
err := xml.Unmarshal(body, &theRequest)
|
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Error(serviceName, err.Error(), "ReadXML", string(op), "", "", 0)
|
logging.Error(types.ServiceName, err.Error(), "Read body error", string(op), "", "", 0)
|
||||||
|
theResponse.Data = payment.BuildFailureURL(types.ResultError, "Failed to read request body")
|
||||||
|
writeTransactionResult(w, http.StatusBadRequest, theResponse)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := xml.Unmarshal(body, &theRequest); err != nil {
|
||||||
|
logging.Error(types.ServiceName, err.Error(), "ReadXML", string(op), "", "", 0)
|
||||||
theResponse.Data = payment.BuildFailureURL(types.ResultError, "Invalid XML payload")
|
theResponse.Data = payment.BuildFailureURL(types.ResultError, "Invalid XML payload")
|
||||||
writeTransactionResult(w, http.StatusBadRequest, theResponse)
|
writeTransactionResult(w, http.StatusBadRequest, theResponse)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("Transaction payload: Amount=%s, Type=%s", theRequest.AmountMinorUnits, theRequest.TransactionType)
|
|
||||||
|
log.Printf("Transaction payload: Amount=%s, Type=%s",
|
||||||
|
theRequest.AmountMinorUnits,
|
||||||
|
theRequest.TransactionType,
|
||||||
|
)
|
||||||
|
|
||||||
client := &http.Client{Timeout: 300 * time.Second}
|
client := &http.Client{Timeout: 300 * time.Second}
|
||||||
response, err := client.Post(types.LinkTakePayment, "text/xml", bytes.NewBuffer(body))
|
|
||||||
|
// ---- START TRANSACTION ----
|
||||||
|
|
||||||
|
body, err = callChipDNA(client, types.LinkStartTransaction, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Error(serviceName, err.Error(), "Payment processing error", string(op), "", "", 0)
|
logging.Error(types.ServiceName, err.Error(), "Start transaction error", string(op), "", "", 0)
|
||||||
|
|
||||||
theResponse.Data = payment.BuildFailureURL(types.ResultError, "No response from payment processor")
|
theResponse.Data = payment.BuildFailureURL(types.ResultError, "No response from payment processor")
|
||||||
writeTransactionResult(w, http.StatusBadGateway, theResponse)
|
writeTransactionResult(w, http.StatusBadGateway, theResponse)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer response.Body.Close()
|
|
||||||
|
|
||||||
body, err = io.ReadAll(response.Body)
|
if err := trResult.ParseTransactionResult(body); err != nil {
|
||||||
|
logging.Error(types.ServiceName, err.Error(), "Parse transaction result error", string(op), "", "", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.FillFromTransactionResult(trResult)
|
||||||
|
|
||||||
|
res := result.Fields[types.TransactionResult]
|
||||||
|
|
||||||
|
if !strings.EqualFold(res, types.ResultApproved) {
|
||||||
|
printer.PrintReceipt(result.CardholderReceipt)
|
||||||
|
desc := result.Fields[types.ErrorDescription]
|
||||||
|
if desc == "" {
|
||||||
|
desc = result.Fields[types.Errors]
|
||||||
|
}
|
||||||
|
logging.Error(types.ServiceName, "Preauthorization failed", "Result: "+res+" Description: "+desc, string(op), "", app.cfg.Hotel, app.cfg.Kiosk)
|
||||||
|
theResponse.Status = result.Status
|
||||||
|
theResponse.Data = payment.BuildFailureURL(res, result.Fields[types.Errors])
|
||||||
|
|
||||||
|
writeTransactionResult(w, http.StatusOK, theResponse)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- CONFIRM TRANSACTION ----
|
||||||
|
|
||||||
|
ref := result.Fields[types.Reference]
|
||||||
|
log.Printf("Preauth approved, reference: %s. Sending confirm...", ref)
|
||||||
|
confirmReq := payment.ConfirmTransactionRequest{
|
||||||
|
Amount: theRequest.AmountMinorUnits,
|
||||||
|
Reference: ref,
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err = confirmWithRetry(client, confirmReq, 2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Error(serviceName, err.Error(), "Read response body error", string(op), "", "", 0)
|
logging.Error(types.ServiceName, err.Error(), "Confirm transaction error", string(op), "", "", 0)
|
||||||
theResponse.Data = payment.BuildFailureURL(types.ResultError, "Failed to read response body")
|
mail.SendEmailOnError(app.cfg.Hotel, app.cfg.Kiosk, "Payment confirmation failed", "Reference: "+ref+", Error: "+err.Error())
|
||||||
writeTransactionResult(w, http.StatusInternalServerError, theResponse)
|
theResponse.Data = payment.BuildFailureURL(types.ResultError, "ConfirmTransactionError")
|
||||||
|
writeTransactionResult(w, http.StatusBadGateway, theResponse)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := trResult.ParseTransactionResult(body); err != nil {
|
if err := trResult.ParseTransactionResult(body); err != nil {
|
||||||
logging.Error(serviceName, err.Error(), "Parse transaction result error", string(op), "", "", 0)
|
logging.Error(types.ServiceName, err.Error(), "Parse confirm result error", string(op), "", "", 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compose JSON from responseEntries
|
|
||||||
result.FillFromTransactionResult(trResult)
|
result.FillFromTransactionResult(trResult)
|
||||||
|
|
||||||
if err := printer.PrintCardholderReceipt(result.CardholderReceipt); err != nil {
|
res = result.Fields[types.TransactionResult]
|
||||||
log.Errorf("PrintCardholderReceipt error: %v", err)
|
|
||||||
|
if !strings.EqualFold(res, types.ResultApproved) {
|
||||||
|
printer.PrintReceipt(result.CardholderReceipt)
|
||||||
|
desc := result.Fields[types.ErrorDescription]
|
||||||
|
if desc == "" {
|
||||||
|
desc = result.Fields[types.Errors]
|
||||||
|
}
|
||||||
|
logging.Error(types.ServiceName, "Transaction not approved after confirm", "Confirm result: "+res+" Description: "+desc, string(op), "", app.cfg.Hotel, app.cfg.Kiosk)
|
||||||
|
mail.SendEmailOnError(app.cfg.Hotel, app.cfg.Kiosk, "Payment confirmation failed", "Reference: "+ref+", Confirm result: "+res+" Description: "+desc)
|
||||||
|
theResponse.Status = result.Status
|
||||||
|
theResponse.Data = payment.BuildFailureURL(res, result.Fields[types.Errors])
|
||||||
|
|
||||||
|
writeTransactionResult(w, http.StatusOK, theResponse)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- SUCCESS ----
|
||||||
|
|
||||||
|
printer.PrintReceipt(result.CardholderReceipt)
|
||||||
|
log.Printf("Transaction approved and confirmed, reference: %s", ref)
|
||||||
theResponse.Status = result.Status
|
theResponse.Status = result.Status
|
||||||
theResponse.Data = payment.BuildPaymentRedirectURL(result.Fields)
|
theResponse.Data = payment.BuildSuccessURL(result.Fields)
|
||||||
|
|
||||||
writeTransactionResult(w, http.StatusOK, theResponse)
|
writeTransactionResult(w, http.StatusOK, theResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,7 +350,7 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&doorReq); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&doorReq); err != nil {
|
||||||
logging.Error(serviceName, err.Error(), "ReadJSON", string(op), "", "", 0)
|
logging.Error(types.ServiceName, err.Error(), "ReadJSON", string(op), "", "", 0)
|
||||||
errorhandlers.WriteError(w, http.StatusBadRequest, "Invalid JSON payload: "+err.Error())
|
errorhandlers.WriteError(w, http.StatusBadRequest, "Invalid JSON payload: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -270,13 +358,13 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
// parse times
|
// parse times
|
||||||
checkIn, err := time.Parse(types.CustomLayout, doorReq.CheckinTime)
|
checkIn, err := time.Parse(types.CustomLayout, doorReq.CheckinTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Error(serviceName, err.Error(), "Invalid checkinTime format", string(op), "", "", 0)
|
logging.Error(types.ServiceName, err.Error(), "Invalid checkinTime format", string(op), "", "", 0)
|
||||||
errorhandlers.WriteError(w, http.StatusBadRequest, "Invalid checkinTime format: "+err.Error())
|
errorhandlers.WriteError(w, http.StatusBadRequest, "Invalid checkinTime format: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
checkOut, err := time.Parse(types.CustomLayout, doorReq.CheckoutTime)
|
checkOut, err := time.Parse(types.CustomLayout, doorReq.CheckoutTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Error(serviceName, err.Error(), "Invalid checkoutTime format", string(op), "", "", 0)
|
logging.Error(types.ServiceName, err.Error(), "Invalid checkoutTime format", string(op), "", "", 0)
|
||||||
errorhandlers.WriteError(w, http.StatusBadRequest, "Invalid checkoutTime format: "+err.Error())
|
errorhandlers.WriteError(w, http.StatusBadRequest, "Invalid checkoutTime format: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -286,7 +374,7 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
status, err := app.disp.DispenserStart(r.Context())
|
status, err := app.disp.DispenserStart(r.Context())
|
||||||
app.SetCardWellStatus(status)
|
app.SetCardWellStatus(status)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Error(serviceName, err.Error(), "Dispense error", string(op), "", "", 0)
|
logging.Error(types.ServiceName, err.Error(), "Dispense error", string(op), "", "", 0)
|
||||||
errorhandlers.WriteError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error())
|
errorhandlers.WriteError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -302,7 +390,7 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
status, ferr := app.disp.DispenserFinal(ctx)
|
status, ferr := app.disp.DispenserFinal(ctx)
|
||||||
if ferr != nil {
|
if ferr != nil {
|
||||||
logging.Error(serviceName, ferr.Error(), "Dispenser final error", string(op), "", "", 0)
|
logging.Error(types.ServiceName, ferr.Error(), "Dispenser final error", string(op), "", "", 0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
app.SetCardWellStatus(status)
|
app.SetCardWellStatus(status)
|
||||||
@ -313,7 +401,7 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// lock server sequence
|
// lock server sequence
|
||||||
if err := app.lockserver.LockSequence(); err != nil {
|
if err := app.lockserver.LockSequence(); err != nil {
|
||||||
logging.Error(serviceName, err.Error(), "Key encoding", string(op), "", "", 0)
|
logging.Error(types.ServiceName, err.Error(), "Key encoding", string(op), "", "", 0)
|
||||||
finalize()
|
finalize()
|
||||||
errorhandlers.WriteError(w, http.StatusBadGateway, err.Error())
|
errorhandlers.WriteError(w, http.StatusBadGateway, err.Error())
|
||||||
return
|
return
|
||||||
@ -352,21 +440,21 @@ func (app *App) printRoomTicket(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
if err := xml.NewDecoder(r.Body).Decode(&roomDetails); err != nil {
|
if err := xml.NewDecoder(r.Body).Decode(&roomDetails); err != nil {
|
||||||
logging.Error(serviceName, err.Error(), "ReadXML", string(op), "", "", 0)
|
logging.Error(types.ServiceName, err.Error(), "ReadXML", string(op), "", "", 0)
|
||||||
errorhandlers.WriteError(w, http.StatusBadRequest, "Invalid XML payload: "+err.Error())
|
errorhandlers.WriteError(w, http.StatusBadRequest, "Invalid XML payload: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := printer.BuildRoomTicket(roomDetails)
|
data, err := printer.BuildRoomTicket(roomDetails)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Error(serviceName, err.Error(), "BuildRoomTicket", string(op), "", "", 0)
|
logging.Error(types.ServiceName, err.Error(), "BuildRoomTicket", string(op), "", "", 0)
|
||||||
errorhandlers.WriteError(w, http.StatusInternalServerError, "BuildRoomTicket failed: "+err.Error())
|
errorhandlers.WriteError(w, http.StatusInternalServerError, "BuildRoomTicket failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send to the Windows Epson TM-T82II via the printer package
|
// Send to the Windows Epson TM-T82II via the printer package
|
||||||
if err := printer.SendToPrinter(data); err != nil {
|
if err := printer.SendToPrinter(data); err != nil {
|
||||||
logging.Error(serviceName, err.Error(), "printRoomTicket", "printRoomTicket", "", "", 0)
|
logging.Error(types.ServiceName, err.Error(), "printRoomTicket", "printRoomTicket", "", "", 0)
|
||||||
errorhandlers.WriteError(w, http.StatusInternalServerError, "Print failed: "+err.Error())
|
errorhandlers.WriteError(w, http.StatusInternalServerError, "Print failed: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -391,8 +479,13 @@ func (app *App) reportDispenserStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (app *App) SetCardWellStatus(s string) {
|
func (app *App) SetCardWellStatus(s string) {
|
||||||
app.cardWellMu.Lock()
|
app.cardWellMu.Lock()
|
||||||
|
prev := app.cardWellStatus
|
||||||
app.cardWellStatus = s
|
app.cardWellStatus = s
|
||||||
app.cardWellMu.Unlock()
|
app.cardWellMu.Unlock()
|
||||||
|
|
||||||
|
if s != "" && prev != s {
|
||||||
|
mail.SendEmailOnError(app.cfg.Hotel, app.cfg.Kiosk, "Dispenser Error Status", "Status: "+s)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) CardWellStatus() string {
|
func (app *App) CardWellStatus() string {
|
||||||
|
|||||||
@ -1,20 +1,71 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.futuresens.co.uk/futuresens/cmstypes"
|
"gitea.futuresens.co.uk/futuresens/cmstypes"
|
||||||
|
"gitea.futuresens.co.uk/futuresens/hardlink/payment"
|
||||||
|
"gitea.futuresens.co.uk/futuresens/hardlink/types"
|
||||||
"gitea.futuresens.co.uk/futuresens/logging"
|
"gitea.futuresens.co.uk/futuresens/logging"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
const serviceName = "hardlink"
|
|
||||||
|
|
||||||
func writeTransactionResult(w http.ResponseWriter, status int, theResponse cmstypes.ResponseRec) {
|
func writeTransactionResult(w http.ResponseWriter, status int, theResponse cmstypes.ResponseRec) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
if err := json.NewEncoder(w).Encode(theResponse); err != nil {
|
if err := json.NewEncoder(w).Encode(theResponse); err != nil {
|
||||||
logging.Error(serviceName, err.Error(), "JSON encode error", "startTransaction", "", "", 0)
|
logging.Error(types.ServiceName, err.Error(), "JSON encode error", "startTransaction", "", "", 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func callChipDNA(client *http.Client, url string, payload []byte) ([]byte, error) {
|
||||||
|
|
||||||
|
resp, err := client.Post(url, "text/xml", bytes.NewBuffer(payload))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return io.ReadAll(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func confirmWithRetry(client *http.Client, req payment.ConfirmTransactionRequest, attempts int) ([]byte, error) {
|
||||||
|
|
||||||
|
payload, err := xml.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
|
||||||
|
for i := 1; i <= attempts; i++ {
|
||||||
|
|
||||||
|
resp, err := client.Post(types.LinkConfirmTransaction, "text/xml", bytes.NewBuffer(payload))
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
} else {
|
||||||
|
|
||||||
|
body, readErr := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
if readErr != nil {
|
||||||
|
lastErr = readErr
|
||||||
|
} else {
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warnf("ConfirmTransaction attempt %d/%d failed: %v", i, attempts, lastErr)
|
||||||
|
|
||||||
|
if i < attempts {
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, lastErr
|
||||||
|
}
|
||||||
|
|||||||
252
handlers/testhandlers.go
Normal file
252
handlers/testhandlers.go
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
68
mail/mail.go
Normal file
68
mail/mail.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.futuresens.co.uk/futuresens/logging"
|
||||||
|
mailjet "github.com/mailjet/mailjet-apiv3-go"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
apiKey = "60f358a27e98562641c08f51e5450c9e"
|
||||||
|
secretKey = "068b65c3b337a0e3c14389544ecd771f"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
moduleName = "mail"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// sendErrorEmail is the e-mail address to which to send an e-mail if there is an error during checkin or payment
|
||||||
|
SendErrorEmails []string
|
||||||
|
)
|
||||||
|
|
||||||
|
// SendMail will send reception an e-mail
|
||||||
|
func SendMail(recipient, title, message string) {
|
||||||
|
const funcName = "SendMail"
|
||||||
|
|
||||||
|
mailjetClient := mailjet.NewMailjetClient(apiKey, secretKey)
|
||||||
|
messagesInfo := []mailjet.InfoMessagesV31{
|
||||||
|
mailjet.InfoMessagesV31{
|
||||||
|
From: &mailjet.RecipientV31{
|
||||||
|
Email: "kiosk@cms.futuresens.co.uk",
|
||||||
|
Name: "Futuresens Kiosk",
|
||||||
|
},
|
||||||
|
To: &mailjet.RecipientsV31{
|
||||||
|
mailjet.RecipientV31{
|
||||||
|
Email: recipient,
|
||||||
|
Name: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Subject: title,
|
||||||
|
TextPart: message,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
messages := mailjet.MessagesV31{Info: messagesInfo}
|
||||||
|
_, err := mailjetClient.SendMailV31(&messages)
|
||||||
|
if err != nil {
|
||||||
|
theFields := log.Fields{}
|
||||||
|
theFields["mailerror"] = true
|
||||||
|
theFields["recipient"] = recipient
|
||||||
|
theFields[logging.LogFunction] = funcName
|
||||||
|
theFields[logging.LogModule] = moduleName
|
||||||
|
theFields[logging.LogError] = err.Error()
|
||||||
|
theFields["error"] = err.Error()
|
||||||
|
|
||||||
|
log.WithFields(theFields).Error("sendmail error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendEmailOnError(hotel string, kiosk int, title, errMsg string) {
|
||||||
|
log.Println("sendEmailOnError called")
|
||||||
|
|
||||||
|
message := fmt.Sprintf("Hotel: %s, kiosk: %d.\n%s", hotel, kiosk, errMsg)
|
||||||
|
for _, recipient := range SendErrorEmails {
|
||||||
|
SendMail(recipient, title, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
9
mail/mail_test.go
Normal file
9
mail/mail_test.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_SendMail(t *testing.T) {
|
||||||
|
SendMail("zotacrtx5@gmail.com", "Test Subjectp", "Test Message")
|
||||||
|
}
|
||||||
42
main.go
42
main.go
@ -23,11 +23,13 @@ import (
|
|||||||
"gitea.futuresens.co.uk/futuresens/hardlink/handlers"
|
"gitea.futuresens.co.uk/futuresens/hardlink/handlers"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/lockserver"
|
"gitea.futuresens.co.uk/futuresens/hardlink/lockserver"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/logging"
|
"gitea.futuresens.co.uk/futuresens/hardlink/logging"
|
||||||
|
"gitea.futuresens.co.uk/futuresens/hardlink/mail"
|
||||||
|
"gitea.futuresens.co.uk/futuresens/hardlink/payment"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/printer"
|
"gitea.futuresens.co.uk/futuresens/hardlink/printer"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
buildVersion = "1.1.3"
|
buildVersion = "1.2.5"
|
||||||
serviceName = "hardlink"
|
serviceName = "hardlink"
|
||||||
pollingFrequency = 8 * time.Second
|
pollingFrequency = 8 * time.Second
|
||||||
)
|
)
|
||||||
@ -39,6 +41,11 @@ func main() {
|
|||||||
printer.PrinterName = cfg.PrinterName
|
printer.PrinterName = cfg.PrinterName
|
||||||
lockserver.Cert = cfg.Cert
|
lockserver.Cert = cfg.Cert
|
||||||
lockserver.LockServerURL = cfg.LockserverUrl
|
lockserver.LockServerURL = cfg.LockserverUrl
|
||||||
|
mail.SendErrorEmails = cfg.SendErrorEmails
|
||||||
|
|
||||||
|
// Root context for background goroutines
|
||||||
|
// rootCtx, rootCancel := context.WithCancel(context.Background())
|
||||||
|
// defer rootCancel()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
dispPort *serial.Port
|
dispPort *serial.Port
|
||||||
@ -51,7 +58,9 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to set up logging: %v\n", err)
|
log.Printf("Failed to set up logging: %v\n", err)
|
||||||
}
|
}
|
||||||
defer logFile.Close()
|
if logFile != nil {
|
||||||
|
defer logFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize dispenser
|
// Initialize dispenser
|
||||||
if !cfg.TestMode {
|
if !cfg.TestMode {
|
||||||
@ -60,21 +69,21 @@ func main() {
|
|||||||
|
|
||||||
dispPort, err = dispenser.InitializeDispenser()
|
dispPort, err = dispenser.InitializeDispenser()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
mail.SendEmailOnError(cfg.Hotel, cfg.Kiosk, "Dispenser Initialization Error", fmt.Sprintf("Failed to initialize dispenser: %v", err))
|
||||||
errorhandlers.FatalError(err)
|
errorhandlers.FatalError(err)
|
||||||
}
|
}
|
||||||
defer dispPort.Close()
|
defer dispPort.Close()
|
||||||
|
|
||||||
// Start queued dispenser client (single goroutine owns the serial port)
|
|
||||||
disp = dispenser.NewClient(dispPort, 32)
|
disp = dispenser.NewClient(dispPort, 32)
|
||||||
defer disp.Close()
|
defer disp.Close()
|
||||||
|
|
||||||
// Prepare dispenser (ensures card at encoder position unless empty)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
cardWellStatus, err = disp.DispenserPrepare(ctx)
|
cardWellStatus, err = disp.DispenserPrepare(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("%s; wrong dispenser address: %s", err, cfg.DispenserAdrr)
|
err = fmt.Errorf("%s; wrong dispenser address: %s", err, cfg.DispenserAdrr)
|
||||||
|
mail.SendEmailOnError(cfg.Hotel, cfg.Kiosk, "Dispenser Preparation Error", err.Error())
|
||||||
errorhandlers.FatalError(err)
|
errorhandlers.FatalError(err)
|
||||||
}
|
}
|
||||||
fmt.Println(cardWellStatus)
|
fmt.Println(cardWellStatus)
|
||||||
@ -83,12 +92,13 @@ func main() {
|
|||||||
// Test lock-server connection
|
// Test lock-server connection
|
||||||
switch strings.ToLower(cfg.LockType) {
|
switch strings.ToLower(cfg.LockType) {
|
||||||
case lockserver.TLJ:
|
case lockserver.TLJ:
|
||||||
// TLJ uses HTTP - skip TCP probe here (as you did before)
|
// TLJ uses HTTP - skip TCP probe here
|
||||||
default:
|
default:
|
||||||
lockConn, err := lockserver.InitializeServerConnection(cfg.LockserverUrl)
|
lockConn, err := lockserver.InitializeServerConnection(cfg.LockserverUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err.Error())
|
fmt.Println(err.Error())
|
||||||
log.Errorf(err.Error())
|
log.Errorf(err.Error())
|
||||||
|
mail.SendEmailOnError(cfg.Hotel, cfg.Kiosk, "Lock Server Connection Error", err.Error())
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Connected to the lock server successfuly at %s\n", cfg.LockserverUrl)
|
fmt.Printf("Connected to the lock server successfuly at %s\n", cfg.LockserverUrl)
|
||||||
log.Infof("Connected to the lock server successfuly at %s", cfg.LockserverUrl)
|
log.Infof("Connected to the lock server successfuly at %s", cfg.LockserverUrl)
|
||||||
@ -100,23 +110,37 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("DB init failed: %v", err)
|
log.Warnf("DB init failed: %v", err)
|
||||||
}
|
}
|
||||||
defer database.Close()
|
if database != nil {
|
||||||
|
defer database.Close()
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.IsPayment {
|
if cfg.IsPayment {
|
||||||
fmt.Println("Payment processing is enabled")
|
fmt.Println("Payment processing is enabled")
|
||||||
log.Info("Payment processing is enabled")
|
log.Info("Payment processing is enabled")
|
||||||
|
|
||||||
startChipDnaClient()
|
startChipDnaClient()
|
||||||
|
|
||||||
|
// check ChipDNA and PDQ status and log any errors, but continue running even if it fails
|
||||||
|
go func() {
|
||||||
|
time.Sleep(30 * time.Second) // give ChipDNA client a moment to start
|
||||||
|
pdqstatus, err := payment.ReadPdqStatus(cfg.Hotel, cfg.Kiosk)
|
||||||
|
if err != nil {
|
||||||
|
mail.SendEmailOnError(cfg.Hotel, cfg.Kiosk, "PDQ Status Read Error", err.Error())
|
||||||
|
} else {
|
||||||
|
fmt.Printf("\nPDQ availabile: %v\n", pdqstatus.IsAvailable)
|
||||||
|
log.Infof("PDQ availabile: %v", pdqstatus.IsAvailable)
|
||||||
|
}
|
||||||
|
}()
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Payment processing is disabled")
|
fmt.Println("Payment processing is disabled")
|
||||||
log.Info("Payment processing is disabled")
|
log.Info("Payment processing is disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create App and wire routes
|
// Create App and wire routes
|
||||||
// NOTE: change handlers.NewApp signature to accept *dispenser.Client instead of *serial.Port
|
|
||||||
app := handlers.NewApp(disp, cfg.LockType, cfg.EncoderAddress, cardWellStatus, database, &cfg)
|
app := handlers.NewApp(disp, cfg.LockType, cfg.EncoderAddress, cardWellStatus, database, &cfg)
|
||||||
|
|
||||||
// Update cardWellStatus when dispenser status changes
|
// Update cardWellStatus when dispenser status changes
|
||||||
if !cfg.TestMode {
|
if !cfg.TestMode && disp != nil {
|
||||||
// Set initial cardWellStatus
|
// Set initial cardWellStatus
|
||||||
app.SetCardWellStatus(cardWellStatus)
|
app.SetCardWellStatus(cardWellStatus)
|
||||||
|
|
||||||
@ -181,6 +205,7 @@ func startChipDnaClient() {
|
|||||||
err := cmd.Wait()
|
err := cmd.Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("ChipDnaClient exited unexpectedly: %v", err)
|
log.Errorf("ChipDnaClient exited unexpectedly: %v", err)
|
||||||
|
fmt.Printf("ChipDnaClient exited unexpectedly: %v", err)
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
cmd, err = startClient()
|
cmd, err = startClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -188,6 +213,7 @@ func startChipDnaClient() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Info("ChipDnaClient restarted successfully")
|
log.Info("ChipDnaClient restarted successfully")
|
||||||
|
fmt.Printf("ChipDnaClient restarted successfully")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
278
payment/chipdnastatus.go
Normal file
278
payment/chipdnastatus.go
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
package payment
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.futuresens.co.uk/futuresens/hardlink/types"
|
||||||
|
"gitea.futuresens.co.uk/futuresens/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
KeyErrors = "ERRORS"
|
||||||
|
KeyVersionInformation = "VERSION_INFORMATION"
|
||||||
|
KeyChipDnaStatus = "CHIPDNA_STATUS"
|
||||||
|
KeyPaymentDeviceStatus = "PAYMENT_DEVICE_STATUS"
|
||||||
|
KeyRequestQueueStatus = "REQUEST_QUEUE_STATUS"
|
||||||
|
KeyTmsStatus = "TMS_STATUS"
|
||||||
|
KeyPaymentPlatform = "PAYMENT_PLATFORM_STATUS"
|
||||||
|
KeyPaymentDeviceModel = "PAYMENT_DEVICE_MODEL"
|
||||||
|
KeyPaymentDeviceIdentifier = "PAYMENT_DEVICE_IDENTIFIER"
|
||||||
|
KeyIsAvailable = "IS_AVAILABLE"
|
||||||
|
KeyAvailabilityError = "AVAILABILITY_ERROR"
|
||||||
|
KeyAvailabilityErrorInformation = "AVAILABILITY_ERROR_INFORMATION"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
ArrayOfParameter struct {
|
||||||
|
Parameters []Parameter `xml:"Parameter" json:"Parameters"`
|
||||||
|
}
|
||||||
|
|
||||||
|
Parameter struct {
|
||||||
|
Key string `xml:"Key" json:"Key"`
|
||||||
|
Value string `xml:"Value" json:"Value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerStatus struct {
|
||||||
|
IsProcessingTransaction bool `xml:"IsProcessingTransaction" json:"IsProcessingTransaction"`
|
||||||
|
ChipDnaServerIssue string `xml:"ChipDnaServerIssue" json:"ChipDnaServerIssue"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayOfPaymentDeviceStatus struct {
|
||||||
|
Items []PaymentDeviceStatus `xml:"PaymentDeviceStatus" json:"Items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PaymentDeviceStatus struct {
|
||||||
|
ConfiguredDeviceId string `xml:"ConfiguredDeviceId" json:"ConfiguredDeviceId"`
|
||||||
|
ConfiguredDeviceModel string `xml:"ConfiguredDeviceModel" json:"ConfiguredDeviceModel"`
|
||||||
|
ProcessingTransaction bool `xml:"ProcessingTransaction" json:"ProcessingTransaction"`
|
||||||
|
AvailabilityError string `xml:"AvailabilityError" json:"AvailabilityError"`
|
||||||
|
AvailabilityErrorInformation string `xml:"AvailabilityErrorInformation" json:"AvailabilityErrorInformation"`
|
||||||
|
ConfigurationState string `xml:"ConfigurationState" json:"ConfigurationState"`
|
||||||
|
IsAvailable bool `xml:"IsAvailable" json:"IsAvailable"`
|
||||||
|
BatteryPercentage int `xml:"BatteryPercentage" json:"BatteryPercentage"`
|
||||||
|
BatteryChargingStatus string `xml:"BatteryChargingStatus" json:"BatteryChargingStatus"`
|
||||||
|
BatteryStatusUpdateDateTime string `xml:"BatteryStatusUpdateDateTime" json:"BatteryStatusUpdateDateTime"`
|
||||||
|
BatteryStatusUpdateDateTimeFormat string `xml:"BatteryStatusUpdateDateTimeFormat" json:"BatteryStatusUpdateDateTimeFormat"`
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestQueueStatus struct {
|
||||||
|
CreditRequestCount int `xml:"CreditRequestCount" json:"CreditRequestCount"`
|
||||||
|
CreditConfirmRequestCount int `xml:"CreditConfirmRequestCount" json:"CreditConfirmRequestCount"`
|
||||||
|
CreditVoidRequestCount int `xml:"CreditVoidRequestCount" json:"CreditVoidRequestCount"`
|
||||||
|
DebitRequestCount int `xml:"DebitRequestCount" json:"DebitRequestCount"`
|
||||||
|
DebitConfirmRequestCount int `xml:"DebitConfirmRequestCount" json:"DebitConfirmRequestCount"`
|
||||||
|
DebitVoidRequestCount int `xml:"DebitVoidRequestCount" json:"DebitVoidRequestCount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
TmsStatus struct {
|
||||||
|
LastConfigUpdateDateTime string `xml:"LastConfigUpdateDateTime" json:"LastConfigUpdateDateTime"`
|
||||||
|
DaysUntilConfigUpdateIsRequired int `xml:"DaysUntilConfigUpdateIsRequired" json:"DaysUntilConfigUpdateIsRequired"`
|
||||||
|
RequiredConfigUpdateDateTime string `xml:"RequiredConfigUpdateDateTime" json:"RequiredConfigUpdateDateTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
PaymentPlatformStatus struct {
|
||||||
|
MachineLocalDateTime string `xml:"MachineLocalDateTime" json:"MachineLocalDateTime"`
|
||||||
|
PaymentPlatformLocalDateTime string `xml:"PaymentPlatformLocalDateTime" json:"PaymentPlatformLocalDateTime"`
|
||||||
|
PaymentPlatformLocalDateTimeFormat string `xml:"PaymentPlatformLocalDateTimeFormat" json:"PaymentPlatformLocalDateTimeFormat"`
|
||||||
|
State string `xml:"State" json:"State"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ParsedStatus struct {
|
||||||
|
Errors []string `json:"Errors"`
|
||||||
|
VersionInfo map[string]string `json:"VersionInfo"`
|
||||||
|
ChipDnaStatus *ServerStatus `json:"ChipDnaStatus"`
|
||||||
|
PaymentDevices []PaymentDeviceStatus `json:"PaymentDevices"`
|
||||||
|
RequestQueue *RequestQueueStatus `json:"RequestQueue"`
|
||||||
|
TMS *TmsStatus `json:"TMS"`
|
||||||
|
PaymentPlatform *PaymentPlatformStatus `json:"PaymentPlatform"`
|
||||||
|
Unknown map[string]string `json:"Unknown"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// ===========================
|
||||||
|
// Parser
|
||||||
|
// ===========================
|
||||||
|
|
||||||
|
func ParseStatusResult(data []byte) (*ParsedStatus, error) {
|
||||||
|
var tr TransactionResultXML
|
||||||
|
if err := tr.ParseTransactionResult(data); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal TransactionResult: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := &ParsedStatus{
|
||||||
|
VersionInfo: make(map[string]string),
|
||||||
|
Unknown: make(map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range tr.Entries {
|
||||||
|
switch e.Key {
|
||||||
|
|
||||||
|
// Some responses return plain text (not escaped XML) for ERRORS.
|
||||||
|
case KeyErrors:
|
||||||
|
msg := html.UnescapeString(e.Value) // safe even if not escaped
|
||||||
|
if msg != "" {
|
||||||
|
out.Errors = append(out.Errors, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything below is escaped XML inside <Value>
|
||||||
|
case KeyVersionInformation:
|
||||||
|
unescaped := html.UnescapeString(e.Value)
|
||||||
|
|
||||||
|
var a ArrayOfParameter
|
||||||
|
if err := xml.Unmarshal([]byte(unescaped), &a); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal %s: %w", e.Key, err)
|
||||||
|
}
|
||||||
|
for _, p := range a.Parameters {
|
||||||
|
out.VersionInfo[p.Key] = p.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
case KeyChipDnaStatus:
|
||||||
|
unescaped := html.UnescapeString(e.Value)
|
||||||
|
|
||||||
|
var s ServerStatus
|
||||||
|
if err := xml.Unmarshal([]byte(unescaped), &s); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal %s: %w", e.Key, err)
|
||||||
|
}
|
||||||
|
out.ChipDnaStatus = &s
|
||||||
|
|
||||||
|
case KeyPaymentDeviceStatus:
|
||||||
|
unescaped := html.UnescapeString(e.Value)
|
||||||
|
|
||||||
|
var a ArrayOfPaymentDeviceStatus
|
||||||
|
if err := xml.Unmarshal([]byte(unescaped), &a); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal %s: %w", e.Key, err)
|
||||||
|
}
|
||||||
|
out.PaymentDevices = append(out.PaymentDevices, a.Items...)
|
||||||
|
|
||||||
|
case KeyRequestQueueStatus:
|
||||||
|
unescaped := html.UnescapeString(e.Value)
|
||||||
|
|
||||||
|
var s RequestQueueStatus
|
||||||
|
if err := xml.Unmarshal([]byte(unescaped), &s); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal %s: %w", e.Key, err)
|
||||||
|
}
|
||||||
|
out.RequestQueue = &s
|
||||||
|
|
||||||
|
case KeyTmsStatus:
|
||||||
|
unescaped := html.UnescapeString(e.Value)
|
||||||
|
|
||||||
|
var s TmsStatus
|
||||||
|
if err := xml.Unmarshal([]byte(unescaped), &s); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal %s: %w", e.Key, err)
|
||||||
|
}
|
||||||
|
out.TMS = &s
|
||||||
|
|
||||||
|
case KeyPaymentPlatform:
|
||||||
|
unescaped := html.UnescapeString(e.Value)
|
||||||
|
|
||||||
|
var s PaymentPlatformStatus
|
||||||
|
if err := xml.Unmarshal([]byte(unescaped), &s); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal %s: %w", e.Key, err)
|
||||||
|
}
|
||||||
|
out.PaymentPlatform = &s
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Keep for logging / future additions. Unescape so it's readable XML if it was escaped.
|
||||||
|
out.Unknown[e.Key] = html.UnescapeString(e.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchChipDNAStatus() (*ParsedStatus, error) {
|
||||||
|
const op = logging.Op("fetchChipDNAStatus")
|
||||||
|
|
||||||
|
body := []byte{}
|
||||||
|
client := &http.Client{Timeout: 300 * time.Second}
|
||||||
|
response, err := client.Post(types.LinkChipDNAStatus, "text/xml", bytes.NewBuffer(body))
|
||||||
|
if err != nil {
|
||||||
|
logging.Error(types.ServiceName, err.Error(), "error fetching ChipDNA status", string(op), "", "", 0)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
body, err = io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
logging.Error(types.ServiceName, err.Error(), "Read response body error", string(op), "", "", 0)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseStatusResult(body)
|
||||||
|
if err != nil {
|
||||||
|
logging.Error(types.ServiceName, err.Error(), "Parse ChipDNA status error", string(op), "", "", 0)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadPdqStatus(hotel string, kiosk int) (PaymentDeviceStatus, error) {
|
||||||
|
const op = logging.Op("readPdqStatus")
|
||||||
|
|
||||||
|
status, err := fetchChipDNAStatus()
|
||||||
|
if err != nil {
|
||||||
|
logging.Error(types.ServiceName, "pdq_unavailable", "Failed to fetch ChipDNA status: "+err.Error(), string(op), "", hotel, kiosk)
|
||||||
|
return PaymentDeviceStatus{}, fmt.Errorf("error fetch ChipDNA status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(status.Errors) > 0 {
|
||||||
|
msg := strings.Join(status.Errors, "; ")
|
||||||
|
logging.Error(types.ServiceName, "pdq_unavailable", "ChipDNA status errors: "+msg, string(op), "", hotel, kiosk)
|
||||||
|
return PaymentDeviceStatus{}, fmt.Errorf("ChipDNA status errors: %s", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(status.PaymentDevices) == 0 {
|
||||||
|
logging.Error(types.ServiceName, "pdq_unavailable", "ChipDNA status has no PAYMENT_DEVICE_STATUS items", string(op), "", hotel, kiosk)
|
||||||
|
return PaymentDeviceStatus{}, fmt.Errorf("no payment devices returned")
|
||||||
|
}
|
||||||
|
|
||||||
|
dev := status.PaymentDevices[0]
|
||||||
|
if !dev.IsAvailable {
|
||||||
|
logging.Error(types.ServiceName, "pdq_unavailable", "Payment device unavailable", string(op), "", hotel, kiosk)
|
||||||
|
return dev, fmt.Errorf("device unavailable")
|
||||||
|
}
|
||||||
|
|
||||||
|
return dev, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartPdqHourlyCheck(ctx context.Context, hotel string, kiosk int) {
|
||||||
|
// waitUntilNextHour(ctx)
|
||||||
|
|
||||||
|
// First execution exactly at round hour
|
||||||
|
_, _ = ReadPdqStatus(hotel, kiosk)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(10 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
_, _ = ReadPdqStatus(hotel, kiosk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitUntilNextHour(ctx context.Context) {
|
||||||
|
now := time.Now()
|
||||||
|
next := now.Truncate(time.Hour).Add(time.Hour)
|
||||||
|
d := time.Until(next)
|
||||||
|
|
||||||
|
timer := time.NewTimer(d)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
case <-timer.C:
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -50,6 +50,12 @@ type (
|
|||||||
transactionRes string
|
transactionRes string
|
||||||
transactionState string
|
transactionState string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConfirmTransactionRequest struct {
|
||||||
|
XMLName xml.Name `xml:"ConfirmTransactionRequest"`
|
||||||
|
Amount string `xml:"Amount"`
|
||||||
|
Reference string `xml:"TransactionReference"`
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseTransactionResult parses the XML into entries.
|
// ParseTransactionResult parses the XML into entries.
|
||||||
@ -72,9 +78,7 @@ func (ti *TransactionInfo) FillFromTransactionResult(trResult TransactionResultX
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *PaymentResult) FillFromTransactionResult(trResult TransactionResultXML) {
|
func (r *PaymentResult) FillFromTransactionResult(trResult TransactionResultXML) {
|
||||||
if r.Fields == nil {
|
r.Fields = make(map[string]string)
|
||||||
r.Fields = make(map[string]string)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, e := range trResult.Entries {
|
for _, e := range trResult.Entries {
|
||||||
switch e.Key {
|
switch e.Key {
|
||||||
@ -107,7 +111,7 @@ func BuildPaymentRedirectURL(result map[string]string) string {
|
|||||||
log.WithField(types.LogResult, result[types.ConfirmResult]).
|
log.WithField(types.LogResult, result[types.ConfirmResult]).
|
||||||
Info("Transaction approved and confirmed")
|
Info("Transaction approved and confirmed")
|
||||||
|
|
||||||
return buildSuccessURL(result)
|
return BuildSuccessURL(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not confirmed
|
// Not confirmed
|
||||||
@ -133,7 +137,7 @@ func BuildPreauthRedirectURL(result map[string]string) (string, bool) {
|
|||||||
log.WithField(types.LogResult, result[types.TransactionResult]).
|
log.WithField(types.LogResult, result[types.TransactionResult]).
|
||||||
Info("Account verification approved")
|
Info("Account verification approved")
|
||||||
|
|
||||||
return buildSuccessURL(result), false
|
return BuildSuccessURL(result), false
|
||||||
|
|
||||||
// Transaction type Sale?
|
// Transaction type Sale?
|
||||||
case strings.EqualFold(tType, types.SaleTransactionType):
|
case strings.EqualFold(tType, types.SaleTransactionType):
|
||||||
@ -141,7 +145,7 @@ func BuildPreauthRedirectURL(result map[string]string) (string, bool) {
|
|||||||
log.WithField(types.LogResult, result[types.ConfirmResult]).
|
log.WithField(types.LogResult, result[types.ConfirmResult]).
|
||||||
Info("Amount preauthorized successfully")
|
Info("Amount preauthorized successfully")
|
||||||
|
|
||||||
return buildSuccessURL(result), true
|
return BuildSuccessURL(result), true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,7 +153,7 @@ func BuildPreauthRedirectURL(result map[string]string) (string, bool) {
|
|||||||
return BuildFailureURL(res, result[types.Errors]), false
|
return BuildFailureURL(res, result[types.Errors]), false
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildSuccessURL(result map[string]string) string {
|
func BuildSuccessURL(result map[string]string) string {
|
||||||
q := url.Values{}
|
q := url.Values{}
|
||||||
q.Set("CardNumber", hex.EncodeToString([]byte(result[types.PAN_MASKED])))
|
q.Set("CardNumber", hex.EncodeToString([]byte(result[types.PAN_MASKED])))
|
||||||
q.Set("ExpiryDate", hex.EncodeToString([]byte(result[types.EXPIRY_DATE])))
|
q.Set("ExpiryDate", hex.EncodeToString([]byte(result[types.EXPIRY_DATE])))
|
||||||
|
|||||||
@ -167,7 +167,19 @@ func BuildRoomTicket(details RoomDetailsRec) ([]byte, error) {
|
|||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func PrintCardholderReceipt(cardholderReceipt string) error {
|
func PrintReceipt(receipt string) {
|
||||||
|
|
||||||
|
if len(receipt) == 0 {
|
||||||
|
log.Warn("Empty cardholder receipt, skipping print")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := printCardholderReceipt(receipt); err != nil {
|
||||||
|
log.Errorf("PrintCardholderReceipt error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printCardholderReceipt(cardholderReceipt string) error {
|
||||||
receiptEntries, err := ParseCardholderReceipt([]byte(cardholderReceipt))
|
receiptEntries, err := ParseCardholderReceipt([]byte(cardholderReceipt))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ParseCardholderReceipt: %w", err)
|
return fmt.Errorf("ParseCardholderReceipt: %w", err)
|
||||||
|
|||||||
@ -2,6 +2,28 @@
|
|||||||
|
|
||||||
builtVersion is a const in main.go
|
builtVersion is a const in main.go
|
||||||
|
|
||||||
|
#### 1.2.5 - 20 March 2026
|
||||||
|
removed early return on error when checking dispenser status in the start and final loops.
|
||||||
|
|
||||||
|
#### 1.2.4 - 18 March 2026
|
||||||
|
added check if keycard at the encoder position before trying to encode key
|
||||||
|
|
||||||
|
#### 1.2.3 - 17 March 2026
|
||||||
|
added check if keycard at the encoder position before trying to encode key
|
||||||
|
|
||||||
|
#### 1.2.2 - 11 February 2026
|
||||||
|
increased waiting time befor sending email on PDQ unavailability to 30 seconds day time and 10 minutes night time
|
||||||
|
to give it a chance to become available again
|
||||||
|
|
||||||
|
#### 1.2.1 - 09 February 2026
|
||||||
|
increased waiting time befor sending email on PDQ unavailability to 60 seconds
|
||||||
|
|
||||||
|
#### 1.2.0 - 09 February 2026
|
||||||
|
added testissuedoorcard endpoint for testing the full workflow of encoding a door card without moving the card out
|
||||||
|
added ping-pdq endpoint to check the status of the pdq terminal
|
||||||
|
added sending the email on the pdq disconnect event to notify support about the issue
|
||||||
|
added sending the email on the dispenser error status to notify support about the issue
|
||||||
|
|
||||||
#### 1.1.3 - 02 February 2026
|
#### 1.1.3 - 02 February 2026
|
||||||
increased timeout for reading response from the Assa abloy lock server to 20 seconds
|
increased timeout for reading response from the Assa abloy lock server to 20 seconds
|
||||||
|
|
||||||
|
|||||||
@ -7,11 +7,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
ServiceName = "hardlink"
|
||||||
DateOnly = "2006-01-02"
|
DateOnly = "2006-01-02"
|
||||||
CustomLayout = "2006-01-02 15:04:05 -0700"
|
CustomLayout = "2006-01-02 15:04:05 -0700"
|
||||||
LinkTakePreauthorization = "http://127.0.0.1:18181/start-transaction/"
|
LinkStartTransaction = "http://127.0.0.1:18181/start-transaction/"
|
||||||
LinkTakePayment = "http://127.0.0.1:18181/start-and-confirm-transaction/"
|
LinkConfirmTransaction = "http://127.0.0.1:18181/confirm-transaction/"
|
||||||
LinkTransactionInformation = "http://127.0.0.1:18181/transaction-information/"
|
LinkTransactionInformation = "http://127.0.0.1:18181/transaction-information/"
|
||||||
|
LinkChipDNAStatus = "http://127.0.0.1:18181/chipdna-status/"
|
||||||
LinkVoidTransaction = "http://127.0.0.1:18181/void-transaction/"
|
LinkVoidTransaction = "http://127.0.0.1:18181/void-transaction/"
|
||||||
// Transaction types
|
// Transaction types
|
||||||
SaleTransactionType = "sale"
|
SaleTransactionType = "sale"
|
||||||
@ -32,6 +34,7 @@ const (
|
|||||||
CardReference = "CARD_REFERENCE"
|
CardReference = "CARD_REFERENCE"
|
||||||
CardHash = "CARD_HASH"
|
CardHash = "CARD_HASH"
|
||||||
Errors = "ERRORS"
|
Errors = "ERRORS"
|
||||||
|
ErrorDescription = "ERROR_DESCRIPTION"
|
||||||
ReceiptData = "RECEIPT_DATA"
|
ReceiptData = "RECEIPT_DATA"
|
||||||
ReceiptDataMerchant = "RECEIPT_DATA_MERCHANT"
|
ReceiptDataMerchant = "RECEIPT_DATA_MERCHANT"
|
||||||
ReceiptDataCardholder = "RECEIPT_DATA_CARDHOLDER"
|
ReceiptDataCardholder = "RECEIPT_DATA_CARDHOLDER"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user