package payment import ( "context" "database/sql" "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 { // 1) normalize the result code res := strings.ToLower(result["TRANSACTION_RESULT"]) // 2) pick base path and optional error params var basePath string var msgType, description string switch res { case ResultApproved: basePath = CheckinSuccessfulEndpoint case ResultDeclined: basePath = CheckinUnsuccessfulEndpoint msgType = "declined" description = "payment declined" case ResultCancelled: basePath = CheckinUnsuccessfulEndpoint msgType = "cancelled" description = "payment cancelled by customer" case ResultPending: // you could choose to treat pending as unsuccessful or special-case it basePath = CheckinUnsuccessfulEndpoint msgType = "pending" description = "payment pending" case ResultError: basePath = CheckinUnsuccessfulEndpoint msgType = "error" description = result["ERROR"] default: basePath = CheckinUnsuccessfulEndpoint msgType = "error" description = "unknown transaction result" } if msgType != "" { log.Warnf("Transaction %s: %s - %s", res, msgType, description) } // 3) build query params q := url.Values{} q.Set("TxnReference", result["REFERENCE"]) q.Set("CardHash", result["CARD_HASH"]) q.Set("CardReference", result["CARD_REFERENCE"]) // only append these when non-approved if msgType != "" { q.Set("MsgType", msgType) q.Set("Description", description) } // 4) assemble final URL // note: url.URL automatically escapes values in RawQuery u := url.URL{ Path: basePath, RawQuery: q.Encode(), } return u.String() }