Compare commits
1 Commits
2a3a5bee7b
...
61d9c03837
| Author | SHA1 | Date | |
|---|---|---|---|
| 61d9c03837 |
@ -34,7 +34,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
buildVersion = "1.3.2"
|
buildVersion = "1.3.0"
|
||||||
serviceName = "hardlink"
|
serviceName = "hardlink"
|
||||||
pollingFrequency = 8 * time.Second
|
pollingFrequency = 8 * time.Second
|
||||||
)
|
)
|
||||||
@ -206,8 +206,8 @@ func main() {
|
|||||||
// Only PayBridge and Dojo use POST /api/payment/sale.
|
// Only PayBridge and Dojo use POST /api/payment/sale.
|
||||||
if provider != nil {
|
if provider != nil {
|
||||||
app.SetPaymentService(paymentsvc.NewService(provider))
|
app.SetPaymentService(paymentsvc.NewService(provider))
|
||||||
log.Infof("Payment provider enabled for POST /api/payment/sale: %s", cmstypes.PaySystemNames[paymentProvider])
|
log.Infof("Payment provider enabled for POST /api/payment/sale: %s", cfg.PaymentProvider)
|
||||||
fmt.Printf("Payment provider enabled for POST /api/payment/sale: %s\n", cmstypes.PaySystemNames[paymentProvider])
|
fmt.Printf("Payment provider enabled for POST /api/payment/sale: %s\n", cfg.PaymentProvider)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Payment processing is disabled")
|
fmt.Println("Payment processing is disabled")
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import (
|
|||||||
|
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/paymentsvc"
|
"gitea.futuresens.co.uk/futuresens/hardlink/internal/paymentsvc"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/types"
|
"gitea.futuresens.co.uk/futuresens/hardlink/internal/types"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@ -89,9 +88,7 @@ func (c *Client) Sale(ctx context.Context, req paymentsvc.SaleRequest) (*payment
|
|||||||
if strings.ToLower(intent.Status) != types.ResultCaptured {
|
if strings.ToLower(intent.Status) != types.ResultCaptured {
|
||||||
return nil, fmt.Errorf("Dojo terminal session is %s but payment intent is %s", session.Status, intent.Status)
|
return nil, fmt.Errorf("Dojo terminal session is %s but payment intent is %s", session.Status, intent.Status)
|
||||||
}
|
}
|
||||||
result := c.mapCapturedResult(req, intent)
|
return c.mapCapturedResult(req, intent), nil
|
||||||
result.CustomerReceipt = receiptToText(session.Receipt)
|
|
||||||
return result, nil
|
|
||||||
|
|
||||||
case types.ResultCancelled, types.ResultCanceled, types.ResultDeclined, types.ResultExpired, types.ResultSignatureRejected:
|
case types.ResultCancelled, types.ResultCanceled, types.ResultDeclined, types.ResultExpired, types.ResultSignatureRejected:
|
||||||
return c.mapTerminalFailure(req, session), nil
|
return c.mapTerminalFailure(req, session), nil
|
||||||
@ -154,39 +151,20 @@ func (c *Client) waitForTerminalSession(ctx context.Context, terminalSessionID s
|
|||||||
ticker := time.NewTicker(time.Second)
|
ticker := time.NewTicker(time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
signatureRejected := false
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
session, err := c.getTerminalSession(ctx, terminalSessionID)
|
session, err := c.getTerminalSession(ctx, terminalSessionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("session status:", session.Status)
|
|
||||||
|
|
||||||
switch strings.ToLower(session.Status) {
|
switch strings.ToLower(session.Status) {
|
||||||
case types.ResultInitiateRequested, types.ResultInitiated, types.ResultAuthorized, types.ResultCancelRequested:
|
case types.ResultInitiateRequested, types.ResultInitiated, types.ResultAuthorized, types.ResultCancelRequested:
|
||||||
|
// Still processing. For this integration payment intents use captureMode Auto,
|
||||||
|
// so Authorized is expected to continue to Captured.
|
||||||
|
|
||||||
case types.ResultSignatureRequired:
|
case types.ResultCaptured, types.ResultCancelled, types.ResultCanceled, types.ResultDeclined, types.ResultExpired,
|
||||||
if !signatureRejected {
|
types.ResultSignatureRequired, types.ResultSignatureAccepted,
|
||||||
rejectedSession, err := c.rejectSignature(ctx, terminalSessionID)
|
types.ResultSignatureRejected:
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
signatureRejected = true
|
|
||||||
|
|
||||||
if rejectedSession != nil {
|
|
||||||
switch strings.ToLower(rejectedSession.Status) {
|
|
||||||
case types.ResultCaptured, types.ResultCancelled, types.ResultCanceled, types.ResultDeclined,
|
|
||||||
types.ResultExpired, types.ResultSignatureAccepted, types.ResultSignatureRejected:
|
|
||||||
return rejectedSession, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case types.ResultCaptured, types.ResultCancelled, types.ResultCanceled, types.ResultDeclined,
|
|
||||||
types.ResultExpired, types.ResultSignatureAccepted, types.ResultSignatureRejected:
|
|
||||||
return session, nil
|
return session, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -201,21 +179,6 @@ func (c *Client) waitForTerminalSession(ctx context.Context, terminalSessionID s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) rejectSignature(ctx context.Context, terminalSessionID string) (*terminalSessionResponse, error) {
|
|
||||||
payload := signatureVerificationRequest{
|
|
||||||
Accepted: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
var response terminalSessionResponse
|
|
||||||
path := "/terminal-sessions/" + url.PathEscape(terminalSessionID) + "/signature"
|
|
||||||
|
|
||||||
if err := c.doJSON(ctx, http.MethodPut, path, payload, true, &response); err != nil {
|
|
||||||
return nil, fmt.Errorf("reject Dojo signature verification: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) getTerminalSession(ctx context.Context, terminalSessionID string) (*terminalSessionResponse, error) {
|
func (c *Client) getTerminalSession(ctx context.Context, terminalSessionID string) (*terminalSessionResponse, error) {
|
||||||
var response terminalSessionResponse
|
var response terminalSessionResponse
|
||||||
path := "/terminal-sessions/" + url.PathEscape(terminalSessionID)
|
path := "/terminal-sessions/" + url.PathEscape(terminalSessionID)
|
||||||
@ -256,7 +219,6 @@ func (c *Client) mapTerminalFailure(req paymentsvc.SaleRequest, session *termina
|
|||||||
result := c.baseResult(req, session.PaymentDetails)
|
result := c.baseResult(req, session.PaymentDetails)
|
||||||
result.Status = strings.ToUpper(session.Status)
|
result.Status = strings.ToUpper(session.Status)
|
||||||
result.ErrorMessage = "Dojo terminal session ended with status " + session.Status
|
result.ErrorMessage = "Dojo terminal session ended with status " + session.Status
|
||||||
result.CustomerReceipt = receiptToText(session.Receipt)
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,10 +243,7 @@ func (c *Client) baseResult(req paymentsvc.SaleRequest, details *paymentDetails)
|
|||||||
result.CardNumber = details.Card.CardNumber
|
result.CardNumber = details.Card.CardNumber
|
||||||
result.CardType = details.Card.CardType
|
result.CardType = details.Card.CardType
|
||||||
result.ExpiryDate = details.Card.ExpiryDate
|
result.ExpiryDate = details.Card.ExpiryDate
|
||||||
result.LastFourDigits = details.Card.Last4PAN
|
result.LastFourDigits = details.Card.CardNumber
|
||||||
if result.LastFourDigits == "" {
|
|
||||||
result.LastFourDigits = lastFour(details.Card.CardNumber)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@ -353,25 +312,6 @@ func dojoReference(req paymentsvc.SaleRequest) string {
|
|||||||
return string(runes)
|
return string(runes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func receiptToText(receipt *receipt) string {
|
|
||||||
if receipt == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var builder strings.Builder
|
|
||||||
|
|
||||||
for _, line := range receipt.Lines {
|
|
||||||
if line.LineType != "Text" || line.Text == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.WriteString(normalizeReceiptText(line.Text.Value))
|
|
||||||
builder.WriteByte('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func lastFour(cardNumber string) string {
|
func lastFour(cardNumber string) string {
|
||||||
digits := make([]byte, 0, len(cardNumber))
|
digits := make([]byte, 0, len(cardNumber))
|
||||||
for i := 0; i < len(cardNumber); i++ {
|
for i := 0; i < len(cardNumber); i++ {
|
||||||
@ -384,13 +324,3 @@ func lastFour(cardNumber string) string {
|
|||||||
}
|
}
|
||||||
return string(digits[len(digits)-4:])
|
return string(digits[len(digits)-4:])
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeReceiptText(value string) string {
|
|
||||||
replacer := strings.NewReplacer(
|
|
||||||
"£", "GBP ",
|
|
||||||
"€", "EUR ",
|
|
||||||
"$", "USD ",
|
|
||||||
)
|
|
||||||
|
|
||||||
return replacer.Replace(value)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -25,32 +25,11 @@ type terminalSessionSale struct {
|
|||||||
PaymentIntentID string `json:"paymentIntentId"`
|
PaymentIntentID string `json:"paymentIntentId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type signatureVerificationRequest struct {
|
|
||||||
Accepted bool `json:"accepted"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type terminalSessionResponse struct {
|
type terminalSessionResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
TerminalID string `json:"terminalId"`
|
TerminalID string `json:"terminalId"`
|
||||||
PaymentDetails *paymentDetails `json:"paymentDetails"`
|
PaymentDetails *paymentDetails `json:"paymentDetails"`
|
||||||
Receipt *receipt `json:"receipt,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type receipt struct {
|
|
||||||
Lines []receiptLine `json:"lines"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type receiptLine struct {
|
|
||||||
LineType string `json:"lineType"`
|
|
||||||
Text *receiptText `json:"text,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type receiptText struct {
|
|
||||||
Align string `json:"align"`
|
|
||||||
EmphasisBold bool `json:"emphasisBold"`
|
|
||||||
Size string `json:"size"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type paymentIntentResponse struct {
|
type paymentIntentResponse struct {
|
||||||
@ -75,7 +54,6 @@ type dojoCard struct {
|
|||||||
ExpiryDate string `json:"expiryDate"`
|
ExpiryDate string `json:"expiryDate"`
|
||||||
CardType string `json:"cardType"`
|
CardType string `json:"cardType"`
|
||||||
CardFundingType string `json:"cardFundingType"`
|
CardFundingType string `json:"cardFundingType"`
|
||||||
Last4PAN string `json:"last4PAN"`
|
|
||||||
EntryMode string `json:"entryMode"`
|
EntryMode string `json:"entryMode"`
|
||||||
VerificationMethod string `json:"verificationMethod"`
|
VerificationMethod string `json:"verificationMethod"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -403,7 +403,7 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) {
|
|||||||
app.SetCardWellStatus(status)
|
app.SetCardWellStatus(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
// doorReq.RoomField = "104"
|
doorReq.RoomField = "104"
|
||||||
// build lock server command
|
// build lock server command
|
||||||
app.lockserver.BuildCommand(doorReq, checkIn, checkOut)
|
app.lockserver.BuildCommand(doorReq, checkIn, checkOut)
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import (
|
|||||||
"gitea.futuresens.co.uk/futuresens/cmstypes"
|
"gitea.futuresens.co.uk/futuresens/cmstypes"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/mail"
|
"gitea.futuresens.co.uk/futuresens/hardlink/internal/mail"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/paymentsvc"
|
"gitea.futuresens.co.uk/futuresens/hardlink/internal/paymentsvc"
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/printer"
|
|
||||||
"gitea.futuresens.co.uk/futuresens/hardlink/internal/types"
|
"gitea.futuresens.co.uk/futuresens/hardlink/internal/types"
|
||||||
"gitea.futuresens.co.uk/futuresens/logging"
|
"gitea.futuresens.co.uk/futuresens/logging"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -127,8 +126,6 @@ func (app *App) salePayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
response.Status.Code = http.StatusOK
|
response.Status.Code = http.StatusOK
|
||||||
|
|
||||||
if result.Success && strings.EqualFold(result.Status, "APPROVED") {
|
if result.Success && strings.EqualFold(result.Status, "APPROVED") {
|
||||||
printer.PrintSaleReceipt(result.CustomerReceipt)
|
|
||||||
response.Status.Code = http.StatusOK
|
|
||||||
response.Status.Message = result.Message
|
response.Status.Message = result.Message
|
||||||
response.Data = buildPaymentSuccessURL(result)
|
response.Data = buildPaymentSuccessURL(result)
|
||||||
writeTransactionResult(w, http.StatusOK, response)
|
writeTransactionResult(w, http.StatusOK, response)
|
||||||
@ -143,8 +140,6 @@ func (app *App) salePayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
description = result.Status
|
description = result.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Transaction failed: %s", description)
|
|
||||||
printer.PrintSaleReceipt(result.CustomerReceipt)
|
|
||||||
response.Status.Message = "Payment unsuccessful"
|
response.Status.Message = "Payment unsuccessful"
|
||||||
response.Data = buildPaymentFailureURL(types.ResultError, description)
|
response.Data = buildPaymentFailureURL(types.ResultError, description)
|
||||||
writeTransactionResult(w, http.StatusOK, response)
|
writeTransactionResult(w, http.StatusOK, response)
|
||||||
|
|||||||
@ -185,23 +185,6 @@ func PrintReceipt(receipt string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func PrintSaleReceipt(receipt string) {
|
|
||||||
if len(receipt) == 0 {
|
|
||||||
log.Warn("Empty sale receipt, skipping print")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var buf bytes.Buffer
|
|
||||||
buf.WriteString(receipt)
|
|
||||||
buf.WriteByte('\n')
|
|
||||||
buf.Write([]byte{ESC, 'd', 7})
|
|
||||||
buf.Write([]byte{GS, 'V', 1})
|
|
||||||
|
|
||||||
|
|
||||||
if err := SendToPrinter(buf.Bytes()); err != nil {
|
|
||||||
log.Errorf("SendToPrinter error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printCardholderReceipt(cardholderReceipt string) error {
|
func printCardholderReceipt(cardholderReceipt string) error {
|
||||||
receiptEntries, err := ParseCardholderReceipt([]byte(cardholderReceipt))
|
receiptEntries, err := ParseCardholderReceipt([]byte(cardholderReceipt))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -2,12 +2,6 @@
|
|||||||
|
|
||||||
builtVersion is a const in main.go
|
builtVersion is a const in main.go
|
||||||
|
|
||||||
#### 1.3.2 - 20 July 2026
|
|
||||||
make dojo decline payment in case of signature required
|
|
||||||
|
|
||||||
#### 1.3.1 - 17 July 2026
|
|
||||||
added receipt printing functionality to the Dojo and Paybridge payment flow
|
|
||||||
|
|
||||||
#### 1.3.0 - 03 July 2026
|
#### 1.3.0 - 03 July 2026
|
||||||
added pluggable PayBridge and Dojo payment flow with CMS credentials
|
added pluggable PayBridge and Dojo payment flow with CMS credentials
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user