Limit Handling

CoreWallet provides generic limit handling that can be used by business processes to define limits for certain events or transactions.

Limit examples

The following list shows a few possible limits that can be defined with the limit module:

  • The maximum number of failed login attempts per timeframe
  • The maximum number of wallet-accounts that can be created for a wallet
  • The minimum funding amount for a given currency
  • The maximum number of purchases in status 'in progress'
  • The maximum amount receivable via P2P-transactions per timeframe
  • The minimum account balance for a given currency
  • The maximum account turn over for a given currency in a given timeframe

Limit Groups

Limit Definitions can be grouped in Limit Groups, so that for specific wallets you can define non-standard limits. Wallets can be optionally assigned to a specific Limit Group, instead of using the subsidiary-default Limit Group.

Limit Definitions

The limit module provides Limit Definitions that define how a limit is determined for a given context.

The following parameters are used to determine if a Limit Definition applies to a given context:

  • Subsidiary - if set, the limit will only apply to wallets/accounts that belong to the given subsidiary
  • Limit group - if set, the limit will only apply when the wallet/account belongs to the given Limit Group
  • Custom type - if set, the limit will only apply for transactions with the given custom-type (which is an extension point)
  • Limit context - defines the transactional context that the limit applies to, e.g. funding a wallet. The set of limit context values can be extended to allow to define limits for product-specific transaction processes.
  • Currency - if set, the limit will only apply to transactions with the given currency
  • Wallet country - if set, the limit will only apply for wallets in the given country
  • Wallet type - if set, the limit will only apply for wallets with the given type (e.g. full or guest)
  • User verification level - if set, the limit will only apply for users that have the given verification level (e.g. unverified or verified)
  • Timestamp range - if set, the limit will only be applied if the current timestamp is in the given range

Each Limit Definition specifies the actual limit amount that some calculated value will be checked against, what type of limit should be checked (count, amount) and what timeframe the limit applies to (e.g. single transaction, or a timeframe). The timeframe can be either relative to the current timestamp (e.g. the last 24 hours) or semi-relative to the current timestamp (e.g. the current year).

Limit calculation

The Limit Definitions only define the limits, but the actual calculation is typically business process specific, and requires the implementation of several interfaces for each business process that wants to use the limit module.

The LimitCheckService is the main interface for calculating and checking limits. It has several relevant methods:

  • getLimitsViolated() - checks all applicable limits and returns the limits that are violated
  • checkLimits() - checks all applicable limits and throws an exception if at least one limit was violated
  • getLimitRangeSummary() - finds all applicable limits and returns an overview of the limit ranges that apply. This is typically used to determine the delta for a given value until a limit is reached (e.g. how much can the wallet still be funded this month?).

Each method takes a LimitDataProviderContext as input, which contains all required context data for finding relevant Limit Definitions and calculating actual limit values, e.g. the transaction amount, the wallet account and any additional business process specific data required for the calculation.

The LimitCheckService creates a LimitDataProvider instance based on the LimitDataProviderContext, and uses that to calculate all required limit values based on the relevant Limit Definitions. To be able to do this, each business process using the limit module registers a LimitDataProviderFactory at application startup.

Adding a new limit

The following steps should be taken to add a new limit, e.g. in a product-specific transactional process. In this specific example, we are going to build a limit for buying a Bitcoin in a transaction called 'ExchangeDeal':

  • Define the Limit Context e.g. as 'ExchangeDeal' - this makes sure that the Limit Definitions can be defined for this context
  • Provide a LimitDataProviderContext implementation that contains all required transaction data and services needed to calculate actual values. This typically includes the ExchangeDealDAO, the wallet account and the transaction amount.
  • Provide a LimitDataProvider implementation that is able to calculate actual values using several context parameters (e.g. timeframe). Typically the implementation subclasses BaseWalletLimitDataProviderImpl, which provides common calculation implementations for e.g. account balance calculations.
  • Provide a LimitDataProviderBuilder implementation that is able to instantiate a LimitDataProvider based on a LimitDataProviderContext.
  • Register the LimitDataProviderBuilder with the LimitDataProviderFactory so that the LimitCheckService can instantiate the LimitDataProvider when needed.
  • Adjust the ExchangeDealService to check the limits using LimitCheckService.checkLimits(), providing the wallet, limit context and a LimitDataProviderContext instance with all required transaction data and services. The method succeeds when no limits are violated, or throws a LimitViolationException if at least one limit is violated.
  • Create Limit Definitions via the Admin UI or via a DB script

In the following code examples, method signatures have been removed for brevity.

Code example for a LimitDataProviderContext for ExchangeDeals:

public class ExchangeDealLimitDataProviderContext extends LimitDataProviderContext {
    @Getter
    private final ExchangeDealDAO dao;
    public ExchangeDealLimitDataProviderContext(...) {
        super(walletAccount, dealAmount, dealAmountLeadCurrency);
        this.dao = exchangeDealDAO;
    }
}

Code example for a LimitDataProvider for ExchangeDeals:

public class ExchangeDealLimitDataProvider extends BaseWalletLimitDataProviderImpl {
    private final ExchangeDealLimitDataProviderContext context;
    public ExchangeDealLimitDataProvider(...) {
        super(context);
        this.context = context;
    }
    // Returns the number of exchange-deals for a given timeframe
    public int getCount(...) {
        return context.getDao().countExchangeDeals(context.getWalletAccount().getId(), fromTimestamp, untilTimestamp);
    }
    // Returns the effect that an exchange-deal has on the account-balance (either positive or negative)
    public Money getAccountBalanceChangeAmount(...) {
        if (limitCurrencyType == LEAD_CURRENCY) {
            return context.getAmountLC();
        } else {
            return context.getAmount();
        }
    }
    // Returns the total amount of exchange-deals for a given timeframe
    public Money getAmount(...) {
        if (limitPeriod == CURRENT) {
            if (limitCurrencyType == LEAD_CURRENCY) {
                return context.getAmountLC();
            } else {
                return context.getAmount();
            }
        }
        if (limitCurrencyType == LEAD_CURRENCY) {
            return context.getDao().sumDealAmountLeadCurrency(context.getWalletAccount().getId(), fromTimestamp, untilTimestamp);
        } else {
            return context.getDao().sumDealAmount(context.getWalletAccount().getId(), fromTimestamp, untilTimestamp);
        }
    }
}

Code example for a LimitDataProviderContextBuilder for ExchangeDeals:

public class ExchangeDealLimitDataProviderBuilder extends LimitDataProviderBuilder {
    // Returns the LimitContexts that this builder will be used for
    public Collection<String> getLimitContexts() {
        return Arrays.asList("ExchangeDeal");
    }
    // Returns a new LimitDataProvider for the given LimitDataProviderContext
    public LimitDataProvider create(...) {
        return new ExchangeDealLimitDataProvider(context);
    }
}