Service Hierarchy Extensibility Guide
Purpose: Guide for adding new service types and extending existing hierarchies.
Adding a New Service Type
Step 1: Define Service Type Characteristics
Before implementation, answer these questions:
| Question | Answer Format | Example |
|---|---|---|
| What is the base/root service? | Service name | "Satellite Internet Base" |
| What resources are allocated? | List of INVENTORY services | Dish, Modem, IP Address |
| What optional features exist? | List of ADDON services | Speed Boost, Static IP, Data Packs |
| Is regulatory tracking needed? | Yes/No + which registries | No (satellite not in IPND) |
| Is there nested hierarchy? | Yes/No + diagram | No (flat like Mobile) |
Step 2: Create Hierarchy Specification Document
Create a new file: .planning/phases/XX/research/{service-type}-hierarchy-specification.md
Template:
# {Service Type} Hierarchy Specification
## Overview
- **Service Type**: {Name}
- **Base Service Code**: SVC_{TYPE}_BASE
- **Hierarchy Pattern**: {Flat/Nested/Shared}
- **Max Depth**: {1/2/3}
## Service Catalogue
### PRIMARY Service
| Code | Role | Parent | Inclusion |
|------|------|--------|-----------|
| SVC_{TYPE}_BASE | PRIMARY | null | N/A |
### INVENTORY Services
| Code | Role | Parent | Inclusion | Pool |
|------|------|--------|-----------|------|
| SVC_{TYPE}_DEVICE | INVENTORY | SVC_{TYPE}_BASE | OPTIONAL | device_pool |
### ADDON Services
| Code | Role | Parent | Inclusion | Price |
|------|------|--------|-----------|-------|
| SVC_{TYPE}_DATA | ADDON | SVC_{TYPE}_BASE | OPTIONAL | $X |
### REGISTRY Services (if applicable)
| Code | Role | Parent | Inclusion | Registry |
|------|------|--------|-----------|----------|
| N/A | - | - | - | - |
## Hierarchy Diagram
\`\`\`mermaid
flowchart TD
BASE[SVC_{TYPE}_BASE<br/>PRIMARY] --> DEVICE[SVC_{TYPE}_DEVICE<br/>INVENTORY]
BASE --> DATA[SVC_{TYPE}_DATA<br/>ADDON]
\`\`\`
## Activation Flow
1. Customer purchases SVC_{TYPE}_BASE
2. User selects OPTIONAL children
3. MANDATORY children auto-activate
4. Inventory allocated for INVENTORY services
5. Carrier notification (if applicable)
Step 3: Create Service Templates
Database Migration:
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<changeSet id="staqr-{type}-service-templates" author="developer">
<!-- PRIMARY: Base Service -->
<insert tableName="cat_service_template">
<column name="code" value="SVC_{TYPE}_BASE"/>
<column name="description" value="{Type} Base Service"/>
<column name="role" value="PRIMARY"/>
<column name="parent_service_template_id" valueComputed="null"/>
<column name="inclusion_type" valueComputed="null"/>
<column name="sort_order" value="1"/>
<column name="auto_activate_children" valueBoolean="true"/>
<column name="inherit_parent_lifecycle" valueBoolean="false"/>
</insert>
<!-- INVENTORY: Device -->
<insert tableName="cat_service_template">
<column name="code" value="SVC_{TYPE}_DEVICE"/>
<column name="description" value="{Type} Device"/>
<column name="role" value="INVENTORY"/>
<column name="parent_service_template_id"
valueComputed="(SELECT id FROM cat_service_template WHERE code = 'SVC_{TYPE}_BASE')"/>
<column name="inclusion_type" value="OPTIONAL"/>
<column name="sort_order" value="10"/>
<column name="inherit_parent_lifecycle" valueBoolean="true"/>
</insert>
<!-- ADDON: Data -->
<insert tableName="cat_service_template">
<column name="code" value="SVC_{TYPE}_DATA"/>
<column name="description" value="{Type} Data Pack"/>
<column name="role" value="ADDON"/>
<column name="parent_service_template_id"
valueComputed="(SELECT id FROM cat_service_template WHERE code = 'SVC_{TYPE}_BASE')"/>
<column name="inclusion_type" value="OPTIONAL"/>
<column name="sort_order" value="20"/>
<column name="inherit_parent_lifecycle" valueBoolean="true"/>
</insert>
</changeSet>
</databaseChangeLog>
Step 4: Add TypeScript Types
File: packages/shared/src/lib/catalog/service-composition.types.ts
// Add to ServiceTypeCode enum
export enum ServiceTypeCode {
// ... existing codes
{TYPE} = '{TYPE}',
}
// Add to service type mapping
export const SERVICE_TYPE_MAPPING: Record<ServiceTypeCode, ServiceRole[]> = {
// ... existing mappings
[{TYPE}]: [
ServiceRole.PRIMARY, // SVC_{TYPE}_BASE
ServiceRole.INVENTORY, // SVC_{TYPE}_DEVICE
ServiceRole.ADDON, // SVC_{TYPE}_DATA, etc.
],
};
Step 5: Configure Carrier Integration (if applicable)
File: packages/server/api/src/app/carrier/adapters/{type}-carrier.adapter.ts
@Injectable()
export class {Type}CarrierAdapter extends CarrierAdapterBase {
readonly carrierType = CarrierType.{TYPE};
async activate(service: ServiceInstance): Promise<CarrierResult> {
// Implement carrier-specific activation
}
async suspend(service: ServiceInstance): Promise<CarrierResult> {
// Implement carrier-specific suspension
}
async terminate(service: ServiceInstance): Promise<CarrierResult> {
// Implement carrier-specific termination
}
}
Step 6: Add to UI Service Configurator
File: packages/react-ui/src/features/catalog/components/ServiceConfigurator/
- Add service type icon
- Add service type label translations
- Configure hierarchy display for new type
Step 7: Test New Service Type
# 1. Run migration
pnpm run migration:run
# 2. Verify templates created
psql -c "SELECT code, role, parent_service_template_id FROM cat_service_template WHERE code LIKE 'SVC_{TYPE}%';"
# 3. Test activation via API
curl -X POST "https://api.staqr.com/v1/services/activate" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"serviceCode": "SVC_{TYPE}_BASE", "customerId": "TEST123"}'
# 4. Verify hierarchy in UI
# Navigate to service configurator and select new type
Extending Existing Hierarchies
Adding a New Child Service
Step 1: Determine Service Characteristics
| Attribute | Value | Rationale |
|---|---|---|
| Role | INVENTORY, ADDON, or REGISTRY | Based on service role decision tree |
| Parent | SVC_PARENT_* | Based on parent selection tree |
| Inclusion Type | MANDATORY, OPTIONAL, or CONFIGURABLE | Based on inclusion type tree |
| Sort Order | N | Position among siblings |
Step 2: Create Database Migration
<changeSet id="staqr-add-{new-service}" author="developer">
<insert tableName="cat_service_template">
<column name="code" value="SVC_{NEW_SERVICE}"/>
<column name="description" value="{New Service Description}"/>
<column name="role" value="{ROLE}"/>
<column name="parent_service_template_id"
valueComputed="(SELECT id FROM cat_service_template WHERE code = 'SVC_{PARENT}')"/>
<column name="inclusion_type" value="{INCLUSION_TYPE}"/>
<column name="sort_order" value="{SORT_ORDER}"/>
<column name="inherit_parent_lifecycle" valueBoolean="true"/>
</insert>
</changeSet>
Step 3: Add Charges (if applicable)
<changeSet id="staqr-add-{new-service}-charges" author="developer">
<insert tableName="cat_charge_template">
<column name="service_template_id"
valueComputed="(SELECT id FROM cat_service_template WHERE code = 'SVC_{NEW_SERVICE}')"/>
<column name="charge_type" value="RECURRING"/>
<column name="amount" value="{PRICE}"/>
<column name="currency_code" value="AUD"/>
</insert>
</changeSet>
Step 4: Add Eligibility Constraints (if CONFIGURABLE)
<changeSet id="staqr-add-{new-service}-constraints" author="developer">
<insert tableName="cat_eligibility_constraint">
<column name="service_template_id"
valueComputed="(SELECT id FROM cat_service_template WHERE code = 'SVC_{NEW_SERVICE}')"/>
<column name="constraint_type" value="CUSTOMER_TIER"/>
<column name="constraint_value" value="PREMIUM"/>
</insert>
</changeSet>
Converting Standalone Services to Hierarchical
Step 1: Identify Standalone Services
-- Find services that should be children but have no parent
SELECT code, role, description
FROM cat_service_template
WHERE role IN ('INVENTORY', 'ADDON', 'REGISTRY')
AND parent_service_template_id IS NULL;
Step 2: Map to Parent Services
| Standalone Service | Correct Parent | Rationale |
|---|---|---|
| SVC_EXAMPLE_ADDON | SVC_MOBILE_BASE | Mobile-specific feature |
| SVC_SHARED_FEATURE | (multiple) | Shared across types |
Step 3: Create Migration to Set Parents
<changeSet id="staqr-fix-standalone-services" author="developer">
<!-- Set parent for Mobile-specific -->
<update tableName="cat_service_template">
<column name="parent_service_template_id"
valueComputed="(SELECT id FROM cat_service_template WHERE code = 'SVC_MOBILE_BASE')"/>
<where>code = 'SVC_EXAMPLE_ADDON'</where>
</update>
<!-- For shared services, may need multiple parent support -->
</changeSet>
Step 4: Migrate Existing Instances
-- Update existing service instances to have correct parent
UPDATE billing_service_instance si
SET parent_service_id = (
SELECT parent_si.id
FROM billing_service_instance parent_si
JOIN cat_service_template parent_st ON parent_si.service_template_id = parent_st.id
WHERE parent_st.code = 'SVC_MOBILE_BASE'
AND parent_si.subscription_id = si.subscription_id
)
WHERE si.service_template_id = (
SELECT id FROM cat_service_template WHERE code = 'SVC_EXAMPLE_ADDON'
)
AND si.parent_service_id IS NULL;
Step 5: Validate Migration
-- Verify no orphaned services remain
SELECT code FROM cat_service_template
WHERE role IN ('INVENTORY', 'ADDON', 'REGISTRY')
AND parent_service_template_id IS NULL;
-- Should return 0 rows
Adding Nested Hierarchy (Multi-Level)
When to Use Nested Hierarchy
Use nested hierarchy when:
- Technical dependency exists (NBN_AVC depends on NBN_ACCESS)
- Logical grouping required (VOICEMAIL → VOICEMAIL_BASIC, VOICEMAIL_VISUAL)
- Activation order matters (parent must activate before child)
Example: Adding NBN Speed Tiers Under NBN_ACCESS
flowchart TD
BASE[SVC_NBN_BASE<br/>PRIMARY] --> ACCESS[SVC_NBN_ACCESS<br/>ADDON]
ACCESS --> AVC[SVC_NBN_AVC<br/>ADDON]
ACCESS --> SPEED_25[SVC_NBN_SPEED_25<br/>ADDON]
ACCESS --> SPEED_50[SVC_NBN_SPEED_50<br/>ADDON]
ACCESS --> SPEED_100[SVC_NBN_SPEED_100<br/>ADDON]
Migration
<changeSet id="staqr-add-nbn-speed-tiers" author="developer">
<!-- Speed tier services as children of NBN_ACCESS (not NBN_BASE) -->
<insert tableName="cat_service_template">
<column name="code" value="SVC_NBN_SPEED_25"/>
<column name="role" value="ADDON"/>
<column name="parent_service_template_id"
valueComputed="(SELECT id FROM cat_service_template WHERE code = 'SVC_NBN_ACCESS')"/>
<column name="inclusion_type" value="OPTIONAL"/>
<column name="sort_order" value="100"/>
</insert>
<insert tableName="cat_service_template">
<column name="code" value="SVC_NBN_SPEED_50"/>
<column name="role" value="ADDON"/>
<column name="parent_service_template_id"
valueComputed="(SELECT id FROM cat_service_template WHERE code = 'SVC_NBN_ACCESS')"/>
<column name="inclusion_type" value="OPTIONAL"/>
<column name="sort_order" value="110"/>
</insert>
<insert tableName="cat_service_template">
<column name="code" value="SVC_NBN_SPEED_100"/>
<column name="role" value="ADDON"/>
<column name="parent_service_template_id"
valueComputed="(SELECT id FROM cat_service_template WHERE code = 'SVC_NBN_ACCESS')"/>
<column name="inclusion_type" value="OPTIONAL"/>
<column name="sort_order" value="120"/>
</insert>
</changeSet>
Testing New Hierarchies
Comprehensive Checklist
Template Configuration Tests
- All services have correct role
- All non-PRIMARY services have parent
- PRIMARY services have no parent
- Sort order is set for all children
- Inclusion type is appropriate
- Lifecycle inheritance is configured
Activation Tests
- PRIMARY service activates successfully
- MANDATORY children auto-activate
- OPTIONAL children can be selected/deselected
- CONFIGURABLE children respect eligibility
- INVENTORY services allocate resources
- Carrier notifications sent (if applicable)
Lifecycle Tests
- Parent suspension cascades to children
- Parent termination cascades to children
- Child can be terminated independently (if allowed)
- Reactivation works correctly
UI Tests
- Hierarchy displays correctly in service configurator
- Correct services are selectable
- Eligibility indicators work
- Pricing displays correctly
- Order summary is accurate
Automated Test Script
#!/bin/bash
# test-hierarchy.sh
SERVICE_TYPE=$1
echo "=== Testing $SERVICE_TYPE Hierarchy ==="
# 1. Check templates exist
echo "Checking templates..."
psql -c "
SELECT code, role, inclusion_type
FROM cat_service_template
WHERE code LIKE 'SVC_${SERVICE_TYPE}%'
ORDER BY sort_order;
"
# 2. Verify parent relationships
echo "Checking parent relationships..."
psql -c "
SELECT st.code, parent.code as parent_code
FROM cat_service_template st
LEFT JOIN cat_service_template parent ON st.parent_service_template_id = parent.id
WHERE st.code LIKE 'SVC_${SERVICE_TYPE}%';
"
# 3. Check for orphans
echo "Checking for orphans..."
ORPHANS=$(psql -t -c "
SELECT COUNT(*) FROM cat_service_template
WHERE code LIKE 'SVC_${SERVICE_TYPE}%'
AND role != 'PRIMARY'
AND parent_service_template_id IS NULL;
")
if [ "$ORPHANS" -gt 0 ]; then
echo "❌ Found $ORPHANS orphaned services!"
exit 1
fi
echo "✅ All checks passed"
Performance Considerations
Hierarchy Depth Limits
- Recommended max depth: 3 levels
- Hard limit: 5 levels (performance degrades beyond this)
- Reason: Recursive queries become expensive at deep levels
Indexing Requirements
-- Essential indexes for hierarchy queries
CREATE INDEX idx_service_template_parent ON cat_service_template(parent_service_template_id);
CREATE INDEX idx_service_template_role ON cat_service_template(role);
CREATE INDEX idx_service_instance_parent ON billing_service_instance(parent_service_id);
CREATE INDEX idx_service_instance_subscription ON billing_service_instance(subscription_id);
Caching Strategy
- Cache service templates (rarely change)
- Don't cache service instances (frequently change)
- Invalidate cache on template updates
Backwards Compatibility
Adding New Services
- Safe: New services can be added without affecting existing
- Migration: Run in non-production first
Changing Parent Relationships
- Risky: May break existing service instances
- Migration: Update both templates AND instances
- Rollback: Keep old parent until instances migrated
Removing Services
- Very Risky: May orphan existing instances
- Process:
- Mark as deprecated (don't delete)
- Migrate existing instances
- Delete only when no instances remain
Rollback Procedures
Rolling Back Template Changes
-- Identify changed templates
SELECT code, updated_date
FROM cat_service_template
WHERE updated_date > NOW() - INTERVAL '1 hour';
-- Restore from backup (example)
INSERT INTO cat_service_template
SELECT * FROM cat_service_template_backup
WHERE code = 'SVC_AFFECTED_SERVICE';
Rolling Back Instance Changes
-- Find affected instances
SELECT id, status, parent_service_id
FROM billing_service_instance
WHERE updated_date > NOW() - INTERVAL '1 hour';
-- Restore parent relationships
UPDATE billing_service_instance
SET parent_service_id = (SELECT parent_service_id FROM billing_service_instance_backup WHERE id = ?)
WHERE id = ?;