201 lines
6.1 KiB
Go
201 lines
6.1 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
mssqldb "github.com/denisenkom/go-mssqldb" // for error inspection
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"gitea.futuresens.co.uk/futuresens/hardlink/types"
|
|
)
|
|
|
|
// InitMSSQL opens and pings the SQL Server instance (keeps your original behaviour)
|
|
func InitMSSQL(port int, user, password, database string) (*sql.DB, error) {
|
|
if port <= 0 || user == "" || database == "" {
|
|
return nil, errors.New("incomplete database configuration")
|
|
}
|
|
|
|
dsn := fmt.Sprintf(
|
|
"sqlserver://%s:%s@%s:%d?database=%s&encrypt=disable",
|
|
user, password, "localhost", port, database,
|
|
)
|
|
|
|
db, err := sql.Open("sqlserver", dsn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
if err := db.PingContext(ctx); err != nil {
|
|
db.Close()
|
|
return nil, err
|
|
}
|
|
|
|
if err := db.QueryRowContext(ctx, "SELECT 1").Scan(new(int)); err != nil {
|
|
db.Close()
|
|
return nil, err
|
|
}
|
|
log.Info("Database connection established")
|
|
return db, nil
|
|
}
|
|
|
|
func parseDateOnly(s string) (time.Time, error) {
|
|
parsed, err := time.Parse(types.CustomLayout, s)
|
|
if err == nil {
|
|
// construct midnight in local timezone, then convert to UTC for storage consistency
|
|
localMidnight := time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 0, 0, 0, 0, time.Local)
|
|
return localMidnight.UTC(), nil
|
|
}
|
|
return time.Time{}, fmt.Errorf("parseDateOnly error: parsing %q: %w", s, err)
|
|
}
|
|
|
|
// InsertPreauth stores a preauthorization record. It is idempotent: duplicate TxnReference is ignored.
|
|
func InsertPreauth(ctx context.Context, db *sql.DB, m map[string]string, checkoutDate string) error {
|
|
const funcName = "InsertPreauth"
|
|
|
|
totalMinorUnits := m[types.TotalAmount]
|
|
txnReference := m[types.Reference]
|
|
if txnReference == "" {
|
|
return fmt.Errorf("%s: missing REFERENCE", funcName)
|
|
}
|
|
if totalMinorUnits == "" {
|
|
return fmt.Errorf("%s: missing TotalAmount", funcName)
|
|
}
|
|
|
|
// parse minor units, fallback to 0 on parse error but report it
|
|
amountInt, err := strconv.ParseInt(totalMinorUnits, 10, 64)
|
|
if err != nil {
|
|
log.WithFields(log.Fields{
|
|
"func": funcName,
|
|
"value": totalMinorUnits,
|
|
"error": err,
|
|
}).Warnf("parsing TotalAmount, defaulting to 0")
|
|
amountInt = 0
|
|
}
|
|
totalAmount := float64(amountInt) / 100.0
|
|
|
|
txnTime := time.Now().UTC()
|
|
|
|
// parse departure / checkout date and compute release date (48h after departure)
|
|
checkOutDate, err := parseDateOnly(checkoutDate)
|
|
if err != nil {
|
|
return fmt.Errorf("InsertPreauth: parsing checkoutDate %q: %w", checkoutDate, err)
|
|
}
|
|
releaseDate := checkOutDate.Add(48 * time.Hour)
|
|
|
|
const stmt = `
|
|
INSERT INTO dbo.Preauthorizations
|
|
(TxnReference, TotalMinorUnits, TotalAmount, TxnDateTime, DepartureDate, ReleaseDate)
|
|
VALUES
|
|
(@TxnReference, @TotalMinorUnits, @TotalAmount, @TxnDateTime, @DepartureDate, @ReleaseDate);
|
|
`
|
|
_, err = db.ExecContext(ctx, stmt,
|
|
sql.Named("TxnReference", txnReference),
|
|
sql.Named("TotalMinorUnits", totalMinorUnits),
|
|
sql.Named("TotalAmount", totalAmount),
|
|
sql.Named("TxnDateTime", txnTime),
|
|
sql.Named("DepartureDate", checkOutDate),
|
|
sql.Named("ReleaseDate", releaseDate),
|
|
)
|
|
if err != nil {
|
|
// handle duplicate-key (unique constraint) gracefully: SQL Server error numbers 2601/2627
|
|
var sqlErr mssqldb.Error
|
|
if errors.As(err, &sqlErr) {
|
|
if sqlErr.Number == 2627 || sqlErr.Number == 2601 {
|
|
log.Infof("InsertPreauth: preauth %s already exists (duplicate key) - ignoring", txnReference)
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("InsertPreauth exec: %w", err)
|
|
}
|
|
|
|
log.Infof("Inserted preauth %s amount=%s minorUnits release=%s", txnReference, totalMinorUnits, releaseDate.Format(time.RFC3339))
|
|
return nil
|
|
}
|
|
|
|
// GetDuePreauths returns preauths with ReleaseDate <= now where Released = 0.
|
|
// If limit > 0, the query uses TOP(limit) to bound results at DB level.
|
|
func GetDuePreauths(ctx context.Context, db *sql.DB, now time.Time, limit int) ([]types.PreauthRec, error) {
|
|
baseQuery := `
|
|
SELECT Id, TxnReference, TotalMinorUnits, TotalAmount, TxnDateTime, DepartureDate, ReleaseDate, Released, ReleasedAt
|
|
FROM dbo.Preauthorizations
|
|
WHERE Released = 0 AND ReleaseDate <= @Now
|
|
ORDER BY ReleaseDate ASC
|
|
`
|
|
query := baseQuery
|
|
if limit > 0 {
|
|
// embed TOP to keep DB from returning everything; limit is controlled by the caller.
|
|
query = strings.Replace(baseQuery, "SELECT", fmt.Sprintf("SELECT TOP (%d)", limit), 1)
|
|
}
|
|
|
|
rows, err := db.QueryContext(ctx, query, sql.Named("Now", now))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GetDuePreauths query: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []types.PreauthRec
|
|
for rows.Next() {
|
|
var r types.PreauthRec
|
|
var departure sql.NullTime
|
|
var releasedAt sql.NullTime
|
|
// Note: adjust scanning targets if types.PreauthRec fields differ
|
|
if err := rows.Scan(
|
|
&r.Id,
|
|
&r.TxnReference,
|
|
&r.TotalMinorUnits,
|
|
&r.TotalAmount,
|
|
&r.TxnDateTime,
|
|
&departure,
|
|
&r.ReleaseDate,
|
|
&r.Released,
|
|
&releasedAt,
|
|
); err != nil {
|
|
return nil, fmt.Errorf("GetDuePreauths scan: %w", err)
|
|
}
|
|
|
|
if departure.Valid {
|
|
r.DepartureDate = departure.Time
|
|
} else {
|
|
r.DepartureDate = time.Time{}
|
|
}
|
|
r.ReleasedAt = releasedAt
|
|
|
|
out = append(out, r)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("GetDuePreauths rows: %w", err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// MarkPreauthReleased sets Released=1 and stores the timestamp. Returns error if no rows updated.
|
|
func MarkPreauthReleased(ctx context.Context, db *sql.DB, txnReference string, releasedAt time.Time) error {
|
|
const stmt = `
|
|
UPDATE dbo.Preauthorizations
|
|
SET Released = 1, ReleasedAt = @ReleasedAt
|
|
WHERE TxnReference = @TxnReference AND Released = 0;
|
|
`
|
|
res, err := db.ExecContext(ctx, stmt, sql.Named("ReleasedAt", releasedAt), sql.Named("TxnReference", txnReference))
|
|
if err != nil {
|
|
return fmt.Errorf("MarkPreauthReleased exec: %w", err)
|
|
}
|
|
n, err := res.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("MarkPreauthReleased RowsAffected: %w", err)
|
|
}
|
|
if n == 0 {
|
|
return fmt.Errorf("no rows updated for %s (maybe already released)", txnReference)
|
|
}
|
|
log.Infof("Marked preauth %s released at %s", txnReference, releasedAt.Format(time.RFC3339))
|
|
return nil
|
|
}
|