1. Documentation
  2. AppFlow
  3. Introduction
  4. Guides

Guides


Topics

Transaction Details

Transactions in AppFlow are a bit more complex than most SDKs due to the support for split transactions and the ability for multiple applications in a transaction to pay portions of the requested amounts.

Transaction models

When a Payment is received by FPS, it will create an initial Transaction instance representing the total requested amounts. For non-split scenarios, this will be the only Transaction instance created. For split scenarios, a new instance of Transaction will be created for each split, reflecting the amounts the split application has requested.

The Transaction represents the flow journey from PRE_TRANSACTION to POST_TRANSACTION, during which a number of flow services may add amounts, pay amounts, etc. For each application that is called throughout a transaction, a request that reflects the current state and is appropriate for that stage is created. All transaction stages receive a TransactionRequest object except for POST_TRANSACTION, which receives a TransactionSummary object.

Any stage that receives a TransactionRequest has the ability to indirectly generate a TransactionResponse. Eligible flow services may do so via paying off amounts, which is the case for example loyalty applications and payment applications. Others, like in the PAYMENT_CARD_READING stage, may lead to a declined transaction. In addition, a split application, may in the SPLIT stage, pay off an entire Transaction in one go. Typically via offering a customer the ability to pay via cash.

Transaction ids

It is important to know what ids to use for what as there are a number of functions that require applications to read or specify ids:

  • For multi-stage applications to track a transaction through the stages
  • To create id-based requests such as reversals
  • To look up the correct responses for a given request

The following unique ids are generated, all available via the getId() method for each model:

  • One for each Payment and PaymentResponse pair to represent that payment
  • One for each Transaction, which uniquely identifies that transaction throughout all the stages,
  • One for each TransactionRequest and TransactionResponse pair (where a response is applicable)

In addition,

  • The TransactionSummary.getId() will return the id of the Transaction it represents (as it extends it)
  • Each TransactionRequest has a getTransactionId() method that represents the Transaction that it was created for, allowing flow services to identify the same transaction throughout unique requests for different stages

Dealing With Amounts

Dealing with money and all possible currencies can be quite a complex task. The AppFlow Amounts object can help you deal with money within flows in a simple and standardised way.

Amount units and currencies

To keep the AppFlow models as simple as possible, money is represented as follows

  • Amount value as a long representing the base unit for that currency
  • An associated ISO-4217 three-letter currency code

ISO-4217 defines currencies via a currency number, code and more importantly in this case, the fraction relationship between the main unit and a sub-unit, if one exists. The sub-unit is what you would know as cents, pence, and so on. Not all currencies, like the Japanese Yen, have any sub-unit. See ISO-4217 Definitions for a list of currencies in this format.

Most currencies, such as the US Dollar, British Pound, EURO, etc all have a sub-unit with a fraction of 2, meaning that in the case of US Dollar, 10 dollars equals 1000 cents.

The amount value in AppFlow is defined as the main unit times the sub-unit fraction, so in the example above, a client would pass 1000 (10 * 100) for the currency code USD.

long amountValue = 1000;
String currencyCode = "USD";
Currency currency = Currency.getInstance(currencyCode);
int subUnitFraction = currency.getDefaultFractionDigits(); // This will return 2 for USD
BigDecimal amount = BigDecimal.valueOf(amountValue).movePointLeft(subUnitFraction);
String readableAmount = currency.getSymbol() + amount.toString(); // "$10.00

It is up to each application to manage the currencies correctly, which is fairly easily done with the help of the Java Currency class with which you can query for the sub-unit fractions. The snippet below shows how you can find out what the fraction is, and via BigDecimal easily represent that for purposes such as retrieving a human readable representation, etc.

Amounts structure

The Amounts class consists of three main entities

  • A base amount
  • Additional amounts
  • Currency

In addition, it contains the following fields for supporting currency conversions

  • Currency exchange rate
  • Original currency

The baseAmount represents the actual value of goods and services. If a basket is set, the baseAmount will reflect the basket total value. On top of this base amount, there may be additional amounts for scenarios such as

  • tipping
  • cashback
  • fees
  • charity donations
  • etc

See Additional Amounts for a list of defined identifiers.

The total amount (Amounts.getTotalAmount()) will reflect the base amount + all additional amounts.

// Input amounts
Amounts amounts = new Amounts(1000, "EUR");
amounts.addAdditionalAmount("tip", 200);
amounts.addAdditionalAmount("cashback", 50);
amounts.addAdditionalAmount("charityDonation", 100);

// Extract relevant amounts
// baseAmount now includes base and charityDonation, but not tip and cashback
long baseAmount = amounts.getTotalExcludingAmounts("tip", "cashback");
long tip = amounts.getAdditionalAmountValue("tip");
long cashback = amounts.getAdditionalAmountValue("cashback");
long total = amounts.getTotalAmountValue();

There may be scenarios (often in payment applications), where some additional amounts are supported natively in that environment, but not others. As an example, many payment hosts/gateways supports setting tip and cashback as individual fields in the host message, but would rarely support something like charity donation. For these cases, the getTotalExcludingAmounts(String... amountsIdentifiers) is a useful method to help calculate a base amount that includes some additional amounts. Below is an example that reflects the above scenario

Zero base amount value

Due to the ability for flow services to pay off amounts and that AppFlow does not block zero-based requests, it is possible that the TransactionRequest amounts will have a base amount value of zero. For payment applications specifically, applications will never be called if the total if zero, but if the base amount is zero and there are additional amounts left to pay (such as tip or fees), then your payment application will still be called and must be able to handle this scenario.


Using Basket

AppFlow has native support for baskets, used to represent the goods and services that form a transaction.

The Basket model represents a single basket which consists of a name, a list of BasketItem and optionally additional associated data. A basket will be flagged as a primary basket if it was provided by the initiation (POS) application. Flow services may also add additional baskets that are considered as secondary baskets.

Basket concepts

Before we get into building or parsing the basket, it is important to understand some of the less trivial concepts of it. The basket itself is very simple and mainly consists of a list of basket items. The basket items have the expected data associated with it, such as id, label, category, quantity and amount. In addition, it has a few other features discussed below.

Quantity vs Measurement

The quantity field in the basket item can be used to set a whole number quantity, which is sufficient for most cases. However, if the item in question is measured in some form with an associated unit, a floating point representation may be required together with a unit definition, such as "1.25 kilograms".

AppFlow has a Measurement class to represent this scenario which can be associated with a basket item.

Both quantity and measurement can be set individually to represent cases such as "2 x 1.25 kilograms of Sand" as an example.

Item Modifiers

There are cases where an item may require associated information, such as tax information, extras, deductions or discounts. For these purposes, BasketItemModifier instances can be created and added to the item.

A BasketItemModifier has five primary fields;

  • id : optional identifier
  • name : mandatory name/label of the modifier, such as "Value Added Tax" or "Cheese"
  • type : mandatory type defining what type of modifier it is - see here for examples
  • amount : absolute amount value of the modifier in subunit form, such as 150.0f
  • percentage : the percentage value of this modifier as applied to the item amount, such as 2.5f for 2.5%

Either amount or percentage must be set - not both are required.

The basket item amount field represents the total amount for the item, inclusive of any modifiers. The basket item baseAmount field however represents the item amount value without any modifiers applied.

Below is an example to illustrate this;

  • Basket consists of a Burger basket item - the burger costs $10.00
  • The Burger item has an "extra" modifier that represents added cheese to the burger, so "+ Cheese $0.50"
  • The item amount value for this would be $10.50
  • The item baseAmount value for this would be $10.00

Note that modifiers can also reduce the amount, such as deductions or discounts. In these cases, the amount may be less than the baseAmount.

Additional item data

Additional item data can be associated with the item via the itemData. As per all other additional data in AppFlow, this allows for any arbitrary data to be set defined by a key.

Building the Basket

Basket basket = new Basket("myBasket");
basket.addItems(<your basket items>);

The basket requires a basket name to be constructed, and optionally a list of basket items. The items can be added after construction.

There are various useful utility methods in the basket class for clients that provide a basket.

  • hasItemWithId / getItemById can be used to check if the basket already has an item with the provided id and retrieve it if so
  • incrementItemQuantity / decrementItemQuantity can be used to increment or decrement the quantity of an item in the basket by item id
  • removeItem / clearItem can be used to remove an item by id or remove all items
  • addAdditionalData can be used to associate arbitrary data with the basket

Constructing basket items

BasketItem basketItem = new BasketItemBuilder()
        .withId("fruits-123456")
        .withLabel("banana")
        .withCategory("fruit")
        .withQuantity(2)
        .withAmount(200) // Can be negative for discounts, etc
        .build();

A BasketItem can be constructed via the BasketItemBuilder class. A basket item can optionally be instantiated with an id (typically SKU). If an id is not set, a unique id will be generated automatically. Below is a trivial example of a basket item (without modifiers) being constructed. The only mandatory data to set for a basket item is the label.

A basket item can represent goods and services, but also discounts and deductions. For instance a rewards program may provide a free coffee, in which case there may be items like;

  • 1 Latte @ $3.00
  • 1 Reward Free Latte @ -$3.00

The basket total for the above would be 0.

The following sub-sections will cover some more advanced features of the basket items. All references to methods with the prefix with, are in relation to the BasketItemBuilder class.

Quantity / Measurement

If an item required a measurement as described in the concepts section, you can use the withMeasurement(float value, String unit) to specify this, such as 1.25 kilograms. You can also specify the quantity separately, allowing definitions such as 2 x 1.25 kilograms of "Sand" for instance.

If you application represents quantity as a floating point internally, you may want to look at the withFractionalQuantity(float quantity, String unit) method, which is a convenience method that allows passing a floating point quantity with an optional unit value. It will set the item quantity and measurement according to these rules;

  • If a unit is set (not null), the provided quantity and unit is set as per the withMeasurement() method and the quantity defaults to 1 (you can still override and set the quantity separately if required)
  • If no unit is set, then
    • if the quantity is a whole number (no fractional part), then it is set as per withQuantity()
    • if the quantity does have a fractional part, an exception is thrown as we do not allow a measurement to be set without an associated unit
Item modifiers

BasketItemModifier instances can be created via the BasketItemModifierBuilder and added to the item together with a base amount via the different withBaseAmountAndModifiers() methods.

The withAmount(long amount) may not be set when this approach is used - any attempt to do so will throw an exception.

If you are using item modifiers, the rounding of the basket total becomes relevant. You can specify a rounding of up, down or nearest via the Basket.setRoundingStrategy(RoundingStrategy) method. This is then taken into account when calculating the basket total from all the items.

Parsing the basket

Depending on the flow and what models are being interacted with, there can either be a single basket or multiple baskets. The latter is the case when flow services in the flow provide additional baskets, typically as part of an upsell or similar.

It is possible to check whether a particular basket was defined by the initiating (POS) app or not via the isPrimaryBasket() method.

Below are a few note-worthy basket methods to review the basket contents;

  • getBasketItems() - retrieve the list of BasketItem entries
  • getBasketName() - get the name of the basket
  • getUniqueNumberOfItems() - get the number of unique items (not taking quantities into account)
  • getTotalNumberOfItems() - get the total number of items (taking quantities into account)
  • getTotalBasketValue() - get the total value of the basket in subunit form
  • getAdditionalBasketData() - get any additional data associated with the basket

Parsing the basket items

A BasketItem will typically have the below data set via their mentioned getters;

  • getId() - a client defined (SKU typically) or uniquely generated id that that item
  • getLabel() - the label for the item, such as "Tomato"
  • getCategory() - the category the item belongs to, such as "Vegetables" (optional)
  • getQuantity() - the quantity of this item, such as 5 (whole numbers only - can be 0 or positive)
  • getIndividualAmount() - the cost/value of a single item (can be negative, zero or positive)
  • getTotalAmount() - the cost/value of the item with quantity in mind (individual x quantity) (can be negative, zero or positive)

In addition to the above fairly trivial data, there are also some more advanced concepts.

Quantity / Measurement

As mentioned earlier, AppFlow has a Measurement class to represent scenarios where an item is measured by some unit and you can check if a basket item has an associated measurement via the hasMeasurement() method, and if there is, retrieve it via the getMeasurement() method. Note that a unit is mandatory for this class.

Both quantity and measurement can be set to represent cases such as "2 x 1.25 kilograms of Sand" as an example.

Item Modifiers

You can check if any modifiers have been set via the hasModifiers() method. If there are modifiers, you can retrieve the list via getModifiers().

Note that the id of the modifier is optional and may not be set.

The type can be used to determine what type of modifier it is. The AEVI defined types are listed here.

As not both the amount and percentage are mandatory, you must check which is set. Both values are boxed and as such you can simply check if (modifier.getAmount() != null) or if (modifier.getPercentage() != null) to see which one is set.

Note that the amount is represented as a floating point to allow for cases where a percentage is applied and it does not end up as whole number. In certain situations, adding the individual values up will then have an impact of the final rounding. You can get the the value as a float via the getFractionalAmount() if that is required.

Both amounts and percentage can be negative, zero or positive values. The positive values adds cost to the item and the negative values subtract cost from the item.

Handling Card Tokens

Using card tokens to identify a payment card and from that the customer, is a useful way to make the point of sale experience smoother for a customer.

AppFlow supports various ways for payment services to supply card tokens and various ways for value added services and POS applications to retrieve these card tokens. The ideal scenario is for a card token and/or customer details to be available from the post-card-reading stage to allow value added services (such as loyalty schemes) to offer rewards or use of loyalty points before the transaction processing stage.

Payment card reading stage

AppFlow supports a dedicates payment card reading stage in its payment flows. Assuming the payment service and/or the acquirer/host supports generating a token separately, this would be the preferred way to provide a value added service in the post card reading stage with a token. The card details will be passed down to all services in the flow and be available for the POS application in the final response.

Tokenisation request

A POS application can initiate a dedicated tokenisation request and provided that there is a flow and a payment service that supports this, retrieve a card token that way. The POS app can then use this when initiating a payment in two ways;

  • Setting it as the cardToken in the Payment model
  • Assigning it to a Customer and pass that in the Payment model

Generally speaking, the first approach here is intended for use by payment services for scenarios such as subscriptions that may be linked to a card token. It is however available for use by any flow service. The second approach is the preferred option when there is a representation of customer data and is more useful for value added services.

Transaction processing response

The payment service (possibly via the acquirer host) can provide card details, including a token, in the TransactionResponse created during transaction processing.

This provides any post-transaction flow services with access to card details and the POS app can retrieve it from the response once the flow is completed.

Summary

The latter two approaches put a lot more responsibility onto the POS application, as it has to store tokens and/or customer details in order to allow value added services to access the token before transaction processing.

Using Additional Data

Many of the AppFlow data models contain an AdditionalData object. This object is essentially a bucket for storing any kind of data. This AdditionalData will be passed around with the primary flow data and can be accessed for read and write by flow services.

Adding data

Simple data types and complex objects can be stored. However, regardless of the object type stored it must be able to be serialised to JSON. Primitive types String, int, and boolean are supported and will be automatically converted. Arrays of the same type of data can also be stored.

additionalData.addData("myExtra", "ext"); // store a String
additionalData.addData("myBoolean", true); // store a boolean
additionalData.addData("myInt", 42322); // store an int
additionalData.addData("myLong", 7736663L); // store a long

Data is added using the addData method along with a key that they should be stored against.

long myLong = additionalData.getValue("myLong");

To get the data stored back at a later point the getValue method will return it automatically converted back to the original type. e.g.

String str = additionalData.getValue("myLong");
// str above will be null

If an attempt is made to get the data back as a different type then a null/empty value will be returned e.g.

Helper methods are available to return primitive String, int and boolean getStringValue, getIntegerValue and getBooleanValue. Each of these can also optionally include the passing of a default if the key (of the correct type) is not found.

Complex objects

If a complex object is added to the additional data then when it is passed between flow services it will be serialized to JSON and stored in the data along with a field indicating the object type. Therefore, any object stored must be capable of being serialised. Generally speaking the objects stored should be simple POJO objects. You may also wish to use our own Jsonable interface to mark the class as able to be converted to/from JSON (for more details about Jsonable see our json-utils repo). If this object is to be passed between one or more flow services they must both have access to the class in there classpath in order that it can be de-serialised correctly.

Customer customer = new Customer("123");
additionalData.addData("customer", customer);

// reading the customer object back
Customer customerRet = additionalData.getValue("customer", Customer.class)

Complex objects are stored in the same way a primitive types and retrieved by adding the class to the getValue method e.g.

Dog dog = new Dog("rover");
additionalData.addDataWithType("myPet", dog, Animal.class);

If you need to store your object as it own super type and not the actual subclass you can specify the type it is stored as using the addDataWithType method.

Arrays

additionalData.addData("myArray", "hello", "bye", "hi", "goodbye");
String[] myArrayVals = additionalData.getValue("myArray", String[].class)

Arrays can be simply stored and retrieved as above.

Extra finders and utilities

Get all data by type
additionalData.addData("one", 1);
additionalData.addData("two", 2);
additionalData.addData("three", "hello");

Map<String, Integer> dataOfType = additionalData.getDataOfType(Integer.class);

The getDataOfType method will scan an entire AdditionalData object for all instances stored of the type given.

The code above will return a Map containing two integers

Get classname of data

Occasionally it may be useful to know the class type of a data key before attempting to get it. This can be done by using the getValueClassName simply passing in the key you want to know the class type of, which will be returned as a String.

Cleaning up

Various clear, remove methods are also available to delete and remove keys from the additional data.

Handling tax

Tax is applied to goods and services around the world and requires special attention in some regions. AppFlow itself does not require or make use of tax amounts as a concept, but supports it being defined where required. Note that in general all amounts are inclusive of tax in AppFlow. This is the case for the Amount and Amounts classes, as well as the amount in BasketItem, etc. Any tax information provided is simply a breakdown of what tax is applied to those amounts.

Tax information can be applied in two ways - TaxInfo added to additional data for any model that supports this, or as a modifier to a basket item. Note that the TaxInfo is part of the api-constants library (v2.2.1+) and not the core AppFlow APIs.

Providing tax information

If your application is providing amounts and/or a basket, it may also be relevant to attach tax information.

Tax information can be applied either to the overall request (which is usally a Payment) with the use of TaxInfo, or per individual basket item with the use of modifiers.

If you are providing a basket, it is always recommended to do this per basket item if possible, as this allows for different tax rates to be applied on an item basis.

Applying TaxInfo

float taxAmount = 5200.0f;
TaxInfo taxInfo = new TaxInfo(taxAmount, new TaxRate(0.20f, "VAT"), new TaxRate(0.05f, "Sugar"));

Both the taxAmount andtaxRates are optional - a client can set what is available to them. The taxAmount represents the absolute tax amount, whereas a TaxRate indicates at what rate a specific type of tax is applied at, such as 20% VAT/GST. Multiple tax rates can be added to a single TaxInfo.

paymentBuilder.addAdditionalData("taxInfo", taxInfo);

To set in a Payment,

request.addAdditionalData("taxInfo", taxInfo);

If the tax info is relevant for a generic request or status update,

Per BasketItem

Basket item supports the concept of modifiers as explained in the basket docs. Any number of modifiers may be added to the item.

BasketItemModifier basketItemModifier = new BasketItemModifierBuilder("Value Added Tax", "tax")
  .withAmount(500)
  .withPercentage(20.0f)
  .build();
basketItemBuilder.withModifiers(basketItemModifier);

Either the absolute amount or the percentage must be set, but not both are required. If you have both values, it is recommended you set both. The amount can be set as a whole number or as a floating point (via withFractionalAmount().

Reading tax information

As the tax information can be set in various ways, and different data can be available, it is important that your application checks for this properly.

Payment Flow

if (transactionSummary.getAdditionalData().hasData("taxInfo")) {
  TaxInfo overallTransactionTax = transactionSummary.getAdditionalData().getValue("taxInfo", TaxInfo.class);
}
Basket basket = transactionSummary.getPrimaryBasket();
if (basket != null) {
  for (BasketItem basketItem : basketItems) {
    List<BasketItemModifier> taxModifiers = getTaxModifiers(basketItem);
    if (!taxModifiers.isEmpty()) {
      // Check all modifiers and ensure to check for amount OR percentage being set
    }
  }
}

private List<BasketItemModifier> getTaxModifiers(BasketItem item) {
  List<BasketItemModifier> taxModifiers = new Arraylist<>();
  if (item.hasModifiers()) {
    for (BasketItemModifier modifier : item.getModifiers()) {
      if (modifier.getType().equals("tax")) {
        taxModifiers.add(modifier);
      }
    }
  }
  return taxModifiers;
}

This assumes that you are a flow service with access to either the Payment, TransactionRequest or TransactionSummary models. The sample below uses the TransactionSummary as an example but the same mechanism can be applied to all three models. See basket docs for more details on the basket items and basket modifiers.

Generic Flow

if (request.getRequestData().hasData("taxInfo")) {
    TaxInfo overallRequestTax = request.getRequestData().getValue("taxInfo", TaxInfo.class);
}

Parsing TaxInfo

if (taxInfo.getTaxAmount() != null) {
    float taxAmount = taxInfo.getTaxAmount();
  // Absolute tax amount for overall request OR basket item (depending on where it came from)
}
if (!taxInfo.getTaxRates().isEmpty()) {
  for (TaxRate taxRate : taxInfo.getTaxRates()) {
    // taxRate.getLabel(), taxRate.getRate());
  }
}

This assumes access to a TaxInfo instance via one of the methods above.

App Filtering

When FPS receives a request, it will match the request data against the service info details provided by each application defined in the active flow to ensure only eligible applications are called for that request.

If there is a single match after filtering, that application will be automatically selected. If there are more than one match, a dialog will be shown to allow the operator/merchant to select. If there are no matching applications, an error will be returned to the calling POS app.

Payments

Payment requests are filtered against application manifest data and the PaymentFlowServiceInfo data as follows

  • Enable state -> Is the flow service installed and enabled on the device
  • API version -> Is the flow service integrated with the same major API version as FPS
  • Flow stage -> Does the flow service have an entry point for the current flow stage defined or not
  • Flow type -> Does the flow service support the flow type
  • Currency -> Does the flow service support the request currency

If any of these checks fail, the flow service will be deemed ineligible for handling the request in the current flow stage.

In addition, for the TRANSACTION_PROCESSING stage, a check will be made to ensure only the relevant payment application is called for cases where a card token has been specified.

Generic requests

For generic Request cases, the following data is used for filtering

  • Enable state -> Is the flow service installed and enabled on the device
  • API version -> Is the flow service integrated with the same major API version as FPS
  • Flow stage -> Does the flow service have an entry point for the current flow stage defined or not
  • Request type -> Does the flow service support the flow type, or has it defined as a custom request type
  • Target app id -> If the request specified a flow service, and the above criteria is met for that flow service, it will be selected automatically