# Developing a Gift Card App

On a traditional terminal, a merchant must select the gift card option before swiping a gift card. Poynt Payment Fragment determines whether or not the card is a gift card based on its BIN range and automatically routes the transaction to the custom transaction processor.

Additionally, based on the configuration of the custom transaction processor, you can also add a button in the payment fragment's payment options screen to process a non-swipe input (e.g. scan QR code, gift card number manual input, coupon code, etc.)

# General Overview

PoyntOS architecture provides an extensible way to add new payment methods, also known as custom tenders. This article describes how a developer can build a custom transaction processor by implementing an IPoyntTransactionService (opens new window) interface to create a Gift Card application for GoDaddy Poynt.

Although the scope of this document is to address integration with gift card providers, you can also use this interface to implement Loyalty, Discount, or any other type of service that can create a custom tender transaction.

TIP

If you would like to see a practical example, our PoyntSamples App (opens new window) details the implementation of a custom transaction processor.

# Prerequisites

If you wish to begin developing applications for GoDaddy Poynt, it's important to become familiar with our platform's recommendations and prerequisites.

  1. Create a Developer Account.

  2. Set up a Developer Kit or a Developer Emulator.

  3. Create an Android Studio project with the target set on API Level 19.

    NOTE

    Anything higher than API Level 19 will render your application incompatible with the P61 device.

  4. Add Poynt SDK to your application.

TIP

We also recommend going through our Development Guidelines, which will ensure your compliance with our requirements and speed up the review process.

# Integration

Please follow the steps outlined below to perform a successful integration.

  1. Create a service that implements IPoyntTransactionService (opens new window).

  2. Specify the configuration in the giftcard_transaction_capabilities.xml file.

  3. Update the card reader configuration on the smart terminal.

By default, the GoDaddy Poynt card reader encrypts all the tracking data outside of the card BIN range. If you would like to encrypt the tracking data for a non-payment card BIN range, please refer to the Code Example (opens new window).

NOTE

Please note that this setting will not apply to the cards that start with BIN ranges reserved by specific Payment Card Brands (opens new window)

# Capabilities Configuration

This configuration file specifies entry methods and the BIN range of the payment cards supported by your application. This can be used to configure all the transaction capabilities available.

<!-- giftcard_transaction_capabilities.xml -->
<?xml version="1.0" encoding="utf-8"?>
<capability>
    <!-- Special App ID for Transaction Processing-->
    <!-- has to match your app's package name -->
    <appid>co.poynt.samplegiftcardprocessor</appid>

    <!-- required element, do not remove -->
    <type>TRANSACTION</type>

    <!-- descriptive name of this capability -->
    <!-- this is how your option will show up in Payment Fragment payment options -->
    <provider>My Gift Card</provider>

    <!-- currently not used -->
    <logo>@drawable/ic_launcher</logo>

    <!-- entry method could be one of the following
        CARDREAD -> to support card swipe. card data will be directly passed to this capability provider.
        CUSTOM -> allows your app to launch its own Activity from the Payment Flow to support other entry methods such as
                  manual input form, scanning QR code, etc.
    -->

    <!-- value is a first6 digits of card number also referred to as binrange.
        value >=601056 && value <= 601056 -->
    <!-- for eval expression definition please refer to https://github.com/uklimaschewski/EvalEx -->

    <!-- Any card swiped on the terminal which has the first 6 digits fall between 700000 and 736014
        will be processed by your application. All standard payment cards will still be processed by the default
        transaction service irrespective of this BIN range
    -->
    <entry_method
        eval="value &lt;= 736014 &amp;&amp; value &gt;=  700000"
        type="CARDREAD" />
    <!-- declaring support for "CUSTOM" will add a button in payment options of the Payment Fragment
         pressing the button will call your transaction service directly.
    -->
    <entry_method type="CUSTOM" />
</capability>

This configration file should be located in the res/xml directory of your application and referenced from your manifest:

<!-- snippet of AndroidManifest.xml -->
...
  <service
      android:name=".MyGiftCardTransactionProcessorService"
      android:exported="true">
      <intent-filter>
          <action android:name="co.poynt.os.services.v1.IPoyntTransactionService" />
      </intent-filter>

      <!-- additional configuration -->
      <meta-data
          android:name="co.poynt.os.service.capability"
          android:resource="@xml/giftcard_transaction_capabilities" />
  </service>
...

NOTE

The value of provider element shows the payment options

Gift Card Payment

# IPoyntTransactionService UI

IPoyntTransactionService has a large number of methods. However, the only ones necessary for implementation are listed in boldface below:

  1. Create a Service class that implements the IPoyntTransactionService.Stub class and returns it from onBind

  2. Override processTransaction() to handle SALE and REFUND (referenced and non-referenced) requests.

    NOTE

    Please note that the Transaction object you receive here will carry unencrypted track data.

  3. Override the captureTransaction() to capture an AUTH.

    If your backend API does not follow auth/capture paradigm, this method does not need to be implemented.

  4. Override the voidTransaction() to void an AUTH.

    If your backend API does not follow auth/capture paradigm, this method does not need to be implemented.

  5. Override reverseTransaction() to void/reverse any transaction (SALE, CAPTURE, REFUND or even an AUTH).

    Note: reverseTransaction() on an AUTH is the same as voidTransaction() on an AUTH. A reverseTransaction() gets called when the Poynt Payment Fragment/Card Reader is unable to complete the transaction. This could happen because of the following reasons:

    • An online authorization request (processTransaction) has timed out
    • A merchant clicked on the CANCEL button in the payment fragment before the transaction was processed
  6. Override the updateTransaction() to adjust an AUTH or SALE transaction (e.g. to add a tip or adjust the base amount).

    If your service does not support the auth/sale adjustment, it will return a PoyntError.

  7. Override getTransaction() to return the details about the transactions

  8. captureEMVData() Is not currently being used.

  9. checkCard() Should not be used.

  10. captureAllTransactions() is currently not used in the Terminal but will be used in the future to capture all previously authorized and valid transactions.

  11. createTransaction() Is not currently being used.

  12. saveTransaction() Is not currently being used.

WARNING

All API calls must respond to the corresponding callbacks. Not doing so could cause poor UX with Payment Fragment waiting for a response from your processor for a long time.

# IPoyntTransactionServiceListener Callbacks:

  1. onResponse(Transaction, RequestId, PoyntError) - This is for a processed transaction object, or an error indicating why the transaction could not be processed. You can find additional information below on what must be loaded in the processed Transaction object.

NOTE

Please keep in mind that a processed transaction could be either approved or declined.

  1. onLoginRequired() - This is used when your transaction processor determines that the merchant session has timed out. This usually happens when your JWT expires.

  2. onLaunchActivity(Intent, requestId) - You can use this callback when you need to collect additional information that is not collected by the Poynt Payment Fragments such as ZIP Code or CVV.

The intent must carry whatever information you would need to handle UI/UX based on your requirement. This intent will be launched as an Activity with a result. It is very important that you return a result with 3 Parcelable extras containing "transaction", "payment" and "error".

  • The transaction object contains the processed transaction that you would otherwise return in the onResponse() callback,

  • The payment object is used in case you have updated the payment object based on the additional data that you collected,

  • The error object is used if the transaction has failed. You can also use this callback to implement support for manual entries.

TIP

Setting the Transaction.setSignatureCaptured(false)) will skip the signature screen and it is ideal for cases where you don't need to collect a signature.

# Handling SALE Requests

When a gift card is swiped on a GoDaddy Poynt smart terminal, PoyntOS will call the processTransaction() of your transaction service and pass a transaction object similar to the one below:

{
    "action": "SALE",
    "amounts": {
        "currency": "USD",
        "orderAmount": 5200,
        "tipAmount": 0,
        "transactionAmount": 5200
    },
    "authOnly": false,
    "fundingSource": {
        "card": {
            "numberFirst6": "197610",
            "numberLast4": "8554",
            "track1data": "B1976100999009668554^GETI^10010000000000000",
            "track2data": "1976100999009668554=10010000000000000",
            "track3data": ""
        },
        "emvData": {
            "emvTags": {
                "0xD3": "",
                "0xD2": "313937363130303939393030393636383535343d3130303130303030303030303030303030",
                "0xD1": "42313937363130303939393030393636383535345e474554495e3130303130303030303030303030303030"
            }
        },
        "entryDetails": {
            "customerPresenceStatus": "PRESENT",
            "entryMode": "TRACK_DATA_FROM_MAGSTRIPE"
        },
        "type": "CUSTOM_FUNDING_SOURCE"
    },
    "references": [
        {
            "customType": "referenceId",
            "id": "65422c59-0158-1000-ca4c-d43b0932f8ff",
            "type": "CUSTOM"
        }
    ],
    "signatureCaptured": true
}

In the code snippet below, you will find the logic used to determine if a merchant swiped a card.

@Override
public void processTransaction(final Transaction transaction, final String requestId,
  final IPoyntTransactionServiceListener listener) throws RemoteException {
    if (transaction.getAction() == TransactionAction.SALE && transaction.getFundingSource().getCard() != null){
        // this is a card swipe
    }
    //...
}

Returning the response to Payment Fragment:

// Updating Transaction object inside processTransaction

      // your code to call the gift card processor
      // always make sure we set ID, created_at and updated_at time stamps

      // This is the id used to identify this transaction in the Poynt system
      // It is always a UUID.
      if (transaction.getId() == null) {
          transaction.setId(UUID.randomUUID());
      }
      if (transaction.getCreatedAt() == null) {
          transaction.setCreatedAt(Calendar.getInstance());
      }
      if (transaction.getUpdatedAt() == null) {
          transaction.setUpdatedAt(Calendar.getInstance());
      }


      ProcessorResponse processorResponse = new ProcessorResponse();

      // SALE transactions should have status CAPTURED
      transaction.setStatus(TransactionStatus.CAPTURED);
      // This is the processor's transaction id
      processorResponse.setTransactionId(processorTransactionId);
      // If you would like processor transaction id (or any other field) be returned in refund requests
      // you should set it as a transaction reference
      // make sure you don't store any sensitive information as a reference (like gift card number, etc.)
      TransactionReference processorTxnIdReference = new TransactionReference();
      processorTxnIdReference.setType(TransactionReferenceType.CUSTOM);
      processorTxnIdReference.setCustomType("processorTransactionId");
      processorTxnIdReference.setId(processorTransactionId);
      transaction.setReferences(Collections.singletonList(processorTxnIdReference));
      CustomFundingSource customFundingSource = transaction.getFundingSource().getCustomFundingSource();
      if (customFundingSource == null) {
          customFundingSource = new CustomFundingSource();
      }
      // set the type of the custom funding source
      customFundingSource.setType(CustomFundingSourceType.GIFT_CARD);
      // This is the tender name that will show up in Transaction List on the terminal
      customFundingSource.setName("GIFT CARD");
      // optional processor's account identifier
      customFundingSource.setAccountId("1234567890");
      // it's important to set this to your app's package name
      // if not set, refund requests will not be routed to your transaction service and will fail
      customFundingSource.setProcessor("co.poynt.samplegiftcardprocessor");
      // processorName is the name of the gift card provider
      customFundingSource.setProvider(processorName);
      customFundingSource.setDescription("My giftcard");
      transaction.getFundingSource().setCustomFundingSource(customFundingSource);

      processorResponse.setStatus(ProcessorStatus.Successful);
      // processor internal approval code and status code (if applicable)
      processorResponse.setApprovalCode("123456");
      processorResponse.setStatusCode("200");

      // if transaction is fully approved
      long approvedAmount = transaction.getAmounts().getTransactionAmount();
      processorResponse.setApprovedAmount(approvedAmount);
      // if the remaining balance is provided it will be printed on the receipt.
      processorResponse.setRemainingBalance(200L);
      transaction.getAmounts().setOrderAmount(approvedAmount);
      transaction.getAmounts().setTransactionAmount(approvedAmount);

      processorResponse.setStatusMessage("Approved");
      transaction.setProcessorResponse(processorResponse);

      // if you don't need to capture signature for this transaction
      transaction.setSignatureCaptured(false);

      try {
          // return transaction to Payment Fragment
          listener.onResponse(transaction, requestId, null);
      } catch (RemoteException e) {
          e.printStackTrace();
          PoyntError poyntError = new PoyntError();
          poyntError.setCode(PoyntError.CARD_DECLINE);
          try {
              listener.onResponse(transaction, requestId, poyntError);
          } catch (RemoteException e1) {
              e1.printStackTrace();
          }
      }

# Partial Approval

The implementation of partial approvals requires changing a few lines of code.

// This will approve half of the requested amount
long approvedAmount = transaction.getAmounts().getTransactionAmount()/2;
processorResponse.setApprovedAmount(approvedAmount);
transaction.getAmounts().setOrderAmount(approvedAmount);
transaction.getAmounts().setTransactionAmount(approvedAmount);

# Refund Requests

To determine if the merchant has performed a refund action and handling the refund request, you will need to perform the following check:

@Override
public void processTransaction(final Transaction transaction, final String requestId,
  final IPoyntTransactionServiceListener listener) throws RemoteException {
    if (transaction.getAction() == TransactionAction.REFUND) {
        // this is a refund request
    }
    // ...
}

Below is a detailed example of a transaction object passed as an argument to processTransaction in a refund use case.

{
    "action": "REFUND",
    "amounts": {
        "cashbackAmount": 0,
        "currency": "USD",
        "orderAmount": 2000,
        "tipAmount": 0,
        "transactionAmount": 2000
    },
    "createdAt": {
        "year": 2016,
        "month": 10,
        "dayOfMonth": 8,
        "hourOfDay": 13,
        "minute": 39,
        "second": 18
    },
    "fundingSource": {
        "card": {
            "numberFirst6": "197610",
            "numberLast4": "1505"
        },
        "customFundingSource": {
            "accountId": "1234567890",
            "processor": "co.poynt.samplegiftcardprocessor",
            "provider": "My Gift Card Provider",
            "type": "GIFT_CARD"
        },
        "type": "CUSTOM_FUNDING_SOURCE"
    },
    "id": "45e17262-0158-1000-444f-3876cfd6af03",
    "parentId": "69b65396-3994-4ec4-bc87-89b3a4b88939",
    "processorResponse": {
        "status": "Successful",
        "statusCode": "1",
        "transactionId": "45e17262-0158-1000-444f-3876cfd6af03"
    },
    "references": [
        {
            "customType": "processorTransactionId",
            "id": "d430d1c8-d960-4c3f-b645-4df7e5bb1957",
            "type": "CUSTOM"
        }
    ],
    "status": "REFUNDED",
    "updatedAt": {
        "year": 2016,
        "month": 10,
        "dayOfMonth": 8,
        "hourOfDay": 13,
        "minute": 39,
        "second": 18
    }
}

NOTE

Please keep in mind that this is not the original sale transaction, but rather a transaction object that needs to be updated by your transaction service once the refund is processed.

The GoDaddy Poynt transaction id of the original sale is referenced as the value of parentId in the transaction object.

As you can see, the Refund transaction object contains all the references (i.e. processorTransactionId) set during the processing of the SALE transaction. You can also use that id to determine which transaction needs to be refunded on your backend.

After you perform the refund by calling your backend, you will need to create a ProcessorResponse. To do this, you must update the transaction object and return it using the listener.onResponse callback.

// add processor response
transaction.setStatus(TransactionStatus.REFUNDED);
// refund transaction id used by gift card provider's backend.
processorResponse.setTransactionId("1234567890");
processorResponse.setStatus(ProcessorStatus.Successful);
processorResponse.setApprovalCode("123456");
processorResponse.setStatusCode("200");
processorResponse.setApprovedAmount(transaction.getAmounts().getTransactionAmount());
processorResponse.setStatusMessage("Approved");
transaction.setProcessorResponse(processorResponse);

# Linking Refund Requests to the Original Sale

If you need to store a reference id (e.g. processor transaction id, invoice id, etc.) to facilitate linking the REFUND to the original SALE, you can do that by adding your own reference id to the transaction object when processing the SALE:

// 'transaction' is the Transaction object passed into processTransaction() of your transaction service during the sale
List<TransactionReference> references = transaction.getReferences();
if (references == null) { references = new ArrayList<>(); }
TransactionReference reference = new TransactionReference();
reference.setType(TransactionReferenceType.CUSTOM);
// set the name of your custom value
reference.setCustomType("my_processor_transaction_id");
reference.setId("1234567890");
references.add(reference);

# Partial Refund

The Payment Fragment UI allows merchants to specify a partial refund amount.

Partial Refund

If your gift card service does not support partial refunds and you determine that the amount passed in the refund request does not match the sale amount, you will need to return a PoyntError to the listener.

# Manual Entry

In addition to the card swipe entry method, you may need to support other entry methods like QR Code Scanning or Manual Card Number Entry. This can be accomplished by adding the "CUSTOM" entry method in the capabilities configuration. Adding this entry method will add a new payment option button in the Payment Fragment's payment options menu.

When that button is pressed, Payment Fragment will call the processTransaction of your transaction service and pass a Transaction object as shown below:

{
    "action": "SALE",
    "amounts": {
        "currency": "USD",
        "orderAmount": 5000,
        "tipAmount": 0,
        "transactionAmount": 5000
    },
    "fundingSource": {
        "customFundingSource": {
            "accountId": "6642d2c7-0158-1000-5c07-6753468a5859",
            "provider": "Poynt",
            "type": "OTHER"
        },
        "type": "CUSTOM_FUNDING_SOURCE"
    },
    "id": "6642d2c7-0158-1000-5c07-6753468a5859",
    "references": [
        {
            "customType": "referenceId",
            "id": "6642c34c-0158-1000-5c07-6753468a5859",
            "type": "CUSTOM"
        }
    ],
}

NOTE

The transaction object does not have a nested card object inside its FundingSource.

Below is a code snippet that can help you determine if the merchant initiated the transaction by pressing your custom button in the payment options menu of the Payment Fragment:

@Override
public void processTransaction(final Transaction transaction, final String requestId,
  final IPoyntTransactionServiceListener listener) throws RemoteException {
    if (transaction.getAction() == TransactionAction.SALE && transaction.getFundingSource().getCard() == null) {
        // this is a sale request initiated by pressing the custom button in payment options menu of the Payment Fragment

        // Instruct the Payment Fragment to launch your custom activity.
        // Assuming your app has a PaymentActivity class that listens to the following intent
        Intent paymentActivity = new Intent("COLLECT_CUSTOM_PAYMENT");
        paymentActivity.setComponent(new ComponentName(getPackageName(), PaymentActivity.class.getName()));
        paymentActivity.putExtra("transaction", transaction);
        listener.onLaunchActivity(paymentActivity, requestId);
    }
    // ...
}

Once your activity is finished collecting additional information, it should call your transaction service to process the request and get the updated transaction object back.

Your activity should finish by creating the following intent:

Intent result = new Intent(Intents.ACTION_COLLECT_PAYMENT_RESULT);
result.putExtra("transaction", transaction);
result.putExtra("error", error);
setResult(Activity.RESULT_OK, result);
finish();

This sequence diagram explains the manual entry flow from start to finish:

Manual Entry

# Card Functionalities

To be able to activate, reload or check the balance of a gift card, your application needs to be able to read the card's tracking data. This can be accomplished by launching the Payment Fragment with a readCardData only flag.

Please include the following code in your activity:

private void launchPoyntPayment() {
    Locale locale = new Locale("en", "US");
    String currencyCode = NumberFormat.getCurrencyInstance(locale).getCurrency().getCurrencyCode();

    Payment payment = new Payment();
    String referenceId = UUID.randomUUID().toString();
    payment.setReferenceId(referenceId);
    payment.setCurrency(currencyCode);
    // the flag that tells Payment Fragment to read card data only and not attempt a transaction
    payment.setReadCardDataOnly(true);

    // start Payment activity for result
    try {
        Intent collectPaymentIntent = new Intent(Intents.ACTION_COLLECT_PAYMENT);
        collectPaymentIntent.putExtra(Intents.INTENT_EXTRAS_PAYMENT, payment);
        startActivityForResult(collectPaymentIntent, COLLECT_PAYMENT_REQUEST);
    } catch (ActivityNotFoundException ex) {
        Log.e("ConfigurationTest", "Poynt Payment Activity not found - did you install PoyntServices?", ex);
    }
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (resultCode == Activity.RESULT_OK) {
        if (data != null) {
            Payment payment = data.getParcelableExtra(Intents.INTENT_EXTRAS_PAYMENT);
            Log.d("ConfigurationTest", "Received onPaymentAction from PaymentFragment w/ Status:" + payment.getStatus());

            if (payment.getTransactions() != null && payment.getTransactions().size() > 0) {
                Transaction transaction = payment.getTransactions().get(0);
                Log.d(TAG, "READ CARD DATA: " + new Gson().toJson(transaction, transactionType));
            }
        }
    } else if (resultCode == Activity.RESULT_CANCELED) {
        // prompt merchant to re-swipe
    }
}

Please note that when Payment Fragment comes up and it has no amount, and the operation type is displayed as READ CARD.

Read Card

The transaction object returned after a readCardOnly operation should be similar to the one below:

{
    "action": "SALE",
    "amounts": {
        "currency": "USD",
        "orderAmount": 0,
        "tipAmount": 0,
        "transactionAmount": 0
    },
    "authOnly": false,
    "fundingSource": {
        "card": {
            "numberFirst6": "197610",
            "numberLast4": "8554",
            "track1data": "B1976100999009668554^GETI^10010000000000000",
            "track2data": "1976100999009668554=10010000000000000"
            "track3data": ""
        },
        "emvData": {
            "emvTags": {
                "0xD3": "",
                "0xD2": "313937363130303939393030393636383535343d3130303130303030303030303030303030",
                "0xD1": "42313937363130303939393030393636383535345e474554495e3130303130303030303030303030303030"
            }
        },
        "entryDetails": {
            "customerPresenceStatus": "PRESENT",
            "entryMode": "TRACK_DATA_FROM_MAGSTRIPE"
        },
        "type": "CUSTOM_FUNDING_SOURCE"
    },
    "references": [
        {
            "customType": "referenceId",
            "id": "7a7b98a2-efc6-4623-8e85-39a0ab7aeb07",
            "type": "CUSTOM"
        }
    ]
}

# Error Handling

In case of any failure other than a processor decline, your transaction should return a PoyntError to the listener. To address this, we have added a list of applicable error codes that can be set by your application.

CODE_NETWORK_UNAVAILABLE
CODE_NETWORK_ERROR
CODE_NETWORK_CONNECTION_TIMEOUT
CODE_NETWORK_READ_TIMEOUT
CODE_UNAUTHORIZED
CODE_API_ERROR
CODE_API_SERVICE_NOT_AVAILABLE
CODE_LOST_CONNECTION_WITH_SERVICE
CODE_BAD_PARAMETER_PASSED
CARD_DECLINE
CARD_CANCELED
CODE_UNEXPECTED_EXCEPTION
Last Updated: 9/4/2023, 1:28:22 PM