hardlink/internal/handlers/payment_handlers.go

205 lines
6.1 KiB
Go

package handlers
import (
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"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/types"
"gitea.futuresens.co.uk/futuresens/logging"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
)
type SalePaymentRequest struct {
Reference string `json:"reference,omitempty"`
ConfirmNo string `json:"confirmNo,omitempty"`
Amount int64 `json:"amount"`
Currency string `json:"currency,omitempty"`
}
func (app *App) salePayment(w http.ResponseWriter, r *http.Request) {
const op = logging.Op("salePayment")
var response = cmstypes.ResponseRec{
Status: cmstypes.StatusRec{
Code: http.StatusInternalServerError,
Message: http.StatusText(http.StatusInternalServerError),
},
}
setPaymentCORS(w)
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
if r.Method != http.MethodPost {
response.Data = buildPaymentFailureURL(types.ResultError, "Method not allowed; use POST")
writeTransactionResult(w, http.StatusMethodNotAllowed, response)
return
}
if app.paymentService == nil {
mail.SendEmailOnError(app.cfg.Hotel, app.cfg.Kiosk, "Payment Service Not Configured", "Payment service is not configured; cannot process payment requests")
response.Data = buildPaymentFailureURL(types.ResultError, "Payment service is not configured")
writeTransactionResult(w, http.StatusInternalServerError, response)
return
}
if ct := r.Header.Get("Content-Type"); ct != "" && !strings.Contains(ct, "application/json") {
response.Data = buildPaymentFailureURL(types.ResultError, "Content-Type must be application/json")
writeTransactionResult(w, http.StatusUnsupportedMediaType, response)
return
}
defer r.Body.Close()
var req SalePaymentRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
logging.Error(types.ServiceName, err.Error(), "ReadJSON", string(op), "", app.cfg.Hotel, app.cfg.Kiosk)
response.Data = buildPaymentFailureURL(types.ResultError, "invalid JSON payload: "+err.Error())
writeTransactionResult(w, http.StatusBadRequest, response)
return
}
if req.Amount <= 0 {
response.Data = buildPaymentFailureURL(types.ResultError, "Amount must be greater than zero")
writeTransactionResult(w, http.StatusBadRequest, response)
return
}
if req.Currency == "" {
req.Currency = "GBP"
}
if req.Reference == "" {
req.Reference = req.ConfirmNo
}
if req.Reference == "" {
req.Reference = uuid.NewString()
}
requestID := buildPaymentRequestID(req.Reference)
timeoutSeconds := app.cfg.TimeoutSeconds
if timeoutSeconds <= 0 {
timeoutSeconds = 300
}
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(timeoutSeconds)*time.Second)
defer cancel()
result, err := app.paymentService.Sale(ctx, paymentsvc.SaleRequest{
RequestID: requestID,
Reference: req.Reference,
Amount: req.Amount,
Currency: req.Currency,
})
if err != nil {
status := http.StatusBadGateway
if errors.Is(err, paymentsvc.ErrPaymentInProgress) {
status = http.StatusConflict
}
logging.Error(types.ServiceName, err.Error(), "Payment provider error", string(op), req.Reference, app.cfg.Hotel, app.cfg.Kiosk)
response.Status.Code = status
response.Status.Message = http.StatusText(status)
response.Data = buildPaymentFailureURL(types.ResultError, err.Error())
writeTransactionResult(w, status, response)
return
}
if result == nil {
response.Status.Code = http.StatusBadGateway
response.Status.Message = "Empty payment result"
response.Data = buildPaymentFailureURL(types.ResultError, "Payment provider returned an empty result")
writeTransactionResult(w, http.StatusBadGateway, response)
return
}
response.Status.Code = http.StatusOK
if result.Success && strings.EqualFold(result.Status, "APPROVED") {
response.Status.Message = result.Message
response.Data = buildPaymentSuccessURL(result)
writeTransactionResult(w, http.StatusOK, response)
return
}
description := result.ErrorMessage
if description == "" {
description = result.Message
}
if description == "" {
description = result.Status
}
response.Status.Message = "Payment unsuccessful"
response.Data = buildPaymentFailureURL(types.ResultError, description)
writeTransactionResult(w, http.StatusOK, response)
}
func buildPaymentRequestID(reference string) string {
const prefix = "REQ_"
const maxLength = 60
suffix := fmt.Sprintf("_%d", time.Now().UnixMilli())
maxReferenceLength := maxLength - len(prefix) - len(suffix)
runes := []rune(reference)
if len(runes) > maxReferenceLength {
runes = runes[:maxReferenceLength]
}
return prefix + string(runes) + suffix
}
func setPaymentCORS(w http.ResponseWriter) {
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")
}
func buildPaymentSuccessURL(result *paymentsvc.Result) string {
txnReference := result.ReferenceNumber
if txnReference == "" {
txnReference = result.TransactionID
}
q := url.Values{}
q.Set("CardNumber", hex.EncodeToString([]byte(result.CardNumber)))
q.Set("CardType", hex.EncodeToString([]byte(result.CardType)))
q.Set("ExpiryDate", hex.EncodeToString([]byte(result.ExpiryDate)))
q.Set("TxnReference", txnReference)
q.Set("CardHash", hex.EncodeToString([]byte(result.CardHash)))
q.Set("CardReference", hex.EncodeToString([]byte(result.CardReference)))
return (&url.URL{
Path: types.CheckinSuccessfulEndpoint,
RawQuery: q.Encode(),
}).String()
}
func buildPaymentFailureURL(msgType, description string) string {
log.WithFields(log.Fields{
types.LogFieldError: msgType,
types.LogFieldDescription: description,
}).Error("Transaction failed")
q := url.Values{}
q.Set("MsgType", msgType)
q.Set("Description", description)
return (&url.URL{
Path: types.CheckinUnsuccessfulEndpoint,
RawQuery: q.Encode(),
}).String()
}