resolveProcessForPO
resolveProcessForPO is the shared utility that answers one question: which Active process should this purchase order run on? It's used by po-add (when no process is passed) and by po-submit (when the PO has no process link yet). It walks a three-step fallback chain โ unit-specific, then product-specific, then organization default โ and returns the first Active process it finds.
Package import
The tutorial source imports the framework as "lesan" โ in this repo that alias maps to the local framework source (deno.json โ ../../src/mod.ts). In your own app import from @hemedani/lesan (npm/Bun) or jsr:@hemedani/lesan (Deno). @lib is the tutorial's alias for utils/.
The functionโ
import { ObjectId } from "lesan";
import { process } from "../mod.ts";
import { throwError } from "./throwError.ts";
interface ResolveProcessParams {
organizationId: string;
requestingUnitId?: string;
productId: string;
}
export async function resolveProcessForPO(
params: ResolveProcessParams,
): Promise<string> {
const {
organizationId,
requestingUnitId,
productId,
} = params;
const orgId = new ObjectId(organizationId);
if (requestingUnitId) {
const unitProcess = await process.findOne({
filters: {
"organization._id": orgId,
"unit._id": new ObjectId(requestingUnitId),
status: "Active",
},
projection: { _id: 1 },
});
if (unitProcess) return unitProcess._id.toString();
}
const doc = await process.findOne({
filters: {
"organization._id": orgId,
"product._id": new ObjectId(productId),
status: "Active",
},
projection: { _id: 1 },
});
if (doc) return doc._id.toString();
const [orgProcess] = await process.aggregation({
pipeline: [
{
$match: {
"organization._id": orgId,
status: "Active",
$and: [
{ "unit._id": { $exists: false } },
{ "product._id": { $exists: false } },
],
},
},
{ $limit: 1 },
],
projection: { _id: 1 },
}).toArray();
if (orgProcess) return orgProcess._id.toString();
throwError(
"No active process found for this organization. Please create and activate a process first.",
);
return "";
}
The fallback chainโ
The function tries, in order:
- Unit-scoped process (only when
requestingUnitIdis passed): anActiveprocess whose embeddedorganization._idmatches and whoseunit._idmatches the requesting unit. If found โ return it. - Product-scoped process: an
Activeprocess matchingorganization._idandproduct._id. If found โ return it. - Organization default: the first
Activeprocess for the organization that is scoped to neither a unit nor a product โ the$andwith{ exists: false }on both fields is what makes it the catch-all. If found โ return it. - None found โ throw
No active process found for this organization. Please create and activate a process first.
The precedence is deliberate: a PO that comes from a specific requesting unit should inherit the unit's tailored workflow before falling back to a generic one, and a product-specific workflow (e.g. "medical equipment") beats the organization default.
Why $exists: false matters for step 3โ
Step 3 must exclude processes that do have a unit or product scope โ otherwise the default would accidentally pick up a unit-scoped process from step 1's query. The $and: [{ "unit._id": { $exists: false } }, { "product._id": { $exists: false } }] guarantees the fallback only matches processes with neither scope set.
In the workflowโ
Called in two places:
- po-add:
if (!resolvedProcess && organization)โ resolve when the caller didn't passprocess. The PO is created already bound to the resolved process. - po-submit:
if (!processId && po.organization?._id)โ resolve when the PO has no process link yet (e.g. created without an organization). The resolved process is then attached to the PO viaaddRelation.
Both call sites pass requestingUnitId only when the PO has a requestingUnit.
Links: overview, process model, purchaseOrder model, po-add, po-submit.
Errors & fixesโ
| Error | Meaning | Fix |
|---|---|---|
No active process found for this organization. Please create and activate a process first. | No Active process matched at any level of the chain | Create a process (addProcess), add steps (addProcessStep), and activate it (activateProcess); or pass process explicitly to add |
Notes:
- The function only ever returns an
Activeprocess โ aDraftprocess is invisible to the resolver, which is whyactivateProcessis a mandatory setup step. - A wrong
organizationId/productIdyields the same throw โ make sure the referenced documents exist.