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 ( // Transaction types SaleTransactionType = "sale" AccountVerificationType = "account verification" // Transaction results ResultApproved = "approved" ResultDeclined = "declined" ResultCancelled = "cancelled" ResultPending = "pending" ResultError = "error" CheckinSuccessfulEndpoint = "/successful" // Endpoint to send guest to after successful payment CheckinUnsuccessfulEndpoint = "/unsuccessful" // Response map keys CardReference = "CARD_REFERENCE" CardHash = "CARD_HASH" Errors = "ERRORS" ReceiptData = "RECEIPT_DATA" ReceiptDataMerchant = "RECEIPT_DATA_MERCHANT" ReceiptDataCardholder = "RECEIPT_DATA_CARDHOLDER" Reference = "REFERENCE" TransactionResult = "TRANSACTION_RESULT" TransactionType = "TRANSACTION_TYPE" ConfirmResult = "CONFIRM_RESULT" ConfirmErrors = "CONFIRM_ERRORS" // Log field keys LogFieldError = "error" LogFieldDescription = "description" LogResult = "transactionResult" ) // 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"` } TransactionConfirmation struct { XMLName xml.Name `xml:"TransactionConfirmation"` Result string `xml:"Result"` Errors string `xml:"Errors"` ErrorDescription string `xml:"ErrorDescription"` ReceiptDataCardholder string `xml:"ReceiptDataCardholder"` } ) // 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 } // BuildRedirectURL builds the redirect URL to send the guest to after payment. func BuildRedirectURL(result map[string]string) string { res := result[TransactionResult] tType := result[TransactionType] // Transaction approved? if strings.EqualFold(res, ResultApproved) { switch { // Transaction type AccountVerification? case strings.EqualFold(tType, AccountVerificationType): log.WithField(LogResult, result[TransactionResult]). Info("Account verification approved") return buildSuccessURL(result) // Transaction type Sale? case strings.EqualFold(tType, SaleTransactionType): // Transaction confirmed? if strings.EqualFold(result[ConfirmResult], ResultApproved) { log.WithField(LogResult, result[ConfirmResult]). Info("Transaction approved and confirmed") return buildSuccessURL(result) } // Not confirmed log.WithFields(log.Fields{LogFieldError: result[ConfirmResult], LogFieldDescription: result[ConfirmErrors]}). Error("Transaction approved but not confirmed") return BuildFailureURL(result[ConfirmResult], result[ConfirmErrors]) } } // Not approved return BuildFailureURL(res, result[Errors]) } func buildSuccessURL(result map[string]string) string { q := url.Values{} q.Set("TxnReference", result[Reference]) q.Set("CardHash", hex.EncodeToString([]byte(result[CardHash]))) q.Set("CardReference", hex.EncodeToString([]byte(result[CardReference]))) return (&url.URL{ Path: CheckinSuccessfulEndpoint, RawQuery: q.Encode(), }).String() } func BuildFailureURL(msgType, description string) string { q := url.Values{} if msgType == "" { msgType = ResultError } if description == "" { description = "Transaction failed" } log.WithFields(log.Fields{LogFieldError: msgType, LogFieldDescription: description}). Error("Transaction failed") q.Set("MsgType", msgType) q.Set("Description", description) return (&url.URL{ Path: CheckinUnsuccessfulEndpoint, RawQuery: q.Encode(), }).String() }