From 8f75ef20417fb8b08b0d0487dbe8e24f466053c7 Mon Sep 17 00:00:00 2001 From: BlockChyp SDK Builder Date: Wed, 18 Sep 2024 15:38:13 +0000 Subject: [PATCH] Merge pull request #278 from blockchyp/feature/CHYP-3573 Card Metadata and HSA/EBT --- README.md | 74 +++++++ blockchyp.go | 47 +++++ cli.go | 4 + cmd/blockchyp/main.go | 55 +++++ models.go | 277 +++++++++++++++++++++++++- pkg/examples/card_metadata_example.go | 39 ++++ 6 files changed, 490 insertions(+), 6 deletions(-) create mode 100644 pkg/examples/card_metadata_example.go diff --git a/README.md b/README.md index 5a99120..7172b49 100644 --- a/README.md +++ b/README.md @@ -536,6 +536,80 @@ func voidExample() { ``` +#### Card Metadata + + + +* **API Credential Types:** Merchant +* **Required Role:** Payment API Access + +This API allows you to retrieve card metadata. + +Card metadata requests can use a payment terminal to retrieve metadata or +use a previously enrolled payment token. + +**Terminal Transactions** + +For terminal transactions, make sure you pass in the terminal name using the `terminalName` property. + +**Token Transactions** + +If you have a payment token, omit the `terminalName` property and pass in the token with the `token` +property instead. + +**Card Numbers and Mag Stripes** + +You can also pass in PANs and Mag Stripes, but you probably shouldn't, as this will +put you in PCI scope and the most common vector for POS breaches is keylogging. +If you use terminals for manual card entry, you'll bypass any keyloggers that +might be maliciously running on the point-of-sale system. + + + + +```go +package main + +import ( + "fmt" + "log" + + blockchyp "github.com/blockchyp/blockchyp-go/v2" +) + +func cardMetadataExample() { + // sample credentials + creds := blockchyp.APICredentials{ + APIKey: "ZDSMMZLGRPBPRTJUBTAFBYZ33Q", + BearerToken: "ZLBW5NR4U5PKD5PNP3ZP3OZS5U", + SigningKey: "9c6a5e8e763df1c9256e3d72bd7f53dfbd07312938131c75b3bfd254da787947", + } + + // instantiate the client + client := blockchyp.NewClient(creds) + + // setup request object + request := blockchyp.CardMetadataRequest{ + Test: true, + TerminalName: "Test Terminal", + } + + response, err := client.CardMetadata(request) + + if err != nil { + log.Fatal(err) + } + + //process the result + if response.Success { + fmt.Println("success") + } + + fmt.Printf("Response: %+v\n", response) +} + +``` + #### Time Out Reversal diff --git a/blockchyp.go b/blockchyp.go index dc75090..defd7d1 100644 --- a/blockchyp.go +++ b/blockchyp.go @@ -371,6 +371,53 @@ func (client *Client) Enroll(request EnrollRequest) (*EnrollResponse, error) { return &response, err } +// CardMetadata retrieves card metadata. +func (client *Client) CardMetadata(request CardMetadataRequest) (*CardMetadataResponse, error) { + var response CardMetadataResponse + var err error + + if err := populateSignatureOptions(&request); err != nil { + return nil, err + } + + if request.TerminalName != "" { + var route TerminalRoute + route, err = client.resolveTerminalRoute(request.TerminalName) + if err != nil { + if errors.Is(err, ErrUnknownTerminal) { + response.ResponseDescription = ResponseUnknownTerminal + return &response, err + } + + return nil, err + } + + if route.CloudRelayEnabled { + err = client.RelayRequest("/api/card-metadata", "POST", request, &response, request.Test, request.Timeout) + } else { + authRequest := TerminalCardMetadataRequest{ + APICredentials: route.TransientCredentials, + Request: request, + } + err = client.terminalRequest(route, "/api/card-metadata", "POST", authRequest, &response, request.Timeout) + } + } else { + err = client.GatewayRequest("/api/card-metadata", "POST", request, &response, request.Test, request.Timeout) + } + + if timeout, ok := err.(net.Error); ok && timeout.Timeout() { + response.ResponseDescription = ResponseTimedOut + } else if err != nil { + response.ResponseDescription = err.Error() + } + + if err := handleSignature(request, &response); err != nil { + log.Printf("Failed to write signature: %+v", err) + } + + return &response, err +} + // GiftActivate activates or recharges a gift card. func (client *Client) GiftActivate(request GiftActivateRequest) (*GiftActivateResponse, error) { var response GiftActivateResponse diff --git a/cli.go b/cli.go index ba71bde..f14aa60 100644 --- a/cli.go +++ b/cli.go @@ -170,6 +170,10 @@ type CommandLineArguments struct { DeleteProtected bool `json:"deteleProtected"` Roles string `json:"roles"` Notes string `json:"notes"` + Healthcare bool `json:"healthcare"` + HealthcareTotal string `json:"healthcareTotal"` + EBTTotal string `json:"ebtTotal"` + CardMetadataLookup bool `json:"cardMetadataLookup"` CredType string `json:"credType"` } diff --git a/cmd/blockchyp/main.go b/cmd/blockchyp/main.go index 963af4f..ed4b1ad 100644 --- a/cmd/blockchyp/main.go +++ b/cmd/blockchyp/main.go @@ -203,6 +203,10 @@ func parseArgs() blockchyp.CommandLineArguments { flag.BoolVar(&args.DeleteProtected, "deleteProtected", false, "protects the credentials from deletion.") flag.StringVar(&args.Roles, "roles", "", "an optional array of role codes that will be assigned to the credentials.") flag.StringVar(&args.Notes, "notes", "", "free form description of the purpose or intent behind the credentials.") + flag.BoolVar(&args.Healthcare, "healthcare", false, "indicates this transaction is HSA/FSA") + flag.StringVar(&args.HealthcareTotal, "healthcareTotal", "", "total amount of healthcare") + flag.StringVar(&args.EBTTotal, "ebtTotal", "", "total amount of ebt") + flag.BoolVar(&args.CardMetadataLookup, "cardMetadataLookup", false, "requests card metatdata instead of enrolling a card.") flag.StringVar(&args.CredType, "credType", "", "is the type of credential to be generated, API or TOKENIZING.") flag.Parse() @@ -476,6 +480,8 @@ func processCommand(args blockchyp.CommandLineArguments) { processUnlinkToken(client, args) case "drop-terminal-socket": processDropSocket(client, args) + case "card-metadata": + processCardMetadata(client, args) case "submit-application": processSubmitApplication(client, args) default: @@ -1615,6 +1621,7 @@ func processRefund(client *blockchyp.Client, args blockchyp.CommandLineArguments PAN: args.PAN, ExpMonth: args.ExpiryMonth, ExpYear: args.ExpiryYear, + CardMetadataLookup: args.CardMetadataLookup, } if args.Debit { @@ -1832,6 +1839,7 @@ func processEnroll(client *blockchyp.Client, args blockchyp.CommandLineArguments EntryMethod: args.EntryMethod, Recurring: args.Recurring, Subscription: args.Subscription, + CardMetadataLookup: args.CardMetadataLookup, } if hasCustomerFields(args) { req.Customer = populateCustomer(args) @@ -1907,6 +1915,10 @@ func processAuth(client *blockchyp.Client, args blockchyp.CommandLineArguments) TransactionID: args.TransactionID, PurchaseOrderNumber: args.PONumber, SupplierReferenceNumber: args.SupplierReferenceNumber, + Healthcare: args.Healthcare, + HealthcareTotal: args.HealthcareTotal, + EBTTotal: args.EBTTotal, + CardMetadataLookup: args.CardMetadataLookup, } displayTx := assembleDisplayTransaction(args) @@ -2749,6 +2761,49 @@ func processTokenDelete(client *blockchyp.Client, args blockchyp.CommandLineArgu dumpResponse(&args, res) } +func processCardMetadata(client *blockchyp.Client, args blockchyp.CommandLineArguments) { + + req := &blockchyp.CardMetadataRequest{} + + if !parseJSONInput(args, req) { + + if (args.TerminalName == "") && (args.Token == "") && (args.PAN == "") { + fatalError("-terminal or -token requred") + } + + req = &blockchyp.CardMetadataRequest{ + Timeout: args.Timeout, + Test: args.Test, + TransactionRef: args.TransactionRef, + WaitForRemovedCard: args.WaitForRemovedCard, + Force: args.Force, + Token: args.Token, + PAN: args.PAN, + ExpMonth: args.ExpiryMonth, + ExpYear: args.ExpiryYear, + Address: args.Address, + PostalCode: args.PostalCode, + ManualEntry: args.ManualEntry, + TerminalName: args.TerminalName, + ResetConnection: args.ResetConnection, + } + + if args.Debit { + req.CardType = blockchyp.CardTypeDebit + } else if args.EBT { + req.CardType = blockchyp.CardTypeEBT + } + } + + res, err := client.CardMetadata(*req) + + if err != nil { + handleError(&args, err) + } + + dumpResponse(&args, res) +} + func processSubmitApplication(client *blockchyp.Client, args blockchyp.CommandLineArguments) { request := &blockchyp.SubmitApplicationRequest{} diff --git a/models.go b/models.go index 71e2aae..67d0606 100644 --- a/models.go +++ b/models.go @@ -952,6 +952,10 @@ type TokenMetadataResponse struct { // Token the token metadata for a given query. Token CustomerToken `json:"token"` + + // CardMetadata contains details about a payment card derived from its + // BIN/IIN. + CardMetadata *CardMetadata `json:"cardMetadata,omitempty"` } // CustomerToken models a customer token. @@ -1236,8 +1240,8 @@ type AuthorizationRequest struct { // calculated values like surcharges. Rounding up is the default behavior. RoundingMode *RoundingMode `json:"roundingMode"` - // Healthcare contains details for HSA/FSA transactions. - Healthcare *Healthcare `json:"healthcare,omitempty"` + // HealthcareMetadata contains details for HSA/FSA transactions. + HealthcareMetadata *HealthcareMetadata `json:"healthcareMetadata,omitempty"` // Cryptocurrency indicates that the transaction should be a cryptocurrency // transaction. Value should be a crypto currency code (ETH, BTC) or ANY to @@ -1280,6 +1284,245 @@ type AuthorizationRequest struct { // amount will be passed directly to the gateway and is not directly // calculated. PassthroughSurcharge string `json:"passthroughSurcharge,omitempty"` + + // Healthcare marks a transaction as HSA/FSA. + Healthcare bool `json:"healthcare,omitempty"` + + // HealthcareTotal is the total amount to process as healthcare. + HealthcareTotal string `json:"healthcareTotal,omitempty"` + + // EBTTotal is the total amount to process as ebt. + EBTTotal string `json:"ebtTotal,omitempty"` + + // CardMetadataLookup indicates that this transaction will include a card + // metadata lookup. + CardMetadataLookup bool `json:"cardMetadataLookup,omitempty"` +} + +// CardMetadata contains essential information about a payment card derived +// from its BIN/IIN. +type CardMetadata struct { + // CardBrand is the brand or network of the card (e.g., Visa, Mastercard, + // Amex). + CardBrand string `json:"cardBrand"` + + // IssuerName is the name of the financial institution that issued the card. + IssuerName string `json:"issuerName"` + + // L3 indicates whether the card supports Level 3 processing for detailed + // transaction data. + L3 bool `json:"l3"` + + // L2 indicates whether the card supports Level 2 processing for additional + // transaction data. + L2 bool `json:"l2"` + + // ProductType is the general category or type of the card product. + ProductType string `json:"productType"` + + // ProductName is the specific name or designation of the card product. + ProductName string `json:"productName"` + + // EBT indicates whether the card is an Electronic Benefit Transfer (EBT) + // card. + EBT bool `json:"ebt"` + + // Debit indicates whether the card is a debit card. + Debit bool `json:"debit"` + + // Healthcare indicates whether the card is a healthcare-specific payment + // card. + Healthcare bool `json:"healthcare"` + + // Prepaid indicates whether the card is a prepaid card. + Prepaid bool `json:"prepaid"` + + // Region is the geographical region associated with the card's issuer. + Region string `json:"region"` + + // Country is the country associated with the card's issuer. + Country string `json:"country"` +} + +// CardMetadataRequest retrieves card metadata. +type CardMetadataRequest struct { + // Timeout is the request timeout in seconds. + Timeout int `json:"timeout"` + + // Test specifies whether or not to route transaction to the test gateway. + Test bool `json:"test"` + + // TransactionRef contains a user-assigned reference that can be used to + // recall or reverse transactions. + TransactionRef string `json:"transactionRef,omitempty"` + + // AutogeneratedRef indicates that the transaction reference was + // autogenerated and should be ignored for the purposes of duplicate + // detection. + AutogeneratedRef bool `json:"autogeneratedRef"` + + // Async defers the response to the transaction and returns immediately. + // Callers should retrive the transaction result using the Transaction Status + // API. + Async bool `json:"async"` + + // Queue adds the transaction to the queue and returns immediately. Callers + // should retrive the transaction result using the Transaction Status API. + Queue bool `json:"queue"` + + // WaitForRemovedCard specifies whether or not the request should block until + // all cards have been removed from the card reader. + WaitForRemovedCard bool `json:"waitForRemovedCard,omitempty"` + + // Force causes a transaction to override any in-progress transactions. + Force bool `json:"force,omitempty"` + + // OrderRef is an identifier from an external point of sale system. + OrderRef string `json:"orderRef,omitempty"` + + // DestinationAccount is the settlement account for merchants with split + // settlements. + DestinationAccount string `json:"destinationAccount,omitempty"` + + // TestCase can include a code used to trigger simulated conditions for the + // purposes of testing and certification. Valid for test merchant accounts + // only. + TestCase string `json:"testCase,omitempty"` + + // Token is the payment token to be used for this transaction. This should be + // used for recurring transactions. + Token string `json:"token,omitempty"` + + // Track1 contains track 1 magnetic stripe data. + Track1 string `json:"track1,omitempty"` + + // Track2 contains track 2 magnetic stripe data. + Track2 string `json:"track2,omitempty"` + + // PAN contains the primary account number. We recommend using the terminal + // or e-commerce tokenization libraries instead of passing account numbers in + // directly, as this would put your application in PCI scope. + PAN string `json:"pan,omitempty"` + + // RoutingNumber is the ACH routing number for ACH transactions. + RoutingNumber string `json:"routingNumber,omitempty"` + + // CardholderName is the cardholder name. Only required if the request + // includes a primary account number or track data. + CardholderName string `json:"cardholderName,omitempty"` + + // ExpMonth is the card expiration month for use with PAN based transactions. + ExpMonth string `json:"expMonth,omitempty"` + + // ExpYear is the card expiration year for use with PAN based transactions. + ExpYear string `json:"expYear,omitempty"` + + // CVV is the card CVV for use with PAN based transactions. + CVV string `json:"cvv,omitempty"` + + // Address is the cardholder address for use with address verification. + Address string `json:"address,omitempty"` + + // PostalCode is the cardholder postal code for use with address + // verification. + PostalCode string `json:"postalCode,omitempty"` + + // ManualEntry specifies that the payment entry method is a manual keyed + // transaction. If this is true, no other payment method will be accepted. + ManualEntry bool `json:"manualEntry,omitempty"` + + // KSN is the key serial number used for DUKPT encryption. + KSN string `json:"ksn,omitempty"` + + // PINBlock is the encrypted pin block. + PINBlock string `json:"pinBlock,omitempty"` + + // CardType designates categories of cards: credit, debit, EBT. + CardType CardType `json:"cardType,omitempty"` + + // PaymentType designates brands of payment methods: Visa, Discover, etc. + PaymentType string `json:"paymentType,omitempty"` + + // TerminalName is the name of the target payment terminal. + TerminalName string `json:"terminalName,omitempty"` + + // ResetConnection forces the terminal cloud connection to be reset while a + // transactions is in flight. This is a diagnostic settings that can be used + // only for test transactions. + ResetConnection bool `json:"resetConnection"` + + // Healthcare marks a transaction as HSA/FSA. + Healthcare bool `json:"healthcare,omitempty"` +} + +// CardMetadataResponse contains the response to a card metadata request. +type CardMetadataResponse struct { + // Success indicates whether or not the request succeeded. + Success bool `json:"success"` + + // Error is the error, if an error occurred. + Error string `json:"error"` + + // ResponseDescription contains a narrative description of the transaction + // result. + ResponseDescription string `json:"responseDescription"` + + // Token is the payment token, if the payment was enrolled in the vault. + Token string `json:"token,omitempty"` + + // EntryMethod is the entry method for the transaction (CHIP, MSR, KEYED, + // etc). + EntryMethod string `json:"entryMethod,omitempty"` + + // PaymentType is the card brand (VISA, MC, AMEX, DEBIT, etc). + PaymentType string `json:"paymentType,omitempty"` + + // Network provides network level detail on how a transaction was routed, + // especially for debit transactions. + Network string `json:"network,omitempty"` + + // Logo identifies the card association based on bin number. Used primarily + // used to indicate the major logo on a card, even when debit transactions + // are routed on a different network. + Logo string `json:"logo,omitempty"` + + // MaskedPAN is the masked primary account number. + MaskedPAN string `json:"maskedPan,omitempty"` + + // PublicKey is the BlockChyp public key if the user presented a BlockChyp + // payment card. + PublicKey string `json:"publicKey,omitempty"` + + // ScopeAlert indicates that the transaction did something that would put the + // system in PCI scope. + ScopeAlert bool `json:"ScopeAlert,omitempty"` + + // CardHolder is the cardholder name. + CardHolder string `json:"cardHolder,omitempty"` + + // ExpMonth is the card expiration month in MM format. + ExpMonth string `json:"expMonth,omitempty"` + + // ExpYear is the card expiration year in YY format. + ExpYear string `json:"expYear,omitempty"` + + // AVSResponse contains address verification results if address information + // was submitted. + AVSResponse AVSResponse `json:"avsResponse"` + + // ReceiptSuggestions contains suggested receipt fields. + ReceiptSuggestions ReceiptSuggestions `json:"receiptSuggestions"` + + // Customer contains customer data, if any. Preserved for reverse + // compatibility. + Customer *Customer `json:"customer"` + + // Customers contains customer data, if any. + Customers []Customer `json:"customers"` + + // CardMetadata contains details about a payment card derived from its + // BIN/IIN. + CardMetadata *CardMetadata `json:"cardMetadata,omitempty"` } // BalanceRequest contains a request for the remaining balance on a payment @@ -1642,8 +1885,8 @@ type RefundRequest struct { // only for test transactions. ResetConnection bool `json:"resetConnection"` - // Healthcare contains details for HSA/FSA transactions. - Healthcare *Healthcare `json:"healthcare,omitempty"` + // HealthcareMetadata contains details for HSA/FSA transactions. + HealthcareMetadata *HealthcareMetadata `json:"healthcareMetadata,omitempty"` // SimulateChipRejection instructs the terminal to simulate a post auth chip // rejection that would trigger an automatic reversal. @@ -1663,6 +1906,10 @@ type RefundRequest struct { // Mit manually sets the MIT (Merchant Initiated Transaction) flag. Mit bool `json:"mit,omitempty"` + + // CardMetadataLookup indicates that this transaction will include a card + // metadata lookup. + CardMetadataLookup bool `json:"cardMetadataLookup,omitempty"` } // CaptureRequest contains the information needed to capture a preauth. @@ -2170,6 +2417,10 @@ type EnrollRequest struct { // Subscription indicates that this transaction and any using this token // should be treated as a subscription recurring transaction. Subscription bool `json:"subscription,omitempty"` + + // CardMetadataLookup indicates that this transaction will include a card + // metadata lookup. + CardMetadataLookup bool `json:"cardMetadataLookup,omitempty"` } // EnrollResponse contains the response to an enroll request. @@ -2280,6 +2531,10 @@ type EnrollResponse struct { // SigFile contains the hex encoded signature data. SigFile string `json:"sigFile,omitempty"` + + // CardMetadata contains details about a payment card derived from its + // BIN/IIN. + CardMetadata *CardMetadata `json:"cardMetadata,omitempty"` } // ClearTerminalRequest contains the information needed to enroll a new @@ -2910,6 +3165,10 @@ type AuthorizationResponse struct { // Status indicates the current status of a transaction. Status string `json:"status"` + + // CardMetadata contains details about a payment card derived from its + // BIN/IIN. + CardMetadata *CardMetadata `json:"cardMetadata,omitempty"` } // TransactionStatusRequest models the request for updated information about a @@ -5184,8 +5443,8 @@ type UnlinkTokenRequest struct { CustomerID string `json:"customerId"` } -// Healthcare contains fields for HSA/FSA transactions. -type Healthcare struct { +// HealthcareMetadata contains fields for HSA/FSA transactions. +type HealthcareMetadata struct { // Types is a list of healthcare categories in the transaction. Types []HealthcareGroup `json:"types"` @@ -7780,6 +8039,12 @@ type TerminalAuthorizationRequest struct { Request AuthorizationRequest `json:"request"` } +// TerminalCardMetadataRequest retrieves card metadata. +type TerminalCardMetadataRequest struct { + APICredentials + Request CardMetadataRequest `json:"request"` +} + // TerminalBalanceRequest contains a request for the remaining balance on a // payment type. type TerminalBalanceRequest struct { diff --git a/pkg/examples/card_metadata_example.go b/pkg/examples/card_metadata_example.go new file mode 100644 index 0000000..e056eaf --- /dev/null +++ b/pkg/examples/card_metadata_example.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "log" + + blockchyp "github.com/blockchyp/blockchyp-go/v2" +) + +func cardMetadataExample() { + // sample credentials + creds := blockchyp.APICredentials{ + APIKey: "ZDSMMZLGRPBPRTJUBTAFBYZ33Q", + BearerToken: "ZLBW5NR4U5PKD5PNP3ZP3OZS5U", + SigningKey: "9c6a5e8e763df1c9256e3d72bd7f53dfbd07312938131c75b3bfd254da787947", + } + + // instantiate the client + client := blockchyp.NewClient(creds) + + // setup request object + request := blockchyp.CardMetadataRequest{ + Test: true, + TerminalName: "Test Terminal", + } + + response, err := client.CardMetadata(request) + + if err != nil { + log.Fatal(err) + } + + //process the result + if response.Success { + fmt.Println("success") + } + + fmt.Printf("Response: %+v\n", response) +}