Catalogues Domain
The Catalogues domain (catalogues_v1) is a bounded context that manages the complete specification and cataloging of assets available in the CO2 asset management system. This domain handles catalogue creation and lifecycle, asset type specifications through listings, supplier information, and taxonomies for categorizing equipment.
Overview
Section titled “Overview”The Catalogues domain serves as the central repository for asset specifications and definitions. It bridges the gap between administrative configuration and operational asset tracking. While the Trackable Assets domain manages actual asset instances deployed in estates, the Catalogues domain defines what types of assets can exist and their specification templates.
Core Responsibilities
Section titled “Core Responsibilities”- Catalogues: Groups of asset specifications organized by workspace or owner (estate/supplier)
- Listings: Individual asset specifications with custom fields, pricing, supplier info, and physical dimensions
- Taxonomies: Hierarchical classification systems (categories and subcategories) used to organize listings
- Suppliers: Vendor management with membership tracking
- Custom Fields: Extensible field definitions for capturing asset-specific data
Key Aggregates and Responsibilities
Section titled “Key Aggregates and Responsibilities”CatalogueAggregate
Section titled “CatalogueAggregate”The root aggregate representing a collection of asset specifications.
Identity:
CatalogueId: Unique identifier for the catalogue
Core Attributes:
final CatalogueName name;final CatalogueDescription description;final CatalogueStatus status; // draft, active, archivedfinal EstateId? ownerEstateId; // Optional: estate ownershipfinal SupplierId? ownerSupplierId; // Optional: supplier ownershipfinal TaxonomyId? taxonomyId; // Associated taxonomy for listingsfinal DomainText referenceCodePrefix; // Prefix for generated reference codesfinal int nextReferenceNumber; // Counter for reference code generationMetadata:
final CatalogueMetadata catalogueMetadata; // Type-safe metadata storagefinal UserId createdByUserId; // Creator auditfinal DateTime createdAt; // Timestampsfinal DateTime updatedAt;Key Methods:
isActive,isDraft,isArchived: Status convenience propertieshasOwner,isEstateOwned,isSupplierOwned: Ownership checkinglistingsWorkspace: Returns the workspace ID for querying listings in this catalogueownsListing(listing): Predicate to check listing ownership
Notes on Ownership:
- A catalogue can be globally accessible (no owner), owned by a single estate, or owned by a single supplier
- A catalogue cannot be owned by both estate and supplier simultaneously
- Ownership affects access control and visibility in the UI
ListingAggregate
Section titled “ListingAggregate”Represents a single asset specification within a catalogue.
Identity:
ListingId: Unique identifier for the listing
Core Attributes:
final CatalogueId catalogueId; // Parent cataloguefinal ListingTitle name;final ListingDescription description;final ListingStatus status; // draft, submitted, approved, rejectedfinal TaxonomyReference taxonomyRef; // Classification within catalogue's taxonomyfinal SupplierId? supplierId; // Optional supplier associationfinal SupplierName? supplierName;final bool isCertified; // Certification flagPhysical Properties:
final double scaleX; // Width in meters (required)final double scaleY; // Height/depth in meters (required)final ListingShape shape; // rectangle or circlefinal bool isHidden; // Visibility toggle (approved but hidden from users)Specifications:
final CustomFieldMap customFields; // Taxonomy-defined custom fieldsfinal List<DomainText> tags; // User-defined tagsfinal Pricing? pricing; // Optional pricing informationfinal FileUrl? thumbnailUrl; // Optional product imagefinal TimelineName? timelineName; // Optional timeline for lifecycle trackingReference Tracking:
final int? referenceNumber; // Auto-generated from catalogue prefixfinal String? referenceCode; // Human-readable code (e.g., "HVAC-001")final int? publishedAtSequence; // Sequence number when published (for time-travel)Audit:
final UserId createdByUserId;final DateTime createdAt;final DateTime updatedAt;Key Methods:
- Status predicates:
isDraft,isSubmitted,isApproved,isRejected,isActive canBeSubmitted(): Validates required fields before submissioncanBeApproved(): Checks approval eligibilityisValidForCatalogue(): Comprehensive validation
Publishing System:
When a listing is first approved (status transitions to approved), it is automatically published. The publishedAtSequence records the workspace intent sequence at publication, allowing retrieval of the published snapshot via Nomos time-travel queries.
TaxonomyAggregate
Section titled “TaxonomyAggregate”Manages hierarchical classification systems for listings.
Identity:
TaxonomyId: Unique identifier for the taxonomy
Core Attributes:
final TaxonomyName name;final TaxonomyDescription description;final List<TaxonomyCategory> categories; // Top-level categoriesfinal UserId createdByUserId;final DateTime createdAt;final DateTime updatedAt;Hierarchy:
Taxonomy ├── Category (e.g., "HVAC Equipment") │ ├── SubCategory (e.g., "Air Handling Units") │ │ └── CustomFields │ └── SubCategory (e.g., "Boilers") └── Category (e.g., "Electrical") ├── SubCategoryKey Methods:
getCategory(categoryId): Retrieve category by IDgetSubCategory(categoryId, subCategoryId): Nested lookupgetCustomFieldSchema(categoryId, subCategoryId): Returns merged custom field definitionsgetRequiredCustomFields(...): Filters required fields onlyvalidateCustomFields(...): Validates field values against schema
Custom Field Support: Custom fields can be defined at both category and subcategory levels. Subcategory fields override category fields with the same key, allowing specialization.
SupplierAggregate
Section titled “SupplierAggregate”Manages supplier information and membership.
Identity:
SupplierId: Unique identifier for the supplier
Core Attributes:
final SupplierName name;final SupplierStatus status; // active or inactivefinal Set<UserId> memberUserIds; // Team membersfinal UserId createdByUserId;final DateTime createdAt;final DateTime updatedAt;Key Methods:
- Status predicates:
isActive,isInactive hasMember(userId): Check membershipmemberCount: Get team size
Domain Events
Section titled “Domain Events”The domain publishes events capturing all significant state changes.
Catalogue Events
Section titled “Catalogue Events”CatalogueCreatedEvent Fired when a new catalogue is created.
CatalogueCreatedEvent( catalogueId: CatalogueId('CAT-001'), name: CatalogueName('Estate HVAC Equipment'), description: CatalogueDescription('HVAC specs for main estate'), ownerEstateId: EstateId('EST-001'), createdByUserId: UserId('USER-123'), createdAt: DateTime.now(), metadata: {'region': 'north', 'type': 'specialized'}, referenceCodePrefix: DomainText('HVAC'), taxonomyId: TaxonomyId('TAX-HVAC'),)CatalogueUpdatedEvent Fired when catalogue properties change (name, description, metadata, ownership, taxonomy).
CatalogueStatusChangedEvent Fired when status transitions (draft ↔ active ↔ archived).
CatalogueReferenceCounterIncrementedEvent Fired when listing reference numbers are generated, incrementing the internal counter.
Listing Events
Section titled “Listing Events”ListingCreatedEvent Fired when a new listing is created in draft status.
ListingCreatedEvent( listingId: ListingId('LST-001'), catalogueId: CatalogueId('CAT-001'), name: ListingTitle('LED Panel 200W'), description: ListingDescription('High efficiency LED panel'), taxonomyRef: TaxonomyReference( catalogueId: CatalogueId('CAT-001'), taxonomyId: TaxonomyId('TAX-001'), categoryId: TaxonomyCategoryId('lighting'), subCategoryId: TaxonomySubCategoryId('panels'), ), supplierId: SupplierId('SUP-001'), createdByUserId: UserId('USER-123'), isCertified: false, customFields: CustomFieldMap.fromJson({ 'wattage': 200, 'lumens': 2000, 'efficiency_rating': 'A+', }), tags: [DomainText('LED'), DomainText('energy-efficient')], scaleX: 0.6, scaleY: 0.6, shape: ListingShape.rectangle, createdAt: DateTime.now(),)ListingStatusChangedEvent Fired when listing status transitions through the approval workflow: draft → submitted → approved/rejected.
ListingCustomFieldsUpdatedEvent Fired when custom field values change.
ListingUpdatedEvent Fired when listing properties change (name, description, pricing, supplier, physical dimensions, etc.).
ListingVisibilityChangedEvent
Fired when the isHidden flag toggles.
ListingVersionPublishedEvent Fired when listing is published (automatically on first approval, or manually).
Taxonomy Events
Section titled “Taxonomy Events”TaxonomyCreatedEvent Fired when a new taxonomy is created.
TaxonomyUpdatedEvent Fired when taxonomy name/description changes.
TaxonomyDeletedEvent Fired when taxonomy is deleted.
TaxonomyCategoryAddedEvent Fired when a category is added to a taxonomy.
TaxonomyCategoryUpdatedEvent Fired when category properties change (name, description, color, icon).
TaxonomyCategoryRemovedEvent Fired when a category is removed.
TaxonomySubCategoryAddedEvent Fired when a subcategory is added to a category.
TaxonomySubCategoryUpdatedEvent Fired when subcategory properties change.
TaxonomySubCategoryRemovedEvent Fired when a subcategory is removed.
TaxonomyCategoryCustomFieldAddedEvent Fired when a custom field is added to a category.
TaxonomySubCategoryCustomFieldAddedEvent Fired when a custom field is added to a subcategory.
TaxonomyCustomFieldUpdatedEvent Fired when a custom field definition changes (name, type, required flag, options, etc.).
TaxonomyCustomFieldRemovedEvent Fired when a custom field is removed.
Supplier Events
Section titled “Supplier Events”SupplierCreatedEvent Fired when a new supplier is created.
SupplierMembershipChangedEvent Fired when team members are added to or removed from a supplier.
SupplierStatusChangedEvent Fired when supplier status changes (active ↔ inactive).
Listing Lifecycle and Status Workflow
Section titled “Listing Lifecycle and Status Workflow”Listings follow a strict approval workflow:
draft ↓ (user submits)submitted ↓ (admin approves OR rejects)approved ←── rejected ↓ (auto-publish on first approval)publishedStatus Details:
- draft: Initial creation state; editable by creator
- submitted: Submitted for approval; awaiting admin review
- approved: Approved by admin; automatically published; can be hidden without losing published state
- rejected: Rejected during review; can return to draft for revision
Auto-Publishing:
On the first status transition to approved, the listing is automatically published. The publishedAtSequence field captures the workspace intent sequence at publication, enabling time-travel queries to retrieve the approved state.
How Catalogues Relate to Trackable Assets
Section titled “How Catalogues Relate to Trackable Assets”Catalogues and Trackable Assets are complementary domains:
| Aspect | Catalogues | Trackable Assets |
|---|---|---|
| Purpose | Define asset types and specifications | Track actual asset instances |
| Lifecycle | Administrative (specs change rarely) | Operational (assets move through sites) |
| Ownership | Administrative (catalogues managed by ops) | User-managed (assets assigned by field teams) |
| Scope | Global templates | Per-estate or per-site instances |
| Change Frequency | Infrequent (new specs added periodically) | Frequent (assets created/updated daily) |
Creating Assets from Listings: When a user creates a trackable asset, they:
- Select a Catalogue (filters available listings)
- Select a Listing from that catalogue (defines asset type and custom fields)
- The listing’s custom field schema is inherited by the new asset
- A
CatalogueReferencein the asset tracks which listing was used
Custom Field Inheritance: Assets created from listings inherit the listing’s custom field definitions. If the listing belongs to a taxonomy subcategory, the asset inherits merged custom fields from both the category and subcategory.
Field Extension: After creation, users can add additional custom fields to assets beyond those inherited from the listing.
Domain Directives
Section titled “Domain Directives”Directives are command objects that drive state changes. The domain implements directives for all major operations:
Listing Directives
Section titled “Listing Directives”CreateListingDirective: Create new listingUpdateListingDirective: Update listing propertiesUpdateListingStatusDirective: Change status (submit/approve/reject)ToggleListingVisibilityDirective: Hide/unhide approved listingPublishListingVersionDirective: Manually publish
Catalogue Directives
Section titled “Catalogue Directives”CreateCatalogueDirective: Create new catalogueUpdateCatalogueDirective: Update catalogue metadataUpdateCatalogueStatusDirective: Change statusIncrementCatalogueReferenceCounterDirective: Generate reference numbers
Taxonomy Directives
Section titled “Taxonomy Directives”CreateTaxonomyDirective: Create taxonomyUpdateTaxonomyDirective: Update taxonomyDeleteTaxonomyDirective: Delete taxonomyAddTaxonomyCategoryDirective: Add categoryUpdateTaxonomyCategoryDirective: Update categoryRemoveTaxonomyCategoryDirective: Remove categoryAddTaxonomySubCategoryDirective: Add subcategoryUpdateTaxonomySubCategoryDirective: Update subcategoryRemoveTaxonomySubCategoryDirective: Remove subcategoryAddTaxonomyCategoryCustomFieldDirective: Add custom field to categoryAddTaxonomySubCategoryCustomFieldDirective: Add custom field to subcategoryUpdateTaxonomyCustomFieldDirective: Update custom field definitionRemoveTaxonomyCustomFieldDirective: Remove custom field
Supplier Directives
Section titled “Supplier Directives”CreateSupplierDirective: Create supplierChangeSupplierStatusDirective: Change supplier statusChangeSupplierMembershipDirective: Add/remove team members
Code Examples
Section titled “Code Examples”Creating a Catalogue
Section titled “Creating a Catalogue”import 'package:catalogues_v1/catalogues_v1.dart';import 'package:nomos_core/nomos_core.dart';import 'package:contracts_v1/contracts_v1.dart';
// Register domain typesregisterCataloguesV1();
// Create a new cataloguefinal engine = TestDirectiveEngine();final payload = CreateCataloguePayload( catalogueId: CatalogueId('CAT-HVAC-001'), name: CatalogueName('HVAC Equipment Catalogue'), description: CatalogueDescription('All HVAC equipment specifications'), ownerEstateId: EstateId('EST-001'), createdByUserId: UserId('USER-ADMIN'), referenceCodePrefix: DomainText('HVAC'), taxonomyId: TaxonomyId('TAX-HVAC'), metadata: {'region': 'north', 'version': '2024-01-01'},);
final result = await engine.execute<CatalogueAggregate, CreateCataloguePayload>( directive: CreateCatalogueDirective(payload: payload), timelineId: const NomosTimelineId('main'), workspaceId: CatalogueWorkspace(CatalogueId('CAT-HVAC-001')).toNomos,);
final catalogue = result.updatedAggregate;print('Created catalogue: ${catalogue.name.value}');Creating a Listing
Section titled “Creating a Listing”import 'package:catalogues_v1/catalogues_v1.dart';
final payload = CreateListingPayload( listingId: ListingId.fromString('LST-LED-001'), catalogueId: CatalogueId('CAT-HVAC-001'), name: const ListingTitle('LED Panel 200W'), description: const ListingDescription('High efficiency LED panel for indoor use'), taxonomyRef: TaxonomyReference( catalogueId: CatalogueId('CAT-HVAC-001'), taxonomyId: TaxonomyId('TAX-HVAC'), categoryId: TaxonomyCategoryId('lighting'), ), supplierId: SupplierId.fromString('SUP-LED-CORP'), createdByUserId: UserId.fromString('USER-001'), isCertified: false, customFields: CustomFieldMap.fromJson({ 'wattage': 200, 'lumens': 2000, 'color_temp_k': 4000, }), tags: [DomainText('LED'), DomainText('energy-efficient')], scaleX: 0.6, // 60cm width scaleY: 0.6, // 60cm height shape: ListingShape.rectangle,);
final directive = CreateListingDirective(payload: payload);final result = await engine.execute<ListingAggregate, CreateListingPayload>( directive: directive, timelineId: const NomosTimelineId('main'), workspaceId: CatalogueWorkspace(CatalogueId('CAT-HVAC-001')).toNomos,);
final listing = result.updatedAggregate;print('Created listing: ${listing.name.value}');Creating a Taxonomy with Categories
Section titled “Creating a Taxonomy with Categories”final taxonomyPayload = CreateTaxonomyPayload( taxonomyId: TaxonomyId('TAX-HVAC'), name: const TaxonomyName('HVAC Equipment Classification'), description: const TaxonomyDescription('Hierarchical classification for HVAC equipment'), createdByUserId: UserId.fromString('USER-ADMIN'),);
final taxonomyResult = await engine.execute<TaxonomyAggregate, CreateTaxonomyPayload>( directive: CreateTaxonomyDirective(payload: taxonomyPayload), timelineId: CatalogueWorkspace.defaultTimeline, workspaceId: CatalogueWorkspace(CatalogueId('CAT-HVAC-001')).toNomos,);
// Add a category to the taxonomyfinal categoryPayload = AddTaxonomyCategoryPayload( taxonomyId: TaxonomyId('TAX-HVAC'), categoryId: TaxonomyCategoryId('air-handling'), name: const TaxonomyCategoryName('Air Handling Units'), description: const TaxonomyCategoryDescription('Equipment for air circulation'), icon: 'air', color: TaxonomyColor.fromHex('#FF5722'), addedByUserId: UserId.fromString('USER-ADMIN'),);
await engine.execute<TaxonomyAggregate, AddTaxonomyCategoryPayload>( directive: AddTaxonomyCategoryDirective(payload: categoryPayload), timelineId: CatalogueWorkspace.defaultTimeline, workspaceId: CatalogueWorkspace(CatalogueId('CAT-HVAC-001')).toNomos,);
// Add custom fields to the categoryfinal fieldPayload = AddTaxonomyCategoryCustomFieldPayload( taxonomyId: TaxonomyId('TAX-HVAC'), categoryId: TaxonomyCategoryId('air-handling'), customField: CustomFieldDefinition( fieldId: CustomFieldKey.fromString('airflow_capacity'), name: 'Airflow Capacity', fieldType: CustomFieldType.number, isRequired: true, description: 'CFM (Cubic Feet per Minute)', ), addedByUserId: UserId.fromString('USER-ADMIN'),);
await engine.execute<TaxonomyAggregate, AddTaxonomyCategoryCustomFieldPayload>( directive: AddTaxonomyCategoryCustomFieldDirective(payload: fieldPayload), timelineId: CatalogueWorkspace.defaultTimeline, workspaceId: CatalogueWorkspace(CatalogueId('CAT-HVAC-001')).toNomos,);Validating Custom Fields Against Taxonomy Schema
Section titled “Validating Custom Fields Against Taxonomy Schema”// Get the taxonomy aggregatefinal taxonomy = /* retrieved from storage */;
// Get the schema for a specific category/subcategoryfinal schema = taxonomy.getCustomFieldSchema( TaxonomyCategoryId('air-handling'), null, // No subcategory in this example);
// Create custom field values for a listingfinal customFields = CustomFieldMap.fromJson({ 'airflow_capacity': 5000, 'noise_level': 65, 'efficiency_rating': 'A',});
// Validate against schemafinal errors = taxonomy.validateCustomFields( TaxonomyCategoryId('air-handling'), null, customFields,);
if (errors.isNotEmpty) { print('Validation errors: $errors');} else { print('Custom fields are valid');}Submitting and Approving a Listing
Section titled “Submitting and Approving a Listing”// Submit the listing for approvalfinal submitPayload = UpdateListingStatusPayload( listingId: ListingId.fromString('LST-LED-001'), newStatus: ListingStatus.submitted, changedByUserId: UserId.fromString('USER-001'),);
await engine.execute<ListingAggregate, UpdateListingStatusPayload>( directive: UpdateListingStatusDirective(payload: submitPayload), timelineId: const NomosTimelineId('main'), workspaceId: CatalogueWorkspace(CatalogueId('CAT-HVAC-001')).toNomos,);
// Admin approves the listing (triggers auto-publish)final approvePayload = UpdateListingStatusPayload( listingId: ListingId.fromString('LST-LED-001'), newStatus: ListingStatus.approved, changedByUserId: UserId.fromString('USER-ADMIN'),);
final result = await engine.execute<ListingAggregate, UpdateListingStatusPayload>( directive: UpdateListingStatusDirective(payload: approvePayload), timelineId: const NomosTimelineId('main'), workspaceId: CatalogueWorkspace(CatalogueId('CAT-HVAC-001')).toNomos,);
final listing = result.updatedAggregate;// listing.isApproved == true// listing.publishedAtSequence != null (auto-published)Dependencies
Section titled “Dependencies”The domain depends on:
nomos_core: Core event sourcing and DDD infrastructurecontracts_v1: Shared value objects and contracts (IDs, enums, etc.)
The domain exports all aggregates, events, directives, and value objects for use by applications and other domains.
Workspace Organization
Section titled “Workspace Organization”Catalogues use the CatalogueWorkspace pattern for organizing data:
// Each catalogue has its own workspace for storing listingsfinal workspace = CatalogueWorkspace(catalogueId).toNomos;
// Listings are queried using this workspacefinal listings = await nomos.watchQuery<ListingAggregate>( workspace, timelineId, null, // Query all listings in this catalogue);This allows efficient querying of all listings within a catalogue without scanning global data.
Key Design Decisions
Section titled “Key Design Decisions”-
Listing Auto-Publishing: On first approval, listings are automatically published and a sequence number is captured. This enables time-travel queries to retrieve the approved snapshot.
-
Custom Field Merging: Taxonomy subcategories can override category-level custom fields. The
getCustomFieldSchema()method handles merging with subcategory fields taking precedence. -
Ownership Constraints: A catalogue cannot be owned by both an estate and supplier. Global catalogues have no owner. This prevents ambiguous access control.
-
Reference Code Generation: Catalogues provide a prefix and counter mechanism for generating human-readable reference codes (e.g., “HVAC-001”).
-
Supplier Agnostic Listings: While listings can reference a supplier, they are not exclusive to that supplier. Suppliers are tracked for audit and business intelligence.
-
Visibility Toggle: Approved listings can be hidden without losing their published state or custom field definitions. This allows temporary removal from UI without resetting the approval workflow.
Integration Points
Section titled “Integration Points”The domain integrates with:
- Trackable Assets Domain: Assets inherit custom fields and metadata from listings
- Estate Structures Domain: Catalogues can be owned by estates
- Identity Domain: User audit trails (createdBy, updatedBy)
- Attachments Domain: Listings can reference thumbnail images
- Admin Frontend: UI for catalogue and listing management
Validation and Constraints
Section titled “Validation and Constraints”Each aggregate enforces business rules during state transitions:
CatalogueAggregate:
- Catalogue ID must be resolved (not unresolved)
- Name and description must be non-empty
- Cannot be owned by both estate and supplier
- Status must be valid (draft, active, archived)
ListingAggregate:
- Listing ID and catalogue ID must be resolved
- Name, description, and taxonomy reference required
- Scale dimensions (scaleX, scaleY) must be positive
- Custom fields must conform to taxonomy schema (if applicable)
TaxonomyAggregate:
- Taxonomy ID must be resolved
- Name must be non-empty
- All categories must pass internal validation
- Custom field definitions must be unique within category/subcategory