Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ thirty days and the arrears lists are always in arrears.

A seed cannot create a user, so `pnpm demo` produces exactly one account. To see
the permission model of [DESIGN.md](./DESIGN.md) §04 do anything, create
accounts in **Setup → Users** and assign the seven positions:
accounts in **Setup → Users**, assign the seven positions, and give every business
requester a manager ([below](#rung-1-of-the-approval-ladder-is-a-manager)):

| Position | How many | What it changes |
|---|---|---|
Expand Down Expand Up @@ -132,6 +133,35 @@ admin: it is `required: true`, so a name that resolves to nothing takes the row
with it, and the dev admin is the only account that exists while the seed runs.
Reassign it once the real accounts are there.

### Rung 1 of the approval ladder is a manager

Every contract sent for approval opens on rung 1: the direct manager of the
contract's **owner** — the business requester who launched it, not the lawyer
who pressed **Send for Approval** — read from that account's
`sys_user.manager_id`. No seed, position or permission set fills that column,
so on an install set up from the tables above alone, no contract gets past
rung 1.

Set it in **Setup → Users → the user → ⋯ → Set Manager**, for each of the three
business requesters and for anyone else who launches contracts. Any other
account in the organization will do — the persona mix has no line managers of
its own, so point all three at one of the accounts above (the executive, say).
The manager needs nothing more: `approve_contract`, the gate a rung acts
through, is in `clm_requester`, which every account holds.

Skip it and the request opens on a slot nobody holds — `pending_approvers`
reads `manager:undefined` (upstream `objectstack-ai/objectstack#22558`) — and
the contract stays locked for approval. Setting the manager afterwards does not
reach a request that is already open: its approvers were resolved when the rung
opened. Only a platform admin moves it on, from **Setup → Approvals → Approvals
Inbox → All**, with **Override Reassign** (or Override Approve / Reject).

No rung sends its approver a message when it opens — nothing in their inbox,
no email. They see it waiting in the Console: the bell counts the approvals
waiting on them and **我的合同 › Waiting on Me** lists them. A reminder sent on
a request does arrive as a message; the missing one on opening is the
platform's, not this app's.

### Assigning a position from a script

Clicking through Setup → Users needs nothing more than the table above. Automating it does: the
Expand Down Expand Up @@ -169,6 +199,17 @@ empty lists, with nothing in the response that created it pointing at why. The s
the platform's — `objectstack-ai/objectstack#16712`, still open — so until that lands, the spelling is
the whole defence.

A manager is an admin operation, not a data write. **Set Manager** posts exactly this, as a platform
admin (the dev admin is one):

```http
POST /api/v1/auth/admin/set-user-manager
{ "userId": "…", "managerId": "…" }
```

The data API refuses the column outright: a `PATCH /api/v1/data/sys_user/…` carrying `manager_id`
answers `403 PERMISSION_DENIED`, because it edits only `name`, `image` and `locale` on a user.

### What the seeded rows do NOT carry, and why

The demo is loaded as **system data**, not driven through the flows. Two
Expand Down
10 changes: 8 additions & 2 deletions scripts/demo.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ const expectedPort = (argv) => {
const OPERATOR_SETUP_NOTE = [
'',
' ────────────────────────────────────────────────────────────────────────',
' Before you open the app — one setup step, and one thing about the log',
' Before you open the app — two setup steps, and one thing about the log',
' ────────────────────────────────────────────────────────────────────────',
'',
' 1. The seeded rows have no people on them yet. Contracts are launched by',
Expand All @@ -418,7 +418,13 @@ const OPERATOR_SETUP_NOTE = [
' them in Setup → Users and run this again; the README names them and',
' says who gets what.',
'',
' 2. The `ERROR [SeedLoader]` lines are that same missing owner: one per',
' 2. Then give each business requester a manager: Setup → Users → the',
' user → ⋯ → Set Manager (any other account will do). Rung 1 of the',
' approval ladder is the contract owner\'s manager; with none set, a',
' contract sent for approval waits on nobody, locked, until a platform',
' admin overrides it. The README\'s "Rung 1" section says more.',
'',
' 3. The `ERROR [SeedLoader]` lines are that same missing owner: one per',
' contract whose requester account does not exist yet, expected on a',
' first boot, and nothing is lost to them. The loader defers `owner_id`,',
' finds no such account on its last pass and writes the row anyway with',
Expand Down
34 changes: 22 additions & 12 deletions src/flows/contract-approval.flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,16 +179,23 @@ const RUNG_POLICY = {
// RECORD_LOCKED even for the platform admin. The recovery is real — a
// platform admin's `POST /api/v1/approvals/requests/<id>/approve` answered
// 200, resumed the run and carried the contract to `approved`, while the
// same call from a non-approver non-admin answered 403 FORBIDDEN — but it
// is API-ONLY in Console 17.3.0: neither the inbox's All tab nor the
// request record page renders an approve/reject/reassign control for the
// override actor (#3424). That residual is a platform gap, recorded in the
// PR rather than papered over here: the lint's suggested fallback
// (`{ type: 'org_membership_level', value: 'owner' }`) would seat the org
// owner on the legal, finance, executive and GM rungs as a silent
// co-approver, and DESIGN.md §03 fixes the ladder at five NAMED rungs with
// the matrix deciding only which are climbed (§13 Q3 puts an extra approver
// in the customer overlay, not the standard product).
// same call from a non-approver non-admin answered 403 FORBIDDEN. On 17.3.0
// that override was API-only in the Console (#3424); re-measured on 17.7.0
// (#93), the platform admin's Setup → Approvals → Approvals Inbox → All tab
// offers Override Approve / Reject / Reassign / Recall on the request.
//
// The lint's suggested fallbacks stay unauthored. An extra
// `{ type: 'org_membership_level', value: 'owner' }` approver would seat the
// org owner on every rung as a silent co-approver; `'fallback'` needs a
// `fallbackApprovers` list naming who takes an unstaffed rung, a choice
// DESIGN.md does not make. §03 fixes the ladder at five NAMED rungs with the
// matrix deciding only which are climbed (§13 Q3 puts an extra approver in
// the customer overlay, not the standard product).
//
// The rung an install hits FIRST is the manager rung: its slate is
// `sys_user.manager_id`, runtime data no seed, position or permission set
// fills. Its remedy is operator setup, named in the README ("Rung 1 of the
// approval ladder is a manager"), not a policy here.
onEmptyApprovers: 'admin_rescue' as const,
lockRecord: true,
approvalStatusField: 'approval_status',
Expand Down Expand Up @@ -264,8 +271,11 @@ export const ContractApprovalFlow: Flow = {
type: 'approval',
label: 'Direct Manager',
config: {
// No `value`: the engine resolves the submitter's `sys_user.manager_id`
// (`APPROVER_VALUE_BINDINGS.manager` is `{ source: 'auto' }`).
// No `value`: the engine resolves the contract OWNER's
// `sys_user.manager_id` (`record[value] ?? record.owner_id` — see the
// header), never the submitter's. Only an operator fills that column,
// which is why the README's operator setup names the step; re-measured
// on 17.7.0 (#93): unset, the request opens on `manager:undefined`.
approvers: [{ type: 'manager' }],
behavior: 'first_response',
...RUNG_POLICY,
Expand Down
Loading