Skip to content

Simplify repeitive rules

Now you know how to write a basic import rule to match a recurring transaction. However, we have only imported DoorDash transactions. There are still many open transactions in the import command's output.

Open transactions section in the import command output
Open transactions section in the import command output

Those in the open transactions are transactions extracted from the input CSV files but don't match any rules. The Netflix and Comcast transactions are obviously repeating. We need to add new rules for them. To match both of them, you can add two new rules to your file to make your import rule file look like this:

inputs:
  - match: "import-data/connect/chase/*.csv"

imports:
  - name: DoorDash Food Delivery
    match:
      extractor:
        equals: "plaid"
      desc:
        prefix: "DD *DOORDASH"
    actions:
      - file: main.bean
        txn:
          flag: "{{ '!' if pending else '*' }}"
          payee: "{{ payee | default(omit, true) }}"
          narration: "DoorDash food delivery"
          postings:
            - account: Liabilities:ChaseCreditCard
              amount:
                number: "{{ -amount }}"
                currency: "{{ currency | default('USD', true) }}"
            - account: "Expenses:FoodDelivery:DoorDash"
              amount:
                number: "{{ amount }}"
                currency: "{{ currency | default('USD', true) }}"
  - name: Netflix
    match:
      extractor:
        equals: "plaid"
      desc: "Netflix"
    actions:
      - file: main.bean
        txn:
          flag: "{{ '!' if pending else '*' }}"
          payee: "{{ payee | default(omit, true) }}"
          narration: "Netflix subscription"
          postings:
            - account: Liabilities:ChaseCreditCard
              amount:
                number: "{{ -amount }}"
                currency: "{{ currency | default('USD', true) }}"
            - account: "Expenses:Entertainment:StreamingService"
              amount:
                number: "{{ amount }}"
                currency: "{{ currency | default('USD', true) }}"
  - name: Comcast
    match:
      extractor:
        equals: "plaid"
      desc: "Comcast"
    actions:
      - file: main.bean
        txn:
          flag: "{{ '!' if pending else '*' }}"
          payee: "{{ payee | default(omit, true) }}"
          narration: "Comcast internet service"
          postings:
            - account: Liabilities:ChaseCreditCard
              amount:
                number: "{{ -amount }}"
                currency: "{{ currency | default('USD', true) }}"
            - account: "Expenses:Internet"
              amount:
                number: "{{ amount }}"
                currency: "{{ currency | default('USD', true) }}"

And then you run the import command, you should see output like this:

Git diff shows Comcast and Netflix transactions are added into the main.bean file
Git diff shows Comcast and Netflix transactions are added into the main.bean file

Avoid repeating with prepending and appending postings for input files

Although this works perfectly, you have probably already noticed. There are many repeating parts in our rules. For example, we always have the same posting for the Chase credit card account Liabilities:ChaseCreditCard for each posting part.

postings:
 - account: Liabilities:ChaseCreditCard
   amount:
     number: "{{ -amount }}"
     currency: "{{ currency | default('USD', true) }}"

Since all these transactions come from a specific credit card, we know which account to use. Therefore, we provide extra options in the input, allowing you to define prepending and appending posting for all Beancount transactions generated from any rules. The output file name is also always main.bean. Unless we want to specify it as a different output file name for a particular rule, we can add the default_file field in the config to make it apply as a default value for all generated transactions from this particular input file. With that in mind, we can update our input config like this:

inputs:
  - match: "import-data/connect/chase/*.csv"
    config:
      default_file: main.bean
      prepend_postings:
        - account: Liabilities:ChaseCreditCard
          amount:
            number: "{{ -amount }}"
            currency: "{{ currency | default('USD', true) }}"

imports:
  - name: DoorDash Food Delivery
    match:
      extractor:
        equals: "plaid"
      desc:
        prefix: "DD *DOORDASH"
    actions:
      - txn:
          flag: "{{ '!' if pending else '*' }}"
          payee: "{{ payee | default(omit, true) }}"
          narration: "DoorDash food delivery"
          postings:
            - account: "Expenses:FoodDelivery:DoorDash"
              amount:
                number: "{{ amount }}"
                currency: "{{ currency | default('USD', true) }}"
  - name: Netflix
    match:
      extractor:
        equals: "plaid"
      desc: "Netflix"
    actions:
      - txn:
          flag: "{{ '!' if pending else '*' }}"
          payee: "{{ payee | default(omit, true) }}"
          narration: "Netflix subscription"
          postings:
            - account: "Expenses:Entertainment:StreamingService"
              amount:
                number: "{{ amount }}"
                currency: "{{ currency | default('USD', true) }}"
  - name: Comcast
    match:
      extractor:
        equals: "plaid"
      desc: "Comcast"
    actions:
      - txn:
          flag: "{{ '!' if pending else '*' }}"
          payee: "{{ payee | default(omit, true) }}"
          narration: "Comcast internet service"
          postings:
            - account: "Expenses:Internet"
              amount:
                number: "{{ amount }}"
                currency: "{{ currency | default('USD', true) }}"

We also removed the balancing posting from all import rules since they are not needed anymore. Not just prepend_postings, you can also use append_postings if you want the postings to append in the end instead of the front. If you rerun the import command right now, you should get exactly the same result without any changes.

Use match with variables syntax to combine rules

We have already simplified the import rules a bit. However, you can see that there are still many repetitive parts. The rules for DoorDash, Netflix, and Comcast are slightly different from each other, but they are mostly the same. To solve the problem, we provide an advanced type of match syntax that allows you to match different transactions and provide corresponding variables to be used in the template. We call it match with variables. You can read its documents here.

With the new syntax, we can rewrite the rules as just like this:

inputs:
  - match: "import-data/connect/chase/*.csv"
    config:
      default_file: "main.bean"
      prepend_postings:
        - account: Liabilities:ChaseCreditCard
          amount:
            number: "{{ -amount }}"
            currency: "{{ currency | default('USD', true) }}"

imports:
  - name: Routines
    common_cond:
      extractor:
        equals: "plaid"
    match:
      - cond:
          desc:
            prefix: "DD *DOORDASH"
        vars:
          narration: DoorDash food delivery
          account: Expenses:FoodDelivery:DoorDash
      - cond:
          desc: "Netflix"
        vars:
          narration: Netflix subscription
          account: Expenses:Entertainment:StreamingService
      - cond:
          desc: "Comcast"
        vars:
          account: Expenses:Internet
    actions:
      - txn:
          flag: "{{ '!' if pending else '*' }}"
          payee: "{{ payee | default(omit, true) }}"
          narration: "{{ narration | default(desc, true) }}"
          postings:
            - account: "{{ account }}"
              amount:
                number: "{{ amount }}"
                currency: "{{ currency | default('USD', true) }}"

This is way better! The common_cond part defines the common conditions we want to match for all conditions in this rule. Under match, each cond defines how it wants to match the transactions. The vars field defines values for variables to be consumed in the action templates.

Starting from here, you can easily extend new conditions to match the new repetitive transactions you have recognized in the Open Transactions section. No more copy-paste is needed.