Skip to content

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=user
    • memberships__is_active=True
    • is_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
  • org
  • data
  • 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

  • name
  • legal_name
  • display_name
  • address_line1
  • address_line2
  • postal_code
  • city
  • state_region
  • country
  • phone
  • email
  • website
  • vat_number
  • coc_number
  • logo
  • logo_pdf
  • brand_primary
  • brand_text
  • brand_muted
  • pdf_footer_note
  • pdf_show_legal_ids
  • pdf_show_contact_details
  • settings
  • is_active

update_org_branding(org, data)

Updates branding/document identity fields only.

  • Inputs
  • org
  • data
  • 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_name
  • display_name
  • address_line1
  • address_line2
  • postal_code
  • city
  • state_region
  • country
  • phone
  • email
  • website
  • vat_number
  • coc_number
  • logo
  • logo_pdf
  • brand_primary
  • brand_text
  • brand_muted
  • pdf_footer_note
  • pdf_show_legal_ids
  • pdf_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
  • user
  • org
  • Returns
  • OrgMembership or None

get_active_user_org_membership(user, org)

Returns only active membership for a user in an organization.

  • Inputs
  • user
  • org
  • Returns
  • OrgMembership or None

Used by most permission helpers.


get_user_role_in_org(user, org)

Returns the active role of a user in an organization.

  • Inputs
  • user
  • org
  • 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
  • user
  • org
  • Returns
  • boolean

is_org_admin(user, org)

Checks whether the user is an active admin-level member in the org.

  • Inputs
  • user
  • org
  • Behavior
  • treats both owner and admin as admin-level
  • Returns
  • boolean

can_manage_members(actor, org)

Checks whether the actor may manage internal org members.

  • Inputs
  • actor
  • org
  • 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_id
  • org
  • 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
  • actor
  • target_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
  • actor
  • membership
  • new_role
  • request (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
  • MembershipPermissionError
  • MembershipValidationError

This is the main workflow for role updates.

deactivate_membership(actor, membership, request=None)

Deactivates a membership.

  • Inputs
  • actor
  • membership
  • request (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
  • MembershipPermissionError
  • MembershipValidationError

This is the safe “soft removal” path for internal members.

reactivate_membership(actor, membership, request=None)

Reactivates a membership.

  • Inputs
  • actor
  • membership
  • request (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
  • MembershipPermissionError
  • MembershipValidationError

create_org_membership(user, org, role=..., is_active=True, actor=None, request=None)

Creates or updates an internal membership.

  • Inputs
  • user
  • org
  • role
  • is_active
  • actor (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