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.