Orgs — services¶
Responsibilities¶
The orgs services contain the business logic for organization and membership workflows.
They are responsible for:
- organization lookup and current-org resolution
- listing organizations and current-org members
- updating organization details and branding
- membership permission checks
- role-change workflows
- activation/deactivation of memberships
- owner-safety rules such as “must keep at least one active owner”
They are not responsible for:
- HTTP request/response handling
- serializer field validation
- URL routing
- admin configuration
- defining persistence structure directly
In this app, services are the place where multi-step org and membership rules live.
Main services¶
services.organization¶
Business logic for organization retrieval, listing, and update workflows.
Responsibilities¶
- find organizations safely
- resolve current organization from request context
- list orgs for a user
- list active members for a current org
- expose resolved branding payload
- update organization detail fields
- update branding/document identity fields
get_org_by_id(org_id)¶
Returns an organization by primary key.
- Inputs
org_id- Behavior
- loads the organization regardless of active state
- Returns
Organization- Raises
OrganizationNotFoundError
Used when the caller needs any org record, not just active ones.
get_active_org_by_id(org_id)¶
Returns an active organization by primary key.
- Inputs
org_id- Behavior
- only returns active orgs
- Returns
Organization- Raises
OrganizationNotFoundError
Useful for flows where inactive orgs should be invisible.
get_current_org(request)¶
Returns the request-scoped current organization.
- Inputs
request- Behavior
- reads
request.org - Returns
Organization- Raises
OrganizationNotFoundError
This is the main bridge between request-scoped org resolution and the service layer.
list_user_orgs(user)¶
Lists active organizations for which the user has an active internal membership.
- Inputs
user- Behavior
- filters by:
memberships__user=usermemberships__is_active=Trueis_active=True
- removes duplicates
- orders by
name,id - Returns
- queryset of
Organization
Used by: - org-switching UI - bootstrap/current-user org discovery
list_current_org_members(org)¶
Lists active internal members for a given organization.
- Inputs
org- Behavior
- returns only active memberships
- excludes inactive users
- uses
select_related("user") - orders by user identity fields
- Returns
- queryset of
OrgMembership
Used by member list endpoints for the current org.
get_org_branding_payload(org)¶
Returns resolved branding data for an organization.
- Inputs
org- Behavior
- delegates to
org.effective_branding() - Returns
- branding dictionary
This is the service-level access point for resolved branding/document identity.
update_org_details(org, data)¶
Updates allowed organization fields.
- Inputs
orgdata- Behavior
- validates requested fields against an allowlist
- assigns values to the model
- runs
full_clean() - saves the organization
- Returns
- updated
Organization - Raises
OrganizationValidationError
This is the general update path for org details/settings.
Allowed fields¶
namelegal_namedisplay_nameaddress_line1address_line2postal_codecitystate_regioncountryphoneemailwebsitevat_numbercoc_numberlogologo_pdfbrand_primarybrand_textbrand_mutedpdf_footer_notepdf_show_legal_idspdf_show_contact_detailssettingsis_active
update_org_branding(org, data)¶
Updates branding/document identity fields only.
- Inputs
orgdata- Behavior
- validates requested fields against a branding-specific allowlist
- assigns values to the model
- runs
full_clean() - saves the organization
- Returns
- updated
Organization - Raises
OrganizationValidationError
This is the narrower update path for branding/admin screens.
Allowed fields¶
legal_namedisplay_nameaddress_line1address_line2postal_codecitystate_regioncountryphoneemailwebsitevat_numbercoc_numberlogologo_pdfbrand_primarybrand_textbrand_mutedpdf_footer_notepdf_show_legal_idspdf_show_contact_details
Exceptions in services.organization¶
OrganizationError¶
Base exception for organization service errors.
OrganizationNotFoundError¶
Raised when the requested org cannot be found or resolved.
OrganizationValidationError¶
Raised when an update request includes unsupported fields or invalid organization data.
services.membership¶
Business logic for internal organization membership management.
Responsibilities¶
- load memberships safely in org context
- determine current user role in org
- determine whether a user may manage members
- count active owners
- validate role-management boundaries
- change member role
- deactivate membership
- reactivate membership
- create or update memberships
- write audit log entries for membership mutations
This module is the main membership lifecycle backbone for internal org users.
It now follows a layered authorization model:
- policy layer (
core.policies.orgs) decides broad capabilities such as whether a user may manage org members at all - service layer enforces target-specific workflow rules such as:
- admins cannot manage owners
- users cannot manage their own membership through admin flows
- the organization must always keep at least one active owner
get_user_org_membership(user, org)¶
Returns any membership for a user in an organization.
- Inputs
userorg- Returns
OrgMembershiporNone
get_active_user_org_membership(user, org)¶
Returns only active membership for a user in an organization.
- Inputs
userorg- Returns
OrgMembershiporNone
Used by most permission helpers.
get_user_role_in_org(user, org)¶
Returns the active role of a user in an organization.
- Inputs
userorg- Returns
- role string or
None
This is a convenience helper for role-aware checks.
is_org_owner(user, org)¶
Checks whether the user is an active owner in the org.
- Inputs
userorg- Returns
- boolean
is_org_admin(user, org)¶
Checks whether the user is an active admin-level member in the org.
- Inputs
userorg- Behavior
- treats both
ownerandadminas admin-level - Returns
- boolean
can_manage_members(actor, org)¶
Checks whether the actor may manage internal org members.
- Inputs
actororg- Behavior
- requires authentication
- allows superuser
- otherwise delegates to
core.policies.orgs.can_manage_org_members(...) - Returns
- boolean
This is a convenience helper used by membership-management flows.
get_membership_by_id_for_org(membership_id, org)¶
Loads one membership only within the given org scope.
- Inputs
membership_idorg- Behavior
- uses
select_related("user", "org") - Returns
OrgMembership- Raises
MembershipNotFoundError
This prevents cross-org membership access by id.
count_active_owners(org)¶
Counts the number of active owners in an organization.
- Inputs
org- Returns
- integer
This is central to safety rules around owner demotion/deactivation.
validate_membership_management(actor, target_membership)¶
Validates whether the actor may manage a target membership.
- Inputs
actortarget_membership- Behavior
- requires authentication
- allows superuser
- otherwise requires:
- active membership in the same org
- org-member-management capability via the policy layer
- blocks self-management through admin flows
- blocks admins from managing owners
- Raises
MembershipPermissionError
This function is the main guard for membership write operations.
Important split: - broad authorization comes from the policy layer - target-specific restrictions remain in the service layer
change_membership_role(actor, membership, new_role, request=None)¶
Changes the role of a membership.
- Inputs
actormembershipnew_rolerequest(optional, for audit metadata)- Behavior
- validates management permission
- validates role value
- no-ops if role unchanged
- blocks admin assigning owner role
- blocks removing/downgrading the last active owner
- updates membership role
- writes audit entry:
org_membership.role_changed
- Returns
- updated
OrgMembership - Raises
MembershipPermissionErrorMembershipValidationError
This is the main workflow for role updates.
deactivate_membership(actor, membership, request=None)¶
Deactivates a membership.
- Inputs
actormembershiprequest(optional, for audit metadata)- Behavior
- validates management permission
- no-ops if already inactive
- blocks deactivation of the last active owner
- sets
is_active=False - writes audit entry:
org_membership.deactivated
- Returns
- updated
OrgMembership - Raises
MembershipPermissionErrorMembershipValidationError
This is the safe “soft removal” path for internal members.
reactivate_membership(actor, membership, request=None)¶
Reactivates a membership.
- Inputs
actormembershiprequest(optional, for audit metadata)- Behavior
- validates management permission
- no-ops if already active
- sets
is_active=True - writes audit entry:
org_membership.reactivated
- Returns
- updated
OrgMembership - Raises
MembershipPermissionErrorMembershipValidationError
create_org_membership(user, org, role=..., is_active=True, actor=None, request=None)¶
Creates or updates an internal membership.
- Inputs
userorgroleis_activeactor(optional, for audit attribution)request(optional, for audit metadata)- Behavior
- creates new membership if missing
- otherwise updates role and/or active state
- writes audit entry when created:
org_membership.created
- writes audit entry when updated:
org_membership.updated
- does not write audit entry for no-op calls
- Returns
OrgMembership
This is a reusable helper for invite acceptance and future membership add flows.
Service relationship overview¶
flowchart TD
OrganizationService["services.organization"]
MembershipService["services.membership"]
Organization["Organization"]
OrgMembership["OrgMembership"]
User["User"]
RequestOrg["request.org"]
OrganizationService --> Organization
OrganizationService --> OrgMembership
OrganizationService --> RequestOrg
MembershipService --> OrgMembership
MembershipService --> Organization
MembershipService --> User
Call flow examples¶
Current org detail update¶
sequenceDiagram
participant View as CurrentOrgUpdateView
participant Serializer as OrganizationDetailSerializer
participant Service as services.organization.update_org_details
participant Org as Organization
View->>Serializer: validate PATCH payload
Serializer-->>View: validated_data
View->>Service: update_org_details(org, validated_data)
Service->>Org: assign fields + full_clean + save
Org-->>Service: updated org
Service-->>View: updated org
View-->>Client: serialized org response
Membership role change¶
sequenceDiagram
participant View as OrgMembershipRoleUpdateView
participant Serializer as OrgMembershipRoleUpdateSerializer
participant Service as services.membership.change_membership_role
participant Membership as OrgMembership
View->>Serializer: validate role payload
Serializer-->>View: validated_data
View->>Service: load membership in current org
View->>Service: change_membership_role(actor, membership, new_role)
Service->>Service: validate management permission
Service->>Membership: save new role
Membership-->>Service: updated membership
Service-->>View: updated membership
View-->>Client: membership response
Design notes¶
¶
Why organization lookup is centralized
A lot of org-scoped logic depends on consistent behavior around: • current org resolution • active-org filtering • member listing
Keeping that in services.organization avoids scattering the same queryset rules across views.
Why owner safety lives in the service layer¶
Rules like: • “admins cannot manage owners” • “must always keep one active owner”
are core business rules, not serializer or view concerns.
Keeping them in services.membership ensures they are: • testable directly • reusable • hard to bypass accidentally
Why membership deactivation is soft-state¶
The service layer keeps membership removal as: • is_active=False
instead of deleting rows.
That preserves: • history • auditability • ability to reactivate later
This is safer operationally than hard deletion.
Why policy and service layers are split¶
The orgs app now uses a two-layer authorization model:
Policy layer¶
Located in core.policies.orgs.
It answers broad questions such as: - can this user manage org members? - can this user manage teams? - can this user manage org settings?
Service layer¶
Located in orgs.services.membership.
It answers target-specific workflow questions such as: - can this admin manage this specific membership? - is this the last active owner? - is this a self-management attempt?
This split keeps: - permission rules centralized - workflow rules explicit - service behavior easier to test
Why membership mutations are audited¶
Membership changes affect tenant access and authorization boundaries.
For that reason, the following service mutations now write audit logs:
- role changes
- deactivation
- reactivation
- membership creation
- membership update through create-or-update flow
Audit logging is written through core.audit.log_audit(...).
This provides: - traceability - accountability - safer operational debugging - a future base for audit-history UI endpoints