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 Term | Code Name | UI Label | Do NOT Use |
|---|---|---|---|
| Workspace | Workspace | Your Company | Account, Organization, Tenant |
| Employee | Employee | Employee | Staff, Worker, User, Team Member |
| Leave Request | LeaveRequest | Leave Request | Time Off Request, Vacation Request |
| Revenue Entry | RevenueEntry | Revenue | Income, Sales, Earnings |
| Expense | Expense | Expense | Cost, Bill, Payment |
| Document | DocumentRecord | Document | File, Attachment |
| Knowledge Article | KnowledgeArticle | Article | Post, Wiki, Note |
| Certification | Certification | Certification | License, Credential, Qualification |
| Team Member | TeamMember | Team Member | Employee (in team context), User |
| Integration | Integration | Connected App | Connection, 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 invariantsRevenueEntry.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 methodExpense.AttachReceipt(path)— attaches receipt blob pathExpense.UpdateAmount(amount)— updates amount, raisesExpenseAmountUpdatedEvent
3.2 Value Objects
Money
Money
├── Amount (decimal)
└── Currency (string — "USD" only in v1)
- Immutable
Money.Zerostatic 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
| Event | Raised By | Consumed By |
|---|---|---|
RevenueEntryCreatedEvent | RevenueEntry.Create() | Dashboard cache invalidation |
ExpenseCreatedEvent | Expense.Create() | Dashboard cache invalidation |
ExpenseAmountUpdatedEvent | Expense.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 methodEmployee.Deactivate()— sets IsActive = false, raisesEmployeeDeactivatedEventEmployee.AddCertification(...)— adds certification child entity, validates no duplicate nameEmployee.UpdateCertification(id, ...)— updates certificationEmployee.AddLeaveRequest(...)— creates pending leave requestEmployee.ApproveLeave(leaveRequestId)— approves leave, adjusts PTO balance, raisesLeaveApprovedEventEmployee.DenyLeave(leaveRequestId, reason)— denies leave requestEmployee.AddSkill(skill)— adds skill to list (no duplicates)Employee.RemoveSkill(skill)— removes skillEmployee.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:
Statusis computed from ExpiryDate relative to today- No expiry date → Always
Valid - Expiry > 60 days →
Valid - Expiry 1-60 days →
Expiring - Expiry passed →
Expired
- No expiry date → Always
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
| Event | Raised By | Consumed By |
|---|---|---|
EmployeeCreatedEvent | Employee.Create() | Team context sync |
EmployeeDeactivatedEvent | Employee.Deactivate() | Team context sync |
LeaveApprovedEvent | Employee.ApproveLeave() | Dashboard widget, notification |
CertificationExpiringEvent | Hangfire job (daily check) | Notification handler |
CertificationExpiredEvent | Hangfire 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 automaticallyDocumentRecord.AddVersion(blobPath, sizeBytes, uploadedBy)— adds new versionDocumentRecord.SoftDelete()— marks deleted, raisesDocumentDeletedEventDocumentRecord.Restore()— un-deletes within 30-day windowDocumentRecord.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 PublishedKnowledgeArticle.Unpublish()— changes status to DraftKnowledgeArticle.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
| Event | Raised By | Consumed By |
|---|---|---|
DocumentUploadedEvent | DocumentRecord.Create() | Search index update |
DocumentDeletedEvent | DocumentRecord.SoftDelete() | Search index removal |
ArticlePublishedEvent | KnowledgeArticle.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
| Event | Raised By | Consumed By |
|---|---|---|
TeamMemberAddedEvent | TeamMember.Create() | Dashboard widget refresh |
TeamMemberDeactivatedEvent | HR context event handler | Dashboard 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 provisioningWorkspace.UpdateSettings(...)— updates company settingsWorkspace.ConnectRoundTrip(rtTenantId)— adds RoundTrip integrationWorkspace.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:
| Type | Description |
|---|---|
Money | Amount + Currency value object |
TenantId | Strongly typed tenant identifier |
Result<T> | Operation result pattern |
DomainException | Base domain exception |
AggregateRoot | Base aggregate root with domain events |
Entity | Base entity |
IUnitOfWork | Unit 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
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | July 2026 | Pete Carroll | Initial draft |