238 lines
5.1 KiB
Go
238 lines
5.1 KiB
Go
package payment
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.futuresens.co.uk/futuresens/hardlink/db"
|
|
"gitea.futuresens.co.uk/futuresens/hardlink/types"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
httpTimeout = 120 * time.Second
|
|
)
|
|
|
|
/* ==============================
|
|
Public Entry Point (LEGACY)
|
|
============================== */
|
|
|
|
func ReleasePreauthorizations(database *sql.DB) error {
|
|
ctx := context.Background()
|
|
now := time.Now().UTC()
|
|
client := &http.Client{Timeout: httpTimeout}
|
|
|
|
preauths, err := fetchDuePreauths(ctx, database, now)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(preauths) == 0 {
|
|
log.Info("No preauthorizations due for release")
|
|
return nil
|
|
} else {
|
|
log.Infof("%d preauthorizations due for release", len(preauths))
|
|
}
|
|
|
|
var failed []string
|
|
var completed int
|
|
for _, p := range preauths {
|
|
if err := handlePreauthRelease(ctx, database, client, p, now); err != nil {
|
|
log.Errorf("Preauth %s failed: %v", p.TxnReference, err)
|
|
failed = append(failed, p.TxnReference)
|
|
}
|
|
completed++
|
|
}
|
|
log.Infof("Preauth release completed: %d processed, %d failed", completed, len(failed))
|
|
|
|
if len(failed) > 0 {
|
|
return fmt.Errorf("preauth release incomplete, failed refs: %s", strings.Join(failed, ", "))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/* ==============================
|
|
Core Business Logic
|
|
============================== */
|
|
|
|
func handlePreauthRelease(
|
|
ctx context.Context,
|
|
dbConn *sql.DB,
|
|
client *http.Client,
|
|
preauth types.PreauthRec,
|
|
now time.Time,
|
|
) error {
|
|
|
|
ref := preauth.TxnReference
|
|
log.Infof("Evaluating preauth %s", ref)
|
|
|
|
info, err := fetchTransactionInfo(ctx, client, ref)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If already voided or declined → mark released
|
|
if isAlreadyReleased(info) {
|
|
return markReleased(ctx, dbConn, ref, now)
|
|
}
|
|
|
|
// Only void approved + uncommitted
|
|
if !isVoidable(info) {
|
|
log.Infof("Preauth %s not eligible for void (res=%s state=%s)",
|
|
ref, info.transactionRes, info.transactionState)
|
|
return nil
|
|
}
|
|
|
|
// Void transaction
|
|
if err := voidPreauth(ctx, client, ref); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify final state
|
|
finalInfo, err := fetchTransactionInfo(ctx, client, ref)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !isSuccessfullyVoided(finalInfo) {
|
|
return fmt.Errorf("unexpected final state res=%s state=%s",
|
|
finalInfo.transactionRes, finalInfo.transactionState)
|
|
}
|
|
|
|
log.Infof("Preauth %s successfully voided", ref)
|
|
return markReleased(ctx, dbConn, ref, now)
|
|
}
|
|
|
|
/* ==============================
|
|
State Evaluation Helpers
|
|
============================== */
|
|
|
|
func isVoidable(info TransactionInfo) bool {
|
|
return strings.EqualFold(info.transactionRes, types.ResultApproved) &&
|
|
strings.EqualFold(info.transactionState, types.ResultStateUncommitted)
|
|
}
|
|
|
|
func isAlreadyReleased(info TransactionInfo) bool {
|
|
return strings.EqualFold(info.transactionState, types.ResultStateVoided) ||
|
|
strings.EqualFold(info.transactionRes, types.ResultDeclined)
|
|
}
|
|
|
|
func isSuccessfullyVoided(info TransactionInfo) bool {
|
|
return strings.EqualFold(info.transactionRes, types.ResultDeclined) &&
|
|
strings.EqualFold(info.transactionState, types.ResultStateVoided)
|
|
}
|
|
|
|
/* ==============================
|
|
External Operations
|
|
============================== */
|
|
|
|
func fetchDuePreauths(
|
|
ctx context.Context,
|
|
dbConn *sql.DB,
|
|
now time.Time,
|
|
) ([]types.PreauthRec, error) {
|
|
|
|
return db.GetDuePreauths(ctx, dbConn, now, 0)
|
|
}
|
|
|
|
func markReleased(
|
|
ctx context.Context,
|
|
dbConn *sql.DB,
|
|
ref string,
|
|
now time.Time,
|
|
) error {
|
|
|
|
return db.MarkPreauthReleased(ctx, dbConn, ref, now)
|
|
}
|
|
|
|
func fetchTransactionInfo(
|
|
ctx context.Context,
|
|
client *http.Client,
|
|
ref string,
|
|
) (TransactionInfo, error) {
|
|
|
|
var tr TransactionResultXML
|
|
var info TransactionInfo
|
|
|
|
payload, _ := xml.Marshal(types.TransactionReferenceRequest{
|
|
TransactionReference: ref,
|
|
})
|
|
|
|
body, err := postXML(ctx, client, types.LinkTransactionInformation, payload)
|
|
if err != nil {
|
|
return info, err
|
|
}
|
|
|
|
if err := tr.ParseTransactionResult(body); err != nil {
|
|
return info, err
|
|
}
|
|
|
|
info.FillFromTransactionResult(tr)
|
|
return info, nil
|
|
}
|
|
|
|
func voidPreauth(
|
|
ctx context.Context,
|
|
client *http.Client,
|
|
ref string,
|
|
) error {
|
|
|
|
var tr TransactionResultXML
|
|
var info TransactionInfo
|
|
|
|
payload, _ := xml.Marshal(types.TransactionReferenceRequest{
|
|
TransactionReference: ref,
|
|
})
|
|
|
|
body, err := postXML(ctx, client, types.LinkVoidTransaction, payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := tr.ParseTransactionResult(body); err != nil {
|
|
return err
|
|
}
|
|
|
|
info.FillFromTransactionResult(tr)
|
|
|
|
if !strings.EqualFold(info.transactionRes, types.ResultApproved) {
|
|
return fmt.Errorf("void rejected: %s", info.transactionRes)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/* ==============================
|
|
Low-level HTTP
|
|
============================== */
|
|
|
|
func postXML(
|
|
ctx context.Context,
|
|
client *http.Client,
|
|
url string,
|
|
payload []byte,
|
|
) ([]byte, error) {
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "text/xml")
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
return io.ReadAll(resp.Body)
|
|
}
|