hardlink/payment/creditcall.go

264 lines
7.2 KiB
Go

package payment
import (
"context"
"database/sql"
"encoding/hex"
"encoding/xml"
"fmt"
"net/url"
"strconv"
"strings"
"time"
_ "github.com/denisenkom/go-mssqldb"
log "github.com/sirupsen/logrus"
)
const (
ResultApproved = "approved"
ResultDeclined = "declined"
ResultCancelled = "cancelled"
ResultPending = "pending"
ResultError = "error"
CheckinSuccessfulEndpoint = "/successful" // Endpoint to send guest to after successful payment
CheckinUnsuccessfulEndpoint = "/unsuccessful"
)
// XML parsing structs
type (
TransactionRec struct {
XMLName xml.Name `xml:"TransactionPayload"`
AmountMinorUnits string `xml:"amount"`
TransactionType string `xml:"transactionType"`
}
TransactionResultXML struct {
XMLName xml.Name `xml:"TransactionResult"`
Entries []EntryXML `xml:"Entry"`
}
EntryXML struct {
Key string `xml:"Key"`
Value string `xml:"Value"`
}
)
// ParseTransactionResult parses the XML into entries.
func ParseTransactionResult(data []byte) ([]EntryXML, error) {
var tr TransactionResultXML
if err := xml.Unmarshal(data, &tr); err != nil {
return nil, fmt.Errorf("XML unmarshal: %w", err)
}
return tr.Entries, nil
}
// initMSSQL opens and pings the SQL Server instance localhost\SQLEXPRESS
// using user=Kiosk, password=Gr33nfarm, database=TransactionDatabase.
func InitMSSQL(port int, user, password, database string) (*sql.DB, error) {
const server = "localhost"
// Use TCP; drop the \SQLEXPRESS instance name
connString := fmt.Sprintf(
"sqlserver://%s:%s@%s:%d?database=%s&encrypt=disable",
user, password, server, port, database,
)
db, err := sql.Open("sqlserver", connString)
if err != nil {
return nil, fmt.Errorf("opening DB: %w", err)
}
// Verify connectivity
if err := db.PingContext(context.Background()); err != nil {
db.Close()
return nil, fmt.Errorf("pinging DB: %w", err)
}
return db, nil
}
// insertTransactionRecord inserts one row into TransactionRecords.
// m is the map from keys to string values as returned by ChipDNA.
func InsertTransactionRecord(ctx context.Context, db *sql.DB, m map[string]string) error {
// Extract fields with defaults or NULL handling.
// 1. TxnReference <- REFERENCE
ref, ok := m["REFERENCE"]
if !ok || ref == "" {
return fmt.Errorf("missing REFERENCE in result map")
}
// 2. TxnDateTime <- parse AUTH_DATE_TIME (layout "20060102150405"), else use now
var txnTime time.Time
if s, ok := m["AUTH_DATE_TIME"]; ok && s != "" {
t, err := time.ParseInLocation("20060102150405", s, time.UTC)
if err != nil {
// fallback: use now
txnTime = time.Now().UTC()
} else {
txnTime = t
}
} else {
txnTime = time.Now().UTC()
}
// 3. TotalAmount <- parse TOTAL_AMOUNT minor units into float (divide by 100)
var totalAmount sql.NullFloat64
if s, ok := m["TOTAL_AMOUNT"]; ok && s != "" {
if iv, err := strconv.ParseInt(s, 10, 64); err == nil {
// convert minor units to major (e.g. 150 -> 1.50)
totalAmount.Float64 = float64(iv) / 100.0
totalAmount.Valid = true
}
}
// 4. MerchantId <- MERCHANT_ID_MASKED
merchantId := sql.NullString{String: m["MERCHANT_ID_MASKED"], Valid: m["MERCHANT_ID_MASKED"] != ""}
// 5. TerminalId <- TERMINAL_ID_MASKED
terminalId := sql.NullString{String: m["TERMINAL_ID_MASKED"], Valid: m["TERMINAL_ID_MASKED"] != ""}
// 6. CardSchemeName <- CARD_SCHEME
cardScheme := sql.NullString{String: m["CARD_SCHEME"], Valid: m["CARD_SCHEME"] != ""}
// 7. ExpiryDate <- EXPIRY_DATE
expiryDate := sql.NullString{String: m["EXPIRY_DATE"], Valid: m["EXPIRY_DATE"] != ""}
// 8. RecordReference <- CARD_REFERENCE
recordRef := sql.NullString{String: m["CARD_REFERENCE"], Valid: m["CARD_REFERENCE"] != ""}
// 9. Token1 <- CARD_HASH
token1 := sql.NullString{String: m["CARD_HASH"], Valid: m["CARD_HASH"] != ""}
// 10. Token2 <- CARDEASE_REFERENCE
token2 := sql.NullString{String: m["CARDEASE_REFERENCE"], Valid: m["CARDEASE_REFERENCE"] != ""}
// 11. PanMasked <- PAN_MASKED
panMasked := sql.NullString{String: m["PAN_MASKED"], Valid: m["PAN_MASKED"] != ""}
// 12. AuthCode <- AUTH_CODE
authCode := sql.NullString{String: m["AUTH_CODE"], Valid: m["AUTH_CODE"] != ""}
// 13. TransactionResult <- TRANSACTION_RESULT
txnResult := sql.NullString{String: m["TRANSACTION_RESULT"], Valid: m["TRANSACTION_RESULT"] != ""}
// Build INSERT statement with named parameters.
// Assuming your table is [TransactionDatabase].[dbo].[TransactionRecords].
const stmt = `
INSERT INTO [TransactionDatabase].[dbo].[TransactionRecords]
(
[TxnReference],
[TxnDateTime],
[TotalAmount],
[MerchantId],
[TerminalId],
[CardSchemeName],
[ExpiryDate],
[RecordReference],
[Token1],
[Token2],
[PanMasked],
[AuthCode],
[TransactionResult]
)
VALUES
(
@TxnReference,
@TxnDateTime,
@TotalAmount,
@MerchantId,
@TerminalId,
@CardSchemeName,
@ExpiryDate,
@RecordReference,
@Token1,
@Token2,
@PanMasked,
@AuthCode,
@TransactionResult
);
`
// Execute with sql.Named parameters:
_, err := db.ExecContext(ctx, stmt,
sql.Named("TxnReference", ref),
sql.Named("TxnDateTime", txnTime),
sql.Named("TotalAmount", nullableFloatArg(totalAmount)),
sql.Named("MerchantId", nullableStringArg(merchantId)),
sql.Named("TerminalId", nullableStringArg(terminalId)),
sql.Named("CardSchemeName", nullableStringArg(cardScheme)),
sql.Named("ExpiryDate", nullableStringArg(expiryDate)),
sql.Named("RecordReference", nullableStringArg(recordRef)),
sql.Named("Token1", nullableStringArg(token1)),
sql.Named("Token2", nullableStringArg(token2)),
sql.Named("PanMasked", nullableStringArg(panMasked)),
sql.Named("AuthCode", nullableStringArg(authCode)),
sql.Named("TransactionResult", nullableStringArg(txnResult)),
)
if err != nil {
return fmt.Errorf("insert TransactionRecords: %w", err)
}
// Successfully inserted
log.Infof("Inserted transaction record for reference %s", ref)
return nil
}
// Helpers to pass NULL when appropriate:
func nullableStringArg(ns sql.NullString) interface{} {
if ns.Valid {
return ns.String
}
return nil
}
func nullableFloatArg(nf sql.NullFloat64) interface{} {
if nf.Valid {
return nf.Float64
}
return nil
}
func BuildRedirectURL(result map[string]string) string {
var msgType, description string
q := url.Values{}
res := strings.ToLower(result["TRANSACTION_RESULT"])
if res == ResultApproved {
q.Set("TxnReference", result["REFERENCE"])
q.Set("CardHash", hex.EncodeToString([]byte(result["CARD_HASH"])))
q.Set("CardReference", hex.EncodeToString([]byte(result["CARD_REFERENCE"])))
u := url.URL{
Path: CheckinSuccessfulEndpoint,
RawQuery: q.Encode(),
}
return u.String()
}
msgType = ResultError
if res != "" {
msgType = res
}
errors, ok := result["ERRORS"]
if ok && errors != "" {
description = errors
}
errors, ok = result["ERROR"]
if ok && errors != "" {
description += " " + errors
}
if description == "" {
description = "Transaction failed"
}
log.Errorf("Transaction %s: %s", msgType, description)
q.Set("MsgType", msgType)
q.Set("Description", description)
u := url.URL{
Path: CheckinUnsuccessfulEndpoint,
RawQuery: q.Encode(),
}
return u.String()
}