279 lines
9.3 KiB
Go
279 lines
9.3 KiB
Go
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:
|
|
}
|
|
}
|