hardlink/internal/handlers/creditcall_sale.go

131 lines
4.9 KiB
Go

package handlers
import (
"net/http"
"strings"
"time"
"gitea.futuresens.co.uk/futuresens/cmstypes"
"gitea.futuresens.co.uk/futuresens/hardlink/internal/creditcall"
"gitea.futuresens.co.uk/futuresens/hardlink/internal/mail"
"gitea.futuresens.co.uk/futuresens/hardlink/internal/printer"
"gitea.futuresens.co.uk/futuresens/hardlink/internal/types"
"gitea.futuresens.co.uk/futuresens/logging"
log "github.com/sirupsen/logrus"
)
// creditCallSaleOutcome is the transaction outcome before either wire format is applied.
type creditCallSaleOutcome struct {
HTTPStatus int
Status cmstypes.StatusRec
Payment creditcall.PaymentResult
Approved bool
FailureType string
FailureDescription string
}
func (app *App) creditCallClient() *http.Client {
// Each start/confirm call gets the original independent timeout. In particular,
// neither a kiosk disconnect nor a whole-sale deadline bounds confirmation.
return &http.Client{Timeout: 300 * time.Second, Transport: app.creditCallTransport}
}
func (app *App) printCreditCallReceipt(receipt string) {
if app.creditCallReceipt != nil {
app.creditCallReceipt(receipt)
return
}
printer.PrintReceipt(receipt)
}
func (app *App) executeCreditCallSale(
request cmstypes.TransactionRec,
start func(*http.Client) (creditcall.TransactionResultXML, error),
) creditCallSaleOutcome {
const op = logging.Op("takePayment")
outcome := creditCallSaleOutcome{
HTTPStatus: http.StatusBadGateway,
Status: cmstypes.StatusRec{Code: http.StatusInternalServerError, Message: "500 Internal server error"},
}
client := app.creditCallClient()
trResult, err := start(client)
if err != nil {
logging.Error(types.ServiceName, err.Error(), "Start transaction error", string(op), "", "", 0)
outcome.FailureType = types.ResultError
outcome.FailureDescription = "No response from payment processor"
return outcome
}
var result creditcall.PaymentResult
result.FillFromTransactionResult(trResult)
res := result.Fields[types.TransactionResult]
if !strings.EqualFold(res, types.ResultApproved) {
app.printCreditCallReceipt(result.CardholderReceipt)
desc := result.Fields[types.ErrorDescription]
if desc == "" {
desc = result.Fields[types.Errors]
}
logging.Error(types.ServiceName, "Preauthorization failed", "Result: "+res+" Description: "+desc, string(op), "", app.cfg.Hotel, app.cfg.Kiosk)
outcome.HTTPStatus = http.StatusOK
outcome.Status = result.Status
outcome.Payment = result
outcome.FailureType = res
outcome.FailureDescription = result.Fields[types.Errors]
return outcome
}
ref := result.Fields[types.Reference]
log.Printf("Preauth approved, reference: %s. Sending confirm...", ref)
body, err := confirmWithRetry(client, creditcall.ConfirmTransactionRequest{
Amount: request.AmountMinorUnits, Reference: ref,
}, 2)
if err != nil {
logging.Error(types.ServiceName, err.Error(), "Confirm transaction error", string(op), "", "", 0)
mail.SendEmailOnError(app.cfg.Hotel, app.cfg.Kiosk, "Payment confirmation failed", "Reference: "+ref+", Error: "+err.Error())
outcome.FailureType = types.ResultError
outcome.FailureDescription = "ConfirmTransactionError"
return outcome
}
// Intentionally reuse both records: XML appends confirmation entries, and
// missing confirmation fields/receipts retain their existing legacy behavior.
if err := trResult.ParseTransactionResult(body); err != nil {
logging.Error(types.ServiceName, err.Error(), "Parse confirm result error", string(op), "", "", 0)
}
result.FillFromTransactionResult(trResult)
res = result.Fields[types.TransactionResult]
if !strings.EqualFold(res, types.ResultApproved) {
app.printCreditCallReceipt(result.CardholderReceipt)
desc := result.Fields[types.ErrorDescription]
if desc == "" {
desc = result.Fields[types.Errors]
}
logging.Error(types.ServiceName, "Transaction not approved after confirm", "Confirm result: "+res+" Description: "+desc, string(op), "", app.cfg.Hotel, app.cfg.Kiosk)
mail.SendEmailOnError(app.cfg.Hotel, app.cfg.Kiosk, "Payment confirmation failed", "Reference: "+ref+", Confirm result: "+res+" Description: "+desc)
outcome.HTTPStatus = http.StatusOK
outcome.Status = result.Status
outcome.Payment = result
outcome.FailureType = res
outcome.FailureDescription = result.Fields[types.Errors]
return outcome
}
app.printCreditCallReceipt(result.CardholderReceipt)
log.Printf("Transaction approved and confirmed, reference: %s", ref)
outcome.HTTPStatus = http.StatusOK
outcome.Status = result.Status
outcome.Payment = result
outcome.Approved = true
return outcome
}
func (outcome creditCallSaleOutcome) legacyResponse() cmstypes.ResponseRec {
response := cmstypes.ResponseRec{Status: outcome.Status}
if outcome.Approved {
response.Data = creditcall.BuildSuccessURL(outcome.Payment.Fields)
} else {
response.Data = creditcall.BuildFailureURL(outcome.FailureType, outcome.FailureDescription)
}
return response
}