added creditcall payment method
This commit is contained in:
parent
b002c80b01
commit
5e99453eeb
10
go.mod
10
go.mod
@ -3,13 +3,19 @@ module gitea.futuresens.co.uk/futuresens/hardlink
|
||||
go 1.23.2
|
||||
|
||||
require (
|
||||
gitea.futuresens.co.uk/futuresens/cmstypes v1.0.171
|
||||
gitea.futuresens.co.uk/futuresens/cmstypes v1.0.179
|
||||
gitea.futuresens.co.uk/futuresens/logging v1.0.9
|
||||
github.com/alexbrainman/printer v0.0.0-20200912035444-f40f26f0bdeb
|
||||
github.com/denisenkom/go-mssqldb v0.12.3
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
|
||||
golang.org/x/image v0.27.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.32.0 // indirect
|
||||
require (
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
golang.org/x/crypto v0.3.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
)
|
||||
|
34
go.sum
34
go.sum
@ -1,14 +1,26 @@
|
||||
gitea.futuresens.co.uk/futuresens/cmstypes v1.0.171 h1:/WM3mG5i4VYspeLaGFwjuvQJHM/Pks/dN3RjhrmYaN0=
|
||||
gitea.futuresens.co.uk/futuresens/cmstypes v1.0.171/go.mod h1:ABMUkdm+3VGrkuoCJsXMfPPud9GHDOwBb1NiifFqxes=
|
||||
gitea.futuresens.co.uk/futuresens/cmstypes v1.0.179 h1:3OLzX6jJ2dwfZ9Fcijk5z6/GUdTl5FUNw3eWuRkDhZw=
|
||||
gitea.futuresens.co.uk/futuresens/cmstypes v1.0.179/go.mod h1:ABMUkdm+3VGrkuoCJsXMfPPud9GHDOwBb1NiifFqxes=
|
||||
gitea.futuresens.co.uk/futuresens/fscrypto v0.0.0-20221125125050-9acaffd21362 h1:MnhYo7XtsECCU+5yVMo3tZZOOSOKGkl7NpOvTAieBTo=
|
||||
gitea.futuresens.co.uk/futuresens/fscrypto v0.0.0-20221125125050-9acaffd21362/go.mod h1:p95ouVfK4qyC20D3/k9QLsWSxD2pdweWiY6vcYi9hpM=
|
||||
gitea.futuresens.co.uk/futuresens/logging v1.0.9 h1:uvCQq/plecB0z/bUWOhFhwyYUWGPkTBZHsYNL+3RFvI=
|
||||
gitea.futuresens.co.uk/futuresens/logging v1.0.9/go.mod h1:pepS4+sreKTXJUp1Dq2RunpvQ0oY3vU2AuYjMTZzVQo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
|
||||
github.com/alexbrainman/printer v0.0.0-20200912035444-f40f26f0bdeb h1:OzF7h5OJLiB2QvpxfFdUFdSedYYsEKAXnE8BwsWQPmY=
|
||||
github.com/alexbrainman/printer v0.0.0-20200912035444-f40f26f0bdeb/go.mod h1:aeB9oSJ1VNJXxBkCz6Krw3aW8lPx6rkWnW/hXcoujR4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw=
|
||||
github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
||||
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/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
@ -19,16 +31,34 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
|
||||
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
@ -29,7 +29,7 @@ type (
|
||||
}
|
||||
|
||||
OmniLockServer struct {
|
||||
encoderAddr string // Encoder address for the lock server
|
||||
encoderAddr string // Encoder unit address
|
||||
command []byte // Command to be sent to the lock server
|
||||
}
|
||||
)
|
||||
|
172
main.go
172
main.go
@ -1,13 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/tarm/serial"
|
||||
@ -17,6 +22,7 @@ import (
|
||||
|
||||
"gitea.futuresens.co.uk/futuresens/hardlink/dispenser"
|
||||
"gitea.futuresens.co.uk/futuresens/hardlink/lockserver"
|
||||
"gitea.futuresens.co.uk/futuresens/hardlink/payment"
|
||||
"gitea.futuresens.co.uk/futuresens/hardlink/printer"
|
||||
|
||||
"gitea.futuresens.co.uk/futuresens/cmstypes"
|
||||
@ -24,9 +30,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
buildVersion = "0.9.2"
|
||||
serviceName = "hardlink"
|
||||
customLayout = "2006-01-02 15:04:05 -0700"
|
||||
buildVersion = "1.0.0"
|
||||
serviceName = "hardlink"
|
||||
customLayout = "2006-01-02 15:04:05 -0700"
|
||||
transactionUrl = "http://127.0.0.1:18181/start-transaction/"
|
||||
)
|
||||
|
||||
// configRec holds values from config.yml.
|
||||
@ -39,6 +46,10 @@ type configRec struct {
|
||||
DispenserAdrr string `yaml:"dispensAddr"`
|
||||
PrinterName string `yaml:"printerName"`
|
||||
LogDir string `yaml:"logdir"`
|
||||
dbport int `yaml:"dbport"` // Port for the database connection
|
||||
dbname string `yaml:"dbname"` // Database name for the connection
|
||||
dbuser string `yaml:"dbuser"` // User for the database connection
|
||||
dbpassword string `yaml:"dbpassword"` // Password for the database connection
|
||||
}
|
||||
|
||||
// DoorCardRequest is the JSON payload for /issue-door-card.
|
||||
@ -54,13 +65,15 @@ type App struct {
|
||||
dispPort *serial.Port
|
||||
lockConn net.Conn
|
||||
lockserver lockserver.LockServer
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func newApp(dispPort *serial.Port, lockConn net.Conn, config configRec) *App {
|
||||
func newApp(dispPort *serial.Port, lockConn net.Conn, config configRec, db *sql.DB) *App {
|
||||
return &App{
|
||||
dispPort: dispPort,
|
||||
lockConn: lockConn,
|
||||
dispPort: dispPort,
|
||||
lockConn: lockConn,
|
||||
lockserver: lockserver.NewLockServer(config.LockType, config.EncoderAddress, fatalError),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,8 +120,69 @@ func main() {
|
||||
defer lockConn.Close()
|
||||
log.Infof("Connected to lock server at %s", config.LockserverUrl)
|
||||
|
||||
db, err := payment.InitMSSQL(config.dbport, config.dbname, config.dbuser, config.dbpassword)
|
||||
if err != nil {
|
||||
fatalError(fmt.Errorf("DB init failed: %v", err))
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
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 {
|
||||
fatalError(err)
|
||||
}
|
||||
|
||||
// Restart loop
|
||||
go func() {
|
||||
for {
|
||||
err := cmd.Wait()
|
||||
if err != nil {
|
||||
log.Errorf("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")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 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)
|
||||
}()
|
||||
|
||||
// Create App and wire routes
|
||||
app := newApp(dispHandle, lockConn, config)
|
||||
app := newApp(dispHandle, lockConn, config, db)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
setUpRoutes(app, mux)
|
||||
@ -124,6 +198,7 @@ func main() {
|
||||
func setUpRoutes(app *App, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/issuedoorcard", app.issueDoorCard)
|
||||
mux.HandleFunc("/printroomticket", app.printRoomTicket)
|
||||
mux.HandleFunc("/starttransaction", app.startTransaction)
|
||||
}
|
||||
|
||||
func fatalError(err error) {
|
||||
@ -167,6 +242,87 @@ func writeError(w http.ResponseWriter, status int, msg string) {
|
||||
json.NewEncoder(w).Encode(theResponse)
|
||||
}
|
||||
|
||||
func (app *App) startTransaction(w http.ResponseWriter, r *http.Request) {
|
||||
const op = logging.Op("startTransaction")
|
||||
var (
|
||||
theResponse cmstypes.ResponseRec
|
||||
cardholderReceipt string
|
||||
)
|
||||
|
||||
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("startTransaction called")
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "Method not allowed; use POST")
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
if ct := r.Header.Get("Content-Type"); ct != "text/xml" {
|
||||
writeError(w, http.StatusUnsupportedMediaType, "Content-Type must be text/xml")
|
||||
return
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 90 * time.Second}
|
||||
response, err := client.Post(transactionUrl, "text/xml", r.Body)
|
||||
if err != nil {
|
||||
logging.Error(serviceName, err.Error(), "Payment processing error", string(op), "", "", 0)
|
||||
writeError(w, http.StatusInternalServerError, "Payment processing failed: "+err.Error())
|
||||
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)
|
||||
writeError(w, http.StatusInternalServerError, "Failed to read response body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
responseEntries, _ := payment.ParseTransactionResult(body)
|
||||
|
||||
// Compose JSON from responseEntries
|
||||
result := make(map[string]string)
|
||||
for _, e := range responseEntries {
|
||||
switch e.Key {
|
||||
case "RECEIPT_DATA", "RECEIPT_DATA_MERCHANT":
|
||||
case "RECEIPT_DATA_CARDHOLDER":
|
||||
cardholderReceipt = e.Value
|
||||
case "TRANSACTION_RESULT":
|
||||
theResponse.Status.Message = e.Value
|
||||
theResponse.Status.Code = http.StatusOK
|
||||
result[e.Key] = e.Value
|
||||
default:
|
||||
result[e.Key] = e.Value
|
||||
}
|
||||
}
|
||||
|
||||
if err := printer.PrintCardholderReceipt(cardholderReceipt); err != nil {
|
||||
log.Errorf("PrintCardholderReceipt error: %v", err)
|
||||
}
|
||||
|
||||
// Insert into DB
|
||||
if err := payment.InsertTransactionRecord(r.Context(), app.db, result); err != nil {
|
||||
log.Errorf("DB insert error: %v", err)
|
||||
}
|
||||
|
||||
theResponse.Data = payment.BuildRedirectURL(result)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err := json.NewEncoder(w).Encode(theResponse); err != nil {
|
||||
logging.Error(serviceName, err.Error(), "JSON encode error", string(op), "", "", 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) {
|
||||
const op = logging.Op("issueDoorCard")
|
||||
var (
|
||||
@ -266,7 +422,7 @@ func (app *App) printRoomTicket(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
|
@ -1,42 +1,280 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/denisenkom/go-mssqldb"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// StartTransaction sends a start transaction XML to ChipDNA Server
|
||||
func (pc *PaymentClient) StartTransaction(amountMinorUnits int, reference string) (string, error) {
|
||||
conn, err := net.Dial("tcp", pc.Addr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to connect: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
const (
|
||||
ResultApproved = "approved"
|
||||
ResultDeclined = "declined"
|
||||
ResultCancelled = "cancelled"
|
||||
ResultPending = "pending"
|
||||
ResultError = "error"
|
||||
CheckinSuccessfulEndpoint = "/successful" // Endpoint to send guest to after successful payment
|
||||
CheckinUnsuccessfulEndpoint = "/unsuccessful"
|
||||
)
|
||||
|
||||
// Set timeout
|
||||
conn.SetDeadline(time.Now().Add(15 * time.Second))
|
||||
|
||||
// Format XML request
|
||||
request := fmt.Sprintf(`
|
||||
<StartTransaction>
|
||||
<Amount>%d</Amount>
|
||||
<AmountType>Actual</AmountType>
|
||||
<Reference>%s</Reference>
|
||||
<TransactionType>Sale</TransactionType>
|
||||
</StartTransaction>`, amountMinorUnits, reference)
|
||||
|
||||
_, err = conn.Write([]byte(request))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to write to server: %w", err)
|
||||
// XML parsing structs
|
||||
type (
|
||||
TransactionRec struct {
|
||||
XMLName xml.Name `xml:"TransactionPayload"`
|
||||
AmountMinorUnits string `xml:"amount"`
|
||||
TransactionType string `xml:"transactionType"`
|
||||
}
|
||||
|
||||
// Read response
|
||||
buff := make([]byte, 4096)
|
||||
n, err := conn.Read(buff)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read from server: %w", err)
|
||||
TransactionResultXML struct {
|
||||
XMLName xml.Name `xml:"TransactionResult"`
|
||||
Entries []EntryXML `xml:"Entry"`
|
||||
}
|
||||
|
||||
return string(buff[:n]), nil
|
||||
EntryXML struct {
|
||||
Key string `xml:"Key"`
|
||||
Value string `xml:"Value"`
|
||||
}
|
||||
)
|
||||
|
||||
// ParseTransactionResult parses the XML into entries.
|
||||
func ParseTransactionResult(data []byte) ([]EntryXML, error) {
|
||||
var tr TransactionResultXML
|
||||
if err := xml.Unmarshal(data, &tr); err != nil {
|
||||
return nil, fmt.Errorf("XML unmarshal: %w", err)
|
||||
}
|
||||
return tr.Entries, nil
|
||||
}
|
||||
|
||||
// initMSSQL opens and pings the SQL Server instance localhost\SQLEXPRESS
|
||||
// using user=Kiosk, password=Gr33nfarm, database=TransactionDatabase.
|
||||
func InitMSSQL(port int, user, password, database string) (*sql.DB, error) {
|
||||
const server = "localhost"
|
||||
|
||||
// Use TCP; drop the \SQLEXPRESS instance name
|
||||
connString := fmt.Sprintf(
|
||||
"sqlserver://%s:%s@%s:%d?database=%s&encrypt=disable",
|
||||
user, password, server, port, database,
|
||||
)
|
||||
|
||||
db, err := sql.Open("sqlserver", connString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opening DB: %w", err)
|
||||
}
|
||||
|
||||
// Verify connectivity
|
||||
if err := db.PingContext(context.Background()); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("pinging DB: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// insertTransactionRecord inserts one row into TransactionRecords.
|
||||
// m is the map from keys to string values as returned by ChipDNA.
|
||||
func InsertTransactionRecord(ctx context.Context, db *sql.DB, m map[string]string) error {
|
||||
// Extract fields with defaults or NULL handling.
|
||||
|
||||
// 1. TxnReference <- REFERENCE
|
||||
ref, ok := m["REFERENCE"]
|
||||
if !ok || ref == "" {
|
||||
return fmt.Errorf("missing REFERENCE in result map")
|
||||
}
|
||||
|
||||
// 2. TxnDateTime <- parse AUTH_DATE_TIME (layout "20060102150405"), else use now
|
||||
var txnTime time.Time
|
||||
if s, ok := m["AUTH_DATE_TIME"]; ok && s != "" {
|
||||
t, err := time.ParseInLocation("20060102150405", s, time.UTC)
|
||||
if err != nil {
|
||||
// fallback: use now
|
||||
txnTime = time.Now().UTC()
|
||||
} else {
|
||||
txnTime = t
|
||||
}
|
||||
} else {
|
||||
txnTime = time.Now().UTC()
|
||||
}
|
||||
|
||||
// 3. TotalAmount <- parse TOTAL_AMOUNT minor units into float (divide by 100)
|
||||
var totalAmount sql.NullFloat64
|
||||
if s, ok := m["TOTAL_AMOUNT"]; ok && s != "" {
|
||||
if iv, err := strconv.ParseInt(s, 10, 64); err == nil {
|
||||
// convert minor units to major (e.g. 150 -> 1.50)
|
||||
totalAmount.Float64 = float64(iv) / 100.0
|
||||
totalAmount.Valid = true
|
||||
}
|
||||
}
|
||||
|
||||
// 4. MerchantId <- MERCHANT_ID_MASKED
|
||||
merchantId := sql.NullString{String: m["MERCHANT_ID_MASKED"], Valid: m["MERCHANT_ID_MASKED"] != ""}
|
||||
|
||||
// 5. TerminalId <- TERMINAL_ID_MASKED
|
||||
terminalId := sql.NullString{String: m["TERMINAL_ID_MASKED"], Valid: m["TERMINAL_ID_MASKED"] != ""}
|
||||
|
||||
// 6. CardSchemeName <- CARD_SCHEME
|
||||
cardScheme := sql.NullString{String: m["CARD_SCHEME"], Valid: m["CARD_SCHEME"] != ""}
|
||||
|
||||
// 7. ExpiryDate <- EXPIRY_DATE
|
||||
expiryDate := sql.NullString{String: m["EXPIRY_DATE"], Valid: m["EXPIRY_DATE"] != ""}
|
||||
|
||||
// 8. RecordReference <- CARD_REFERENCE
|
||||
recordRef := sql.NullString{String: m["CARD_REFERENCE"], Valid: m["CARD_REFERENCE"] != ""}
|
||||
|
||||
// 9. Token1 <- CARD_HASH
|
||||
token1 := sql.NullString{String: m["CARD_HASH"], Valid: m["CARD_HASH"] != ""}
|
||||
|
||||
// 10. Token2 <- CARDEASE_REFERENCE
|
||||
token2 := sql.NullString{String: m["CARDEASE_REFERENCE"], Valid: m["CARDEASE_REFERENCE"] != ""}
|
||||
|
||||
// 11. PanMasked <- PAN_MASKED
|
||||
panMasked := sql.NullString{String: m["PAN_MASKED"], Valid: m["PAN_MASKED"] != ""}
|
||||
|
||||
// 12. AuthCode <- AUTH_CODE
|
||||
authCode := sql.NullString{String: m["AUTH_CODE"], Valid: m["AUTH_CODE"] != ""}
|
||||
|
||||
// 13. TransactionResult <- TRANSACTION_RESULT
|
||||
txnResult := sql.NullString{String: m["TRANSACTION_RESULT"], Valid: m["TRANSACTION_RESULT"] != ""}
|
||||
|
||||
// Build INSERT statement with named parameters.
|
||||
// Assuming your table is [TransactionDatabase].[dbo].[TransactionRecords].
|
||||
const stmt = `
|
||||
INSERT INTO [TransactionDatabase].[dbo].[TransactionRecords]
|
||||
(
|
||||
[TxnReference],
|
||||
[TxnDateTime],
|
||||
[TotalAmount],
|
||||
[MerchantId],
|
||||
[TerminalId],
|
||||
[CardSchemeName],
|
||||
[ExpiryDate],
|
||||
[RecordReference],
|
||||
[Token1],
|
||||
[Token2],
|
||||
[PanMasked],
|
||||
[AuthCode],
|
||||
[TransactionResult]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@TxnReference,
|
||||
@TxnDateTime,
|
||||
@TotalAmount,
|
||||
@MerchantId,
|
||||
@TerminalId,
|
||||
@CardSchemeName,
|
||||
@ExpiryDate,
|
||||
@RecordReference,
|
||||
@Token1,
|
||||
@Token2,
|
||||
@PanMasked,
|
||||
@AuthCode,
|
||||
@TransactionResult
|
||||
);
|
||||
`
|
||||
// Execute with sql.Named parameters:
|
||||
_, err := db.ExecContext(ctx, stmt,
|
||||
sql.Named("TxnReference", ref),
|
||||
sql.Named("TxnDateTime", txnTime),
|
||||
sql.Named("TotalAmount", nullableFloatArg(totalAmount)),
|
||||
sql.Named("MerchantId", nullableStringArg(merchantId)),
|
||||
sql.Named("TerminalId", nullableStringArg(terminalId)),
|
||||
sql.Named("CardSchemeName", nullableStringArg(cardScheme)),
|
||||
sql.Named("ExpiryDate", nullableStringArg(expiryDate)),
|
||||
sql.Named("RecordReference", nullableStringArg(recordRef)),
|
||||
sql.Named("Token1", nullableStringArg(token1)),
|
||||
sql.Named("Token2", nullableStringArg(token2)),
|
||||
sql.Named("PanMasked", nullableStringArg(panMasked)),
|
||||
sql.Named("AuthCode", nullableStringArg(authCode)),
|
||||
sql.Named("TransactionResult", nullableStringArg(txnResult)),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert TransactionRecords: %w", err)
|
||||
}
|
||||
// Successfully inserted
|
||||
log.Infof("Inserted transaction record for reference %s", ref)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helpers to pass NULL when appropriate:
|
||||
func nullableStringArg(ns sql.NullString) interface{} {
|
||||
if ns.Valid {
|
||||
return ns.String
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func nullableFloatArg(nf sql.NullFloat64) interface{} {
|
||||
if nf.Valid {
|
||||
return nf.Float64
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func BuildRedirectURL(result map[string]string) string {
|
||||
// 1) normalize the result code
|
||||
res := strings.ToLower(result["TRANSACTION_RESULT"])
|
||||
|
||||
// 2) pick base path and optional error params
|
||||
var basePath string
|
||||
var msgType, description string
|
||||
|
||||
switch res {
|
||||
case ResultApproved:
|
||||
basePath = CheckinSuccessfulEndpoint
|
||||
|
||||
case ResultDeclined:
|
||||
basePath = CheckinUnsuccessfulEndpoint
|
||||
msgType = "declined"
|
||||
description = "payment declined"
|
||||
|
||||
case ResultCancelled:
|
||||
basePath = CheckinUnsuccessfulEndpoint
|
||||
msgType = "cancelled"
|
||||
description = "payment cancelled by customer"
|
||||
|
||||
case ResultPending:
|
||||
// you could choose to treat pending as unsuccessful or special-case it
|
||||
basePath = CheckinUnsuccessfulEndpoint
|
||||
msgType = "pending"
|
||||
description = "payment pending"
|
||||
case ResultError:
|
||||
basePath = CheckinUnsuccessfulEndpoint
|
||||
msgType = "error"
|
||||
description = result["ERROR"]
|
||||
|
||||
default:
|
||||
basePath = CheckinUnsuccessfulEndpoint
|
||||
msgType = "error"
|
||||
description = "unknown transaction result"
|
||||
}
|
||||
|
||||
if msgType != "" {
|
||||
log.Warnf("Transaction %s: %s - %s", res, msgType, description)
|
||||
}
|
||||
|
||||
// 3) build query params
|
||||
q := url.Values{}
|
||||
q.Set("TxnReference", result["REFERENCE"])
|
||||
q.Set("CardHash", result["CARD_HASH"])
|
||||
q.Set("CardReference", result["CARD_REFERENCE"])
|
||||
|
||||
// only append these when non-approved
|
||||
if msgType != "" {
|
||||
q.Set("MsgType", msgType)
|
||||
q.Set("Description", description)
|
||||
}
|
||||
|
||||
// 4) assemble final URL
|
||||
// note: url.URL automatically escapes values in RawQuery
|
||||
u := url.URL{
|
||||
Path: basePath,
|
||||
RawQuery: q.Encode(),
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
@ -1,32 +0,0 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PaymentClient holds connection data
|
||||
type PaymentClient struct {
|
||||
Addr string // e.g., "127.0.0.1:1869"
|
||||
}
|
||||
|
||||
func InitializeConnection(paymentrUrl string) (net.Conn, error) {
|
||||
const funcName = "InitializeServerConnection"
|
||||
// Parse the URL to extract host and port
|
||||
parsedUrl, err := url.Parse(paymentrUrl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[%s] failed to parse LockserverUrl: %v", funcName, err)
|
||||
}
|
||||
|
||||
// Remove any leading/trailing slashes just in case
|
||||
address := strings.Trim(parsedUrl.Host, "/")
|
||||
|
||||
// Establish a TCP connection to the Visionline server
|
||||
conn, err := net.Dial("tcp", address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to payment server: %v", err)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
@ -5,13 +5,13 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
"image/color"
|
||||
"image/draw"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/alexbrainman/printer"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@ -19,6 +19,21 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
CardholderReceipt struct {
|
||||
XMLName xml.Name `xml:"ReceiptData"`
|
||||
ReceiptEntries struct {
|
||||
ReceiptEntry []ReceiptEntryXML `xml:"ReceiptEntry"`
|
||||
} `xml:"ReceiptEntries"`
|
||||
}
|
||||
|
||||
ReceiptEntryXML struct {
|
||||
ReceiptEntryId string `xml:"ReceiptEntryId"`
|
||||
Value string `xml:"Value"`
|
||||
Label string `xml:"Label"`
|
||||
ReceiptItemType string `xml:"ReceiptItemType"`
|
||||
Priority string `xml:"Priority"`
|
||||
}
|
||||
|
||||
RoomDetailsRec struct {
|
||||
XMLName xml.Name `xml:"roomdetails"`
|
||||
Name string `xml:"customername"`
|
||||
@ -54,11 +69,30 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
ESC = 0x1B
|
||||
GS = 0x1D
|
||||
CENTER = 0x01
|
||||
BOLD_ON = 0x01
|
||||
BOLD_OFF = 0x00
|
||||
LARGE_FONT = 0x11
|
||||
WIDER_FONT = 0x10
|
||||
NORMAL_FONT = 0x00
|
||||
)
|
||||
|
||||
var (
|
||||
Layout LayoutOptions
|
||||
PrinterName string
|
||||
)
|
||||
|
||||
func ParseCardholderReceipt(data []byte) ([]ReceiptEntryXML, error) {
|
||||
var tr CardholderReceipt
|
||||
if err := xml.Unmarshal(data, &tr); err != nil {
|
||||
return nil, fmt.Errorf("XML unmarshal: %w", err)
|
||||
}
|
||||
return tr.ReceiptEntries.ReceiptEntry, nil
|
||||
}
|
||||
|
||||
func BuildRoomTicket(details RoomDetailsRec) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
@ -66,17 +100,6 @@ func BuildRoomTicket(details RoomDetailsRec) ([]byte, error) {
|
||||
write := func(b []byte) { buf.Write(b) }
|
||||
writeStr := func(s string) { buf.WriteString(s) }
|
||||
|
||||
const (
|
||||
ESC = 0x1B
|
||||
GS = 0x1D
|
||||
CENTER = 0x01
|
||||
BOLD_ON = 0x01
|
||||
BOLD_OFF = 0x00
|
||||
LARGE_FONT = 0x11
|
||||
WIDER_FONT = 0x10
|
||||
NORMAL_FONT = 0x00
|
||||
)
|
||||
|
||||
// 0) Hotel logo at top
|
||||
logoBytes, err := printLogo(Layout.LogoPath)
|
||||
if err != nil {
|
||||
@ -144,6 +167,114 @@ func BuildRoomTicket(details RoomDetailsRec) ([]byte, error) {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func PrintCardholderReceipt(cardholderReceipt string) error {
|
||||
receiptEntries, err := ParseCardholderReceipt([]byte(cardholderReceipt))
|
||||
if err != nil {
|
||||
return fmt.Errorf("ParseCardholderReceipt: %w", err)
|
||||
}
|
||||
|
||||
data, err := BuildCardholderReceipt(receiptEntries)
|
||||
if err != nil {
|
||||
return fmt.Errorf("BuildCardholderReceipt: %w", err)
|
||||
}
|
||||
|
||||
// Send to the Windows Epson TM-T82II via the printer package
|
||||
if err := SendToPrinter(data); err != nil {
|
||||
return fmt.Errorf("SendToPrinter: %w", err)
|
||||
}
|
||||
log.Info("Cardholder receipt printed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func BuildCardholderReceipt(entries []ReceiptEntryXML) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
write := func(b []byte) { buf.Write(b) }
|
||||
// writeStr := func(s string) { buf.WriteString(s) }
|
||||
writeln := func(s string) {
|
||||
buf.WriteString(s)
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
|
||||
// Build a lookup map by ReceiptEntryId
|
||||
m := make(map[string]ReceiptEntryXML, len(entries))
|
||||
for _, e := range entries {
|
||||
m[e.ReceiptEntryId] = e
|
||||
}
|
||||
|
||||
// Utility to center text
|
||||
// center := func(s string) {
|
||||
// write([]byte{ESC, 'a', CENTER})
|
||||
// writeln(s)
|
||||
// write([]byte{ESC, 'a', 0})
|
||||
// }
|
||||
|
||||
// Utility for bold lines
|
||||
// boldOn := func() { write([]byte{GS, '!', WIDER_FONT}) }
|
||||
// boldOff := func() { write([]byte{GS, '!', NORMAL_FONT}) }
|
||||
|
||||
// boldOn()
|
||||
|
||||
// 1) Header: CARDHOLDER COPY
|
||||
writeln(m["Recipient"].Value)
|
||||
// writeStr("\n\n")
|
||||
// 2) Merchant name
|
||||
writeln(m["MerchantName"].Value)
|
||||
// writeStr("\n\n")
|
||||
// 3) AID
|
||||
writeln("AID: " + m["Aid"].Value)
|
||||
|
||||
// 4) Card scheme & pan
|
||||
writeln(fmt.Sprintf("%s Card: %s", m["CardScheme"].Value, m["PanMasked"].Value))
|
||||
|
||||
// 5) PAN Seq No
|
||||
writeln("PAN Seq No: " + m["PanSequence"].Value)
|
||||
|
||||
// 6) Transaction source
|
||||
writeln(m["TransactionSource"].Value)
|
||||
|
||||
// 7) Transaction type (Sale/Account Verification)
|
||||
writeln(m["TransactionType"].Value)
|
||||
|
||||
// 8) Total
|
||||
// assuming value like "GBP1.50" — insert space after currency
|
||||
total := m["TotalAmount"].Value
|
||||
if !strings.HasPrefix(total, "GBP") && len(total) > 3 {
|
||||
total = total[:3] + " " + total[3:]
|
||||
}
|
||||
writeln("TOTAL: " + total)
|
||||
|
||||
// 9) Cardholder verification
|
||||
writeln(m["CardholderVerification"].Value)
|
||||
|
||||
// 10) Approved/Declined
|
||||
writeln(m["TransactionResult"].Value)
|
||||
|
||||
// 11) Auth code
|
||||
writeln("Auth Code: " + m["AuthCode"].Value)
|
||||
|
||||
// 12) Reference
|
||||
writeln("Ref: " + m["AuthReference"].Value)
|
||||
|
||||
// 13) Merchant & terminal IDs
|
||||
writeln("MID: " + m["MerchantIdMasked"].Value)
|
||||
writeln("TID: " + m["TerminalIdMasked"].Value)
|
||||
|
||||
// 14) Date/time
|
||||
writeln(m["AuthDateTime"].Value)
|
||||
|
||||
// 15) Retention message
|
||||
writeln(m["RetentionMessage"].Value)
|
||||
|
||||
// boldOff()
|
||||
|
||||
// finally feed & cut
|
||||
write([]byte{ESC, 'd', 7}) // Feed 5 lines
|
||||
write([]byte{GS, 'V', 1})
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func printLogo(path string) ([]byte, error) {
|
||||
const maxLogoWidth = 384
|
||||
f, err := os.Open(path)
|
||||
|
@ -2,6 +2,10 @@
|
||||
|
||||
builtVersion is a const in main.go
|
||||
|
||||
#### 1.0.0 - 30 Jun 2024
|
||||
added creditcall payment method
|
||||
`/starttransaction` - API payment endpoint to start a transaction
|
||||
|
||||
#### 0.9.1 - 22 May 2024
|
||||
added lockserver interface and implemented workflow for Omnitec
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user