264 lines
7.2 KiB
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()
|
|
}
|