205 lines
6.1 KiB
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()
|
|
}
|