Teams & reporting
A team is a ConnectedId with Type=Group (see Connected ID). Reporting structure is modeled as edges between ConnectedIds, not columns — three relationship types: member_of (team membership), reporting (reports-to), reference (dotted-line).
Teams — /api/org/{slug}/teams
Section titled “Teams — /api/org/{slug}/teams”| Method · Route | Purpose | Source |
|---|---|---|
GET / | List teams | ReportingEndpoints.cs:23 |
POST / | Create a team (Name, Kind?, LeadConnectedId?, ParentTeamId? — teams nest) | :111 |
PUT /{teamId}/lead | Set/clear the team lead (null clears). Re-syncs every member’s auto reporting line (member → Lead) and emits reporting.team.lead_changed | :142 |
GET /{teamId}/members | Team members (?includeSubTeams=true) | :170 |
POST /{teamId}/members | Add a member (ConnectedIdId). May return needsResolution when a reporting conflict needs the add/replace/none choice | :186 |
DELETE /{teamId}/members/{memberId} | Remove a member | :224 |
When a member is added to a team with a lead, an automatic reporting edge (member → lead) is created. If the member already has a manager, the response carries needsResolution + existingManagers; the client resolves with Resolution = add / replace / none (:266).
Reporting tree & edges
Section titled “Reporting tree & edges”| Method · Route | Purpose | Source |
|---|---|---|
GET /api/org/{slug}/reporting/graph | The full tree — people + teams + all three edge types | :36 |
POST /api/org/{slug}/relationships | Add a reporting or reference edge (SourceConnectedId, TargetConnectedId, Type) | :52 |
DELETE /api/org/{slug}/relationships?sourceId=&targetId=&type= | Remove an edge | :84 |
member_of edges are managed through the team-members endpoints above; /relationships handles reporting/reference only (:254). Edges are many-to-many and nestable. Related: Members, Roles & permissions.
The Reporting Manager
Section titled “The Reporting Manager”A member’s Reporting Manager is simply the target of a reporting edge (member → manager). It is created three ways, all producing the same edge:
- Automatically from a team lead — adding a member to a team that has a Lead creates a
reportingedge (member → Lead) viaTeamReportingSyncService.OnMemberJoinedTeamAsync. Such edges carryConnectedIdRelationship.DerivedFromTeamIdso that later team changes (member moved/removed, Lead changed) re-sync only the auto edges and never touch manually-set ones. If the member already has a manager, the add returnsneedsResolutionand the client picks add / replace / none. - On invite — the “Reporting Manager (optional)” dropdown on Invite Member seeds the edge when the invitee is accepted.
- Manually — the member kebab → “Reporting Manager” (multi-select, with cycle detection) or a drag on the tree, via
POST /relationships.
The reporting tree these edges form is what the approval line walks up to resolve role columns (below).
Board of Directors — /api/org/{slug}/board
Section titled “Board of Directors — /api/org/{slug}/board”The board is a ConnectedId(Type=Group, GroupKind="Board") — one per organization, reusing the
team machinery: member_of edges are the director roster (and the voting register), the group’s
LeadConnectedId is the chairman. Boards exist for Corporation / Project(SPV) organizations only,
and the board group is excluded from team lists, plan team limits, and auto reporting lines.
| Method · Route | Purpose |
|---|---|
GET / | Board + roster. { exists, boardId, chairmanConnectedId, members }; members carry SharePercent (shareholding %, for minutes) |
POST / | Create the board (one per org; also auto-created when a Director invitation is accepted) |
POST /members | Appoint a director (ConnectedIdId) — personal seal required |
POST /members/{memberId}/remove | Remove a director — personal seal required |
PUT /chairman | Set/clear the chairman (LeadConnectedId, null clears) — personal seal required |
Every composition change is a governance boundary and requires a fresh personal-seal (passkey)
step-up: purpose board.member.change, subject {slug}|{targetConnectedId}|{appoint|remove|chairman}.
Appointing via invitation is the primary path: invite a member with the Director or Representative Director role (see Members → Director invitations). The seal is collected when the invite is sent; on acceptance the member is seated automatically (with optional chairman designation and shareholding %).
Board meetings (POST /api/org/{slug}/meetings with Type = BoardMeeting) require the board to
exist and to have a chairman — otherwise the create fails with BOARD_REQUIRED /
BOARD_CHAIRMAN_REQUIRED. See Meetings.
Approval line — /api/org/{slug}/approvals/work-items/{id}/approval-line
Section titled “Approval line — /api/org/{slug}/approvals/work-items/{id}/approval-line”Per-Form columns of the approval seal grid (replaces the hardcoded staff / team-lead / executive columns). Configured in the Create Form wizard (Step 2) — the config lives on the Form because a Form already is a Role and carries its own routing.
| Method · Route | Purpose |
|---|---|
GET /api/org/{slug}/approvals/work-items/{id}/approval-line | Ordered positions for this Form. Empty → the form falls back to the default staff / team-lead / executive columns (opt-in). |
PUT /api/org/{slug}/approvals/work-items/{id}/approval-line | Replace all positions ({ positions: [...] }). Owner/admin only; the Form must belong to the org. Emits reporting.approvalline.changed (RPT038, AggregateId = WorkItemId). |
GET /api/org/{slug}/approvals/work-items/{id}/resolve-line | Preview — resolves the line for the current requester and returns the approver count, without mutating anything (ApprovalLineService.PreviewApproverCountAsync, read-only). |
Each position has per-locale labels (labelKo/labelEn/labelJa) and a resolve type — how it maps to a person when an approval is submitted:
resolveType | Resolves to |
|---|---|
drafter | The requester — no approval step |
role | Nearest holder of resolveRoleId (ConnectedIdRole, RBAC single source) up the requester’s reporting tree; falls back to resolveMemberConnectedId if no one up the tree holds the role |
member | A fixed approver (resolveMemberConnectedId) |
ApprovalLineService derives the line from the Form’s positions (instance.WorkItemId) — dual-role dedup (one stamp if the same person fills two columns), 0 approvers ⇒ auto-grant; on submit each approver is emailed the document. The entity ApprovalLinePosition is sealed (AuthIntegrityBase), scoped per-Form by WorkItemId.
No-approver block
Section titled “No-approver block”An approval form that resolves to 0 approvers (e.g. an Account Owner with no one above them) would silently auto-grant a pointless approval. The New Request UI pre-checks via resolve-line on load and, when the count is 0, shows a blocking popup and disables Submit. The auto-grant core is untouched — the guard is purely a UI gate driven by the read-only preview.
Requests (instances) & submit step-up
Section titled “Requests (instances) & submit step-up”| Method · Route | Purpose |
|---|---|
GET /api/org/{slug}/approvals/instances | List the requester’s / approver’s instances (read model carries IsRequester, IsReceiver, MyStepStatus, CreatorName, per-step ApproverName). |
POST /api/org/{slug}/approvals/instances | Submit a request. Derives the approval line, seals the instance, assigns the document number, and emails approvers. Requires a WebAuthn assertion — see below. |
POST /api/org/{slug}/approvals/instances/{id}/stamp | An approver stamps (approve/reject). |
GET /api/org/{slug}/approvals/instances/by-number/{documentNumber} | Look up a sealed instance by its {slug}-{yyyy}-{000001} document number. |
TPM / passkey step-up on submit
Section titled “TPM / passkey step-up on submit”Submitting (“stamp & submit”) is a human-presence-verified action. The client requests a challenge (api/passkey/login/options), calls navigator.credentials.get (Windows Hello / TPM), and sends the assertion on the submit request. The endpoint verifies it via IFido2Service and requires assertion.UserId == the session owner — mirroring the account-switch step-up. A member with no passkey cannot submit (clear “register a personal seal first” message); this is intended proof-of-decision, not a bug.
Personal seal (single-source render)
Section titled “Personal seal (single-source render)”Every seal in the approval flow renders through one component, Components/Shared/PersonalSignature, from the user’s User.SignatureSpec (exposed on /me):
- Submit popup shows the drafter’s designed personal seal (not a name-based default circle).
MemberDetailrefreshesAuthStoreafter a self signature save so the popup is fresh. - Document seal columns render each participant’s designed seal; the name-based fallback is used only for participants with no spec.
- The org seal on the approval document renders correctly everywhere because the generated seal SVG is self-contained — its fonts are embedded at creation (
CorporateSeal.RenderBytesEmbeddedAsync), so even the<img>path keeps the Korean seal-script face. Inline rendering (PersonalSignature.InlineSizedSvg) is still attempted for user-uploaded SVGs, which carry no embedded fonts.
End-to-end: how a request flows
Section titled “End-to-end: how a request flows”Putting the pieces together — Form, seals, the Reporting Manager, and Requests & Approvals:
-
Define the Form (
WorkItem) — Owner/admin creates it in the Create Form wizard. Type = Approval (picks a Target action from the Action Library, auto-creates the linked Role) or Reporting (picks recipients: team/dept, member, external email). The Form carries its seal columns (ApprovalLinePosition[], inline on create),LineMode(single / chain / quorum),RiskLevel, and language. -
Compose a request — a member opens New Request, picks a Form, and fills the shared A4
ApprovalDocument: letterhead with the company seal (issuer), an auto header block (dept / drafter / date), recipients / cc, title, body, attachments, and the seal grid. The document number is previewed (resolve-linealso pre-checks the approver count — a 0-approver Approval is blocked here; see No-approver block). -
Submit (
POST /approvals/instances→CreateReportingInstanceCommandHandler) — the “stamp & submit” popup shows the drafter’s personal seal, then a WebAuthn/TPM assertion is required (human presence). The handler seals theReportingInstance, assigns the document number{slug}-{yyyy}-{NNNNNN}atomically (ReportingDocumentSequence), and derives the approval line. -
Route to the Reporting Manager —
ApprovalLineService.DeriveFromPositionsAsyncresolves each position:drafter→ the requester (no step);role→ResolveRoleApproverwalks up the requester’s reporting tree (WalkUpAsync) and picks the nearest ancestor who holds that Role (ConnectedIdRole= RBAC single source), falling back to a pinned member;member→ a fixed person. Duplicates and the requester are de-duped. This is how a report reaches the right manager: the Form says “the person holding role X above me”, and the tree says who that is. It writes an orderedApprovalStep(statuspending) per approver and emails each one (NotifyApproversAsync, recordsApprovalStep.NotifiedAt). A Reporting-type Form instead emails the resolved recipients (NotifyRecipientsAsync, groups expanded to members). Zero approvers ⇒ auto-grant. -
Approve / reject in Requests & Approvals — each approver sees the instance under the Received filter and stamps in place (
POST /instances/{id}/stamp). The handler re-checks the ceiling at stamp time (capability may have shrunk since submit), seals the step, emits RPT031/032, and recomputes progress viaApprovalStampLogic.ProgressStatus(single → 1 approval; chain/parallel → all; quorum → K-of-N; any reject → rejected). The approver’s personal seal stamps their column. -
Materialize on completion — when the line completes,
ApprovalStampLogic.DecideMaterializeModereads theusescontrol variable:uses=1→ the sealed instance is the one-time credential (no grant); otherwise the approved scope becomes a persistentConnectedIdPermissionfor the requester (currentlyCreateDirectGrant) andApprovalCompletedEvent(RPT033) fires.
Every step is sealed (AuthIntegrityBase) and emits a 5-piece audit event — the document, its number, each seal, and the final grant are all tamper-evident. See Capability grants for the ceiling model behind the line.