Transit Category Script (NomiScript)

Introduction

Runs on every transaction. When the source account (the split money leaves — the negative-value split) is Suica and the target account (where it lands — the positive-value split) is Metro, and the transaction is not categorized yet (no category tag anywhere on it), the script categorizes it as transportation by tagging each split.

It uses the SPLIT-ACCOUNT-NAME accessor (the posting account's display name, serialized into the trigger context) together with SPLIT-VALUE to tell source from target by sign. Compare with the tag-based Rust SDK groceries script.

Trigger function

Run for every transaction:

(defun should-apply ()
    (= (primary-entity-type) +entity-transaction+))

Helper: collect entity indices by type and parent

(defun entities-for (parent-idx entity-type-val)
    (let ((result nil))
        (do ((i 0 (+ i 1)))
            ((>= i (entity-count)) result)
            (when (and (= (entity-type i) entity-type-val)
                       (= (entity-parent-idx i) parent-idx))
                (setf result (cons i result))))))

Helper: count splits matching a named account on one side

The source side is the split whose value is negative (money out); the target side is the positive split (money in). Counting keeps the predicate in the index stratum so a comparison never mixes a count with money.

(defun count-source (splits name)
    (let ((n 0))
        (dolist (s splits)
            (when (and (< (split-value s) 0)
                       (string= (split-account-name s) name))
                (setf n (+ n 1))))
        n))

(defun count-target (splits name)
    (let ((n 0))
        (dolist (s splits)
            (when (and (> (split-value s) 0)
                       (string= (split-account-name s) name))
                (setf n (+ n 1))))
        n))

Helper: is the transaction already categorized?

Count every category tag in the context (on the transaction or any split), so a transaction that already carries one is left untouched.

(defun category-tag-count ()
    (let ((n 0))
        (do ((i 0 (+ i 1)))
            ((>= i (entity-count)) n)
            (when (and (= (entity-type i) +entity-tag+)
                       (string= (tag-name i) "category"))
                (setf n (+ n 1))))))

Main logic

Suica → Metro, not yet categorized → tag every split category = transportation:

(let* ((tx-idx (primary-entity-idx))
       (splits (entities-for tx-idx +entity-split+)))
    (when (and (> (count-source splits "Suica") 0)
               (> (count-target splits "Metro") 0)
               (= (category-tag-count) 0))
        (dolist (s splits)
            (create-tag s "category" "transportation"))))