Skip to content

Approvals — Services

Overview

The services layer is the core workflow layer of the approvals app.

It is responsible for:

  • computing approval assignees
  • resolving delegation relationships
  • building inbox visibility rules
  • deciding approvals
  • forwarding and unforwarding approvals
  • creating and disabling delegations
  • applying post-decision side effects
  • writing audit records for workflow actions

The services layer should contain the business rules of the approvals system.

It sits between:

  • views and serializers
  • models
  • related domain apps such as timesheets
  • notifications

Service modules

services/approvals.py

Contains the main approval workflow actions.

Primary responsibilities:

  • decide an approval
  • forward an approval
  • unforward an approval
  • determine whether a user may forward an approval
  • expose lightweight workflow helpers such as approval labeling

Typical functions:

  • decide_approval(...)
  • forward_approval(...)
  • unforward_approval(...)
  • can_forward(...)
  • approval_label(...)
  • week_label(...)

This module owns the main write-side workflow for active approvals.


services/delegations.py

Contains delegation lifecycle logic.

Primary responsibilities:

  • validate delegation creation inputs
  • create delegations
  • replace existing active delegations for the same scope
  • disable delegations
  • notify relevant users when delegation changes

Typical functions:

  • create_delegation(...)
  • disable_delegation(...)
  • validate_delegation_users(...)

This module owns delegation workflow behavior.


services/assignees.py

Contains assignee computation and delegation-aware approver resolution.

Primary responsibilities:

  • compute who should be assigned to an approval
  • refresh assignment rows
  • resolve delegates for a manager
  • resolve delegators for a user
  • resolve effective approvers for timesheet approvals

Typical functions:

  • compute_assignees_for_approval(...)
  • refresh_assignees(...)
  • delegate_user_ids_for_manager(...)
  • delegator_user_ids_for_user(...)
  • approver_users_for_timesheet(...)

This module defines who should act on an approval.


services/inbox.py

Contains inbox visibility query logic.

Primary responsibilities:

  • build inbox querysets for a user
  • include direct assignees
  • include delegated visibility
  • include forwarded approvals
  • enforce forwarding visibility rules

Typical functions:

  • inbox_queryset_for_user(...)

This module owns the read-model visibility logic for approval inboxes.


services/handlers.py

Contains post-decision side effects.

Primary responsibilities:

  • apply business-specific target side effects after a decision
  • keep target updates separate from generic approval workflow

Typical functions:

  • apply_decision_side_effects(...)

Currently supports:

  • timesheet approval/rejection updates

This module is the boundary between generic approval workflow and target-specific domain behavior.


services/audit.py

Contains approval-specific audit helpers.

Primary responsibilities:

  • write workflow audit entries
  • capture user snapshots
  • capture delegation context when someone acts on behalf of another user

Typical functions:

  • write_audit(...)
  • user_snapshot(...)
  • delegation_audit_for_decision(...)

This module supports workflow traceability.


services/helpers.py

Contains shared presentation-oriented helper functions used across services and serializers.

Primary responsibilities:

  • build stable user labels
  • build lightweight user meta payloads

Typical functions:

  • user_label(...)
  • user_meta(...)

This module helps keep helper logic centralized instead of duplicated across views and serializers.


Core workflow flows

Approval decision flow

The approval decision workflow is the most important service path.

Main steps

  1. validate approval is still pending
  2. capture delegation audit context if actor is acting on behalf of someone else
  3. mark approval approved or rejected
  4. apply target-specific side effects
  5. notify affected users if needed
  6. return updated approval

flowchart TD
    A[decide_approval] --> B{Approval pending?}
    B -->|no| C[Raise validation error]
    B -->|yes| D[Capture delegation audit context]
    D --> E{Decision}
    E -->|approve| F[mark_approved]
    E -->|reject| G[mark_rejected]
    F --> H[apply_decision_side_effects]
    G --> H
    H --> I[Send notifications if applicable]
    I --> J[Return updated approval]

Forwarding flow

Forwarding allows an approval to be sent to another active org member.

Main steps

  1. ensure approval is pending
  2. ensure actor is allowed to forward
  3. ensure recipient is an active org member
  4. replace previous forwarded assignee if needed
  5. create forwarded assignee row
  6. append forward metadata to payload
  7. notify recipient
  8. return updated approval

flowchart TD
    A[forward_approval] --> B{Approval pending?}
    B -->|no| C[Raise validation error]
    B -->|yes| D{Actor can forward?}
    D -->|no| E[Raise permission error]
    D -->|yes| F{Recipient active in org?}
    F -->|no| G[Raise validation error]
    F -->|yes| H[Remove previous forwarded assignees]
    H --> I[Create or keep forwarded assignee]
    I --> J[Append forward payload audit]
    J --> K[Send forwarded notification]
    K --> L[Return updated approval]


Unforwarding flow

Unforwarding removes a forwarded assignment from an approval.

Main steps

  1. ensure approval is pending
  2. ensure actor is allowed to unforward
  3. remove forwarded assignee row
  4. write audit event
  5. notify recipient that the forward was removed
  6. return updated approval
flowchart TD
    A[unforward_approval] --> B{Approval pending?}
    B -->|no| C[Raise validation error]
    B -->|yes| D{Actor can unforward?}
    D -->|no| E[Raise permission error]
    D -->|yes| F[Delete forwarded assignee]
    F -->|not found| G[Raise validation error]
    F -->|deleted| H[Write audit]
    H --> I[Send removal notification]
    I --> J[Return updated approval]

Delegation creation flow

Delegation creation is a service-backed workflow, not serializer-owned logic.

Main steps

  1. normalize kind and timestamps
  2. validate users
  3. disable existing active delegation in same scope
  4. create new delegation row
  5. notify both parties
  6. return delegation
flowchart TD
    A[create_delegation] --> B[Normalize kind and dates]
    B --> C[Validate users and membership]
    C --> D[Disable existing active delegation]
    D --> E[Create new delegation]
    E --> F[Send delegation created notifications]
    F --> G[Return delegation]

Assignee computation flow

Assignee computation determines who should act on an approval.

Current behavior

For timesheet submission approvals: 1. find effective manager 2. include active delegates for that manager 3. exclude the engineer themselves 4. filter to active org members 5. preserve stable order 6. return assignee tuples

flowchart TD
    A[compute_assignees_for_approval] --> B{Approval kind}
    B -->|timesheet_submit| C[Find effective manager]
    C --> D[Resolve delegates]
    D --> E[Remove submitter]
    E --> F[Filter active org members]
    F --> G[Return assignee tuples]
    B -->|other| H[Return empty list]

Inbox visibility flow

The inbox service defines which approvals a user sees.

Visibility includes

•   directly assigned approvals
•   approvals visible through delegation
•   approvals forwarded to the user

Special rule

If an approval has been forwarded, only the forwarded assignee should see it.

flowchart TD
    A[inbox_queryset_for_user] --> B[Base pending approvals]
    B --> C[Annotate direct assignment]
    C --> D[Annotate forward presence]
    D --> E[Annotate forwarded_to_me]
    E --> F{Include delegated?}
    F -->|yes| G[Annotate delegated visibility]
    F -->|no| H[Skip delegated annotation]
    G --> I[Build visible filter]
    H --> I
    I --> J[Apply forwarded-only visibility rule]
    J --> K[Return ordered queryset]

Side-effect handling

The approvals app is generic, but decisions may affect target objects.

That behavior is deliberately separated into services/handlers.py.

Current supported side effect

For timesheet_submit: • approval approved -> weekly timesheet becomes approved • approval rejected -> weekly timesheet becomes rejected

Why this matters

This keeps: • generic approval workflow • target-specific business rules

cleanly separated.


Service responsibilities

Services layer owns

•   approval workflow actions
•   delegation lifecycle
•   assignee resolution
•   inbox visibility logic
•   post-decision side effects
•   workflow audit metadata
•   notification emission for approval events

Models layer owns

•   persistence
•   field validation
•   status helper methods

Views layer owns

•   HTTP request handling
•   serializer invocation
•   permission wiring
•   response mapping

Data flow

flowchart TD
    A[View] --> B[Serializer validation]
    B --> C[Service]
    C --> D[Models]
    C --> E[Notifications]
    C --> F[Related domain side effects]
    D --> G[Updated state]
    G --> H[Serializer output]
    H --> I[Response]

Service error model

The service layer should raise domain-specific errors rather than HTTP responses.

Examples include: - permission errors - validation errors - workflow state errors

Views should translate these into: - 400 - 403 - 404 - 409

as appropriate.

This keeps services transport-agnostic.


Integration with other apps

The approvals services integrate with:

Timesheets

  • approval side effects update WeeklyTimesheet
  • assignee computation uses reporting structure / manager resolution

Notifications

Approval workflows emit notifications such as: - approval forwarded to user - forward removed - delegation created - delegation removed - timesheet approved - timesheet declined

Orgs

Approval permissions and assignee validation depend on active org membership.

Teams or management logic

Assignee computation may depend on external manager resolution logic.


What services do not do

Services do not: - expose HTTP responses - parse request query parameters - define serializer fields - define model schemas - own provider delivery logic - replace permissions

Those concerns belong to: - views - serializers - models - notifications - permissions


Design principles

Thin views, rich services

Views should call service functions rather than implement workflow inline.


Generic approval core, specific side-effect handlers

Approval state is generic, but target behavior is domain-specific.

The handlers layer bridges that safely.


Delegation-aware workflow

Delegation is a first-class service concern, not a UI-only feature.

It affects: - decision audit - inbox visibility - assignee resolution


Explicit workflow entry points

Actions such as decide, forward, and disable delegation should each have explicit service functions.

This improves: - readability - testability - consistency


Reusable read-model services

Inbox visibility and delegation enrichment should be built once in services, not reimplemented in views repeatedly.


Best practices

  • call service functions for all approval workflow changes
  • keep workflow decisions out of views and serializers
  • raise domain errors from services
  • centralize helper functions instead of duplicating them
  • keep side effects isolated in handler modules
  • keep assignee computation deterministic and explicit
  • use notifications from service workflows, not from views

Future extensions

The service layer is well positioned to support: - multi-step approvals - escalation rules - SLA reminders - approval chains - approval reassignment - comment history - bulk approval actions - richer audit timelines

As the app grows, services can be further split into: - read services - write services - integration services

if needed.


Summary

The approvals services layer is the workflow engine of the app.

It provides the business behavior for: - deciding approvals - forwarding approvals - managing delegations - computing assignees - building inbox visibility - applying side effects - recording workflow context

This makes the approvals app: - reusable - testable - delegation-aware - easy to evolve