Proposals Domain (proposals_v1)
The Proposals domain (proposals_v1) is a bounded context that manages the complete lifecycle of proposals—formal requests for changes, improvements, or strategic initiatives within the organization. This domain handles proposal creation, submission, review, approval workflows, impact assessment, and planning for asset replacement scenarios.
Overview
Section titled “Overview”The Proposals domain provides a structured workflow for managing organizational changes and initiatives. It supports multiple proposal types (asset replacement, site improvements, operational changes, compliance updates, etc.), tracks approval and assessment workflows, and captures the impact analysis required for informed decision-making.
Key responsibilities:
- Proposal Lifecycle Management: Track proposals through draft, submitted, under review, approved, rejected, and implemented states
- Impact Assessment: Store and aggregate impact analysis across carbon reduction, cost, and risk dimensions
- Asset Replacement Planning: Support planning for trackable asset replacement with site and retiring asset tracking
- Approval Workflows: Enforce business rules for state transitions and approval requirements
- Impact Analysis: Maintain comprehensive impact assessments with typed analysis data, carbon impact, cost impact, and risk scoring
Package Location
Section titled “Package Location”dart_packages/co2/domains/proposals_v1/├── lib/│ ├── proposals_v1.dart # Main export and registration│ └── src/│ ├── aggregates/│ │ └── proposal_aggregate.dart # ProposalAggregate root entity│ ├── events/│ │ └── proposal_events.dart # Domain events│ └── directives/│ └── proposal_directives.dart # Command handlers├── pubspec.yaml├── test/│ └── proposals_v1_test.dart└── coverage/Core Concepts
Section titled “Core Concepts”Proposal Types
Section titled “Proposal Types”The domain supports multiple proposal types through the ProposalType value object:
| Type | Code | Purpose |
|---|---|---|
| Asset Replacement | asset_replacement | Retiring old assets and deploying new ones |
| Site Improvement | site_improvement | Enhancements to site facilities |
| Operational Change | operational_change | Process or procedure modifications |
| Compliance Update | compliance_update | Regulatory or standards compliance changes |
| Maintenance Schedule | maintenance_schedule | Planned maintenance operations |
| Compliance System | compliance_system | Compliance technology deployment |
| Business Intelligence | business_intelligence | BI/analytics initiatives |
| Technology Upgrade | technology_upgrade | System or technology improvements |
| Mobile Operations | mobile_operations | Field operations enhancements |
| Sustainability Initiative | sustainability_initiative | Environmental/sustainability projects |
| Office Improvement | office_improvement | Workplace and office enhancements |
Proposal Lifecycle
Section titled “Proposal Lifecycle”States
Section titled “States”A proposal transitions through six distinct states:
┌─────────┐ ┌───────────┐│ DRAFT │──────→│ SUBMITTED │└─────────┘ └─────┬─────┘ │ ├──────────────┐ ↓ ↓ ┌────────────────┐ ┌─────────┐ │ UNDER REVIEW │ │ REJECTED│ └────────┬───────┘ └─────────┘ │ ↓ ┌───────────┐ │ APPROVED │ └─────┬─────┘ │ ↓ ┌───────────────┐ │ IMPLEMENTED │ └───────────────┘- Initial state after creation
- Proposal can be freely edited
- No assessment or approval required
- Used for preparation and refinement
Submitted
Section titled “Submitted”- Proposal formally submitted for review
- Transitions from draft or under review
- Ready for approver assessment
- Cannot be edited directly (must go back to draft)
Under Review
Section titled “Under Review”- Actively being evaluated by approvers
- Impact assessments may be added
- Business rule enforcement begins
- Can transition to approved or rejected
Approved
Section titled “Approved”- Proposal has passed review and been authorized
- Must have at least one impact assessment (business rule)
- Ready for implementation planning
- Can be implemented or rejectedif conditions change
Rejected
Section titled “Rejected”- Proposal declined by approvers
- Considered inactive—cannot be reactivated
- Retains all historical data
- Final terminal state (along with implemented)
Implemented
Section titled “Implemented”- Proposal has been fully executed
- Considered inactive
- Retains complete audit trail
- Final terminal state
Business Rules
Section titled “Business Rules”- Approved proposals must have assessments: Proposals cannot transition to approved status without at least one completed impact assessment
- Implemented requires approval: Only approved proposals can move to implemented status
- Status validation: Only valid status values from the enum are accepted
- Type validation: Only recognized proposal types are permitted
- Cost constraints: Estimated cost must be non-negative if provided
- Active status check: Rejected and implemented proposals are considered inactive
State Queries
Section titled “State Queries”The ProposalAggregate provides convenience properties for status checking:
proposal.isDraft // status.isDraftproposal.isSubmitted // status.isSubmittedproposal.isUnderReview // status.isUnderReviewproposal.isApproved // status.isApprovedproposal.isRejected // status.isRejectedproposal.isImplemented // status.isImplementedproposal.isActive // !isRejected && !isImplementedKey Aggregates
Section titled “Key Aggregates”ProposalAggregate
Section titled “ProposalAggregate”The root aggregate representing a single proposal with complete lifecycle management.
Identity & Core Properties:
final ProposalId proposalId; // Unique proposal identifierfinal ProposalTitle title; // User-visible titlefinal ProposalDescription description; // Detailed descriptionfinal ProposalType proposalType; // Category/type of proposalfinal UserId submittedByUserId; // User who submittedfinal OrganizationId organizationId; // Organization contextfinal DateTime submittedAt; // Submission timestampfinal DateTime updatedAt; // Last modification timefinal UserId submittedBy; // Submitter user referencefinal ProposalStatus status; // Current state in workflowfinal Money? estimatedCost; // Expected financial impactImpact Assessment:
final Map<String, ImpactAssessment> _impactAssessments; // assessmentId → assessmentExtended Metadata:
final Metadata metadata; // Arbitrary key-value datafinal List<ProposedChange> proposedChanges; // Detailed change logAsset Replacement Planning:
final List<SiteId> siteIds; // Affected sitesfinal List<TrackableAssetId> retiringAssetIds; // Assets being retiredfinal List<ReplacementEdge> replacementEdges; // Mapping of retiring to new assetsKey Methods
Section titled “Key Methods”Status & Lifecycle Queries:
isDraft,isSubmitted,isUnderReview,isApproved,isRejected,isImplemented,isActive: Status checkingcanBeApproved(): Verify readiness (must be under review with assessments)canBeImplemented(): Verify approved statehasBeenApproved(): Check if ever approved (including if now implemented)
Impact Analysis:
impactAssessments: List of all completed assessmentsgetAssessment(assessmentId): Retrieve specific assessmenthasAssessments: Boolean check for assessment existencetotalCarbonImpact: Sum of carbon impact across all assessmentstotalCostImpact: Sum of cost impact across all assessmentsaverageRiskScore: Mean risk score if assessments exist
Metadata:
ageInDays: Days since proposal submissionchangeCount: Number of proposed changes
Event Application
Section titled “Event Application”The aggregate applies events through the apply() method:
proposal = proposal.apply(ProposalSubmittedEvent( proposalId: proposalId, title: title, description: description, proposalType: proposalType, submittedByUserId: userId, organizationId: orgId, submittedAt: now, submittedBy: userId, estimatedCost: Money.gbp(5000),));Value Objects
Section titled “Value Objects”ImpactAssessment
Section titled “ImpactAssessment”Represents a completed impact analysis for a proposal:
class ImpactAssessment { final String assessmentId; // Unique assessment identifier final ImpactAnalysis analysis; // Typed analysis data final double carbonImpact; // CO2 reduction in tonnes final double costImpact; // Financial impact (£) final double riskScore; // Risk 0.0-10.0 final List<String> dependencies; // Linked requirements final DateTime assessedAt; // Assessment timestamp final UserId assessedBy; // Assessor user}Query Methods:
hasPositiveCarbonImpact: True if carbon impact > 0hasCostSavings: True if cost impact < 0isHighRisk: True if risk score > 7.0 or analysis indicates high riskhasDependencies: True if dependencies exist or timeline shows blockersageInDays: Days since assessmentoverallImpactScore: Composite score from typed analysis
ProposedChange
Section titled “ProposedChange”Represents a specific change within a proposal:
class ProposedChange { final String changeId; // Unique change identifier final String changeType; // Category of change final String targetEntityId; // Entity being changed final String targetEntityType; // Entity type (e.g., 'Asset', 'Site') final ChangeDiff diff; // Before/after comparison final DomainText rationale; // Why this change}ReplacementEdge
Section titled “ReplacementEdge”Maps retiring assets to replacement (new) assets:
class ReplacementEdge { final ListingId listingId; // New asset listing final int instancesRequired; // Quantity needed final List<TrackableAssetId> retiringAssetIds; // Assets being replaced}Domain Events
Section titled “Domain Events”The domain publishes events capturing all significant state changes:
ProposalSubmittedEvent
Section titled “ProposalSubmittedEvent”Fired when a proposal is initially submitted (draft → submitted transition).
ProposalSubmittedEvent( proposalId: ProposalId.fromString('prop-123'), title: ProposalTitle('Replace HVAC Units'), description: ProposalDescription('Fleet renewal in Building A'), proposalType: ProposalType('asset_replacement'), submittedByUserId: UserId.fromString('user-42'), organizationId: OrganizationId.fromString('org-1'), submittedAt: DateTime.now(), submittedBy: UserId.fromString('user-42'), estimatedCost: Money.gbp(50000), metadata: {'priority': 'high'},)Payload:
proposalId: Identifiertitle: Display namedescription: Full descriptionproposalType: CategorysubmittedByUserId: CreatororganizationId: Organization contextsubmittedAt: TimestampsubmittedBy: User referenceestimatedCost: Financial estimatemetadata: Custom data
ProposalStatusChangedEvent
Section titled “ProposalStatusChangedEvent”Fired when proposal status transitions between states.
ProposalStatusChangedEvent( proposalId: ProposalId.fromString('prop-123'), previousStatus: ProposalStatus.submitted, newStatus: ProposalStatus.approved, changedBy: UserId.fromString('user-42'), changedAt: DateTime.now(), reason: DomainText('Meets all criteria'), typedApprovalData: ApprovalData( approvedAt: DateTime.now(), approver: UserId.fromString('user-42'), ),)Payload:
proposalId: Target proposalpreviousStatus: Former statenewStatus: New statechangedBy: Actor making changechangedAt: Timestampreason: Optional reason/commentapprovalData: Typed approval metadata
ProposalImpactAssessedEvent
Section titled “ProposalImpactAssessedEvent”Fired when impact assessment is completed.
ProposalImpactAssessedEvent( proposalId: ProposalId.fromString('prop-123'), assessmentId: 'assess-001', analysis: ImpactAnalysis( carbon: CarbonImpact(reduction: 150.5), cost: CostImpact(savingsPerYear: 12000), risk: RiskAssessment(score: 3.5), timeline: TimelineAssessment( estimatedMonths: 6, hasDependencies: false, ), ), carbonImpact: 150.5, // Tonnes CO2 equivalent costImpact: 12000.0, // Annual savings in £ riskScore: 3.5, // 0-10 scale dependencies: [], assessedAt: DateTime.now(), assessedBy: UserId.fromString('user-43'),)Payload:
proposalId: Target proposalassessmentId: Unique assessment IDanalysis: Typed impact analysis objectcarbonImpact: Numeric carbon reductioncostImpact: Numeric cost/savingsriskScore: Numeric risk ratingdependencies: List of dependent itemsassessedAt: Assessment timestampassessedBy: Assessor user
Asset Replacement Planning Events
Section titled “Asset Replacement Planning Events”ProposalSitesAddedEvent
Section titled “ProposalSitesAddedEvent”Fired when sites are associated with a proposal.
ProposalSitesAddedEvent( proposalId: ProposalId.fromString('prop-123'), siteIds: [ SiteId.fromString('site-1'), SiteId.fromString('site-2'), ],)ProposalSitesRemovedEvent
Section titled “ProposalSitesRemovedEvent”Fired when sites are removed from proposal scope.
ProposalSitesRemovedEvent( proposalId: ProposalId.fromString('prop-123'), siteIds: [SiteId.fromString('site-1')],)ProposalRetiringAssetsAddedEvent
Section titled “ProposalRetiringAssetsAddedEvent”Fired when assets scheduled for retirement are added to the proposal.
ProposalRetiringAssetsAddedEvent( proposalId: ProposalId.fromString('prop-123'), assetIds: [ TrackableAssetId.fromString('asset-100'), TrackableAssetId.fromString('asset-101'), ],)ProposalRetiringAssetsRemovedEvent
Section titled “ProposalRetiringAssetsRemovedEvent”Fired when retiring assets are removed from the proposal.
ProposalRetiringAssetsRemovedEvent( proposalId: ProposalId.fromString('prop-123'), assetIds: [TrackableAssetId.fromString('asset-100')],)ProposalReplacementEdgesAddedEvent
Section titled “ProposalReplacementEdgesAddedEvent”Fired when mappings between retiring and replacement assets are added.
ProposalReplacementEdgesAddedEvent( proposalId: ProposalId.fromString('prop-123'), edges: [ ReplacementEdge( listingId: ListingId.fromString('hvac-listing-v2'), instancesRequired: 2, retiringAssetIds: [ TrackableAssetId.fromString('asset-100'), TrackableAssetId.fromString('asset-101'), ], ), ],)ProposalReplacementEdgesRemovedEvent
Section titled “ProposalReplacementEdgesRemovedEvent”Fired when replacement mappings are removed.
ProposalReplacementEdgesRemovedEvent( proposalId: ProposalId.fromString('prop-123'), edges: [/* edges to remove */],)Domain Directives
Section titled “Domain Directives”Directives are command handlers that validate inputs and produce events. The proposals domain defines directives for each major operation:
CreateProposalDirective
Section titled “CreateProposalDirective”Creates a new proposal in draft status.
Payload:
class CreateProposalPayload { final ProposalId proposalId; final ProposalTitle title; final ProposalDescription description; final ProposalType proposalType; final UserId createdBy; final Priority priority; final Money estimatedCost; final ProposalDuration estimatedDuration; final ProposalTargetDate targetImplementationDate; final List<TrackableAssetId> affectedAssets; final Metadata metadata; final List<String> requirements; final List<String> stakeholders;}Output Events:
ProposalSubmittedEventwith status set to submitted
Validation:
- All required fields must be provided
- Estimated duration must be valid format
- Target date must be valid format
SubmitProposalDirective
Section titled “SubmitProposalDirective”Submits a proposal for review.
Payload:
class SubmitProposalPayload { final ProposalId proposalId; final ProposalTitle title; final ProposalDescription description; final ProposalType proposalType; final UserId submittedByUserId; final OrganizationId organizationId; final UserId submittedBy; final Map<String, dynamic> metadata; final TrackableAssetReplacementSet replacementSet; final Money estimatedCost;}Output Events:
ProposalSubmittedEventwith complete metadata
UpdateProposalStatusDirective
Section titled “UpdateProposalStatusDirective”Changes proposal status (e.g., draft → submitted, submitted → approved).
Payload:
class UpdateProposalStatusPayload { final ProposalId proposalId; final ProposalStatus previousStatus; final ProposalStatus newStatus; final UserId changedBy; final DomainText? reason; final Map<String, dynamic> approvalData;}Output Events:
ProposalStatusChangedEventwith previous and new status
Business Rule Enforcement:
- Approved status requires existing assessments
- Implemented status requires previous approval
AssessProposalImpactDirective
Section titled “AssessProposalImpactDirective”Completes impact assessment for a proposal.
Payload:
class AssessProposalImpactPayload { final ProposalId proposalId; final String assessmentId; final Map<String, dynamic> impactAnalysis; final double carbonImpact; final double costImpact; final double riskScore; final List<String> dependencies; final UserId assessedBy;}Output Events:
ProposalImpactAssessedEventwith typed impact analysis
Asset Replacement Directives
Section titled “Asset Replacement Directives”AddProposalSitesDirective
Section titled “AddProposalSitesDirective”Adds sites to proposal scope.
RemoveProposalSitesDirective
Section titled “RemoveProposalSitesDirective”Removes sites from proposal scope.
AddProposalRetiringAssetsDirective
Section titled “AddProposalRetiringAssetsDirective”Adds assets marked for retirement to the proposal.
RemoveProposalRetiringAssetsDirective
Section titled “RemoveProposalRetiringAssetsDirective”Removes retiring assets from the proposal.
AddProposalReplacementEdgesDirective
Section titled “AddProposalReplacementEdgesDirective”Adds mappings between retiring and replacement assets.
RemoveProposalReplacementEdgesDirective
Section titled “RemoveProposalReplacementEdgesDirective”Removes replacement asset mappings.
Usage Examples
Section titled “Usage Examples”Creating and Submitting a Proposal
Section titled “Creating and Submitting a Proposal”import 'package:proposals_v1/proposals_v1.dart';import 'package:contracts_v1/contracts_v1.dart';
// Create proposal via directivefinal createPayload = CreateProposalPayload( proposalId: ProposalId.fromString('prop-2024-001'), title: ProposalTitle('Replace Building A HVAC System'), description: ProposalDescription( 'Complete replacement of aging HVAC units in Building A. ' 'Will improve energy efficiency and reduce carbon footprint.' ), proposalType: ProposalType('asset_replacement'), createdBy: UserId.fromString('user-alice'), priority: Priority.high, estimatedCost: Money.gbp(75000), estimatedDuration: ProposalDuration('6 months'), targetImplementationDate: ProposalTargetDate('2025-Q3'), affectedAssets: [ TrackableAssetId.fromString('asset-hvac-01'), TrackableAssetId.fromString('asset-hvac-02'), ], metadata: {'department': 'facilities', 'budgetCode': 'FAC-2025'}, requirements: ['Energy audit completion', 'Vendor selection'], stakeholders: ['Facilities Manager', 'Building Operations'],);
final createDirective = CreateProposalDirective(payload: createPayload);
// Directive produces events that update the aggregateAssessing Proposal Impact
Section titled “Assessing Proposal Impact”// Submit impact assessmentfinal assessPayload = AssessProposalImpactPayload( proposalId: ProposalId.fromString('prop-2024-001'), assessmentId: 'assess-20250101-001', impactAnalysis: { 'carbon': {'reduction': 125.5, 'unit': 'tonnes'}, 'cost': {'savings': 18000, 'payback_years': 4.2}, 'risk': {'score': 2.5, 'factors': ['Vendor reliability']}, 'timeline': {'months': 6, 'dependencies': []}, }, carbonImpact: 125.5, costImpact: 18000.0, riskScore: 2.5, dependencies: [], assessedBy: UserId.fromString('user-bob'),);
final assessDirective = AssessProposalImpactDirective(payload: assessPayload);Approving a Proposal
Section titled “Approving a Proposal”// After assessment, transition to approvedfinal approvePayload = UpdateProposalStatusPayload( proposalId: ProposalId.fromString('prop-2024-001'), previousStatus: ProposalStatus.underReview, newStatus: ProposalStatus.approved, changedBy: UserId.fromString('user-charlie'), reason: DomainText('All criteria met. High impact and low risk.'), approvalData: { 'approvalDate': DateTime.now().toIso8601String(), 'approver': 'user-charlie', 'conditions': ['Vendor contract review required before execution'], },);
final approveDirective = UpdateProposalStatusDirective(payload: approvePayload);Planning Asset Replacement
Section titled “Planning Asset Replacement”// Add sites affected by the proposalfinal addSitesPayload = AddProposalSitesPayload( proposalId: ProposalId.fromString('prop-2024-001'), siteIds: [ 'site-london-hq', 'site-manchester-office', ],);
// Add assets marked for retirementfinal retiringPayload = AddProposalRetiringAssetsPayload( proposalId: ProposalId.fromString('prop-2024-001'), assetIds: [ TrackableAssetId.fromString('asset-hvac-01'), TrackableAssetId.fromString('asset-hvac-02'), TrackableAssetId.fromString('asset-hvac-03'), ],);
// Map retiring assets to replacement listingfinal edgePayload = AddProposalReplacementEdgesPayload( proposalId: ProposalId.fromString('prop-2024-001'), edges: [ ReplacementEdge( listingId: ListingId.fromString('hvac-unit-model-v3'), instancesRequired: 3, retiringAssetIds: [ TrackableAssetId.fromString('asset-hvac-01'), TrackableAssetId.fromString('asset-hvac-02'), TrackableAssetId.fromString('asset-hvac-03'), ], ), ],);Querying Proposal State
Section titled “Querying Proposal State”// After applying events, query aggregate statefinal proposal = /* hydrated from event store */;
// Check lifecycle stateif (proposal.isApproved && proposal.hasAssessments) { print('Ready for implementation');}
// Analyze impactprint('Total carbon reduction: ${proposal.totalCarbonImpact} tonnes');print('Total cost savings: £${proposal.totalCostImpact}');print('Average risk level: ${proposal.averageRiskScore}');
// Check planning dataprint('Sites affected: ${proposal.siteIds.length}');print('Assets retiring: ${proposal.retiringAssetIds.length}');print('Replacement mappings: ${proposal.replacementEdges.length}');
// Get specific assessmentfinal assessment = proposal.getAssessment('assess-20250101-001');if (assessment != null) { print('Assessed by: ${assessment.assessedBy}'); print('High risk: ${assessment.isHighRisk}');}Integration Points
Section titled “Integration Points”With Intents Domain
Section titled “With Intents Domain”High-level user intents trigger proposal workflows:
CreateProposalIntent: Initiates proposal creationApproveProposalIntent: Requests approval with conditionsSubmitProposalIntent: Submits proposal for reviewCreateResponsibilitiesFromProposalIntent: Creates work items from approved proposals
With Contracts Domain
Section titled “With Contracts Domain”Cross-domain communication uses contract types:
ProposalId,ProposalTitle,ProposalDescription,ProposalType: Value objectsProposalStatus: Status enumerationProposalPayloads: Structured payloads for inter-domain eventsMoney: Financial impact representation
With Other Domains
Section titled “With Other Domains”Trackable Assets: Asset retirement and replacement planning
- References retiring asset IDs
- Maps to replacement catalogue listings
Estate Structures: Site and building context
- Associates proposals with specific sites
- Tracks scope within structural hierarchy
Catalogues: Replacement asset definitions
- References catalogue listings for new assets
- Uses listing specifications in replacement planning
Registration and Initialization
Section titled “Registration and Initialization”When using the proposals domain, register types for serialization:
import 'package:proposals_v1/proposals_v1.dart';
// Register all proposals domain typesProposalsV1().registerDomainTypes();
// Or explicitly:registerProposalsV1();This registers:
- Aggregates:
ProposalAggregate - Events: All 8 event types
- Directives: All 10 directive types
- Payloads: All 10 payload types
Error Handling and Validation
Section titled “Error Handling and Validation”Aggregate Validation
Section titled “Aggregate Validation”The ProposalAggregate.validate() method enforces business rules:
void validate() { // Empty aggregates are valid (used during snapshot creation) if (proposalId == ProposalId.unresolved && title == ProposalTitle.unresolved) { return; }
// ID must be resolved if (proposalId == ProposalId.unresolved) { throw StateError('Proposal ID cannot be unresolved'); }
// Title must not be empty if (title.value.trim().isEmpty) { throw StateError('Proposal title cannot be empty'); }
// Cost must be non-negative if (estimatedCost != null && estimatedCost!.amount < 0) { throw StateError('Estimated cost cannot be negative'); }
// Status must be valid if (!ProposalStatus.values.contains(status)) { throw StateError('Invalid proposal status: ${status.value}'); }
// Type must be valid const validTypes = [ 'asset_replacement', 'site_improvement', 'operational_change', 'compliance_update', 'maintenance_schedule', 'compliance_system', 'business_intelligence', 'technology_upgrade', 'mobile_operations', 'sustainability_initiative', 'office_improvement', ]; if (!validTypes.contains(proposalType.value)) { throw StateError('Invalid proposal type: ${proposalType.value}'); }
// Approved proposals must have assessments if (status.isApproved && impactAssessments.isEmpty) { throw StateError( 'Approved proposals must have at least one impact assessment' ); }
// Implemented requires prior approval if (status.isImplemented && !isApproved) { throw StateError('Proposals can only be implemented after approval'); }}Testing
Section titled “Testing”The domain includes comprehensive tests for lifecycle and serialization:
import 'package:test/test.dart';import 'package:proposals_v1/proposals_v1.dart';
group('ProposalAggregate', () { test('lifecycle and equality', () { final id = AggregateId('prop-1'); final now = DateTime(2023);
final proposal = ProposalAggregate( proposalId: ProposalId('PROP-1'), title: ProposalTitle('New Proposal'), description: ProposalDescription('Description'), proposalType: ProposalType('asset_replacement'), submittedByUserId: UserId('USR-1'), organizationId: OrganizationId('ORG-1'), submittedAt: now, updatedAt: now, submittedBy: UserId('USR-1'), status: ProposalStatus.draft, )..setIdForFramework(id);
expect(proposal.isDraft, isTrue);
// Apply event final submitted = proposal.apply(ProposalSubmittedEvent( proposalId: ProposalId('PROP-1'), title: ProposalTitle('New Proposal'), description: ProposalDescription('Description'), proposalType: ProposalType('asset_replacement'), submittedByUserId: UserId('USR-1'), submittedBy: UserId('USR-1'), organizationId: OrganizationId('ORG-1'), submittedAt: DateTime(2024), estimatedCost: Money.gbp(100), ));
expect(submitted.status, ProposalStatus.submitted); expect(submitted.isSubmitted, isTrue); });
test('serialization and deserialization', () { final json = { 'proposalId': 'PROP-1', 'title': 'Test Proposal', 'description': 'Test Description', 'proposalType': 'asset_replacement', 'submittedByUserId': 'USR-1', 'organizationId': 'ORG-1', 'submittedAt': '2023-01-01T00:00:00Z', 'updatedAt': '2023-01-01T00:00:00Z', 'submittedBy': 'USR-1', 'status': 'draft', };
final proposal = ProposalAggregate.fromJson( json, AggregateId('prop-1'), );
expect(proposal.proposalId.value, 'PROP-1'); expect(proposal.title.value, 'Test Proposal'); });});Package Dependencies
Section titled “Package Dependencies”The proposals domain depends on:
-
nomos_core: Core event sourcing framework
Aggregate,Event,Directive,Payloadbase classes- Registry mechanisms for type serialization
AggregateId,AggregateBaseinfrastructure
-
contracts_v1: Cross-domain value objects
ProposalId,ProposalTitle,ProposalDescription,ProposalType,ProposalStatusProposalPayloads(all payload contracts)UserId,OrganizationId,SiteId,TrackableAssetIdMoney,ListingId,ReplacementEdge, etc.
No domain-specific dependencies—designed for loose coupling and cross-domain communication.
Best Practices
Section titled “Best Practices”1. Always Use Value Objects
Section titled “1. Always Use Value Objects”// Good: Type-safefinal proposalId = ProposalId.fromString('prop-123');
// Avoid: Prone to errorsfinal proposalId = 'prop-123';2. Check Status Before Operations
Section titled “2. Check Status Before Operations”// Good: Verify preconditionsif (proposal.canBeApproved()) { // Execute approval}
// Avoid: Assume state// Execute approval directly3. Leverage Impact Aggregations
Section titled “3. Leverage Impact Aggregations”// Good: Use aggregate calculationsfinal totalCarbon = proposal.totalCarbonImpact;final avgRisk = proposal.averageRiskScore;
// Avoid: Calculating manuallyfinal totalCarbon = proposal.impactAssessments .fold(0.0, (sum, a) => sum + a.carbonImpact);4. Store Assessment Results Completely
Section titled “4. Store Assessment Results Completely”// Good: Full context for each assessmentfinal assessment = ImpactAssessment( assessmentId: 'assess-001', analysis: ImpactAnalysis(...), // Typed analysis carbonImpact: 125.5, costImpact: 18000.0, riskScore: 3.5, dependencies: ['vendor-selection'], assessedAt: now, assessedBy: userId,);
// Avoid: Partial or unstructured data// Store only numeric values without context5. Use Metadata for Custom Data
Section titled “5. Use Metadata for Custom Data”// Good: Extend without schema changesfinal proposal = ProposalAggregate( // ... required fields ... metadata: { 'customField': 'value', 'departmentCode': 'FAC-2025', 'approvalChain': ['manager', 'director'], },);
// Avoid: Adding new database columns or type modificationsRelated Documentation
Section titled “Related Documentation”- Contracts Domain: Value object definitions and payloads
- Intents Domain: High-level user intentions that trigger proposals workflows
- Trackable Assets Domain: Asset lifecycle and retirement planning
- Estate Structures Domain: Site and building context for proposals
- Catalogues Domain: Asset definitions and specifications for replacements