Skip to main content

BudgetLine

budgetLine is a budget allocation that purchase orders draw against. The lifecycle: PO submit creates an encumbrance (reserves funds), PO finalize converts the encumbrance to spend, and PO cancel releases it. remainingBudget is kept in sync by the workflow acts โ€” the model itself just stores the numbers.

// models/budgetLine.ts (definition, trimmed of the doc comment)
import { coreApp } from "../mod.ts";
import {
coerce, date, defaulted, number, optional,
type RelationDataType, type RelationSortOrderType, string,
} from "lesan";
import { createUpdateAt } from "@lib";
import { organization_excludes } from "./excludes.ts";

export const budgetLine_pure = {
code: string(),
title: string(),
year: number(),
totalAllocated: defaulted(number(), 0),
totalEncumbered: defaulted(number(), 0),
totalSpent: defaulted(number(), 0),
remainingBudget: defaulted(number(), 0),
startDate: optional(coerce(date(), string(), (value) => new Date(value))),
endDate: optional(coerce(date(), string(), (value) => new Date(value))),
...createUpdateAt,
};

export const budgetLine_relations = {
organization: {
schemaName: "organization",
type: "single" as RelationDataType,
optional: true,
excludes: organization_excludes,
relatedRelations: {
budgetLines: { type: "multiple" as RelationDataType, limit: 50, sort: { field: "_id", order: "desc" as RelationSortOrderType } },
},
},
};

Pure fieldsโ€‹

FieldTypeNotes
codestring()e.g. BL-2024-01
titlestring()e.g. Laboratory Consumables
yearnumber()fiscal year
totalAllocateddefaulted(number(), 0)the original budget
totalEncumbereddefaulted(number(), 0)reserved by submitted POs
totalSpentdefaulted(number(), 0)spent on finalized POs
remainingBudgetdefaulted(number(), 0)allocated โˆ’ encumbered โˆ’ spent
startDate / endDateoptional(coerce(date()))budget period
createdAt / updatedAtspread from createUpdateAt

The encumbrance invariantโ€‹

The four money fields must always satisfy:

remainingBudget = totalAllocated โˆ’ totalEncumbered โˆ’ totalSpent

The acts maintain it:

EventtotalEncumberedtotalSpentremainingBudget
PO submit+= amountโ€”โˆ’= amount
PO finalizeโˆ’= amount+= amount(unchanged)
PO cancelโˆ’= amountโ€”+= amount

If remainingBudget < amount at submit, the submit is rejected (see po-submit).

Relationsโ€‹

RelationTargetTypeBack-reference
organizationorganizationsingle (optional)organization.budgetLines

Factoryโ€‹

export const budgetLines = () =>
coreApp.odm.newModel("budgetLine", budgetLine_pure, budgetLine_relations);

In the workflowโ€‹

Run itโ€‹

curl -X POST http://localhost:1380/lesan \
-H "Content-Type: application/json" \
-H "token: <jwt>" \
-d '{
"service": "main",
"model": "budgetLine",
"act": "getBudgetLines",
"details": {
"set": { "query": { "year": 2024 }, "page": 1 },
"get": { "code": true, "title": true, "totalAllocated": true, "remainingBudget": true }
}
}'

Errors & fixesโ€‹

ErrorCauseFix
budgetLine not foundact given an unknown _idpass a real _id
Insufficient remaining budgeta PO submit would push remainingBudget negativeallocate more budget or reduce the amount
note

Runtime On npm/Bun import the framework from @hemedani/lesan; on Deno from jsr:@hemedani/lesan. The repo app itself uses the lesan path alias.