Following document contains documentation of Debt Manager smart contract and its related components.

Debt manager is currently being implemented as supervisor smart contract supervising main account and pocket accounts of a customer.

main account is the place where all the debts are recorded and which repays all the debts when having enough funds.

The list of fees, debt types, addresses and internal accounts currently used in debt manager is not final and debts can be added / changed according to product specifications.

Debt management

All the unpaid debts are stored on different account addresses - buckets based on their priorities. When there are incoming funds on the main account the buckets are paid from the most priority ones. The next bucket is paid only if the buckets with higher priority are fully paid.

When the funds are sent from the bucket, they are handled by the logic of the destination. E.g. the loan bucket can pay installments from the oldest one, etc. However, there is no log of the particular transactions inside the bucket, only the balance is used.

The repayment logic depends on the number of buckets. E.g. for the personal loan, we can have one or three buckets - for the whole loan or independent ones for fees, interest, and principal. If we have one bucket per loan, we can pay the fees, interest, and principal on an installment basis (but we cannot put any other bucket between these three types of payments since we are using one bucket only). If we have three, we need to pay first all the fees, then all the interest, and all the principals at the end (but we can include other buckets between them based on priority).

Simplified system architecture can be displayed as following:

State flow diagram of high level debt manager’s functionality:

Debt manager’s responsibilities:

  1. Processing a claim payment (anything that is supposed to be paid for now (fees, installments, ..) and might be recorded as debt if not enough funds)

  2. Recording debts

  3. Paying off debts (and allocating incoming money primarily for debts repayments)

Debt manager functionality

Debt manager currently consists of debt manager supervisor smart contract performing all debt management logic and customer and internal vault accounts structure that record current level of debts.

Internal accounts structure

Each possible debt type has its own internal accounts representation (to be able to distinguish between debts when applying prioritization during repayment and for reporting purposes):

  • internal account for current total debt amount in bank - {debt_type}_UNPAID_INTERNAL

  • internal account for current total paid off fees / debts - {debt_type}_PAID_INTERNAL

    • IMPORTANT NOTE: currently we support also option to have customer accounts as a target account for debt payment. A good example is loan debts, which are not being paid directly to an internal account, but are “paid off” to the loan account, where the paid debt is additionally redistributed into individual installments. More information about this will be further in this document.

  • address on individual customer’s account used to record current level of debt for each customer

As a rule of thumb, total sum of all customer’s debts of selected debt_type (on individual customers debt addresses) is equal to current balance on {debt_type}_UNPAID_INTERNAL account.

Debt manager functionality

1. Claim payments handling

When paying any required or scheduled fee / penalty, debt manager is responsible for either marking the payment as done or remembering the debt of unsuccessful payment. Within debt manager, fee / penalty that is supposed to be paid is marked as “claim”. In case that fee is not sucessfuly paid (customer doesn’t have enough funds to process the payment), claim becomes a debt and is later paid off according to its priority when funds more arrive. 
Claim payment is always accepted (if being in correct format and has existing claim type specified), therefore if customer does not have enough funds on their account, it causes the balance to become negative. Debt manager then handles negative amount rebalancing and debt remembering.

1.a Success scenario

Debt manager smart contract is triggered by CLAIM_PAYMENT posting (as specified in payment types - therefore, if you need your service to perform any fee / scheduled payments and want the potential debt be managed by debt manager, given payment scheme must be followed). Each fee payment, that needs to be managed by debt manager, must be moving funds from DEFAULT address on debt manager* account to given {claim_type}_UNAPID_INTERNAL account DEFAULT address, where claim_type is the type of fee / penalty that needs to be paid (equal to debt_type). Current list of supported fee payments is written below in Debt accounts and addresses section.

After receiving and validation of the claim payment (which moved funds from main account to unpaid internal account), successful scenario is triggered by verifying that there were enough funds on the default address of main account (it did not became negative) to actually perform the payment and therefore fulfil its claim. The payment is then finished by moving funds from {claim_type}_UNPAID_INTERNAL to {claim_type}_PAID_INTERNAL account.

1.b Fail scenario

Unsuccessful scenario starts in case there are not enough funds to fulfil the claim - and to pay the fee without falling into negative balance on customer’s main account. Since the original payment, which moved funds from main account to {claim_type}_UNPAID_INTERNAL account, caused the balance on main account to fall into negative, we need to rebalance that. Rebalancing is performed by “borrowing” money from debt’s address on customer’s account, dedicated to record debts of given debt type for current customer. The debt also stays marked down in _UNPAID_INTERNAL account, until debt manager manages to pay it off with future incoming funds.



If the original default account balance was higher than 0, there might be still at least some funds to partially repay the debt immediately. In that case, those funds are used to at least partially repay the new debt: given amount is returned back from default address to customer debt address, and the same amount is then moved from {debt_type}_UNPAID_INTERNAL to {debt_type}_PAID_INTERNAL account. Customer account balances is then 0.

When it comes to partial debt repayment, customer can also have an overdraft or pockets available (with potential funds). When entering a debt state, the business requirement is to try to use overdraft for immediate debt repayment for allowed debt types, and use funds from pockets to (partially) repay the remaining debt if possible. The whole process of paying of debts from overdraft / pockets is represented as following diagram shows:

2. Debt repayment

When being in debt, customer’s account balance is 0. As soon as new funds are received to the account, they are immediately used to repay the debts (or as much of the debts as possible, while considering the debt repayment priority and being able to repay the debt partially). Debt repayment process is as following:

  • iterate over customer’s current debts according to their priority

  • keep repaying the debts one by one by their priority while there is still enough money from the incoming funds posting

  • either pay off all the debts and keep the remaining money on the default account, or repay as much debt as possible (also partially)

3. Overriding debt repayment priority - direct debt repaying payment

Customer is able, under very specific condition, to (partially) repay selected one debt (even if there are current debts with higher priority). When being in debt, customer can forward new incoming money to specific debt. Debt manager then allows to repay the selected debt, given that the incoming funds payment is in specific format, having override_debt_payment in instruction_details specified.

How to try debt manager:

  • Run your own simulation scenarios, inspired by current simulation tests (tm_contracts/smart_contracts/debt/stests)

  • Demo debt manager script simulating some payments - tm_contracts/smart_contracts/utils/debt_manager_demo.py

  • Send postings to debt manager account deployed to Thought machine in formats as specified in this documentation

Debt manager smart contract implementation

Debt manager is currently under implemented and further developed as supervisor smart contract, which supervises main_account and therefore extends its functionality to debt management as specified in original debt manager specification. The whole debt manager logic is written in supervisor post_posting_hook, which overrides post_posting_hook of main_account.

Currently supervised smart contracts:

  • main account (with overridden post posting hook logic, and access to balances and parameter values)

  • pocket (no logic is overridden, we only need to access customers' pockets balances and parameter values when repaying debts from pockets, to see amount of money available for debt repayment)

IMPORTANT NOTE:

  • for debt management, a smart contract does not need to be added to the supervision! The integration between debt manager and other services / products / postings is done by sending postings to main account and listening to postings from main account, for which the other smart contract does not need to be under supervision. You only need to add an account to the supervision if you want it to be used as a source for debt repayments.

Main account smart contract changes

In order for main account to be able to work with debt-related payments in specified way (especially claim postings and override debt postings, as specified further in this document), necessary changes are implemented to main account pre-posting hook:

  • in case of claim payment, we allow to go into negative balance on main account’s default address. This is due to fact that this negative balance will serve as an indication for debt manager that we have fallen into debt and could not pay for original fee, and debt manager will rebalance this to 0 later.

  • in case of incoming / outgoing debt related payments (postings), main account performs basic validation of correct postings format - if given claim / debt type exists and if we are not trying to pay off bigger debt than actual debt (see common_debts_utils.verify_debt_payments(vault, postings), which is called in pre_posting_code hook of the main account). Although originally these checks were a part of debt manager pre_posting_hook, due to working with Vault version which does not support overriding / extending pre_posting_hooks behaviour (only post_posting_hooks), and due to debt payments being made from / to main_account only, it made sense to integrate them into main account as common debt-related functionality.

New debt repayment from overdraft

If current claim payment causes an account to fall into negative balance (and debt is created), in case of any available overdraft, the new debt is immediately repaid also with funds from unused overdraft (if any, possibly partially, up to the amount of debt / available overdraft).

The overdraft rebalancing logic is implemented in post_posting_code hook of main account (it is the same logic as for the normal overdraft usage, which is shared for both main account and debt manager). After identifying that a payment has caused a negative balance on the main account, we try to check if the payment that caused the negative balance was allowed for the overdraft and if we have some overdraft available, if yes, then we rebalance the payments from overdraft.

Only certain types of debts are allowed to be repaid from overdraft. The current list of the allowed debts is set in a global parameter in thought machine overdraft_allowed_debt_types_global_param_resource and currently for dev purposes is set to:

  "MAIN_ACCOUNT_SUBSCRIPTION_FEE", 
  "LOAN_PENALTY",
  "OVERDRAFT_FEE",
  "OVERDRAFT"

loan_penalty might need to be removed for the MVP.

Changing list of debts allowed for overdraft repayment

If you need to change the list of debt types, which are allowed to be repaid from overdraft, you need to change the value of the overdraft_allowed_debt_types_global_param_resource global parameter.

You can do this directly via API, use /global_parameter_values endpoint to create a new value for the param. However, the preferred way due to maintainability is to do this via CLU deployment pipeline:

  • go to /tm-contracts/smart-contracts/deploy/resource_templates/global_parameter_values.resources.yaml file in repo

  • add a new GLOBAL_PARAMETER_VALUE resource:

      - type: GLOBAL_PARAMETER_VALUE
        id: <global_param_id_or_custom_id>_global_param_value_resource
        on_conflict: SKIP
        payload: |
          global_parameter_value:
            global_parameter_id: <tm_global_param_id>
            value: 'new value'
            effective_timestamp: '@{code_files/deploy_effective_datetime}'
  • then add the resource if to manifest.template.yaml in the same subdirectory:

    resource_ids:
     - id: <global_param_id_or_custom_id>_global_param_value_resource
  • run ./deploy.sh (CLU deploy pipeline) to deploy the new values into Thought machine

New debt repayment from pockets

In case of falling into negative balance while performing claim payment (and creating a debt), if we were not able to fully pay the newly created debt from overdraft, we will try to (partially) repay the debt from pockets. This is being done by iterating over all unlocked and locked pockets, sorted by available amount of funds in descending order, and using the funds according to rules specified here to pay off the new debts.

The priority of repaying debts from pockets when having multiple locked and unlocked pockets is as following:

  1. Unlocked pockets first, ordered by amount of funds in them in descending order

  2. When taking funds from unlocked pockets, we take interest with higher priority than principal (however this is implemented in details in pocket’s post_posting hook and might be subject to future changes)

  3. If there are no unlocked pockets left and we still don’t have enough money to repay the debt, go over locked pockets, ordered by amount of funds in them in descending order

Requirements on unlocked pockets:

  1. Pocket account’s pre_posting hook should accept outgoing transfers to its main_account.

  2. Maximum amount of money transferred in such posting agrees with pocket’s min_balance validation in pocket’s pre_posting hook (which is currently implemented as that min_balance on pocket after outgoing transfer is 0, and the maximum possible outgoing balance is pocket’s principal + interest).

  3. Postings that use pocket’s funds for debt repayment are following this scheme:

    account_id: <unlocked_pocket_account>

    account_address: DEFAULT

    credit: False

    amount: max(principal + interest)
    
instruction_details:  {
    
       “transaction_type” : “POCKET_DEBT_REPAY”,
    
       “account_id”: <pocket’s main_account_id>
,
           “debt_type”: <one of allowed debt types, as in global param "Debt types ordered by priority">
    
}

4. In pocket’s post_posting hook, rebalance moved funds on pocket’s principal and interest addresses according to interest / principal priority of usage for debt repayment (first move funds from interest, then from principal). In general, debt manager’s responsibility is just to take money from Pocket’s DEFAULT address to main account, any other necessary following steps on pocket’s side like rebalancing or running nay workflows are handled in Pocket’s post_posting hook.

Requirements on locked pockets:

The requirements on locked pockets match with the ones for unlocked pockets:

  1. Pocket account’s pre_posting hook should accept outgoing transfers to its main_account.

  2. Maximum amount of money transferred in such posting agrees with pocket’s min_balance validation in pocket’s pre_posting hook (maximum allowed money to transfer is also Pocket’s principal + interest here).

  3. Postings that use pocket’s funds for debt repayment are following this scheme:

    account_id: <unlocked_pocket_account>

    account_address: DEFAULT

    credit: False

    amount: max(principal + interest)
    
instruction_details:  {
    
       “transaction_type” : “POCKET_DEBT_REPAY”,
    
       “account_id”: <pocket’s main_account_id>
,
           “debt_type”: <one of allowed debt types, as in global param "Debt types ordered by priority">
    
}
    

4. Distribution of money taken from Pocket between pocket’s interest and principal address is solely handled by Pocket’s post_posting hook
5. When taking money from locked pockets, pockets should automatically become unlocked and Customer needs to be notified about such change.

To compute the available balance to be taken for debt repayment, debt manager uses pocket’s get_available_balance function, implemented on pocket side, which currently aggregates the balance of following pocket’s addresses:
- DEFAULT
- ACCRUED_INTEREST
- MONTHLY_INTEREST

Overriding post posting hook directives in supervisor

When supervising main account’s post_posting hook to extend it with debt management functionality, behaviour of original post_posting_hook is slightly changed:
- if there were any hook directives instructed in body of original post_posting_hook (in main_account), these are not automatically passed for execution, but rather passed as inputs to supervisor’s post_posting hook. It is then supervisor’s (and ours (smile) ) responsibility to make sure that all the original hook directives instructed in main_account are executed and the desired original behaviour of main account stays unchanged. We instruct all the original directives from triggering main_account in the supervisor’s post_posting_hook as the first thing that is executed there (and then add the additional debt manager logic).

List of hook directives covered:

The execution of hook directives is done by this call in post posting hook:

debt_utils.commit_hook_directives_from_supervisees(vault, postings, effective_date)

Main / pocket account integration in production and deployment

In order for customers' main accounts to work under debt managing supervision, we need to reference selected version_id of main account smart contract in supervisor’s settings. This serves as validation that given Supervisor code is able to supervise actual existing accounts, which are created from smart contract with given version_id. To do so, when writing supervisor smart contract, at the beginning for supervisor source code, add list of supervised smart contracts:

api = '3.10.0'
version = '0.2.18'


supervised_smart_contracts = [
    SmartContractDescriptor(
        alias='main_account',
        smart_contract_version_id='&{MAIN_ACCOUNT_VERSION_RESOURCE_PLACEHOLDER}',
        supervise_post_posting_hook=True
    ),
    SmartContractDescriptor(
        alias='pocket_account',
        smart_contract_version_id='&{POCKET_ACCOUNT_VERSION_RESOURCE_PLACEHOLDER}',
        supervise_post_posting_hook=False
    ),
]

This specifies that current supervisor version is able to supervise:

  • main account along with overriding the functionality of its post-posting hook, while the version of currently supported main account is dynamically inserted by CLU and set to the last deployed main account version

  • pocket account, but we do not override it’s functionality, we add pockets to supervision only to be able to access them to read their current balances and parameter values. Again, supervised pocket id is dynamically set by CLU pipeline.

Plans and supervisors

In production Vault, when creating a new supervisor, the supervision of existing (and new) main accounts does not happen automatically. The way how to apply supervision to actual accounts in TM is to create Plans, which are tied to supervisor version and assign supervised main / pockets account to them.

Before continuing with following documentation, please see more information on Plans here: https://documentation.tm3.sandbox.safibank.online/reference/plans/.


Plans are Vault’s mechanism (or approach) on how to map accounts to deployed supervisor logic. One plan represents one product, consisting of one main account and 0 to N pocket accounts, tied together by debt manager supervisor. Each customer has one plan for each main account they have + its connected pocket accounts (and because we can have only one main account for one customer, each customer has exactly one separate plan).

To make a plan for customer work and have a functional ecosystem of debt managed products:

  1. Deploy main / pocket accounts and supervisor

  2. Create a customer

  3. Create a new plan with current supervisor version
    IMPORTANT NOTE - currently deployed supervisor version is always in TM global parameter <PRODUCT_PREFIX>debt_manager_supervisor_version_id - where PRODUCT_PREFIX is currently either debug_ or test_ (but can be specified as anything as well as nothing).

  4. Create customer’s main account and assign it to the plan

  5. Create customer’s pockets and assign them to the plan
    More information on this as well as with examples will be added further to this document.

This behaviour is currently implemented in account-manager as a automated part of account opening workflow.


Supervisor updates


When adding debt manager as supervisor smart contract, after creating a new supervisor smart contract version / updating a supervisor, we need to migrate the existing plans to the new supervisor version

Kafka events

Debt manager is also able to send specific events to Kafka, that inform about debts creation or repayment. Current scenarios which trigger an event being sent to Kafka, under topic vault.api.v1.workflows.workflow_instance.create.requests :

Kafka context event type

When

1

NEW_DEBTS_CREATED

Customer without any current debt records a new debt

2

DEBT_ADDED

Anytime when a new debt is added (created debt is of a debt type that is new - customer did not have any current debts for it)

3

DEBT_PAID_OFF

Debt of selected debt type is completely paid off

4

ALL_DEBTS_PAID

Every debt was paid, customer does not have any current debts anymore


Debt accounts and addresses

Current debts are being recorded on both customer and bank’s side.

This structure might undergo further future changes, based on changing specification from SaFi side or further implementation requirements resulting from future services development.

Note: while being in development, unpaid debt addresses on customer’s side are implemented on Debt smart contract which might be different from their final destination in MVP (Main account for example). In table below, customer accounts that we assume to use are “Main account“ and “Loan account”, but the final decision will be adjusted according to the future SaFi product specifications.

Debts in following table are also ordered by their repayment priority.

Debt type

Customer account & address for unpaid debt

Internal account & address for unpaid debt

Target account & address for paid debt

1

Main account subscription fees

Main account: MAIN_ACCOUNT_SUBSCRIPTION_FEE_DEBT

SUBSCRIPTION_FEES_UNPAID_INTERNAL: DEFAULT

SUBSCRIPTION_FEES_PAID_INTERNAL: DEFAULT

2

After overdue loan penalty (TBA)

Main account: LOAN_PENALTIES_DEBT

LOAN_PENALTIES_UNPAID_INTERNAL: DEFAULT

LOAN_PENALTIES_PAID_INTERNAL: DEFAULT

3

Past due loans debts (all together)

Main account: LOAN_DEBT

LOAN_UNPAID_INTERNAL: DEFAULT

<customer_loan_account>: DEFAULT

4

Past due overdraft penalties (overdraft 2 months penalty)

Main account: OVERDRAFT_PENALTIES_DEBT

OVERDRAFT_PENALTIES_UNPAID_INTERNAL: DEFAULT

OVERDRAFT_PENALTIES_PAID_INTERNAL: DEFAULT

5

Past due overdraft fees (overdraft 1 month )

Main account: OVERDRAFT_FEE_DEBT

OVERDRAFT_FEES_UNPAID_INTERNAL: DEFAULT

OVERDRAFT_FEES_PAID_INTERNAL: DEFAULT

6

Unpaid overdraft (principal)

Main account: OVERDRAFT_DEBT

OVERDRAFT_UNPAID_INTERNAL: DEFAULT

OVERDRAFT_PAID_INTERNAL: DEFAULT

Debt payment target accounts

Although for most of the debt types target “PAID” account is currently an internal account, which is a target for given fee / debt payments payments, there is an option for this account to be a custom customer’s account. To dynamically distinguish between these two options, the value of the target paid account param is set like this:

  "MAIN_ACCOUNT_SUBSCRIPTION_FEE": {
    "type": "internal_account",
    "value": "SUBSCRIPTION_FEES_PAID_INTERNAL"
    },
  "LOAN_PENALTY":{
    "type": "instance_param",
    "value": "current_loan_account_id"
    },

For MAIN_ACCOUNT_SUBSCRIPTION_FEE, type of target debt payment account (where money goes for the original payment when the debt is paid off) is a global internal account, field value contains its TM id - SUBSCRIPTION_FEES_PAID_INTERNAL.

For LOAN_PENALTY, debts are repaid from main account back to the loan account, which then redistributes the paid debt further. For this, we need to store the current loan account id as an instance parameter in main account, and refer to its value within debt manager, so it is able to find the target debt payment account in case of a loan debt. Therefore the type of the target account is in instance param and the value contains the name of instance parameter, which contains the target debt account.

The instance parameter setup needs to be as following:

CURRENT_LOAN_PARAM_NAME = "current_loan_account_id"


parameters = [
    Parameter(
        name=CURRENT_LOAN_PARAM_NAME,
        level=Level.INSTANCE,
        description="Contains id of current active loan account, if customer has any.",
        display_name="Current loan account id",
        shape=OptionalShape(AccountIdShape()),
        update_permission=UpdatePermission.OPS_EDITABLE,
    ),
]

The value of this parameter is then filled at the time of loan opening, where loan manager calls CoreAPI to update the instance parameter to the id of newly opened loan, which is the uuid TM account id.

In case of adding a new target account which is a customer’s individual account, make sure that the parameter is instance level, the parameter shape is optional and it is of account id shape.

Debt manager postings

Following table contains list of all transactions being accepted or generated by debt manager:

Transaction type (as in instruction_details)

Account from

Account to

Purpose

Source

1

CLAIM_PAYMENT

Main account

UNPAID debts account for given debt type

Payment for any service / fee, which when unsuccessful, is marked down as a debt. Input for debt manager.

External

2

None / any, override_debt_payment set to given debt_type

Any (not limited yet)

Main account

Directly repay selected debt (even low priority one)

External

3

CUSTOMER_DEBT_REBALANCE

Main account: DEBT_ADDRESS

Main account: DEFAULT

When claim payment caused account to fall into negative (and debt needs to be recorded), this payment marks down the debt and rebalances negative state on main account.

Generated by debt manager

4

CUSTOMER_DEBT_REPAY

Main account: DEFAULT

Main account: DEBT_ADDRESS

With incoming funds, this posting decreases the recorded debt (the money we borrowed in previous posting), which symbolizes repaid debts.

Generated by debt manager

5

POCKET_DEBT_REPAY

Pocket account: DEFAULT

Main account: DEFAULT

When entering debt and having funds in pockets, this posting transfers the funds for debt payment from pocket to main account.

Generated by debt manager

6

DEBT_PAYMENT_DONE

UNPAID debts account for given debt type

PAID (target) debts account for given debt type

When debt is repaid, actual funds are moved from UNPAID account to PAID account (final account where the original fee / claim payment should arrive when succesful).

Generated by debt manager

Selected transactions are described more in details within next chapter.

Input fee and debt payments identification

For debt manager to determine the purpose of currently processed payment (and apply corresponding logic), postings need to carry certain information. Decision process on how to handle current payment in terms of debt management depends on information about posting type, which will be derived from instruction_details (fields transaction_type and claim_type), and (or) from / to posting addresses.

Exact transaction type information format in posting instruction object:
"instruction_details": { "transaction_type": "VALUE", "claim_type": "CLAIM_TYPE", "override_debt_payment": "CLAIM_TYPE"}

Field explanations:

  • transaction_type: used to mark down main purpose of transaction, for example INTRABANK_TRANSACTION, POCKET, CLAIM_PAYMENT

  • claim_type : Claim is a request or promise to pay selected fee, scheduled payment or debt. Debt manager’s role is to either process the claim and finish the payment, if there are enough funds on the account, or leave the claim marked as unpaid as a debt and pay it off in future, when funds arrive to customer’s account. Since there are multiple fees to be paid, the value of this field determines which certain fee is the claim for (and therefore defines the target unpaid and paid address on bank’s side). Example values: MAIN_ACCOUNT_SUBSCRIPTION_FEE, OVERDRAFT_PENALTY, … Each possible debt type should have its own value.

  • override_debt_payment : In very rare and specific situations, customer who is currently in debt might use incoming funds to pay off specific debt first (and therefore ignore (override) priority order of debt repayments and pay of selected debt first). In order to mark down incoming funds as to be used for specific direct debt repayment, you can include the override_debt_payment field in instruction_details and specify which debt needs to be paid off.

1. Incoming funds transaction

Identified as any incoming - credit posting to main account's default address. Unless specified, posting metadata do not have to be explicitly set (or can be set as anything for transaction_type field, except CLAIM_PAYMENT). override_debt_payment field CAN NOT BE SET.

Payment type

Posting address from

Posting address to

Posting metadata transaction_type VALUE

Posting metadata claim_type CLAIM_TYPE

Posting metadata override_debt_payment CLAIM_TYPE

1

Main account incoming funds

Any

Main account: DEFAULT

None or Any except CLAIM_PAYMENT

None / Any

None


When an incoming funds transaction causes debts to be (partially) paid, a recorded debt is “paid” (or removed) by “REPAY” postings (generated by debt manager), which return the borrowed debt money to customer’s debt addresses. The repay postings' schema is as following:

account_from: <current_main_account_id>
account_address_from: DEFAULT
account_to: <current_main_account_id>
account_address_to: <current_debt_address>
instruction_details: {
    
   “transaction_type”: “CUSTOMER_DEBT_REPAY”,
   “debt_type”: “<debt_type>”
,
   "account_id": "<current_main_account_id>"
}

At the same time, on bank’s level, money are moved from “UNPAID” accounts to “PAID” accounts as described earlier in this document.

2. Incoming claim payment transactions

Transactions for claim payments are usually moving funds from DEFAULT addresses on Main or Loan account to specified unpaid claim addresses, to mark down potential debt of fee / payment to be paid - and have it recorded in case the payment is unsuccessful. Type of fee / penalty being paid by the transaction is then described in posting’s metadata. Currently specified claims:

Payment type

Posting address from

Posting address to

Posting metadata transaction_type VALUE

Posting metadata claim_type CLAIM_TYPE

Posting metadata override_debt_payment CLAIM_TYPE

1

Main account subscription fee

Main account: DEFAULT

SUBSCRIPTION_FEES_UNPAID_INTERNAL: DEFAULT

"CLAIM_PAYMENT"

"MAIN_ACCOUNT_SUBSCRIPTION_FEE"

None

2

Overdraft penalty (2 months)

Main account: DEFAULT

OVERDRAFT_PENALTIES_UNPAID_INTERNAL: DEFAULT

"CLAIM_PAYMENT"

"OVERDRAFT_PENALTY"

None

3

Overdraft fee (1 month)

Main account: DEFAULT

OVERDRAFT_FEES_UNPAID_INTERNAL: DEFAULT

"CLAIM_PAYMENT"

"OVERDRAFT_FEE"

None

4

Overdraft (repayment)

Main account: DEFAULT

OVERDRAFT_UNPAID_INTERNAL: DEFAULT

"CLAIM_PAYMENT"

"OVERDRAFT"

None

5

Loan payments (all types)

Main account: DEFAULT

LOAN_PENALTIES_UNPAID_INTERNAL: DEFAULT

"CLAIM_PAYMENT"

"LOAN_PENALTY"

None

*Exact “address from” for loan fee payments is still to be specified / confirmed.

3. Direct debt repayments

Direct debt repayments allow, under specific circumstances, override debt repayment priorities and allow incoming transaction to be used for specific debt repayment (even if debts with higher priorities currently exist).

Examples for debts specified in table above:

Payment type

Posting address from

Posting address to

Posting metadata transaction_type VALUE

Posting metadata claim_type CLAIM_TYPE

Posting metadata override_debt_payment CLAIM_TYPE

1

Main account subscription fees debt repayment

any

Main account: DEFAULT

None or Any except CLAIM_PAYMENT

None

"MAIN_ACCOUNT_SUBSCRIPTION_FEE"

2

Past due loans debt repayment

any

Main account: DEFAULT

None or Any except CLAIM_PAYMENT

None

"LOAN_PENALTY"

3

Past due overdraft penalties debt repayment

any

Main account: DEFAULT

None or Any except CLAIM_PAYMENT

None

"OVERDRAFT_PENALTY"

4

Past due overdraft fee debt repayment

any

Main account: DEFAULT

None or Any except CLAIM_PAYMENT

None

"OVERDRAFT_FEE"

5

Unpaid overdraft (principal) debt repayment

any

Main account: DEFAULT

None or Any except CLAIM_PAYMENT

None

"OVERDRAFT"

4. Customer debt rebalancing

In case when CLAIM_PAYMENT could not be fulfilled because there were not enough funds on the account (the claim payments caused balance on the account to go under 0), until enough funds arrive on the account, to avoid having negative balance, we “borrow” the CLAIM_PAYMENT amount (or rebalance the account with) from customers account’s debt addresses. That way we always have current debt marked down for each customer and avoid having 0 on the main account at the same time.

Transactions for negative account value rebalancing are defined inside the debt manager smart contract, and their identification is:

instruction_details: {"transaction_type": "CUSTOMER_DEBT_REBALANCE", "claim_type": "CLAIM_TYPE", "account_id" "<CURRENT_MAIN_ACCOUNT_ID>"}

where "CLAIM_TYPE" is type of fee / penalty that caused negative balance on the account and is inherited from original “CLAIM_PAYMENT“ transaction.

Debt manager’s global parameters

Debt manager currently uses multiple TM deployed global parameters:

Global parameter id

Description

Current value

debt_types_ordered_by_priority

Current list of allowed debt types, which debt manager is able to manage. Used to validate claim_type / debt_type in incoming postings to main account.

["MAIN_ACCOUNT_SUBSCRIPTION_FEE", "LOAN_PENALTY",
”OVERDRAFT_PENALTY",
"OVERDRAFT_FEE",
"OVERDRAFT"]

overdraft_allowed_debt_types

List of debt types which are allowed to be rebalanced by overdraft, if available.

["MAIN_ACCOUNT_SUBSCRIPTION_FEE", "LOAN_PENALTY",
"OVERDRAFT_FEE",
"OVERDRAFT"]

<product_prefix>debt_manager_supervisor_version_id

Id of current deployed supervisor debt manager version to be used for plans creation.

uuid of supervisor version currently active, for example
7a3b8873-7f24-46e0-bc88-c9456531d599

debt_type_to_customer_debt_address

Contains mappings of customer debt addresses for each available debt type. This is used to map incoming claim payment’s claim type to correct debt address to record the debt for it / repay the debt.

{
  "MAIN_ACCOUNT_SUBSCRIPTION_FEE": "MAIN_ACCOUNT_SUBSCRIPTION_FEE_DEBT",
  "LOAN_PENALTY": "LOAN_PENALTIES_DEBT",
  "OVERDRAFT_PENALTY": "OVERDRAFT_PENALTIES_DEBT",
  "OVERDRAFT_FEE": "OVERDRAFT_FEE_DEBT",
  "OVERDRAFT": "OVERDRAFT_DEBT"
  }

debt_type_to_unpaid_account

Contains mappings of intermediary unpaid debt collection accounts for each available debt type. This is mostly used to be able to move funds from correct unpaid account to paid account when repaying a debt.

{
  "MAIN_ACCOUNT_SUBSCRIPTION_FEE": "SUBSCRIPTION_FEES_UNPAID_INTERNAL",
  "LOAN_PENALTY": "LOAN_PENALTIES_UNPAID_INTERNAL",
  "OVERDRAFT_PENALTY": "OVERDRAFT_PENALTIES_UNPAID_INTERNAL",
  "OVERDRAFT_FEE": "OVERDRAFT_FEES_UNPAID_INTERNAL",
  "OVERDRAFT": "OVERDRAFT_UNPAID_INTERNAL"
  }

debt_type_to_paid_account

Contains mappings of final paid debt collection accounts for each available debt type. This is mostly used to be able to move funds from correct unpaid account to paid account when repaying a debt.

{
  "MAIN_ACCOUNT_SUBSCRIPTION_FEE": {
    "type": "internal_account",
    "value": "SUBSCRIPTION_FEES_PAID_INTERNAL"
    },
  "LOAN_PENALTY":{
    "type": "instance_param",
    "value": "current_loan_account_id"
    },
  "OVERDRAFT_PENALTY": {
    "type": "internal_account",
    "value": "OVERDRAFT_PENALTIES_PAID_INTERNAL"
    },
  "OVERDRAFT_FEE": {
    "type": "internal_account",
    "value": "OVERDRAFT_FEES_PAID_INTERNAL"
    },
  "OVERDRAFT": {
    "type": "internal_account",
    "value": "OVERDRAFT_PAID_INTERNAL"
    }
  }'

Service integration examples and tutorials

Following chapter contains examples and tutorials on how to correctly implement payments of selected fees or debt types in order to be managed by debt manager.

1. Connecting main account (and pockets accounts) to debt manager

According to the current deployment process of supervisor smart contracts, in order for a main account to be supervised by debt manager, each main account needs to be assigned to a Plan supervised by selected Supervisor debt manager.

According to current knowledge we have about using plans and supervisors in TM environment, the suggested way how to make accounts collaborate in plans with supervisor debt manager is to create separate new plan for each together-working group of main_account + related pocket accounts.

Currently only some of the deployed main_accounts / pocket_accounts / supervisor versions are supported and collaborate with each other in plans. Complete list of usable product versions are listed below the tutorial steps.

Steps to assign a main / pocket accounts to debt manager:

  1. Create new main_account of supported id and version (complete list of supported ids listed below this chapter).

  2. Create a new plan, supervised by currently deployed supervisor:

Request:
POST https://core-api.tm3.sandbox.safibank.online/v1/plans

Headers:
"X-Auth-Token": "AUTH_TOKEN",
"Content-Type": "application/json",

Request body:
{
    "request_id": "<random_generated_uuid4>",
    "plan": {
        "id": "<plan_id>*",
        "supervisor_contract_version_id": "<supervisor_version_id>",
        "status": "PLAN_STATUS_OPEN",
}

*<plan_id> can be any <random_generated_uuid4> or random or 
incremental unique id. Using uuid4 is recommended. 

Current <supervisor_version_id> for the latest deployed versions of accounts is available as global param in TM. More info below in section Supported smart contracts versions.

3. Assign main_account to created plan:

Request:
POST https://core-api.tm3.sandbox.safibank.online/v1/plan-updates

Headers:
"X-Auth-Token": "AUTH_TOKEN",
"Content-Type": "application/json",

Request body:
{
    "request_id": "<random_generated_uuid4>",
    "plan_update": {
        "id": "<random_generated_uuid4>",
        "plan_id": "<plan_id>",
        "job_id": "<random_generated_uuid4>",
        "associate_account_update": {
            "account_id": "<current_main_account_id>"
        }
}

This schedules an asynchronous job in TM, which adds selected main account under debt manager supervision. The operation is usually done immediately, however, during processing a large bulk of accounts, you can verify if the task was done by calling for example:

Request:
GET https://core-api.tm3.sandbox.safibank.online/v1/plan-updates:batchGet?ids=<plan_update_id>

Headers:
"X-Auth-Token": "AUTH_TOKEN",
"Content-Type": "application/json",

This should return result.status as “PLAN_UPDATE_STATUS_COMPLETED" in case that account was successfully assigned under plan’s supervision.

4. [OPTIONAL] Create a new pocket account

5. [OPTIONAL] Assign pocket account to its main_account’s plan

Request:
POST https://core-api.tm3.sandbox.safibank.online/v1/plan-updates

Headers:
"X-Auth-Token": "AUTH_TOKEN",
"Content-Type": "application/json",

Request body:
{
    "request_id": "<random_generated_uuid4>",
    "plan_update": {
        "id": "<random_generated_uuid4>",
        "plan_id": "<plan_id>*",
        "job_id": "<random_generated_uuid4>",
        "associate_account_update": {
            "account_id": "<current_pocket_account_id>"
        }
}

*<plan_id> - id of plan created for main account in previous steps

After performing all of the steps, main account with its pocket account should be working in a plan supervised by debt manager supervisor.


Supported smart contracts versions


One group of products (main_account + supervisor + pockets) a combination of version ids designed to work together and not interchangeable with product versions from other groups.

  1. TM3 (ready to use)

    1. debug products versions:

      1. main_account:
        product_id: debug_main_account_product

      2. supervisor debt manager:
        supervisor_version_id: available in global parameter: debug_debt_manager_supervisor_version_id as latest value set
        (supervisor_product_id: debug_debt_manager_supervisor)

      3. pockets:
        product_id: debug_pocket_account_product

    2. test products versions:

      1. main_account:
        product_id: test_main_account_product

      2. supervisor debt manager:
        supervisor_version_id: available in global parameter: test_debt_manager_supervisor_version_id as latest value set
        (supervisor_product_id: test_debt_manager_supervisor)

      3. pockets:
        product_id: test_pocket_account_product

  2. TM4 (not ready)

    1. main_account:
      product_id: test_main_account_product
      version_id: 6

    2. supervisor debt manager:
      supervisor_version_id: supervisor_debt_manager_0_2_1
      (supervisor_product_id: test_debt_manager_supervisor)

    3. pockets:
      product_id: test_pocket_account_product
      version_id: 7

AUTH_TOKEN in examples above has to have permissions for account creations, plans and plan_updates in core_api.

In future, we are about to discuss all other possible deployment and supervisor-assignments options.

1.1 Main account subscription fee

Prerequisites: main_account that wants to have subscription fee managed needs to be assigned to a plan under Supervisor debt manager supervision (as described in previous chapter).

When paying for scheduled main account subscription fee, posting instruction details for transfer posting or custom posting are as following:

account_from: <current_main_account_id>
account_address_from: DEFAULT
account_to: SUBSCRIPTION_FEES_UNPAID_INTERNAL
account_address_to: DEFAULT
instruction_details: {
    
   “transaction_type”: “CLAIM_PAYMENT”,
   “claim_type”: “MAIN_ACCOUNT_SUBSCRIPTION_FEE”

}

Example for transfer postings (different values for ids can be used in real production):

Request:
POST https://core-api.tm3.sandbox.safibank.online/v1/posting-instruction-batches:asyncCreate

Headers:
"X-Auth-Token": "AUTH_TOKEN",
"Content-Type": "application/json",

Request body:
{
  "request_id": "<random_generated_uuid4>",    
  "posting_instruction_batch": {
    "client_id": "AsyncCreatePostingInstructionBatch",        
    "client_batch_id": "<random_generated_uuid4>",
    "posting_instructions": [
        {
            "client_transaction_id": "<random_generated_uuid4>",                
            "transfer": {
                "amount": "50",                    
                "denomination": "PHP",                   
                "debtor_target_account": {
                    "account_id": <current_main_account_id>
                },                    
                "creditor_target_account": {
                    "account_id": "SUBSCRIPTION_FEES_UNPAID_INTERNAL"
                }
            },           
            "instruction_details": {
    
              "transaction_type": "CLAIM_PAYMENT",
              "claim_type": "MAIN_ACCOUNT_SUBSCRIPTION_FEE"

            } 
        }
    ]
  }
}

In case there is not enough money on main_account, new debt is marked down.

1.2 Overdraft fee

Prerequisites: main_account that wants to have overdraft fee debts managed needs to be assigned to a plan under Supervisor debt manager supervision (as described in previous chapter).

When paying for overdraft fee, posting instruction details for transfer posting or custom posting are as following:

account_from: <current_main_account_id>
account_address_from: DEFAULT
account_to: OVERDRAFT_FEES_UNPAID_INTERNAL
account_address_to: DEFAULT
instruction_details: {
    
   “transaction_type”: “CLAIM_PAYMENT”,
   “claim_type”: “OVERDRAFT_FEE”

}

Example for transfer postings (different values for ids can be used in real production):

Request:
POST https://core-api.tm3.sandbox.safibank.online/v1/posting-instruction-batches:asyncCreate

Headers:
"X-Auth-Token": "AUTH_TOKEN",
"Content-Type": "application/json",

Request body:
{
  "request_id": "<random_generated_uuid4>",    
  "posting_instruction_batch": {
    "client_id": "AsyncCreatePostingInstructionBatch",        
    "client_batch_id": "<random_generated_uuid4>",
    "posting_instructions": [
        {
            "client_transaction_id": "<random_generated_uuid4>",                
            "transfer": {
                "amount": "112",                    
                "denomination": "PHP",                   
                "debtor_target_account": {
                    "account_id": <current_main_account_id>
                },                    
                "creditor_target_account": {
                    "account_id": "OVERDRAFT_FEES_UNPAID_INTERNAL"
                }
            },           
            "instruction_details": {
    
              "transaction_type": "CLAIM_PAYMENT",
              "claim_type": "OVERDRAFT_FEE"

            } 
        }
    ]
  }
}

3. Loan debts

Prerequisites: main_account that records the total of loan debts needs to be assigned to a plan under Supervisor debt manager supervision (as described in previous chapter).

In case of any debts related to loan (which manages the debt sub-types itself for now), we also need to track total loan debts amount in debt manager - to be able to reserve money for the debt repayments. Debt manager therefore records all possible loan debt types as one debt type, loan_penalty. In order to integrate loan fees / installments payments with debt manager, posting instruction details for transfer posting or custom posting are as following:

account_from: <current_main_account_id>
account_address_from: DEFAULT
account_to: "LOAN_PENALTIES_UNPAID_INTERNAL"
account_address_to: DEFAULT
instruction_details: {
    
   “transaction_type”: “CLAIM_PAYMENT”,
   “claim_type”: “LOAN_PENALTY”
,
   // FURTHER NECESSARY DETAILS BASED ON LOAN PAYMENT TYPE WILL BE ADDED
}

Integration with loan is specific because:

  • the claim payments to record debts for loan are being created directly in loan smart contract, when overdue loan is being marked down (but the claim is still being paid from main account to loan intermediary unpaid account)

  • the loan debt is being repaid back to the loan account, which is set in instance parameter of the main account. Then the additional (debt-manager agnostic) loan repaid debt re-distribution to installments is being processed there.

To fully integrate loan debt payments with debt manager, you have to:

  • make sure that at the time of loan opening, you set the instance parameter on main account
    to contain currently open loan account id. The param id is current_loan_account_id.
    NOTE: this is currently implemented as a part of loan-manager workflow and is being done automatically when opening a loan via loan-manager.

  • send the claim posting as an internal transfer from the loan smart contract, as currently implemented in smart-contracts/loan/utils.py: create_overdue_claim_payment()

Adding a new debt type HOW TO

If you want to add a new debt to be managed by debt manager, you have to:

  • think off a name (label) for the debt, for example “mortage_debt“. This name is now the unique key that identifies your debt across the system, which is ensured by using it as key to all of the following items

  • add your debt type into debt_types_ordered_by_priority global param. Keep the debt type order here equal to actual debt repayment priority. because that’s what determines it when actually repaying the debts!

  • map your debt type to the target unpaid account (and perhaps create the account if it does not exist yet) by adding the mapping to global param debt_type_to_unpaid_account

  • map your debt type to the target paid account (and implement it if not existing) by adding the mapping to global param debt_type_to_paid_account

  • map your debt type to the debt address on main account which will be used to record the debt by adding the mapping to global param debt_type_to_customer_debt_address

  • if you wish for your debt type to be repaid by overdrafts, add the debt type also to list in global param overdraft_allowed_debt_types

  • for the fees you would like to record as debts in case of insufficient funds, start sending their payments as CLAIM payments from main account to unpaid accounts, as showed in examples in the previous chapters of this document

  • if you want to listen to the debt state changes, you have multiple options how to do this:

    • listen to messages in tm kafka topics for processed postings, where you should filter postings that have your debt_type in instruction_details.debt_type and the transaction_type is set to either CUSTOMER_DEBT_REPAY / CUSTOMER_DEBT_REBALANCE, where REBALANCE represents addding a debt and REPAY represents that the given debts were repaid, the debt change is equal to the posting’s amount

    • listen to messages in tm kafka topics that reflect balance changes, while filtering the events that belong to your debt type dedicated address

    • listen to kafka events produced in debt manager smart contracts (as described in this document earlier).

Debt manager postings simulation

Following chapter contains simulation of postings that ensure integration between selected products and debt manager.

1. Debt manager and overdraft postings

2. Debt manager and loan postings

Example simulation debt payment workflow

Example simulation of debt manager usage can be found in SaFiMono repository in

tm-contracts/smart_contracts/utils/demos/supervisor_debt_manager_with_pockets_demo.py

Money movements tracking:

Incoming payment to main account

Main account DEFAULT balance

Main account OVERDRAFT balance

Main account subscription fee debt

Pocket account 1

Pocket account 2

Unpaid subscription fees

Paid subscription fees

1

Beginning

0

0

0

0

0

0

0

2

Initial deposits 170 to main account, 50 and 80 to pockets

170

0

0

50

80

0

0

3
  1. Subscription fee payment 110

60

0

0

50

80

0

110

4

2. Subscription fee payment 110

0

0

0

50

30

0

220

5

Open overdraft 50

0

50

0

50

30

0

220

6

3. Subscription fee payment

0

0

0

0

20

0

330

7

4. Subscription fee payment

0

0

90

0

0

90

350

8

Incoming funds 100

10

0

0

0

0

0

440

Debts data analysis HOW TO

1. How to determine current debt for selected debt type for selected customer?

→ Get current balance on given customer’s main account debt address for given debt type

For example, to get current debt for overdraft principals for customer XYZ, go to customer’s main account and view balance on address OVERDRAFT_DEBT.

2. How to determine current debt for selected debt type for whole bank together?

→ Get current balance on an internal UNPAID account for given debt type.

For example, to get current subscription fee payments amount that is owed to bank (for all customers together), go to SUBSCRIPTION_FEES_UNPAID_INTERNAL internal account and view its balance.

3. How to determine total paid amount for a given fee for given customer?

→ Get sum of all DEBT_PAYMENT_DONE postings with given main_account_id and debt_type in details.
-> Get sum of all CLAIM_PAYMENT postings of given debt type on customer’s main account minus current debt recorded on main_account’s debt address on given debt type.


4. How to determine total paid amount for a given fee for whole bank?

→ In case fee has a target internal account, get current balance on the internal PAID account.

5. How to determine number of customers currently in debt?

→ Equal to number of main accounts, which have at least one of the debt addresses non-0.

Attachments:

image-20220821-115850.png (image/png)
image-20220821-115915.png (image/png)
image-20220821-115938.png (image/png)
image-20220821-120252.png (image/png)
image-20220821-120331.png (image/png)
Screenshot 2022-09-15 at 14.24.19.png (image/png)
debt_manager_claim.png (image/png)
image-20221102-214118.png (image/png)
image-20221102-214146.png (image/png)
image-20221102-214227.png (image/png)
image-20221102-214300.png (image/png)
image-20221102-220956.png (image/png)
Overdraft and debt manager postings (2).xlsx (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet)
Overdue loan repayment simulation.xlsx (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet)