Skip to main content

Add Budget Line

addBudgetLine creates a budgetLine โ€” an annual budget allocation that purchase orders draw against. It's the starting point of the Finance chapter: before a purchase order can reserve funds, its budget line has to exist. You need it whenever you allocate money (a code, title, year, and totalAllocated) for a department or the whole organization to spend.

note

Imports Throughout this tutorial the framework imports use the "lesan" alias, matching the app's deno.json. In your own project import from @hemedani/lesan (npm/Bun) or jsr:@hemedani/lesan (Deno) instead.

The validator (addBudgetLine.val.ts)โ€‹

The set object demands three required fields โ€” code, title, year โ€” plus the activeRoleId from activeRoleMixin (every act in this app requires it). Everything else is optional: totalAllocated (the amount of money, defaults to 0 in the model), startDate/endDate as ISO strings, and organization as an ObjectId. The get projection is selectStruct("budgetLine", 1) โ€” one level deep, so you can ask for the embedded organization snapshot too.

import { number, object, objectIdValidation, optional, string } from "lesan";
import { selectStruct } from "../../../mod.ts";
import { activeRoleMixin } from "@lib";

export const addBudgetLineValidator = () => {
return object({
set: object({
...activeRoleMixin,
code: string(),
title: string(),
year: number(),
totalAllocated: optional(number()),
startDate: optional(string()),
endDate: optional(string()),
organization: optional(objectIdValidation),
}),
get: selectStruct("budgetLine", 1),
});
};

The implementation (addBudgetLine.fn.ts)โ€‹

The function unwraps set and get from body.details, then uses stripActiveRole to drop the activeRoleId the validator required โ€” it's request plumbing, not a field you want stored.

If an organization was sent, it becomes a relation: Lesan resolves the ObjectId and stores the organization's Pure snapshot inside the budget line, and pushes the new budget line into the organization's inverse budgetLines back-reference (relatedRelations: { budgetLines: true }).

Then insertOne runs. The three money fields are initialized here, not taken from the client: totalEncumbered: 0, totalSpent: 0, and โ€” the important one โ€” remainingBudget starts equal to totalAllocated (falling back to 0 if it was omitted). From now on the purchase-order workflow will $inc these fields directly, so the ledger stays consistent no matter what order POs are submitted in.

import { type ActFn, type TInsertRelations, ObjectId } from "lesan";
import { budgetLine } from "../../../mod.ts";
import { stripActiveRole } from "@lib";
import type { budgetLine_relations } from "@model";

export const addBudgetLineFn: ActFn = async (body) => {
const { set, get } = body.details;
const { organization, ...rest } = stripActiveRole(set);

const relations: TInsertRelations<typeof budgetLine_relations> = {};

organization &&
(relations.organization = {
_ids: new ObjectId(organization as string),
relatedRelations: { budgetLines: true },
});

return await budgetLine.insertOne({
doc: {
...rest,
totalEncumbered: 0,
totalSpent: 0,
remainingBudget: (rest as any).totalAllocated || 0,
},
relations,
projection: get,
});
};

In the workflowโ€‹

addBudgetLine is the first act of the Finance chapter. Once a budget line exists, a purchaseOrder can point at it and the encumbrance cycle begins:

  • Submit reserves funds โ€” the PO's submit act $incs totalEncumbered up and remainingBudget down by estimatedAmount.
  • Finalize converts the reservation into spend โ€” totalEncumbered down, totalSpent up.
  • Cancel releases it โ€” totalEncumbered down, remainingBudget back up.

See the budgetLine model for the full field list and the organization โ†’ budgetLines back-reference. Sibling pages: getBudgetLines and getBudgetLineBreakdown.

Run itโ€‹

With the server on http://localhost:1380 and a valid token (obtained from the login act in the Auth & Users chapter):

curl -X POST http://localhost:1380/lesan \
-H "Content-Type: application/json" \
-H "token: <your-token>" \
-d '{
"model": "budgetLine",
"act": "addBudgetLine",
"details": {
"set": {
"activeRoleId": "ghost-role",
"code": "BL-2024-01",
"title": "Laboratory Consumables",
"year": 2024,
"totalAllocated": 100000000,
"organization": "<organization-id>"
},
"get": {
"_id": 1,
"code": 1,
"totalAllocated": 1,
"remainingBudget": 1
}
}
}'

A successful response has "success": true and โ€” because remainingBudget is seeded from totalAllocated โ€” the body shows "remainingBudget": 100000000. The returned _id is what you'll pass as budgetLine when adding a purchase order.

Errors & fixesโ€‹

addBudgetLine throws no custom errors of its own โ€” every budgetLine value is a fresh, valid document. The only failures come from the superstruct validator:

  • Missing code, title, or year โ€” they're required. The request returns { success: false } and you'll see the failed path, e.g. At path: set.code. Add the missing field.
  • year is a number โ€” passing "2024" as a string is rejected. Send 2024.
  • organization must be a valid ObjectId โ€” objectIdValidation rejects arbitrary strings. Pass the real _id string from the organization collection.