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 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: } }