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