Skip to content
Merged
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
15 changes: 10 additions & 5 deletions src/flows/contract-intake.flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,16 @@ import { must, ref } from './_values.js';
* version were already committed, orphaning both. WHERE the intake-captured
* arrangement F9 reads should be stored is a design gap, raised as its own
* decision card rather than guessed at here.
* - `clm_party`: refused for a requester the same way, granted to legal
* (§04 gives the requester R, legal RCU). The inline-create branch stays
* because DESIGN.md §06 F1 names it ("相对方查找或新建") and legal and
* admin do launch contracts; it fails ATOMICALLY — `create_party` runs
* before any other write, so a refused requester leaves nothing behind.
* - `clm_party`: created (201) by a requester as well as by legal (#133,
* ruling A: the requester may create a counterparty but not edit one, and
* holds the risk, screening and bank-name fields read-only). That is why
* `create_party` writes only the identity and contact fields the screen
* asks for: naming a locked field would refuse the whole write with 403
* "Field write denied". A new party is therefore born unflagged and
* unscreened, and legal maintains those fields afterwards (on a party
* legal does not own, writes are owner-scoped today, see #152). Should the
* create still be refused, it fails ATOMICALLY: `create_party` runs before
* any other write, so the run leaves nothing behind.
*
* Flow copy is English-only (the source language, DESIGN.md §01); a flow has
* no entry in the translation bundles.
Expand Down
16 changes: 16 additions & 0 deletions src/profiles/_grants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,19 @@ export const CONTRACT_STAMPED_FIELDS = fieldsInGroups(Contract, ['routing', 'lif
* `liability_cap`).
*/
export const CONTRACT_LEGAL_FIELDS = [...fieldsInGroups(Contract, ['legal']), 'risk_level', 'liability_cap'];

/**
* The counterparty fields legal assesses, or the screening connector (§08)
* stamps: never the person who adds the party (#133, ruling A). `clm_requester`
* may create a counterparty, and it holds these read-only, so a new party is
* born unflagged and unscreened whatever the caller sends. The ruling's
* `verified_at` has been `screening_status` since the global-first revision.
*
* Every member holds `clm_requester`, and only the sets that DECLARE a field
* vote on it ({@link open}). So the sets that maintain these fields, `clm_legal`
* and `clm_admin`, open the same list, and `clm_finance` opens the banking
* pair. Without that, the requester's lock would bind them too: measured on
* 18.1.0, Legal Counsel 1 was refused `risk_flag` on her own party (403
* "Field write denied") until legal declared it.
*/
export const PARTY_ASSESSED_FIELDS = ['risk_flag', 'risk_note', 'screening_status', 'is_active', 'bank_name'] as const;
4 changes: 3 additions & 1 deletion src/profiles/admin.profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Party } from '../objects/party.object.js';
import { Review } from '../objects/review.object.js';
import { Signature } from '../objects/signature.object.js';
import { CONTRACT_STATUSES, OBLIGATION_STATUSES } from '../sharing/_lifecycle.js';
import { CONTRACT_STAMPED_FIELDS, inList, openAllExcept, open, readOnly } from './_grants.js';
import { CONTRACT_STAMPED_FIELDS, PARTY_ASSESSED_FIELDS, inList, openAllExcept, open, readOnly } from './_grants.js';

/**
* `clm_admin` — the CLM administrator (DESIGN.md §04): RCUD on every object,
Expand Down Expand Up @@ -80,6 +80,8 @@ export const AdminSet = definePermissionSet({
...openAllExcept(Signature, []),
...open(Review, ['internal_note']),
...open(Party, ['bank_account', 'contact_phone']),
// Read-only for clm_requester, which every member holds (#133).
...open(Party, PARTY_ASSESSED_FIELDS),
},
rowLevelSecurity: [
{
Expand Down
8 changes: 7 additions & 1 deletion src/profiles/finance.profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Contract } from '../objects/contract.object.js';
import { Party } from '../objects/party.object.js';
import { Review } from '../objects/review.object.js';
import { POST_APPROVAL_STATUSES } from '../sharing/_lifecycle.js';
import { CONTRACT_LEGAL_FIELDS, CONTRACT_STAMPED_FIELDS, CONTRACT_TERMINATION_REASON, editableOnly, hidden, inList, readOnly } from './_grants.js';
import { CONTRACT_LEGAL_FIELDS, CONTRACT_STAMPED_FIELDS, CONTRACT_TERMINATION_REASON, editableOnly, hidden, inList, open, readOnly } from './_grants.js';

/**
* `clm_finance` — the finance controller (DESIGN.md §04).
Expand Down Expand Up @@ -48,6 +48,12 @@ export const FinanceSet = definePermissionSet({
...readOnly(Contract, [...CONTRACT_LEGAL_FIELDS, 'status', CONTRACT_TERMINATION_REASON]),
...hidden(Review, ['internal_note']),
...editableOnly(Party, ['bank_name', 'bank_account']),
// editableOnly declares only the fields it locks, so the banking pair has
// to be opened here: clm_requester, which every member holds, hides
// `bank_account` and locks `bank_name`, and a set that says nothing does
// not out-vote it (`_grants.ts`, open). Without this line finance could
// neither read nor write the bank account (#133).
...open(Party, ['bank_name', 'bank_account']),
},
rowLevelSecurity: [
{
Expand Down
4 changes: 3 additions & 1 deletion src/profiles/legal.profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Party } from '../objects/party.object.js';
import { Review } from '../objects/review.object.js';
import { Signature } from '../objects/signature.object.js';
import { CONTRACT_STATUSES, OBLIGATION_STATUSES, REVIEW_STAGES } from '../sharing/_lifecycle.js';
import { CONTRACT_STAMPED_FIELDS, inList, openAllExcept, open, readOnly } from './_grants.js';
import { CONTRACT_STAMPED_FIELDS, PARTY_ASSESSED_FIELDS, inList, openAllExcept, open, readOnly } from './_grants.js';

/**
* `clm_legal` — legal counsel and the head of legal (DESIGN.md §04).
Expand Down Expand Up @@ -70,6 +70,8 @@ export const LegalSet = definePermissionSet({
...openAllExcept(Signature, []),
...open(Review, ['internal_note']),
...open(Party, ['bank_account', 'contact_phone']),
// Read-only for clm_requester, which every member holds (#133).
...open(Party, PARTY_ASSESSED_FIELDS),
},
rowLevelSecurity: [
{
Expand Down
17 changes: 15 additions & 2 deletions src/profiles/requester.profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Contract } from '../objects/contract.object.js';
import { Party } from '../objects/party.object.js';
import { Review } from '../objects/review.object.js';
import { REQUESTER_EDITABLE_STATUSES } from '../sharing/_lifecycle.js';
import { CONTRACT_STAMPED_FIELDS, CONTRACT_TERMINATION_REASON, hidden, inList, readOnly } from './_grants.js';
import { CONTRACT_STAMPED_FIELDS, CONTRACT_TERMINATION_REASON, PARTY_ASSESSED_FIELDS, hidden, inList, readOnly } from './_grants.js';

/**
* `clm_requester` — every employee (DESIGN.md §04 "所有员工默认").
Expand Down Expand Up @@ -74,7 +74,16 @@ export const RequesterSet = definePermissionSet({
clm_obligation: { allowCreate: false, allowRead: true, allowEdit: true, allowDelete: false },
clm_payment_plan: { allowCreate: false, allowRead: true, allowEdit: false, allowDelete: false },
// Configuration objects are public_read: everyone reads every row.
clm_party: { allowCreate: false, allowRead: true, allowEdit: false, allowDelete: false },
//
// A requester may CREATE a counterparty, because the intake flow (§06 F1
// "相对方查找或新建") offers every launcher the inline branch and runs its
// writes as the launching user (#133, ruling A). Editing stays legal's
// (§04 RCU), so a requester cannot change a party once it exists, theirs
// included. The risk and screening fields and the bank are read-only
// below, so a new party is born unflagged and unscreened whatever the
// caller sends. Create is not high-privilege (ADR-0090 D5), so the
// `everyone` anchor binding stands.
clm_party: { allowCreate: true, allowRead: true, allowEdit: false, allowDelete: false },
clm_contract_type: { allowCreate: false, allowRead: true, allowEdit: false, allowDelete: false },
clm_clause: { allowCreate: false, allowRead: true, allowEdit: false, allowDelete: false },
clm_approval_rule: { allowCreate: false, allowRead: true, allowEdit: false, allowDelete: false },
Expand Down Expand Up @@ -146,6 +155,10 @@ export const RequesterSet = definePermissionSet({
...hidden(Review, ['internal_note']),
// Payment and contact details are the easiest to leak (§04 FLS table).
...hidden(Party, ['bank_account', 'contact_phone']),
// Assessed by legal, or stamped by the screening connector (§08), never
// by the person who adds the party (#133, ruling A). Legal, admin and
// finance open what they maintain of this list in their own sets.
...readOnly(Party, PARTY_ASSESSED_FIELDS),
},
rowLevelSecurity: [
{
Expand Down
Loading