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โ
| Field | Type | Notes |
|---|---|---|
code | string() | e.g. BL-2024-01 |
title | string() | e.g. Laboratory Consumables |
year | number() | fiscal year |
totalAllocated | defaulted(number(), 0) | the original budget |
totalEncumbered | defaulted(number(), 0) | reserved by submitted POs |
totalSpent | defaulted(number(), 0) | spent on finalized POs |
remainingBudget | defaulted(number(), 0) | allocated โ encumbered โ spent |
startDate / endDate | optional(coerce(date())) | budget period |
createdAt / updatedAt | spread from createUpdateAt |
The encumbrance invariantโ
The four money fields must always satisfy:
remainingBudget = totalAllocated โ totalEncumbered โ totalSpent
The acts maintain it:
| Event | totalEncumbered | totalSpent | remainingBudget |
|---|---|---|---|
| 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โ
| Relation | Target | Type | Back-reference |
|---|---|---|---|
organization | organization | single (optional) | organization.budgetLines |
Factoryโ
export const budgetLines = () =>
coreApp.odm.newModel("budgetLine", budgetLine_pure, budgetLine_relations);
In the workflowโ
- add-budget-line sets
totalAllocatedand seedsremainingBudget = totalAllocated - get-budget-lines, get-budget-line-breakdown
- Every purchaseOrder references a
budgetLinevia itsbudgetLinerelation
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โ
| Error | Cause | Fix |
|---|---|---|
budgetLine not found | act given an unknown _id | pass a real _id |
Insufficient remaining budget | a PO submit would push remainingBudget negative | allocate more budget or reduce the amount |
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.