bkper

Event handling (events handler)

by @bkper in Tools
0
0
# Install this skill:
npx skills add bkper/skills --skill "Event handling (events handler)"

Install specific skill from multi-skill repository

# Description

Does something useful

# SKILL.md

Bkper App Development

Core knowledge for building Bkper apps using bkper-js SDK and Workers for Platforms.

Development Workflow

Web Handler (UI)

The web handler provides the UI shown when users open your app from Bkper's menu. Development uses a local server with hot-reload:

bun run dev

Changes to frontend code reflect immediately. The local URL is configured in bkperapp.yaml as menuUrlDev.

Events Handler (Webhooks)

The events handler processes webhooks from Bkper. Development uses the dev environment instead of local:

bun run deploy:dev

This deploys to https://{app-id}-dev.bkper.app/events โ€” a stable URL configured as webhookUrlDev in bkperapp.yaml.

Why deploy instead of local?
- Webhook testing involves triggering events from Bkper
- The dev environment tests against real platform resources (KV, secrets)
- No tunnel setup required

Continuous Development Pattern

When iterating on the events handler:

  1. Watch packages/events/src/ for file changes
  2. On change: run bun run deploy:dev
  3. Report deployment status to developer

This pattern enables rapid iteration. AI coding agents can automate this cycle.

Production Deployment

bun run deploy

Builds all packages and deploys to https://{app-id}.bkper.app.

Bkper Core Concepts

Data Model

  • Book: A ledger that tracks resources (money, inventory, etc.) with accounts and transactions
  • Account: A place where resources accumulate (e.g., Bank, Revenue, Inventory)
  • Transaction: A movement of resources between two accounts (always debit + credit)
  • Group: A way to organize accounts (e.g., "Operating Expenses")

Account Types

Type Nature Balance Behavior
ASSET Permanent Increases with debits
LIABILITY Permanent Increases with credits
INCOMING Temporary Increases with credits
OUTGOING Temporary Increases with debits

The Zero-Sum Invariant

Every transaction moves an amount FROM one account TO another. The sum of all account balances in a book always equals zero. This is fundamental โ€” never break this invariant.

bkper-js SDK

Installation

bun add bkper-js

Initialization (Cloudflare Workers)

import { Bkper, Book } from "bkper-js";

// Per-request Bkper instance (recommended for Workers)
function createBkper(env: Env, oauthToken?: string): Bkper {
    return new Bkper({
        oauthTokenProvider: async () => oauthToken,
        agentIdProvider: async () => env.BKPER_AGENT_ID,
    });
}

Book Operations

// Get a book
const book = await bkper.getBook(bookId);

// With accounts pre-loaded
const book = await bkper.getBook(bookId, true);

// Book properties
book.getName();
book.getDatePattern(); // dd/MM/yyyy | MM/dd/yyyy | yyyy/MM/dd
book.getFractionDigits(); // decimal places (0-8)
book.formatDate(new Date());
book.parseDate("25/01/2024");
book.formatValue(1234.56);

// Get accounts
const accounts = await book.getAccounts();
const account = await book.getAccount("Account Name");

Transaction Operations

import { Transaction } from "bkper-js";

// Create and post a transaction
const tx = new Transaction(book)
    .setDate("2024-01-25")
    .setAmount(100.5)
    .from(creditAccount)
    .to(debitAccount)
    .setDescription("Payment #invoice123")
    .setProperty("external_id", "123")
    .addRemoteId("external-system-id");

await tx.create(); // Create as draft
await tx.post(); // Post (affects balances)

// Transaction lifecycle
await tx.check(); // Mark as reconciled
await tx.uncheck(); // Unmark
await tx.trash(); // Move to trash
await tx.update(); // Update

// Query transactions
const txList = await book.listTransactions("account:'Bank' after:2024-01-01");
const transactions = txList.getItems();

Custom Properties

All resources support custom properties for bot integration:

// Set/get properties
tx.setProperty("exchange_rate", "1.25");
tx.getProperty("exchange_rate");
account.setProperty("bank_code", "001");
book.getProperty("base_currency");

Event Handling

Event Types

// Transaction events
TRANSACTION_CREATED; // Draft created
TRANSACTION_POSTED; // Posted to accounts
TRANSACTION_CHECKED; // Marked as reconciled
TRANSACTION_UNCHECKED; // Unmarked
TRANSACTION_UPDATED; // Modified
TRANSACTION_DELETED; // Trashed
TRANSACTION_RESTORED; // Restored from trash

// Account events
ACCOUNT_CREATED | ACCOUNT_UPDATED | ACCOUNT_DELETED;

// Other events
GROUP_CREATED | GROUP_UPDATED | GROUP_DELETED;
FILE_CREATED | FILE_UPDATED;
BOOK_UPDATED | BOOK_DELETED;

Event Handler Pattern

import { Hono } from "hono";
import { Bkper, Book } from "bkper-js";

const app = new Hono<{ Bindings: Env }>();

app.post("/events", async (c) => {
    const event: bkper.Event = await c.req.json();

    const bkper = new Bkper({
        oauthTokenProvider: async () => c.req.header("bkper-oauth-token"),
        agentIdProvider: async () => c.req.header("bkper-agent-id"),
    });

    const book = new Book(event.book, bkper.getConfig());

    switch (event.type) {
        case "TRANSACTION_CHECKED":
            return c.json(await handleTransactionChecked(book, event));
        default:
            return c.json({ result: false });
    }
});

TRANSACTION_CHECKED Handler Example

async function handleTransactionChecked(book: Book, event: bkper.Event): Promise<Result> {
    const operation = event.data.object as bkper.TransactionOperation;
    const transaction = operation.transaction;

    if (!transaction?.posted) {
        return { result: false };
    }

    // Prevent bot loops
    if (transaction.agentId === "my-bot-id") {
        return { result: false };
    }

    // Your logic here
    console.log(`Transaction checked: ${transaction.id}`);

    return {
        result: `CHECKED: ${transaction.date} ${transaction.amount}`,
    };
}

Response Format

type Result = {
    result?: string | string[] | boolean; // Success message(s)
    error?: string; // Error (red in UI)
    warning?: string; // Warning (yellow in UI)
};

// Examples
{
    result: false;
} // No action
{
    result: "CHECKED: 2024-01-15 100.00";
} // Success
{
    result: ["Book A: OK", "Book B: OK"];
} // Multiple
{
    error: "Rate not found";
} // Error

App Configuration (bkperapp.yaml)

id: my-app
name: My App
description: Does something useful

logoUrl: https://example.com/logo.svg
website: https://example.com

ownerName: Your Name
ownerWebsite: https://yoursite.com

developers: your-username

# Menu integration (web handler)
menuUrl: https://${id}.bkper.app?bookId=${book.id}
menuUrlDev: http://localhost:8787?bookId=${book.id}

# Event handling (events handler)
webhookUrl: https://${id}.bkper.app/events
webhookUrlDev: https://${id}-dev.bkper.app/events
apiVersion: v5
events:
    - TRANSACTION_CHECKED
Variable Description
${book.id} Current book ID
${book.properties.xxx} Book property value
${account.id} Current account ID
${transactions.ids} Selected transaction IDs
${transactions.query} Current query

Common Patterns

Linking Transactions (remoteId)

// Create linked transaction
const mirrorTx = new Transaction(connectedBook)
    .setDate(originalTx.date)
    .setAmount(originalTx.amount)
    .addRemoteId(originalTx.id) // Link to original
    .post();

// Find linked transaction
const linked = await book
    .listTransactions(`remoteId:${originalTx.id}`)
    .then((list) => list.getFirst());

Bot Loop Prevention

// Always check agentId to avoid infinite loops
if (transaction.agentId === MY_AGENT_ID) {
    return { result: false };
}

Connected Books

// Find related books in a collection
const collection = await book.getCollection();
const connectedBooks = collection?.getBooks() ?? [];

for (const connectedBook of connectedBooks) {
    if (connectedBook.getId() !== book.getId()) {
        // Process connected book
    }
}

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.