diff --git a/cmd/hardlink/main.go b/cmd/hardlink/main.go index da77452..2ce023e 100644 --- a/cmd/hardlink/main.go +++ b/cmd/hardlink/main.go @@ -34,7 +34,7 @@ import ( ) const ( - buildVersion = "1.3.0" + buildVersion = "1.3.1" serviceName = "hardlink" pollingFrequency = 8 * time.Second ) @@ -206,8 +206,8 @@ func main() { // 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", cfg.PaymentProvider) - fmt.Printf("Payment provider enabled for POST /api/payment/sale: %s\n", cfg.PaymentProvider) + 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") diff --git a/internal/dojo/client.go b/internal/dojo/client.go index 5a479c8..4609bf7 100644 --- a/internal/dojo/client.go +++ b/internal/dojo/client.go @@ -13,6 +13,7 @@ import ( "gitea.futuresens.co.uk/futuresens/hardlink/internal/paymentsvc" "gitea.futuresens.co.uk/futuresens/hardlink/internal/types" + log "github.com/sirupsen/logrus" ) type Config struct { @@ -88,7 +89,9 @@ func (c *Client) Sale(ctx context.Context, req paymentsvc.SaleRequest) (*payment 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 c.mapCapturedResult(req, intent), nil + result := c.mapCapturedResult(req, intent) + result.CustomerReceipt = receiptToText(session.Receipt) + return result, nil case types.ResultCancelled, types.ResultCanceled, types.ResultDeclined, types.ResultExpired, types.ResultSignatureRejected: return c.mapTerminalFailure(req, session), nil @@ -157,14 +160,16 @@ func (c *Client) waitForTerminalSession(ctx context.Context, terminalSessionID s return nil, err } + log.Println("session status:", 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, types.ResultSignatureRequired: // Still processing. For this integration payment intents use captureMode Auto, // so Authorized is expected to continue to Captured. case types.ResultCaptured, types.ResultCancelled, types.ResultCanceled, types.ResultDeclined, types.ResultExpired, - types.ResultSignatureRequired, types.ResultSignatureAccepted, - types.ResultSignatureRejected: + types.ResultSignatureAccepted, types.ResultSignatureRejected /*, types.ResultSignatureRequired*/ : return session, nil default: @@ -219,6 +224,7 @@ func (c *Client) mapTerminalFailure(req paymentsvc.SaleRequest, session *termina result := c.baseResult(req, session.PaymentDetails) result.Status = strings.ToUpper(session.Status) result.ErrorMessage = "Dojo terminal session ended with status " + session.Status + result.CustomerReceipt = receiptToText(session.Receipt) return result } @@ -312,6 +318,25 @@ func dojoReference(req paymentsvc.SaleRequest) string { 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 { digits := make([]byte, 0, len(cardNumber)) for i := 0; i < len(cardNumber); i++ { @@ -324,3 +349,13 @@ func lastFour(cardNumber string) string { } return string(digits[len(digits)-4:]) } + +func normalizeReceiptText(value string) string { + replacer := strings.NewReplacer( + "£", "GBP ", + "€", "EUR ", + "$", "USD ", + ) + + return replacer.Replace(value) +} diff --git a/internal/dojo/types.go b/internal/dojo/types.go index 1836c6d..ab08f32 100644 --- a/internal/dojo/types.go +++ b/internal/dojo/types.go @@ -30,6 +30,23 @@ type terminalSessionResponse struct { Status string `json:"status"` TerminalID string `json:"terminalId"` 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 { @@ -54,6 +71,7 @@ type dojoCard struct { ExpiryDate string `json:"expiryDate"` CardType string `json:"cardType"` CardFundingType string `json:"cardFundingType"` + Last4PAN string `json:"last4PAN"` EntryMode string `json:"entryMode"` VerificationMethod string `json:"verificationMethod"` } diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index eec77f5..2804031 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -403,7 +403,7 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) { app.SetCardWellStatus(status) } - doorReq.RoomField = "104" + // doorReq.RoomField = "104" // build lock server command app.lockserver.BuildCommand(doorReq, checkIn, checkOut) diff --git a/internal/handlers/payment_handlers.go b/internal/handlers/payment_handlers.go index 7587756..caa92c2 100644 --- a/internal/handlers/payment_handlers.go +++ b/internal/handlers/payment_handlers.go @@ -14,6 +14,7 @@ import ( "gitea.futuresens.co.uk/futuresens/cmstypes" "gitea.futuresens.co.uk/futuresens/hardlink/internal/mail" "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/logging" "github.com/google/uuid" @@ -126,6 +127,8 @@ func (app *App) salePayment(w http.ResponseWriter, r *http.Request) { response.Status.Code = http.StatusOK if result.Success && strings.EqualFold(result.Status, "APPROVED") { + printer.PrintSaleReceipt(result.CustomerReceipt) + response.Status.Code = http.StatusOK response.Status.Message = result.Message response.Data = buildPaymentSuccessURL(result) writeTransactionResult(w, http.StatusOK, response) @@ -140,6 +143,8 @@ func (app *App) salePayment(w http.ResponseWriter, r *http.Request) { description = result.Status } + log.Printf("Transaction failed: %s", description) + printer.PrintSaleReceipt(result.CustomerReceipt) response.Status.Message = "Payment unsuccessful" response.Data = buildPaymentFailureURL(types.ResultError, description) writeTransactionResult(w, http.StatusOK, response) diff --git a/internal/printer/printer.go b/internal/printer/printer.go index 39ed3bc..ad0d32c 100644 --- a/internal/printer/printer.go +++ b/internal/printer/printer.go @@ -185,6 +185,23 @@ 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 { receiptEntries, err := ParseCardholderReceipt([]byte(cardholderReceipt)) if err != nil { diff --git a/release notes.md b/release notes.md index b7f2d61..4435509 100644 --- a/release notes.md +++ b/release notes.md @@ -2,6 +2,9 @@ builtVersion is a const in main.go +#### 1.3.1 - 17 July 2026 +added receipt printing functionality to the Dojo and Paybridge payment flow + #### 1.3.0 - 03 July 2026 added pluggable PayBridge and Dojo payment flow with CMS credentials