319 lines
9.6 KiB
Go

package main
import (
"context"
"encoding/xml"
"fmt"
"net/http"
"os"
"os/exec"
"os/signal"
"strings"
"syscall"
"time"
"github.com/tarm/serial"
log "github.com/sirupsen/logrus"
"gitea.futuresens.co.uk/futuresens/cmstypes"
"gitea.futuresens.co.uk/futuresens/hardlink/cms"
"gitea.futuresens.co.uk/futuresens/hardlink/config"
"gitea.futuresens.co.uk/futuresens/hardlink/internal/bootstrap"
"gitea.futuresens.co.uk/futuresens/hardlink/internal/creditcall"
"gitea.futuresens.co.uk/futuresens/hardlink/internal/dispenser"
"gitea.futuresens.co.uk/futuresens/hardlink/internal/dojo"
"gitea.futuresens.co.uk/futuresens/hardlink/internal/errorhandlers"
"gitea.futuresens.co.uk/futuresens/hardlink/internal/handlers"
"gitea.futuresens.co.uk/futuresens/hardlink/internal/lockserver"
"gitea.futuresens.co.uk/futuresens/hardlink/internal/logging"
"gitea.futuresens.co.uk/futuresens/hardlink/internal/mail"
"gitea.futuresens.co.uk/futuresens/hardlink/internal/paybridge"
"gitea.futuresens.co.uk/futuresens/hardlink/internal/paymentsvc"
"gitea.futuresens.co.uk/futuresens/hardlink/internal/printer"
)
const (
buildVersion = "1.3.3"
serviceName = "hardlink"
pollingFrequency = 8 * time.Second
)
func main() {
// Load config
cfg := config.ReadHardlinkConfig()
printer.Layout = readTicketLayout()
printer.PrinterName = cfg.PrinterName
lockserver.Cert = cfg.Cert
lockserver.LockServerURL = cfg.LockserverURL
mail.SendErrorEmails = cfg.SendErrorEmails
// Root context for background goroutines
// rootCtx, rootCancel := context.WithCancel(context.Background())
// defer rootCancel()
var (
dispPort *serial.Port
disp *dispenser.Client
cardWellStatus string
)
// Setup logging and get file handle
logFile, err := logging.SetupLogging(cfg.LogDir, serviceName, buildVersion)
if err != nil {
log.Printf("Failed to set up logging: %v\n", err)
}
if logFile != nil {
defer logFile.Close()
}
// Initialize dispenser
if !cfg.TestMode {
dispenser.SerialPort = cfg.DispenserPort
dispenser.Address = []byte(cfg.DispenserAdrr)
dispPort, err = dispenser.InitializeDispenser()
if err != nil {
errorhandlers.FatalErrorWithMail(cfg.Hotel, cfg.Kiosk, "Dispenser Initialization Error", fmt.Errorf("failed to initialize dispenser: %v", err))
}
defer dispPort.Close()
disp = dispenser.NewClient(dispPort, 32)
defer disp.Close()
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
cardWellStatus, err = disp.DispenserPrepare(ctx)
if err != nil {
err = fmt.Errorf("%s; wrong dispenser address: %s", err, cfg.DispenserAdrr)
errorhandlers.FatalErrorWithMail(cfg.Hotel, cfg.Kiosk, "Dispenser Preparation Error", err)
}
fmt.Println(cardWellStatus)
}
// Test lock-server connection
switch strings.ToLower(cfg.LockType) {
case lockserver.TLJ:
// TLJ uses HTTP - skip TCP probe here
default:
lockConn, err := lockserver.InitializeServerConnection(cfg.LockserverURL)
if err != nil {
fmt.Println(err.Error())
log.Errorf(err.Error())
mail.SendEmailOnError(cfg.Hotel, cfg.Kiosk, "Lock Server Connection Error", err.Error())
} else {
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)
lockConn.Close()
}
}
database, err := bootstrap.OpenDB(&cfg)
if err != nil {
log.Warnf("DB init failed: %v", err)
}
if database != nil {
defer database.Close()
}
// Create App and wire routes
app := handlers.NewApp(disp, cfg.LockType, cfg.EncoderAddress, cardWellStatus, database, &cfg)
if cfg.IsPayment {
fmt.Println("Payment processing is enabled")
log.Info("Payment processing is enabled")
var provider paymentsvc.Provider
reservationSystem, err := cms.ReadHotel(cfg.Hotel, cfg.CMSBaseURL)
if err != nil {
errorhandlers.FatalErrorWithMail(cfg.Hotel, cfg.Kiosk, "Failed to read hotel from CMS", fmt.Errorf("failed to read hotel from CMS: %v", err))
}
paymentProvider := reservationSystem.PaymentSystem
if paymentProvider == 0 {
paymentProvider = cms.PaymentSystemIndex(cfg.PaymentProvider)
}
switch paymentProvider {
case cmstypes.PaySystemCreditCall:
// CreditCall keeps using the existing /takepayment and /takepreauth endpoints.
startChipDnaClient()
go func() {
time.Sleep(30 * time.Second)
pdqStatus, err := creditcall.ReadPdqStatus(cfg.Hotel, cfg.Kiosk)
if err != nil {
mail.SendEmailOnError(cfg.Hotel, cfg.Kiosk, "PDQ Status Read Error", err.Error())
return
}
fmt.Printf("\nPDQ available: %v\n", pdqStatus.IsAvailable)
log.Infof("PDQ available: %v", pdqStatus.IsAvailable)
}()
log.Info("CreditCall payment provider enabled")
fmt.Println("CreditCall payment provider enabled")
case cmstypes.PaySystemPayBridge:
if reservationSystem.PaymentGatewayURL == "" {
errorhandlers.FatalErrorWithMail(cfg.Hotel, cfg.Kiosk, "Payment Gateway URL not found", fmt.Errorf("payment provider paybridge requires paybridge.websocket_url"))
}
if reservationSystem.PaymentGatewayAPIKeyWeb == "" {
errorhandlers.FatalErrorWithMail(cfg.Hotel, cfg.Kiosk, "Payment Gateway API Key not found", fmt.Errorf("payment provider paybridge requires paybridge.api_key"))
}
provider = paybridge.NewClient(
reservationSystem.PaymentGatewayURL,
reservationSystem.PaymentGatewayAPIKeyWeb,
cfg.TimeoutSeconds,
)
case cmstypes.PaySystemDojo:
var TerminalID string
for _, item := range reservationSystem.PDQs {
if item.Kiosk != cfg.Kiosk {
continue
}
TerminalID = item.Serial
break
}
dojoClient, err := dojo.NewClient(dojo.Config{
BaseURL: reservationSystem.PaymentGatewayURL,
APIKey: reservationSystem.PaymentGatewayAPIKeyWeb,
SoftwareHouseID: reservationSystem.PaymentGatewaySiteIDEPOS,
Version: reservationSystem.PaymentGatewayPasswordEPOS,
TerminalID: TerminalID,
})
if err != nil {
errorhandlers.FatalErrorWithMail(cfg.Hotel, cfg.Kiosk, "Failed to create Dojo client", err)
}
provider = dojoClient
case cmstypes.PaySystemNone:
errorhandlers.FatalErrorWithMail(cfg.Hotel, cfg.Kiosk, "No payment provider selected", fmt.Errorf("payment processing is enabled, but no payment provider selected; expected creditcall, paybridge or dojo"))
default:
errorhandlers.FatalErrorWithMail(cfg.Hotel, cfg.Kiosk, "Unsupported Payment Provider", fmt.Errorf(
"unsupported payment provider %q; expected creditcall, paybridge or dojo",
cfg.PaymentProvider,
))
}
// Only PayBridge and Dojo use POST /api/payment/sale.
if provider != nil {
app.SetPaymentService(paymentsvc.NewService(provider))
log.Infof("Payment provider enabled for POST /api/payment/sale: %s", cmstypes.PaySystemNames[paymentProvider])
fmt.Printf("Payment provider enabled for POST /api/payment/sale: %s\n", cmstypes.PaySystemNames[paymentProvider])
}
} else {
fmt.Println("Payment processing is disabled")
log.Info("Payment processing is disabled")
}
// Update cardWellStatus when dispenser status changes
if !cfg.TestMode && disp != nil {
// Set initial cardWellStatus
app.SetCardWellStatus(cardWellStatus)
// Set up callback to update cardWellStatus when dispenser status changes
disp.OnStockUpdate(func(stock string) {
app.SetCardWellStatus(stock)
})
// Start polling for dispenser status every 10 seconds
disp.StartPolling(pollingFrequency)
}
mux := http.NewServeMux()
app.RegisterRoutes(mux)
addr := fmt.Sprintf(":%d", cfg.Port)
log.Infof("Starting HTTP server on http://localhost%s", addr)
fmt.Printf("Starting HTTP server on http://localhost%s", addr)
if err := http.ListenAndServe(addr, mux); err != nil {
errorhandlers.FatalError(err)
}
}
func readTicketLayout() printer.LayoutOptions {
const layoutName = "TicketLayout.xml"
var layout printer.LayoutOptions
// 1) Read the file
data, err := os.ReadFile(layoutName)
if err != nil {
errorhandlers.FatalError(fmt.Errorf("failed to read %s: %v", layoutName, err))
}
// 2) Unmarshal into your struct
if err := xml.Unmarshal(data, &layout); err != nil {
errorhandlers.FatalError(fmt.Errorf("failed to parse %s: %v", layoutName, err))
}
return layout
}
func startChipDnaClient() {
startClient := func() (*exec.Cmd, error) {
cmd := exec.Command("./ChipDNAClient/ChipDnaClient.exe")
err := cmd.Start()
if err != nil {
return nil, fmt.Errorf("failed to start ChipDnaClient: %v", err)
}
log.Infof("ChipDnaClient started with PID %d", cmd.Process.Pid)
return cmd, nil
}
cmd, err := startClient()
if err != nil {
errorhandlers.FatalError(err)
}
// Restart loop
go func() {
for {
err := cmd.Wait()
if err != nil {
log.Errorf("ChipDnaClient exited unexpectedly: %v", err)
fmt.Printf("ChipDnaClient exited unexpectedly: %v", err)
time.Sleep(2 * time.Second)
cmd, err = startClient()
if err != nil {
log.Errorf("Restart failed: %v", err)
return
}
log.Info("ChipDnaClient restarted successfully")
fmt.Printf("ChipDnaClient restarted successfully")
}
}
}()
// Handle shutdown signals
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigs
log.Info("Shutting down...")
if cmd.Process != nil {
log.Info("Sending SIGTERM to ChipDnaClient...")
_ = cmd.Process.Signal(syscall.SIGTERM)
// wait up to 5s for graceful shutdown
done := make(chan error, 1)
go func() { done <- cmd.Wait() }()
select {
case <-time.After(5 * time.Second):
log.Warn("ChipDnaClient did not exit in time, killing...")
_ = cmd.Process.Kill()
case err := <-done:
log.Infof("ChipDnaClient exited cleanly: %v", err)
}
}
os.Exit(0)
}()
}