Get Tenders
getTenders lists the tender collection, newest first, with optional filters by status and organization. Use it to browse the auction board โ which tenders are Open (still taking offers), Awarded, or Closed โ and to inspect the offers that have come in for each one.
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 (getTenders.val.ts)โ
Both filters are optional. status is constrained to the real status enum via tender_status_emums (["Open", "Awarded", "Closed"] from models/tender.ts), and organizationId is a plain optional string. The get projection is selectStruct("tender", 2) โ two levels deep, so you can reach into the embedded organization and createdBy relation snapshots, and of course read the full offers array.
import { object, optional, string } from "lesan";
import { selectStruct } from "../../../mod.ts";
import { activeRoleMixin } from "@lib";
import { tender_status_emums } from "@model";
export const getTendersValidator = () => {
return object({
set: object({
...activeRoleMixin,
status: optional(tender_status_emums),
organizationId: optional(string()),
}),
get: selectStruct("tender", 2),
});
};
The implementation (getTenders.fn.ts)โ
The function builds a filters document and only adds a $match stage when a filter was actually sent:
statusโfilters.statusโ the enum value stored directly on the document ("Open","Awarded", or"Closed").organizationIdโfilters["organization._id"]โ the dot-path into the embedded organization snapshot's_id, cast withnew ObjectId(...)to match how it's stored.
The pipeline always sorts by createdAt descending โ newest tenders first โ and the results flow through the get projection via .toArray().
import { type ActFn, type Document, ObjectId } from "lesan";
import { tender } from "../../../mod.ts";
export const getTendersFn: ActFn = async (body) => {
const {
set: { status, organizationId },
get,
} = body.details;
const filters: Document = {};
status && (filters.status = status as string);
organizationId && (filters["organization._id"] = new ObjectId(organizationId as string));
return await tender
.aggregation({
pipeline: [
...(Object.keys(filters).length > 0 ? [{ $match: filters }] : []),
{ $sort: { createdAt: -1 } },
] as Document[],
projection: get,
})
.toArray();
};
In the workflowโ
getTenders is the read side of the whole Finance tendering arc. Call it with status: "Open" to find tenders still accepting bids via addOffer, inspect an offer's exact supplier name before calling award, or review history of Awarded/Closed tenders. The shape it returns is defined by the tender model: status, deadline, description, the embedded offers array, and the organization/createdBy relation snapshots. Winning tenders feed the purchase order's committed price through the approval workflow.
Run itโ
With the server on http://localhost:1380 and a valid token:
curl -X POST http://localhost:1380/lesan \
-H "Content-Type: application/json" \
-H "token: <your-token>" \
-d '{
"model": "tender",
"act": "getTenders",
"details": {
"set": {
"activeRoleId": "ghost-role",
"status": "Open"
},
"get": {
"_id": 1,
"title": 1,
"status": 1,
"offers": 1,
"organization": { "_id": 1, "name": 1 }
}
}
}'
Both filters are optional โ drop status to see every tender regardless of state. The response is an array under "body", sorted newest first.
Errors & fixesโ
getTenders throws no custom errors. Two things to keep straight:
statusmust be one ofOpen,Awarded,Closedโ thetender_status_emumsenum rejects anything else (e.g."awarded"lowercase, or"Draft"). Filter with the exact enum values.organizationIdisn't pre-validated โ likegetBudgetLines, it's a plainoptional(string())here. Thenew ObjectId(...)cast inside the function assumes a well-formed ID, so pass the real_idstring from theorganizationcollection to keep the$matchon"organization._id"working.