hardlink/payment/preauthReleaser.go
2025-12-22 18:11:05 +00:00

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)
}