Skip to content

Budget Module - Developer Documentation

Overview

The Budget module manages dental treatment quotes with versioning, digital signatures, and PDF generation. This document describes extension points for future modules and third-party integrations.

Note: The workflow supports an optional sent state: draft → [sent] → accepted → completed. Treatment execution tracking is reserved for a future Treatment Plan module.


1. Module Structure

backend/app/modules/budget/
├── __init__.py      # BudgetModule(BaseModule)
├── models.py        # Budget, BudgetItem, BudgetSignature, BudgetHistory
├── schemas.py       # Pydantic schemas
├── router.py        # FastAPI endpoints
├── service.py       # BudgetService, BudgetItemService
├── workflow.py      # BudgetWorkflowService (state machine)
└── pdf.py           # BudgetPDFService (PDF generation)

Dependencies

  • clinical - Patient data
  • catalog - Treatment catalog items and VAT types

2. Data Models

Budget

FieldTypeDescription
idUUIDPrimary key
clinic_idUUIDMulti-tenancy
patient_idUUIDAssociated patient
budget_numberString"PRES-2024-0001" format
versionIntegerVersion number
parent_budget_idUUIDPrevious version link
statusStringWorkflow state (see section 3)
valid_fromDateStart of validity
valid_untilDateExpiration date
global_discount_typeString"percentage" or "absolute"
global_discount_valueDecimalDiscount amount
subtotalDecimalSum before discounts
total_discountDecimalTotal discounts
total_taxDecimalTotal VAT
totalDecimalFinal amount
insurance_estimateDecimalReserved for insurance module

BudgetItem

FieldTypeDescription
idUUIDPrimary key
budget_idUUIDParent budget
catalog_item_idUUIDTreatment reference
unit_priceDecimalPrice snapshot
quantityIntegerUnits
discount_typeStringLine discount type
discount_valueDecimalLine discount
vat_type_idUUIDVAT type reference
vat_rateFloatVAT rate snapshot
line_subtotalDecimalBefore discount
line_discountDecimalDiscount amount
line_taxDecimalVAT amount
line_totalDecimalFinal line total
tooth_numberIntegerFDI notation
surfacesJSONB["M", "O", "D"]
invoiced_quantityIntegerQty already invoiced

BudgetSignature

FieldTypeDescription
idUUIDPrimary key
budget_idUUIDSigned budget
signature_typeStringfull_acceptance / rejection
signed_itemsJSONBItem IDs signed
signed_by_nameStringSigner name
relationship_to_patientStringpatient/guardian/representative
signature_methodStringclick_accept/drawn/external_provider
external_signature_idStringFor external providers
external_providerStringProvider name

3. Workflow States

draft ────────────────────────────────────────────┐
  │                                               │
  ├──→ sent ──→ accepted ──→ completed            │
  │       │                                       │
  │       ├──→ rejected (terminal)                │
  │       │                                       │
  │       └──→ expired (terminal, automatic)      │
  │                                               │
  ├──→ accepted ──→ completed (direct acceptance) │
  │                                               │
  ├──→ rejected (terminal)                        │
  │                                               │
  └──→ cancelled (terminal) ←─────────────────────┘

Status Descriptions

StatusDescription
draftInitial state, budget is editable
sentSent to patient (by email or manually), awaiting response
acceptedPatient accepted, ready for treatment and invoicing
completedAll work done (marked manually)
rejectedPatient rejected the budget (terminal)
expiredValidity period passed (terminal, automatic)
cancelledCancelled by clinic (terminal)

Valid Transitions

FromTo
draftsent, accepted, rejected, cancelled
sentaccepted, rejected, expired, cancelled
acceptedcompleted, cancelled
completed(terminal)
rejected(terminal)
expired(terminal)
cancelled(terminal)

Note: The expired status is set automatically by a scheduled job when valid_until < today for draft and sent budgets.


4. Events Published

Other modules can subscribe to these events via the Event Bus.

Budget Lifecycle Events

EventPayloadWhen
budget.created{budget_id, patient_id, clinic_id, total, items}New budget saved
budget.updated{budget_id, changes}Budget modified
budget.sent{budget_id, send_method, recipient_email}Budget sent to patient
budget.accepted{budget_id, signature_id, total}Budget accepted
budget.rejected{budget_id, reason, rejected_at}Budget rejected
budget.cancelled{budget_id, cancelled_by, reason}Budget cancelled
budget.completed{budget_id, completed_at}Budget marked complete

Budget Item Events

EventPayloadWhen
budget.item.added{budget_id, item_id, catalog_item_id, price}Item added
budget.item.removed{budget_id, item_id}Item removed

Example: Invoice Module Subscriber

python
class InvoiceModule(BaseModule):
    def get_event_handlers(self) -> dict:
        return {
            "budget.accepted": self._notify_ready_for_invoice,
            "budget.completed": self._check_pending_invoices,
        }

    def _notify_ready_for_invoice(self, data: dict) -> None:
        # Budget is now ready to be invoiced
        pass

5. Events Budget Module Listens To

EventSource ModuleAction
odontogram.treatment.performedOdontogramReserved for future treatment plan integration
appointment.completedClinicalReserved for future integration

6. Extension Points

6.1 Insurance Module (Future)

Reserved field: budget.insurance_estimate

python
# Insurance module subscribes to budget.created
def _on_budget_created(self, data: dict) -> None:
    coverage = self.calculate_coverage(data["patient_id"], data["items"])
    # Update budget.insurance_estimate

6.2 External Signature Providers (Future)

Reserved fields in BudgetSignature:

  • external_signature_id
  • external_provider
  • signature_method = "external_provider"
  • signature_data (JSONB)
python
# E-signature module provides alternative signature flow
if signature_handler := module_registry.get_signature_handler():
    return signature_handler.request_signature(budget, signer_info)
else:
    return self._click_to_accept(budget, signer_info)

6.3 Treatment Plan Module (Future)

When a Treatment Plan module is implemented:

  • Budget items can be linked to treatment plan items
  • Treatment execution will be tracked in the plan, not the budget
  • Budget remains a commercial document (quote/invoice)

6.4 PDF Template Customization

python
# Third-party module can add PDF context
class InsuranceModule(BaseModule):
    def get_pdf_extensions(self, document_type: str) -> dict | None:
        if document_type == "budget":
            return {
                "insurance_info": self._get_insurance_section,
                "coverage_breakdown": self._get_coverage_table
            }
        return None

7. API Endpoints

CRUD

MethodEndpointPermissionDescription
GET/api/v1/budget/budgetsbudget.readList with filters
GET/api/v1/budget/budgets/{id}budget.readGet with items
POST/api/v1/budget/budgetsbudget.writeCreate
PUT/api/v1/budget/budgets/{id}budget.writeUpdate (draft only)
DELETE/api/v1/budget/budgets/{id}budget.adminSoft delete

Workflow

MethodEndpointPermissionDescription
POST/budgets/{id}/sendbudget.writeSend to patient (email or manual)
POST/budgets/{id}/acceptbudget.writeAccept with signature
POST/budgets/{id}/rejectbudget.writeReject
POST/budgets/{id}/cancelbudget.writeCancel
POST/budgets/{id}/completebudget.writeMark completed
POST/budgets/{id}/duplicatebudget.writeCreate new version

Items

MethodEndpointPermissionDescription
POST/budgets/{id}/itemsbudget.writeAdd item
PUT/budgets/{id}/items/{item_id}budget.writeUpdate item
DELETE/budgets/{id}/items/{item_id}budget.writeRemove item

PDF

MethodEndpointPermissionDescription
GET/budgets/{id}/pdfbudget.readDownload PDF
GET/budgets/{id}/pdf/previewbudget.readPreview with watermark

Versions & History

MethodEndpointPermissionDescription
GET/budgets/{id}/versionsbudget.readList all versions
GET/budgets/{id}/historybudget.readAudit history

8. Permissions

PermissionDescription
budget.readView budgets
budget.writeCreate, edit, workflow actions
budget.adminDelete budgets

Role Assignments

RolePermissions
adminbudget.*
dentistbudget.*
hygienistbudget.read
assistantbudget.read, budget.write
receptionistbudget.read, budget.write

9. Frontend Components

Pages

  • /budgets - List with search and filters
  • /budgets/new - Create with patient selection
  • /budgets/[id] - Detail/editor view

Components

ComponentPurpose
BudgetStatusBadge.vueStatus indicator
BudgetItemModal.vueAdd items from catalog
BudgetFilters.vueSearch and filter controls

Composable

typescript
const {
  // State
  budgets, currentBudget, isLoading, error, total,

  // CRUD
  fetchBudgets, fetchBudget, createBudget, updateBudget, deleteBudget,

  // Items
  addItem, updateItem, removeItem,

  // Workflow
  acceptBudget, rejectBudget, cancelBudget, completeBudget, duplicateBudget,

  // Versions & History
  fetchVersions, fetchHistory,

  // PDF
  downloadPDF, getPDFPreviewUrl,

  // Helpers
  getStatusColor, canEdit, canAccept, canReject, canCancel, canComplete, canDuplicate
} = useBudgets()

10. Testing

bash
# Run budget module tests
docker-compose exec backend python -m pytest tests/test_budget.py -v

Key Test Cases

  • CRUD operations (create, read, update, delete)
  • Workflow transitions (accept, reject, cancel, complete)
  • Item management (add, update, remove with totals)
  • Discount calculations (line and global)
  • Version duplication
  • Authentication requirements

11. Migration Guide

When extending the budget module:

  1. Never modify existing columns - Add new columns or tables
  2. Use nullable fields for new required data
  3. Publish events for all state changes
  4. Keep backward compatibility in API responses
  5. Version API endpoints if breaking changes needed

12. Future Module Integration Roadmap

ModuleIntegration TypePriorityFields/Events Used
billingBidirectionalP0budget.accepted → create invoice
treatment-planExtendsP1Link items to treatment execution
insuranceExtendsP1insurance_estimate, policy lookup
financingExtendsP1financing_plan_id, payment schedules
e-signatureReplacesP2external_signature_id, signature_method
communicationsSubscribesP1budget.accepted, budget.expired
patient-portalExposesP2Token-based acceptance flow
analyticsReadsP2Budget conversion rates, avg values