Skip to main content

Waypoint — Domain Model

Version: 1.0
Date: July 2026
Author: Pete Carroll, Founder — Traxs Group LLC
Status: Draft


1. Domain Overview

Waypoint's domain is organized into five bounded contexts — one per v1 module. Each context owns its data, its rules, and its aggregate roots. Cross-context communication happens through domain events, not direct aggregate references.

┌─────────────────────────────────────────────────────────────┐
│ WAYPOINT DOMAIN │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ FINANCIAL │ │ HR │ │ DOCUMENTS │ │
│ │ CONTEXT │ │ CONTEXT │ │ CONTEXT │ │
│ │ │ │ │ │ │ │
│ │ RevenueEntry │ │ Employee │ │ DocumentRecord │ │
│ │ Expense │ │ (root) │ │ (root) │ │
│ │ │ │ │ │ KnowledgeArticle │ │
│ └──────────────┘ └──────────────┘ │ (root) │ │
│ └──────────────────┘ │
│ ┌──────────────┐ ┌──────────────────────────────────────┐ │
│ │ TEAM │ │ WORKSPACE │ │
│ │ CONTEXT │ │ CONTEXT │ │
│ │ │ │ │ │
│ │ TeamMember │ │ Workspace (root) │ │
│ │ (root) │ │ Integration │ │
│ └──────────────┘ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

2. Ubiquitous Language

The following terms are used consistently in code, documentation, and conversation. Synonyms are not acceptable.

Domain TermCode NameUI LabelDo NOT Use
WorkspaceWorkspaceYour CompanyAccount, Organization, Tenant
EmployeeEmployeeEmployeeStaff, Worker, User, Team Member
Leave RequestLeaveRequestLeave RequestTime Off Request, Vacation Request
Revenue EntryRevenueEntryRevenueIncome, Sales, Earnings
ExpenseExpenseExpenseCost, Bill, Payment
DocumentDocumentRecordDocumentFile, Attachment
Knowledge ArticleKnowledgeArticleArticlePost, Wiki, Note
CertificationCertificationCertificationLicense, Credential, Qualification
Team MemberTeamMemberTeam MemberEmployee (in team context), User
IntegrationIntegrationConnected AppConnection, Plugin

3. Bounded Context: Financial

3.1 Aggregates

RevenueEntry (Aggregate Root)

Represents a single revenue event — either automatically imported from RoundTrip or manually entered by the owner.

RevenueEntry
├── RevenueEntryId (strongly typed Id)
├── TenantId (strongly typed)
├── Amount (Money value object)
├── Date (DateOnly)
├── Description (string)
├── ClientName (string, optional)
├── Source (RevenueSource enum: Manual | RoundTripJob | RoundTripInvoice)
├── RoundTripReference (string, optional — RT invoice or job ID)
├── Category (RevenueCategory enum)
├── IsRecurring (bool)
├── RecurringInterval (RecurringInterval enum, optional)
└── CreatedAt (DateTime)

Invariants:

  • Amount must be greater than zero
  • Date cannot be in the future
  • RoundTripReference required when Source is not Manual
  • RecurringInterval required when IsRecurring is true

Behaviour:

  • RevenueEntry.Create(...) — factory method, validates all invariants
  • RevenueEntry.MarkAsRoundTripImport(reference) — marks as imported, locks description

Expense (Aggregate Root)

Represents a business expense manually entered by the owner.

Expense
├── ExpenseId (strongly typed Id)
├── TenantId (strongly typed)
├── Amount (Money value object)
├── Date (DateOnly)
├── Description (string)
├── Category (ExpenseCategory enum)
├── ReceiptBlobPath (StoragePath, optional)
├── IsRecurring (bool)
├── RecurringInterval (RecurringInterval enum, optional)
├── RecurringEndDate (DateOnly, optional)
└── CreatedAt (DateTime)

Invariants:

  • Amount must be greater than zero
  • Date cannot be more than 2 years in the past (prevents accidental old entries)
  • RecurringInterval required when IsRecurring is true

Behaviour:

  • Expense.Create(...) — factory method
  • Expense.AttachReceipt(path) — attaches receipt blob path
  • Expense.UpdateAmount(amount) — updates amount, raises ExpenseAmountUpdatedEvent

3.2 Value Objects

Money

Money
├── Amount (decimal)
└── Currency (string — "USD" only in v1)
  • Immutable
  • Money.Zero static property
  • Arithmetic: +, - (same currency only)
  • Never store raw decimal for financial values — always Money

ExpenseCategory (Enum)

Payroll, Rent, Utilities, Insurance, Vehicle,
Materials, Marketing, Software, Professional, Other

RevenueCategory (Enum)

Service, Materials, Subcontract, Retainer, Other

RecurringInterval (Enum)

Daily, Weekly, Monthly, Quarterly, Annual

3.3 Domain Events

EventRaised ByConsumed By
RevenueEntryCreatedEventRevenueEntry.Create()Dashboard cache invalidation
ExpenseCreatedEventExpense.Create()Dashboard cache invalidation
ExpenseAmountUpdatedEventExpense.UpdateAmount()Dashboard cache invalidation

4. Bounded Context: HR

4.1 Aggregates

Employee (Aggregate Root)

The central aggregate of the HR context. All HR data — certifications, leave, performance — belongs to an Employee.

Employee
├── EmployeeId (strongly typed Id)
├── TenantId (strongly typed)
├── FirstName (string)
├── LastName (string)
├── Email (EmailAddress value object)
├── Phone (PhoneNumber value object, optional)
├── Address (EmployeeAddress value object, optional)
├── EmergencyContact (EmergencyContact value object)
├── Role (string — job title, not system role)
├── EmploymentType (EmploymentType enum)
├── StartDate (DateOnly)
├── WorkSchedule (WorkSchedule value object)
├── IsActive (bool)
├── PtoBalance (PtoBalance value object)
├── Skills (IReadOnlyList<string>)
├── Certifications (IReadOnlyList<Certification>) ← child entities
├── LeaveRequests (IReadOnlyList<LeaveRequest>) ← child entities
├── PerformanceReviews (IReadOnlyList<PerformanceReview>) ← child entities
├── Documents (IReadOnlyList<EmployeeDocument>) ← child entities
└── CreatedAt (DateTime)

Invariants:

  • StartDate cannot be in the future
  • Email must be unique within the tenant
  • IsActive defaults to true on creation

Behaviour:

  • Employee.Create(...) — factory method
  • Employee.Deactivate() — sets IsActive = false, raises EmployeeDeactivatedEvent
  • Employee.AddCertification(...) — adds certification child entity, validates no duplicate name
  • Employee.UpdateCertification(id, ...) — updates certification
  • Employee.AddLeaveRequest(...) — creates pending leave request
  • Employee.ApproveLeave(leaveRequestId) — approves leave, adjusts PTO balance, raises LeaveApprovedEvent
  • Employee.DenyLeave(leaveRequestId, reason) — denies leave request
  • Employee.AddSkill(skill) — adds skill to list (no duplicates)
  • Employee.RemoveSkill(skill) — removes skill
  • Employee.AdjustPtoBalance(days, reason) — manual PTO adjustment

Certification (Child Entity)

Certification
├── CertificationId (strongly typed Id)
├── Name (string)
├── IssuingBody (string)
├── IssueDate (DateOnly)
├── ExpiryDate (DateOnly, optional)
├── BlobPath (StoragePath, optional — scanned cert)
└── Status (CertificationStatus: Valid | Expiring | Expired)

Computed property:

  • Status is computed from ExpiryDate relative to today
    • No expiry date → Always Valid
    • Expiry > 60 days → Valid
    • Expiry 1-60 days → Expiring
    • Expiry passed → Expired

LeaveRequest (Child Entity)

LeaveRequest
├── LeaveRequestId (strongly typed Id)
├── LeaveType (LeaveType enum)
├── StartDate (DateOnly)
├── EndDate (DateOnly)
├── WorkingDaysCount (int — calculated, excludes weekends)
├── Notes (string, optional)
├── Status (LeaveStatus: Pending | Approved | Denied)
└── StatusReason (string, optional — reason for denial)

Invariants:

  • EndDate must be >= StartDate
  • Cannot overlap with existing approved leave for same employee
  • PTO leave cannot exceed available PTO balance

PerformanceReview (Child Entity)

PerformanceReview
├── ReviewId (strongly typed Id)
├── ReviewDate (DateOnly)
├── ReviewedBy (string — reviewer name)
├── OverallRating (int — 1 to 5)
├── Comments (string)
├── Goals (IReadOnlyList<ReviewGoal>)
└── Status (ReviewStatus: Draft | Completed)

4.2 Value Objects

PtoBalance

PtoBalance
├── Accrued (decimal — days)
├── Used (decimal — days)
└── Remaining (decimal — computed: Accrued - Used)

WorkSchedule

WorkSchedule
├── WorkDays (IReadOnlyList<DayOfWeek>)
└── HoursPerDay (decimal)

Default: Monday-Friday, 8 hours/day

EmergencyContact

EmergencyContact
├── Name (string)
├── Relationship (string)
└── Phone (PhoneNumber)

4.3 Domain Events

EventRaised ByConsumed By
EmployeeCreatedEventEmployee.Create()Team context sync
EmployeeDeactivatedEventEmployee.Deactivate()Team context sync
LeaveApprovedEventEmployee.ApproveLeave()Dashboard widget, notification
CertificationExpiringEventHangfire job (daily check)Notification handler
CertificationExpiredEventHangfire job (daily check)Notification handler

5. Bounded Context: Documents

5.1 Aggregates

DocumentRecord (Aggregate Root)

Represents metadata about a file stored in Azure Blob Storage. The aggregate does not hold the file itself — only its location and properties.

DocumentRecord
├── DocumentId (strongly typed Id)
├── TenantId (strongly typed)
├── FileName (string)
├── FileExtension (string)
├── FileSizeBytes (long)
├── MimeType (string)
├── BlobPath (StoragePath value object)
├── FolderId (FolderId, optional — null = root)
├── Category (DocumentCategory enum)
├── UploadedBy (string — user display name)
├── ReviewDate (DateOnly, optional)
├── IsDeleted (bool — soft delete)
├── DeletedAt (DateTime, optional)
├── Versions (IReadOnlyList<DocumentVersion>) ← child entities
└── CreatedAt (DateTime)

Behaviour:

  • DocumentRecord.Create(...) — factory, creates v1 automatically
  • DocumentRecord.AddVersion(blobPath, sizeBytes, uploadedBy) — adds new version
  • DocumentRecord.SoftDelete() — marks deleted, raises DocumentDeletedEvent
  • DocumentRecord.Restore() — un-deletes within 30-day window
  • DocumentRecord.SetReviewDate(date) — sets periodic review reminder

DocumentVersion (Child Entity)

DocumentVersion
├── VersionId (strongly typed Id)
├── VersionNumber (int — auto-incremented)
├── BlobPath (StoragePath)
├── FileSizeBytes (long)
├── UploadedBy (string)
└── UploadedAt (DateTime)

KnowledgeArticle (Aggregate Root)

Separate aggregate from DocumentRecord — articles are structured content, not files.

KnowledgeArticle
├── ArticleId (strongly typed Id)
├── TenantId (strongly typed)
├── Title (string)
├── Content (string — TipTap JSON)
├── Category (string — owner-defined tag)
├── Status (ArticleStatus: Draft | Published)
├── AuthorName (string)
├── ReviewDate (DateOnly, optional)
├── RelatedIds (IReadOnlyList<ArticleId>)
└── UpdatedAt (DateTime)

Behaviour:

  • KnowledgeArticle.Publish() — changes status to Published
  • KnowledgeArticle.Unpublish() — changes status to Draft
  • KnowledgeArticle.UpdateContent(title, content) — auto-saves, updates UpdatedAt

5.2 Value Objects

StoragePath

StoragePath
├── TenantId (Guid)
├── Container (string)
├── BlobName (string)
└── FullPath (string — computed: "{tenantId}/{blobName}")

DocumentCategory (Enum)

Policies, SOPs, Templates, HR, Financial, Legal, Other

5.3 Domain Events

EventRaised ByConsumed By
DocumentUploadedEventDocumentRecord.Create()Search index update
DocumentDeletedEventDocumentRecord.SoftDelete()Search index removal
ArticlePublishedEventKnowledgeArticle.Publish()Search index update

6. Bounded Context: Team

6.1 Aggregates

TeamMember (Aggregate Root)

A lightweight projection of an Employee for team-level operations. Synced from the HR context via domain events. Also includes RoundTrip technicians when integration is active.

TeamMember
├── TeamMemberId (strongly typed Id)
├── TenantId (strongly typed)
├── EmployeeId (EmployeeId, optional — null for RT-only members)
├── RoundTripUserId (Guid, optional — for RT technicians)
├── FirstName (string)
├── LastName (string)
├── Role (string)
├── Source (TeamMemberSource: Waypoint | RoundTrip)
├── IsActive (bool)
└── WorkSchedule (WorkSchedule value object)

Note: TeamMember is a read-optimized projection. It is not the source of truth for employee data — that lives in the HR context's Employee aggregate. TeamMember is updated via EmployeeCreatedEvent and EmployeeDeactivatedEvent.

6.2 Domain Events

EventRaised ByConsumed By
TeamMemberAddedEventTeamMember.Create()Dashboard widget refresh
TeamMemberDeactivatedEventHR context event handlerDashboard widget refresh

7. Bounded Context: Workspace

7.1 Aggregates

Workspace (Aggregate Root)

Represents the tenant's Waypoint configuration. One Workspace per tenant.

Workspace
├── WorkspaceId (strongly typed Id)
├── TenantId (strongly typed)
├── CompanyName (string)
├── Industry (string)
├── LogoBlobPath (StoragePath, optional)
├── FinancialYearStart (Month enum — January default)
├── TimeZone (string — IANA timezone)
├── Currency (string — "USD" only in v1)
├── EmployeeCount (int — approximate, for onboarding)
├── Integrations (IReadOnlyList<Integration>) ← child entities
└── CreatedAt (DateTime)

Behaviour:

  • Workspace.Create(...) — factory, called during tenant provisioning
  • Workspace.UpdateSettings(...) — updates company settings
  • Workspace.ConnectRoundTrip(rtTenantId) — adds RoundTrip integration
  • Workspace.DisconnectRoundTrip() — removes integration

Integration (Child Entity)

Integration
├── IntegrationId (strongly typed Id)
├── IntegrationType (IntegrationType: RoundTrip | QuickBooks [future])
├── ExternalTenantId (Guid — RoundTrip TenantId)
├── ConnectedAt (DateTime)
├── IsActive (bool)
└── LastSyncAt (DateTime, optional)

8. Cross-Context Relationships

HR Context Team Context
────────────── ────────────
Employee ──creates──▶ TeamMember (via EmployeeCreatedEvent)
Employee ──deactivates▶ TeamMember (via EmployeeDeactivatedEvent)
LeaveRequest ──approved▶ TeamSchedule (via LeaveApprovedEvent)

Financial Context Dashboard
───────────────── ─────────
RevenueEntry ──created▶ Dashboard cache invalidation
Expense ──created──▶ Dashboard cache invalidation

Workspace Context All Contexts
───────────────── ────────────
Integration.IsActive ──gates▶ Command Center API calls (RT data)

9. Shared Kernel

Waypoint shares the following with RoundTrip via a SharedKernel project:

TypeDescription
MoneyAmount + Currency value object
TenantIdStrongly typed tenant identifier
Result<T>Operation result pattern
DomainExceptionBase domain exception
AggregateRootBase aggregate root with domain events
EntityBase entity
IUnitOfWorkUnit of work interface

Note: In v1, the SharedKernel is duplicated into each product's codebase. In v2, it becomes a private NuGet package shared across RoundTrip, Waypoint, and Relay.


10. Document History

VersionDateAuthorChanges
1.0July 2026Pete CarrollInitial draft