Compare commits

..

1 Commits

7 changed files with 11 additions and 131 deletions

View File

@ -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")

View File

@ -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)
}

View File

@ -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"`
} }

View File

@ -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)

View File

@ -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)

View File

@ -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 {

View File

@ -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