Twisp 101

Step 2: Model Deposits and Withdrawals

The most basic transaction types Zuzu needs to support are to allow customers to move money in and out of their checking account.

In other words, we need to support deposits and withdrawals: a customer needs to be able to deposit money into their checking account and withdraw some or all of their balance out of their account. Our ledger will support ACH debit and credit transaction types to model "withdrawals" and "deposits".

To build out this feature we'll need to do three things:

  • Create some customer Accounts to model money Zuzu holds on behalf of a customer.
  • Create an assets account to model Zuzu's cash assets.
  • Design two TranCodes to use as a templates for ACH transactions.

With double-entry accounting, every transaction needs to write at least two entries to the ledger which balance out across debits and credits. How (with what metadata) and where (to which accounts) these entries are written to is determined by the type of transaction.

In Twisp, transaction types are explicitly defined during the design stage by creating transaction codes, or TranCodes.

When a customer deposits money into their account, Zuzu is effectively acting as a custodian of the customer's money. This is why customer accounts are treated as a liability for the company – they represent money that Zuzu owes the customer.

The assets account represents the cash on hand that Zuzu holds at any given time.

Create accounts

First, let's create checking accounts for some sample customers. We can do this with the createAccount mutation.

002_CreateCustomerAccounts

mutation CreateCustomerAccounts {
  ernie_checking: createAccount(
    input: {
      accountId: "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5"
      name: "Ernie Bishop - Checking"
      code: "ERNIE.CHECKING"
      description: "Ernie's checking account"
      normalBalanceType: CREDIT
    }
  ) {
    accountId
    name
  }

  bert_checking: createAccount(
    input: {
      accountId: "6c6affb0-5cf5-402b-8d84-01bfc1624a2c"
      name: "Bert - Checking"
      code: "BERT.CHECKING"
      description: "Bert's checking account"
      normalBalanceType: CREDIT
    }
  ) {
    accountId
    name
  }
}

Note that customer accounts use a credit-normal balance type because they represent liabilities.

Next, let's create the assets account using a debit-normal balance type:

003_CreateAssetsAccount

mutation CreateAssetsAccount {
  createAccount(
    input: {
      accountId: "78551b96-9c34-46f9-8d5f-c86e4459fcd7"
      name: "Assets"
      code: "ASSET"
      description: "Zuzu's assets (e.g. cash deposits)"
      normalBalanceType: DEBIT
    }
  ) {
    accountId
    name
    normalBalanceType
  }
}

Check account balances

Every account starts with a zero/null balance. We can check the balances of each account by querying the account id and pulling out the account balance for the primary journal we created earlier.

Note that in this example, we use GraphQL variables to store the values used previously and inject them via query params. This makes it easier to re-use values across multiple requests.

004_CheckAccountBalances

query CheckAccountBalances(
  $ernieId: UUID!
  $bertId: UUID!
  $assetsId: UUID!
  $journalId: UUID!
) {
  ernie: account(id: $ernieId) {
    name
    balance(journalId: $journalId) {
      settled {
        normalBalance {
          units
        }
      }
    }
  }

  bert: account(id: $bertId) {
    name
    balance(journalId: $journalId) {
      settled {
        normalBalance {
          units
        }
      }
    }
  }

  assets: account(id: $assetsId) {
    name
    balance(journalId: $journalId) {
      settled {
        normalBalance {
          units
        }
      }
    }
  }
}

Just as expected - balances are null for all accounts. Not very exciting. Let's change that.

Design the transaction type as a TranCode

The only way to write ledger entries in Twisp is by posting a transaction. Furthermore, every transaction is structured by the tran code used. This ensures that the ledger is consistent, predictable, and correct.

To define the tran codes for ACH credits and debit transaction types, we need to first determine:

  • A unique identifier code for the tran code
  • Which accounts will be debited/credited
  • What entry data will be written
  • How we will create parameterize inputs (for values like the amount)

Let's keep it simple and use the codes ACH_CREDIT and ACH_DEBIT for these tran codes.

For deposits (i.e. ACH credits), we'll credit the customer's checking account because this account is credit-normal and represents Zuzu's obligation to the customer, and we'll debit the assets account because this is the debit-normal account which represents how much liquid currency Zuzu has on hand (in this case, on behalf of the customer).

Withdrawals (i.e. ACH debits) are going to be basically the same, but reversed: debit the customer's checking and credit the assets account.

We'll write one entry for the debit and one for the credit, using an entry type to clarify the function of the entry within the context of the transaction.

Finally, we'll need to parameterize both the amount as well as the customer's checking account ID, since these are the salient pieces of information that we want to be able to provide when posting a transaction using this tran code.

Create the TranCodes for DEPOSIT and WITHDRAW

Now we can create these tran codes with GraphQL, plugging in the design decisions we just made to encode these transaction types.

005_CreateDepositAndWithdrawalTranCodes

mutation CreateDepositAndWithdrawalTranCodes($achCrId: UUID!, $achDrId: UUID!) {
  achCredit: createTranCode(
    input: {
      tranCodeId: $achCrId
      code: "ACH_CREDIT"
      description: "An ACH credit into a customer account."
      params: [
        { name: "account", type: UUID, description: "Deposit account ID." }
        {
          name: "amount"
          type: DECIMAL
          description: "Amount with decimal, e.g. `1.23`."
        }
        {
          name: "effective"
          type: DATE
          description: "Effective date for transaction."
        }
      ]
      transaction: {
        journalId: "uuid('822cb59f-ce51-4837-8391-2af3b7a5fc51')"
        effective: "params.effective"
      }
      entries: [
        {
          accountId: "uuid('78551b96-9c34-46f9-8d5f-c86e4459fcd7')"
          units: "params.amount"
          currency: "'USD'"
          entryType: "'ACH_DR'"
          direction: "DEBIT"
          layer: "SETTLED"
        }
        {
          accountId: "params.account"
          units: "params.amount"
          currency: "'USD'"
          entryType: "'ACH_CR'"
          direction: "CREDIT"
          layer: "SETTLED"
        }
      ]
    }
  ) {
    tranCodeId
  }

  achDebit: createTranCode(
    input: {
      tranCodeId: $achDrId
      code: "ACH_DEBIT"
      description: "An ACH debit into a customer account."
      params: [
        { name: "account", type: UUID, description: "Withdraw account ID." }
        {
          name: "amount"
          type: DECIMAL
          description: "Amount with decimal, e.g. `1.23`."
        }
        {
          name: "effective"
          type: DATE
          description: "Effective date for transaction."
        }
      ]
      transaction: {
        journalId: "uuid('822cb59f-ce51-4837-8391-2af3b7a5fc51')"
        effective: "params.effective"
      }
      entries: [
        {
          accountId: "uuid('78551b96-9c34-46f9-8d5f-c86e4459fcd7')"
          units: "params.amount"
          currency: "'USD'"
          entryType: "'ACH_CR'"
          direction: "CREDIT"
          layer: "SETTLED"
        }
        {
          accountId: "params.account"
          units: "params.amount"
          currency: "'USD'"
          entryType: "'ACH_DR'"
          direction: "DEBIT"
          layer: "SETTLED"
        }
      ]
    }
  ) {
    tranCodeId
  }
}

Post a test transaction

With these tran codes defined, we can now post transactions using them.

Let's deposit $9.53 into Ernie's account:

006_PostDeposit

mutation PostDeposit {
  postTransaction(
    input: {
      transactionId: "42847c7f-1972-4448-91b7-652c378760f4"
      tranCode: "ACH_CREDIT"
      params: {
        account: "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5"
        amount: "9.53"
        effective: "2022-09-21"
      }
    }
  ) {
    transactionId
    tranCodeId
    effective
    entries(first: 2) {
      nodes {
        units
        direction
        account {
          name
        }
      }
    }
  }
}

That all looks good.

Now let's withdraw $4.28 from Ernie's account:

007_PostWithdrawal

mutation PostACHWithdrawal {
  postTransaction(
    input: {
      transactionId: "39d2288d-96f9-40c7-b587-e7e75df083fa"
      tranCode: "ACH_DEBIT"
      params: {
        account: "1fd1dd3e-33fe-4ef5-9d58-676ef8d306b5"
        amount: "4.28"
        effective: "2022-09-21"
      }
    }
  ) {
    transactionId
    tranCodeId
    effective
    entries(first: 2) {
      nodes {
        units
        direction
        account {
          name
        }
      }
    }
  }
}

Great! We've posted our first transactions. Let's go to the next feature.

Previous
Create a Primary Journal