Groceries Split Marking Script

Introduction

This is a sample script that checks the transaction details on creation and when the text of transaction note matches is "groceries" and there's only one money transition (2 splits) it sets the category "groceries" for both of them.

Cargo configuration

First we need a Cargo.toml to build this script:

[package]
name = "groceries-markup"
version = "0.1.0"
edition = "2021"

[lib]
name = "groceries_markup"
crate-type = ["cdylib"]

[dependencies]
scripting-sdk = { path = "../../scripting/sdk", default-features = false }
scripting-sdk-macro = { path = "../../scripting/sdk-macro" }

[profile.release]
opt-level = "s"
lto = true
panic = "abort"

[profile.dev]
panic = "abort"

[workspace]

Header

First of all we include the required data and define our code a no_std.

#![no_std]

use scripting_sdk::prelude::*;
use scripting_sdk_macro::script;

Checking the applicability

Trigger function accepts the context:

fn is_groceries_transaction(ctx: &Context) -> bool {

Then we only want to apply it when we got our transaction for processing (scripts can be applied to something else as well):

if ctx.primary_entity_type().ok() != Some(EntityType::Transaction) {
    return false;
}

Then let's validate we only have two splits which is usual case for the whole transaction (one positive and one negative). We can also check the values but let's ignore them for now

let primary_idx = ctx.primary_entity_idx();

if ctx.splits_for(primary_idx).flatten().count() > 2 {
    return false;
}

And at last let's check that the note for transaction is "groceries":

for tag in ctx.tags_for(primary_idx).flatten() {
    if tag.name().ok() == Some("note")
        && tag
        .value()
        .ok()
        .is_some_and(|v| v.eq_ignore_ascii_case("groceries"))
    {
        return true;
    }
}

Okay, the trigger function is complete, just return false if the note did not match.

    false
}

Updating the transaction

First of all we define our checker as a trigger function.

#[script(trigger_fn = is_groceries_transaction)]

The update function receives mutable context to update:

fn categorize_groceries(ctx: &mut Context) -> Result<()> {

And we get the primary entity (which is Transaction as we checked before) and start iteration

let primary_idx = ctx.primary_entity_idx();

for idx in 0..ctx.entity_count() {

We should skit the index if we:

Can't extract entity:

let Ok(entity) = ctx.entity(idx) else {
    continue;
};

The entity is not a split

if entity.entity_type().ok() != Some(EntityType::Split) {
    continue;
}

If it's some additional split not related to the root transaction

if entity.parent_idx() != primary_idx as i32 {
    continue;
}

If everything's fine we create a tag for the split

ctx.create_tag(idx as i32, "category", "groceries")?;

And just finish the function

    }
    Ok(())
}

Building

Just use org-babel-tangle on this file and then you can use

cargo build--release --target=wasm32-unknown-unknown

to build the resulting target/wasm32-unknown-unknown/release/groceries_markup.wasm