@opencxh/domain 1.260.0 → 1.262.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/entities/approval/index.d.ts +2 -0
- package/dist/entities/approval/policy.d.ts +39 -0
- package/dist/entities/approval/policy.test.d.ts +1 -0
- package/dist/entities/approval/types.d.ts +104 -0
- package/dist/entities/work/ladder.d.ts +9 -0
- package/dist/entities/work/types.d.ts +13 -0
- package/dist/index.cjs +15 -15
- package/dist/index.d.ts +1 -0
- package/dist/index.js +504 -471
- package/package.json +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ApprovalDecision, ApprovalFacts, ApprovalPolicy, ApprovalRequest, ApproverSet } from './types';
|
|
2
|
+
/** What a policy is matched against: the resource's kind, the act, and the reported facts. */
|
|
3
|
+
export interface ApprovalMatchInput extends ApprovalFacts {
|
|
4
|
+
scopeKind: string;
|
|
5
|
+
act: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The rule that guards this act, or nothing.
|
|
9
|
+
*
|
|
10
|
+
* **Specificity decides, not an order column.** A rule with a threshold beats one without, and
|
|
11
|
+
* among several thresholds the highest one that still fits wins — so the Ramp/Spendesk ladder
|
|
12
|
+
* ("above 1.000 the manager, above 10.000 finance") reads correctly at 12.000 without anybody
|
|
13
|
+
* keeping a priority number in sync. Ties break on id so two equal rules never swap between reads.
|
|
14
|
+
*
|
|
15
|
+
* A rule with a threshold never matches a caller that reported no amount: the caller is the only
|
|
16
|
+
* one who knows whether its row has a value, and guessing "0" would let every quote through.
|
|
17
|
+
*/
|
|
18
|
+
export declare function matchApprovalPolicy(policies: readonly ApprovalPolicy[], input: ApprovalMatchInput): ApprovalPolicy | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Who still has to sign: the named people plus the named teams' members, minus the requester.
|
|
21
|
+
*
|
|
22
|
+
* Flattened here and stored on the row, because the store cannot join and "what waits on me" has
|
|
23
|
+
* to be one indexed read. Team membership is resolved by the caller — this layer never fetches.
|
|
24
|
+
*/
|
|
25
|
+
export declare function pendingApprovers(approvers: ApproverSet, teamMembers: Readonly<Record<string, readonly string[]>>, requestedBy: string, allowSelfApproval?: boolean): string[];
|
|
26
|
+
/**
|
|
27
|
+
* One verdict folded into a request.
|
|
28
|
+
*
|
|
29
|
+
* Three rules, and the order is the point:
|
|
30
|
+
*
|
|
31
|
+
* 1. A settled request stays settled — a late job or a double click writes nothing.
|
|
32
|
+
* 2. A second vote from the same person counts once; the first one stands.
|
|
33
|
+
* 3. **A rejection always wins.** With `rule: "all"` a single no ends it; with `"any"` a yes ends
|
|
34
|
+
* it. So the two rules differ only in what it takes to say yes, never in what it takes to say no.
|
|
35
|
+
*
|
|
36
|
+
* Pure: the caller decides whether this person may vote at all (the route gates on
|
|
37
|
+
* `pendingUserIds`) and writes the result.
|
|
38
|
+
*/
|
|
39
|
+
export declare function applyApprovalDecision(request: ApprovalRequest, decision: ApprovalDecision): ApprovalRequest;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { OrgTeamScope } from '../scope/types';
|
|
2
|
+
/**
|
|
3
|
+
* Who may decide. People and teams, never a role: `docs/ROLES.md` §7 says every session still
|
|
4
|
+
* gets `{"*":["*"]}`, so a rule naming a role would point at everybody.
|
|
5
|
+
*/
|
|
6
|
+
export interface ApproverSet {
|
|
7
|
+
userIds: string[];
|
|
8
|
+
teamIds: string[];
|
|
9
|
+
}
|
|
10
|
+
/** `any` = one sign-off is enough, `all` = everybody named. No chains; see the plan. */
|
|
11
|
+
export type ApprovalRule = "any" | "all";
|
|
12
|
+
export type ApprovalStatus = "pending" | "approved" | "rejected" | "withdrawn";
|
|
13
|
+
export type ApprovalVerdict = "approved" | "rejected";
|
|
14
|
+
export interface ApprovalDecision {
|
|
15
|
+
userId: string;
|
|
16
|
+
decision: ApprovalVerdict;
|
|
17
|
+
at: number;
|
|
18
|
+
note?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* One request for sign-off on one act, on any resource in the platform.
|
|
22
|
+
*
|
|
23
|
+
* Addressed by `scopeKey` like {@link Pin} and `SlaClock`: this layer knows nothing about quotes
|
|
24
|
+
* or work items, and the app that owns the kind stays the one that authorizes access to it.
|
|
25
|
+
*/
|
|
26
|
+
export interface ApprovalRequest {
|
|
27
|
+
id: string;
|
|
28
|
+
organizationId: string;
|
|
29
|
+
/** `<kind>:<ref>` — the vocabulary `assertScopeAccess` already speaks. */
|
|
30
|
+
scopeKey: string;
|
|
31
|
+
/**
|
|
32
|
+
* What is being asked for: `"issue"`, `"status:published"`. A free string the owning app owns;
|
|
33
|
+
* this layer only compares it. The act's parameters live *inside* it, which is why there is no
|
|
34
|
+
* opaque payload column. A act with real arguments earns a `payload` field then, not now.
|
|
35
|
+
*/
|
|
36
|
+
act: string;
|
|
37
|
+
requestedBy: string;
|
|
38
|
+
requestedAt: number;
|
|
39
|
+
approvers: ApproverSet;
|
|
40
|
+
/**
|
|
41
|
+
* Teams flattened to people at write time, minus the requester unless the rule allows self
|
|
42
|
+
* approval. The only column "what waits on me" queries — the store cannot join, and an array
|
|
43
|
+
* column needs `anyOf` to match at all.
|
|
44
|
+
*/
|
|
45
|
+
pendingUserIds: string[];
|
|
46
|
+
rule: ApprovalRule;
|
|
47
|
+
status: ApprovalStatus;
|
|
48
|
+
decisions: ApprovalDecision[];
|
|
49
|
+
/** Federated card of the source app, `"sales:ApprovalCard"`. Absent = text plus a link. */
|
|
50
|
+
resource?: string;
|
|
51
|
+
/**
|
|
52
|
+
* What the thing is called, snapshotted by the app that asked. Used **cold**: a notification is
|
|
53
|
+
* read long after it was written and the queue lists requests across apps, so neither has the
|
|
54
|
+
* resource at hand. Same doctrine as `Pin.label` — whoever renders it with the live row in
|
|
55
|
+
* reach lets the live value win. Absent = the reader falls back to the key.
|
|
56
|
+
*/
|
|
57
|
+
title?: string;
|
|
58
|
+
/** What the requester wrote when submitting. */
|
|
59
|
+
note?: string;
|
|
60
|
+
policyId?: string;
|
|
61
|
+
settledAt?: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A rule: for this kind of resource and this act, above this amount, these people sign.
|
|
65
|
+
*
|
|
66
|
+
* Same shape as `SlaProfile` — org/team owned configuration, a small matching surface, and no
|
|
67
|
+
* `order` column: the match is decided by specificity (see {@link matchApprovalPolicy}), not by
|
|
68
|
+
* a number somebody has to keep in sync.
|
|
69
|
+
*/
|
|
70
|
+
export interface ApprovalPolicy {
|
|
71
|
+
id: string;
|
|
72
|
+
organizationId: string;
|
|
73
|
+
ownerScope: OrgTeamScope;
|
|
74
|
+
name: string;
|
|
75
|
+
enabled: boolean;
|
|
76
|
+
/** The scopeKey prefix this rule guards: `"quote"`, `"work_item"`. */
|
|
77
|
+
scopeKind: string;
|
|
78
|
+
/** The act it guards: `"issue"`, `"status:published"`. */
|
|
79
|
+
act: string;
|
|
80
|
+
/**
|
|
81
|
+
* Below this the act passes unguarded. Absent = always guarded. In cents, like all money here:
|
|
82
|
+
* a threshold in euros would round differently than the totals it is compared against.
|
|
83
|
+
*/
|
|
84
|
+
minAmountCents?: number;
|
|
85
|
+
approvers: ApproverSet;
|
|
86
|
+
rule: ApprovalRule;
|
|
87
|
+
/** Default false: four eyes does not mean your own two, twice. */
|
|
88
|
+
allowSelfApproval?: boolean;
|
|
89
|
+
}
|
|
90
|
+
/** Facts the caller reports about its own row, for a rule to match on. Never a judgement. */
|
|
91
|
+
export interface ApprovalFacts {
|
|
92
|
+
amountCents?: number;
|
|
93
|
+
}
|
|
94
|
+
/** What the gate answers. `not_required` is the common case and costs the caller nothing. */
|
|
95
|
+
export type ApprovalGateState = "not_required" | "pending" | "approved";
|
|
96
|
+
/**
|
|
97
|
+
* *Why* the gate answered that, for the two cases where "nothing happened" is a surprise.
|
|
98
|
+
*
|
|
99
|
+
* Only `no_approvers` needs saying out loud: a rule whose only approver is the person acting
|
|
100
|
+
* lets the act through, which is the right call (a request nobody can decide is a deadlock with
|
|
101
|
+
* no screen to fix it from) and the wrong silence — somebody configured a gate and watched it
|
|
102
|
+
* not gate. The other reasons exist so a log line can name one.
|
|
103
|
+
*/
|
|
104
|
+
export type ApprovalGateReason = "no_rule" | "no_approvers" | "already_approved" | "already_pending" | "unreachable";
|
|
@@ -95,3 +95,12 @@ export interface StatusLadderProblem {
|
|
|
95
95
|
* finished immediately, and a ladder without `done` makes finishing impossible.
|
|
96
96
|
*/
|
|
97
97
|
export declare function validateStatuses(statuses: readonly WorkStatus[]): StatusLadderProblem[];
|
|
98
|
+
/**
|
|
99
|
+
* Does entering this status need a signature?
|
|
100
|
+
*
|
|
101
|
+
* Here and not at the call site because two places ask it — the server gate and the screen that
|
|
102
|
+
* labels the button — and an unknown key must read as "no". An item pointing at a deleted status
|
|
103
|
+
* is already handled by {@link categoryOf}'s fallback; adding a gate on top of that would block a
|
|
104
|
+
* move nobody can unblock.
|
|
105
|
+
*/
|
|
106
|
+
export declare function statusRequiresApproval(statusKey: string, ladder: readonly WorkStatus[]): boolean;
|
|
@@ -44,6 +44,19 @@ export interface WorkStatus {
|
|
|
44
44
|
order: number;
|
|
45
45
|
/** Optional accent colour for the dots in the list and the admin screen. */
|
|
46
46
|
color?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Entering this status needs a colleague's sign-off.
|
|
49
|
+
*
|
|
50
|
+
* One flag and no second ladder: a campaign template already ships a status called "Review"
|
|
51
|
+
* that does nothing, and this is what turns it into a gate. *Which* colleague is not here —
|
|
52
|
+
* that is an approval rule in `apps/context`, keyed on `work_item` + `status:<key>`, so the
|
|
53
|
+
* same person can change who signs without touching a project's workflow.
|
|
54
|
+
*
|
|
55
|
+
* Only entering is guarded. Leaving a guarded status is not a second approval: the item is
|
|
56
|
+
* already past the gate, and asking twice is how a workflow becomes something people route
|
|
57
|
+
* around.
|
|
58
|
+
*/
|
|
59
|
+
requiresApproval?: boolean;
|
|
47
60
|
}
|
|
48
61
|
/**
|
|
49
62
|
* A resolution: *how* something ended, apart from where it sits.
|