Skip to content

LAYERED ARCHITECTURE — FLOW REFERENCE


Table of Contents


Flow Examples

FLOW 1: Create Knowledge (Local Only)

POST /api/knowledge

KnowledgeController::create()

KnowledgeOrchestrator::createKnowledge()
  ├─→ 1. RESOLVE: Resolve Category and Target Groups (via ResolutionServices)
  ├─→ 2. MAP: KnowledgeMapper::fromCreateRequest() (via MapperRegistry)
  └─→ 3. PERSIST: KnowledgePersistenceService::create()

PostgreSQL Database

FLOW 2: Create Knowledge with Notion Sync

POST /api/knowledge?sync=true

KnowledgeController::create()

KnowledgeOrchestrator::createKnowledge()
  ├─→ 1. RESOLVE: Resolve dependencies
  ├─→ 2. MAP: Create Knowledge entity (MapperRegistry)
  ├─→ 3. PERSIST: LocalKnowledgeRepository → PostgreSQL
  └─→ 4. SYNC: NotionKnowledgeRepository::create() → Notion API

FLOW 3: Fetch Knowledge (Auto-detect Source)

GET /api/knowledge/{id}

KnowledgeController::show()

KnowledgeOrchestrator::getKnowledge()

KnowledgeQueryService::findByNotionId() (if needed)

NotionRepositoryManager::getKnowledgeRepository()
  ├─→ Numeric ID → LocalKnowledgeRepository → PostgreSQL
  └─→ UUID/Notion ID → NotionKnowledgeRepository → Notion API

FLOW 4: Sync All Knowledge from Notion

NotionSyncOrchestrator::syncAllFromNotion()

NotionSyncKnowledge::syncFromNotion()
  ├─→ NotionKnowledgeRepository::findAll() → Notion API
  └─→ KnowledgePersistenceService::upsert() → LocalKnowledgeRepository → PostgreSQL

FLOW 5: Generate AI Summary

AiSummaryOrchestrator::generateSummary()

SymfonyAiProvider::generateCompletion()

CustomGatewayPlatform::request()
  ├─→ Extract messages from Symfony AI objects
  ├─→ Build OpenAI-compatible payload
  └─→ AuthOverrideHttpClient::request()
      ├─→ Override auth_bearer with API key
      ├─→ Rewrite URL: /v1/responses → /v1/chat/completions
      ├─→ Transform payload format
      └─→ Symfony HttpClient → AI Gateway/API

FLOW 6: Sync AI Summaries from Notion

SyncAiSummariesFromNotionCommand::execute()

NotionAiSummaryService::syncFromNotion()
  ├─→ NotionAiSummaryService::fetchFromNotion() → Notion API
  └─→ SummaryPersistenceService::upsert() → AiSummaryRepository → PostgreSQL

FLOW 7: Generate Digest with AI Summaries

DigestOrchestrator::generateDigest()

DigestSummaryOrchestrator::ensureAllSummariesExist()
  ├─→ PromptBuilderService::buildPrompt()
  └─→ SymfonyAiProvider::generateCompletion() → AI Provider Stack

SummaryPersistenceService::create() → AiSummaryRepository → PostgreSQL

FLOW 8: Update Category with Notion Sync

PUT /api/category/{id}

CategoryController::update()

CategoryOrchestrator::updateInNotion()
  ├─→ 1. RESOLVE: Target Groups (ResolutionService)
  ├─→ 2. SYNC: Update Notion page → Notion API
  ├─→ 3. MAP: Update local entity (MapperRegistry)
  └─→ 4. PERSIST: Local database update → PostgreSQL

FLOW 9: Batch Generate AI Summaries

POST /api/summaries/batch

SummaryController::generateBatch()

AiSummaryOrchestrator::generateBatchSummaries()
  ├─→ LocalKnowledgeRepository::findByIds() → PostgreSQL
  └─→ For each knowledge item:
      ├─→ PromptBuilderService::buildPrompt()
      ├─→ SymfonyAiProvider::generateCompletion() → AI Provider Stack
      └─→ SummaryPersistenceService::create() → AiSummaryRepository → PostgreSQL

INPUT FLOWS (Slack Integration)

FLOW 10: Slack Share to Knowledge (App Share Button)

Slack App Share Button

Slack Event: message_action

Slack Bot (Node.js): handlers/shortcuts.ts

POST /api/knowledge (Backend API)

KnowledgeController::create()

KnowledgeOrchestrator::createKnowledge()
  ├─→ Extract URL from Slack message
  ├─→ Scrape content from URL
  ├─→ KnowledgePersistenceService::create() → LocalKnowledgeRepository → PostgreSQL
  └─→ NotionSyncKnowledge::syncToNotion() → NotionKnowledgeRepository → Notion API

Response to Slack (ephemeral message)

Layers involved: 1 (Presentation), 2 (Orchestration), 3 (Sync), 4 (Persistence), 5 (Repository Manager), 6 (Data Access), 7 (Storage), 8 (Cross-cutting)

FLOW 10A: Category Subscription via Slack Overflow Menu

User clicks "Subscribe" in category overflow menu

Slack Event: block_actions (action_id: category_subscribe)

Slack Bot (Node.js): handlers/actions/subscription.ts

POST /api/subscriptions (Backend API)

SubscriptionController::subscribe()

CategorySubscriptionOrchestrator::subscribeUser()
  ├─→ 1. RESOLVE: CategoryResolutionService::resolve()
  ├─→ 2. QUERY: CategorySubscriptionQueryService::findByUserAndCategory() (check existing)
  ├─→ 3. PERSIST: CategorySubscriptionPersistenceService::create()/update() → PostgreSQL
  ├─→ 4. SYNC: NotionSyncSubscription::syncToNotion() → Notion API
  └─→ 5. REFRESH: SlackHomeRefreshService::refreshUserHomePage() → Slack API

Response: Updated Slack UI with "Unsubscribe" button

Layers involved: 1, 2, 3, 5, 7, 8

FLOW 11: Slack App Home Button to Knowledge

Slack App Home Tab

User clicks "Add Knowledge" button

Slack Event: view_submission

Slack Bot (Node.js): handlers/home/manage.ts

Modal opens with form (URL, title, category, tags)

User submits form

POST /api/knowledge (Backend API)

KnowledgeController::create()

KnowledgeOrchestrator::createKnowledge()
  ├─→ KnowledgePersistenceService::create() → LocalKnowledgeRepository → PostgreSQL
  └─→ NotionSyncKnowledge::syncToNotion() → NotionKnowledgeRepository → Notion API

Slack App Home refreshes with new item

Layers involved: 1, 2, 3, 4, 5, 6, 7, 8

FLOW 12: URL Detection in Channel Messages

User posts message with URL in Slack channel

Slack Event: message

Slack Bot (Node.js): handlers/events.ts

URL pattern detection (regex)

Bot sends ephemeral message: "Save this to Knowledge Hub?"

User clicks "Save" button

POST /api/knowledge (Backend API)

KnowledgeController::create()

KnowledgeOrchestrator::createKnowledge()
  ├─→ URL content extraction
  ├─→ KnowledgePersistenceService::create() → LocalKnowledgeRepository → PostgreSQL
  └─→ NotionSyncKnowledge::syncToNotion() → NotionKnowledgeRepository → Notion API

Layers involved: 1, 2, 3, 4, 5, 6, 7, 8

FLOW 13: Slack Command to Create Knowledge

User types: /knowledge add <url>

Slack Event: slash_command

Slack Bot (Node.js): handlers/commands.ts

POST /api/knowledge (Backend API)

KnowledgeController::create()

KnowledgeOrchestrator::createKnowledge()
  ├─→ Parse command arguments
  ├─→ KnowledgePersistenceService::create() → LocalKnowledgeRepository → PostgreSQL
  └─→ NotionSyncKnowledge::syncToNotion() → NotionKnowledgeRepository → Notion API

Response to Slack (ephemeral message with confirmation)

Layers involved: 1, 2, 3, 4, 5, 6, 7, 8


KNOWLEDGE MANAGEMENT FLOWS

FLOW 14: Create/Update/Delete Knowledge (CRUD)

Create:

POST /api/knowledge

KnowledgeController::create()

KnowledgeOrchestrator::createKnowledge()
  ├─→ KnowledgePersistenceService::create() → LocalKnowledgeRepository → PostgreSQL
  └─→ NotionSyncKnowledge::syncToNotion() → NotionKnowledgeRepository → Notion API

Update:

PUT /api/knowledge/{id}

KnowledgeController::update()

KnowledgeOrchestrator::updateKnowledge()
  ├─→ KnowledgePersistenceService::update() → LocalKnowledgeRepository → PostgreSQL
  └─→ NotionSyncKnowledge::syncToNotion() → NotionKnowledgeRepository → Notion API

Delete:

DELETE /api/knowledge/{id}

KnowledgeController::delete()

KnowledgeOrchestrator::deleteKnowledge()
  ├─→ KnowledgePersistenceService::delete() → LocalKnowledgeRepository → PostgreSQL
  └─→ NotionSyncKnowledge::deleteFromNotion() → NotionKnowledgeRepository → Notion API

Layers involved: 1, 2, 3, 4, 5, 6, 7, 8

FLOW 15: Create/Update/Delete Category (CRUD)

Create:

POST /api/categories

CategoryController::create()

CategoryOrchestrator::createCategory()
  ├─→ 1. RESOLVE: Target Groups
  ├─→ 2. MAP: CategoryMapper (MapperRegistry)
  └─→ 3. PERSIST: CategoryPersistenceService::create()

Update:

PUT /api/categories/{id}

CategoryController::update()

CategoryOrchestrator::updateCategory()
  ├─→ 1. RESOLVE: Entity and Target Groups
  ├─→ 2. MAP: CategoryMapper::applyUpdateRequest()
  └─→ 3. PERSIST: CategoryPersistenceService::update()

Delete:

DELETE /api/categories/{id}

CategoryController::delete()

CategoryOrchestrator::deleteCategory()
  ├─→ CategoryPersistenceService::delete() → CategoryRepository → PostgreSQL
  └─→ NotionSyncCategory::deleteFromNotion() → NotionCategoryRepository → Notion API

Layers involved: 1, 2, 3, 4, 5, 6, 7, 8

FLOW 16: Search Knowledge Items

GET /api/knowledge/search?q={query}&category={id}&tags={tags}

KnowledgeController::search()

KnowledgeOrchestrator::searchKnowledge()

KnowledgeQueryService::advancedSearch()

LocalKnowledgeRepository::search()
  ├─→ Full-text search on title, description, content
  ├─→ Filter by category
  ├─→ Filter by tags
  └─→ PostgreSQL query with LIKE/ILIKE

Return paginated results

Layers involved: 1, 2, 4, 5, 6, 7

FLOW 17: Filter Knowledge by Category/Tags

GET /api/knowledge?category={id}&tags={tags}&dateFrom={date}&dateTo={date}

KnowledgeController::list()

KnowledgeOrchestrator::listKnowledge()

LocalKnowledgeRepository::findByFilters()
  ├─→ Filter by category ID
  ├─→ Filter by tags (array intersection)
  ├─→ Filter by date range
  └─→ PostgreSQL query with WHERE clauses

Return filtered results

Layers involved: 1, 2, 4, 5, 6, 7


AI PROCESSING FLOWS

FLOW 18: Generate AI Summary for Knowledge Item

POST /api/summaries/generate

SummaryController::generate()

AiSummaryOrchestrator::generateSummary()
  ├─→ LocalKnowledgeRepository::find() → PostgreSQL
  ├─→ PromptTemplateRepository::findByType() → PostgreSQL
  ├─→ PromptBuilderService::buildPrompt()
  │   ├─→ Load template
  │   ├─→ Substitute variables (title, content, category)
  │   └─→ Return formatted prompt
  ├─→ SymfonyAiProvider::generateCompletion()
  │   ├─→ CustomGatewayPlatform::request()
  │   ├─→ AuthOverrideHttpClient::request()
  │   └─→ AI Gateway/API (OpenAI)
  └─→ SummaryPersistenceService::create() → AiSummaryRepository → PostgreSQL

Layers involved: 1, 2, 4, 5, 6, 7, 8, 9 (AI Provider Stack)

FLOW 19: Regenerate AI Summary

POST /api/summaries/{id}/regenerate

SummaryController::regenerate()

AiSummaryOrchestrator::regenerateSummary()
  ├─→ AiSummaryRepository::find() → PostgreSQL (get existing summary)
  ├─→ LocalKnowledgeRepository::find() → PostgreSQL (get knowledge item)
  ├─→ Increment version number
  ├─→ PromptBuilderService::buildPrompt()
  ├─→ SymfonyAiProvider::generateCompletion() → AI Provider Stack
  └─→ SummaryPersistenceService::create() → AiSummaryRepository → PostgreSQL
      (new version with incremented version number)

Layers involved: 1, 2, 4, 5, 6, 7, 8, 9

FLOW 20: Batch Generate Summaries for Multiple Items

POST /api/summaries/batch
Body: { "knowledgeIds": [1, 2, 3, 4, 5] }

SummaryController::generateBatch()

AiSummaryOrchestrator::generateBatchSummaries()
  ├─→ LocalKnowledgeRepository::findByIds() → PostgreSQL
  └─→ For each knowledge item:
      ├─→ Check if summary exists (skip if exists)
      ├─→ PromptBuilderService::buildPrompt()
      ├─→ SymfonyAiProvider::generateCompletion() → AI Provider Stack
      └─→ SummaryPersistenceService::create() → AiSummaryRepository → PostgreSQL

Return batch results (success count, failed items)

Layers involved: 1, 2, 4, 5, 6, 7, 8, 9


OUTPUT FLOWS (Digest Generation & Distribution)

FLOW 21: Generate Digest Report (Complete Flow)

POST /api/digests/generate
Body: {
  "categoryId": 3,
  "dateRange": "7 days"
}

DigestController::generate()

DigestOrchestrator::generateDigest(categoryId, dateRange, templateId)
  ├─→ DigestQueryService::findKnowledgeForDigest(categoryId, dateRange)
  │   ├─→ Parse date range (relative or absolute)
  │   ├─→ Filter by category (required)
  │   ├─→ Filter by date range
  │   └─→ PostgreSQL query → Return knowledge items

  ├─→ DigestSummaryOrchestrator::ensureAllSummariesExist(knowledgeItems)
  │   └─→ For each knowledge item:
  │       ├─→ Check if AI summary exists
  │       ├─→ If missing: AiSummaryOrchestrator::generateSummary()
  │       │   ├─→ PromptBuilderService::buildPrompt()
  │       │   ├─→ SymfonyAiProvider::generateCompletion() → AI Provider Stack
  │       │   └─→ SummaryPersistenceService::create() → PostgreSQL
  │       └─→ Collect summaries

  ├─→ DigestGenerationService::generateDigestData(knowledgeItems, aiSummaries)
  │   ├─→ Group knowledge by category
  │   ├─→ Extract metadata (authors, tags, dates)
  │   └─→ Build structured digest data

  ├─→ DigestAiSummaryGenerator::generateDigestSummary()
  │   ├─→ PromptBuilderService::buildPrompt()
  │   └─→ SymfonyAiProvider::generateCompletion() → AI Provider Stack

  └─→ DigestContentFormatter::formatDigestContent()

Return DigestGenerationResultDto

Layers involved: 1, 2, 4, 5, 6, 7, 8, 9

FLOW 22: Generate Digest with Template (API Variant)

Same as FLOW 21 but with explicit templateId for prompt template selection. Both use DigestOrchestrator::generateDigest(categoryId, dateRange, templateId). The only difference is whether templateId is null (auto-resolve) or explicit.

Layers involved: 1, 2, 4, 5, 6, 7, 8, 9

FLOW 22B: Create Digest Entity via CLI (Cron Job)

Cron Job (scheduled task)

GenerateDigestCommand::execute() --category=3 --date-range="7 days" --save

DigestOrchestrator::createDigestEntity(categoryId, dateRange)
  ├─→ DigestOrchestrator::generateDigest(categoryId, dateRange)
  │   └─→ (Same flow as FLOW 22 above)

  ├─→ CategoryPersistenceService::findById(categoryId)

  ├─→ DigestEntityBuilder::buildFromData()
  │   ├─→ Create Digest entity
  │   ├─→ Set title, period, status
  │   ├─→ Link knowledge items
  │   ├─→ Link AI summaries
  │   └─→ Link categories

  └─→ DigestPersistenceService::create() → DigestRepository → PostgreSQL

Layers involved: 1, 2, 4, 5, 6, 7, 8, 9

FLOW 23: Publish Digest to Notion

POST /api/digests/{id}/publish

NotionDigestController::publish()

NotionSyncDigest::syncToNotion()
  ├─→ DigestPersistenceService::findById() → PostgreSQL
  ├─→ DigestMapper::toNotionFormat()
  │   ├─→ Map digest properties
  │   ├─→ Format knowledge items
  │   ├─→ Format AI summaries
  │   └─→ Group by category

  ├─→ NotionDigestRepository::create()
  │   ├─→ Create Notion page
  │   ├─→ Add properties (title, period, date range)
  │   ├─→ Add content blocks (headings, lists, summaries)
  │   └─→ Notion API

  └─→ DigestPersistenceService::linkNotionPage()
      ├─→ Store Notion ID and URL
      ├─→ Update last synced timestamp
      └─→ PostgreSQL

Layers involved: 1, 2, 3, 4, 5, 6, 7, 8

FLOW 24: Distribute Digest to Subscribers

POST /api/digests/{id}/distribute
Body: { "initiatorUserId": "U123" }

DigestController::distribute(id)
  ├─→ DigestOrchestrator::getDigest(id) → Digest entity
  └─→ DigestDistributionService::distributeDigest(digest, initiatorUserId)

DigestRecipientResolver::resolveRecipients($digest)
  ├─→ Get digest categories → Category::getActiveSubscriptions()
  ├─→ Get digest target groups → TargetGroupSubscriptionQueryService::findSubscribers()
  ├─→ Add initiator user ID (if provided)
  └─→ Merge + deduplicate → DigestRecipientList

SlackDigestDeliveryService::sendBulk($digest, $recipients)
  ├─→ For each batch (batch size from DigestDistributionConstants):
  │   ├─→ Send digest to each user via Slack
  │   └─→ DigestDeliveryLogService::logDelivery() → PostgreSQL

Return DigestDistributionResult (total, successful, failed)

Layers involved: 1, 2, 4, 7, 8, 9

Note: This flow uses the digest-specific DigestDistributionService which routes directly through SlackDigestDeliveryService. For generic multi-channel delivery of any DeliverableInterface, see FLOW 29 using DistributionService.


SUBSCRIPTION & PREFERENCE FLOWS

FLOW 25: Subscribe to Target Group

POST /api/target-group-subscriptions
Body: { "userId": "U123", "targetGroupId": 1 }

TargetGroupSubscriptionController::subscribe()

TargetGroupSubscriptionOrchestrator::subscribeToTargetGroup()
  ├─→ 1. RESOLVE: TargetGroupResolutionService::resolve()
  ├─→ 2. QUERY: TargetGroupSubscriptionQueryService::findByUserAndTargetGroup()
  ├─→ 3. PERSIST: TargetGroupSubscriptionPersistenceService::create()/update()
  │   └─→ TargetGroupSubscriptionRepository::save() → PostgreSQL
  └─→ 4. REFRESH: SlackHomeRefreshService::refreshUserHomePage() → Slack API

Return TargetGroupSubscriptionResult (success, subscription data)

Layers involved: 1, 2, 5, 7, 8, 9

FLOW 26: Digest Recipient Resolution (Detail)

This flow shows the recipient resolution logic used by DigestDistributionService::distributeDigest() (see FLOW 24).

DigestRecipientResolver::resolveRecipients($digest, $initiatorUserId)
  ├─→ For each digest category:
  │   └─→ Category::getActiveSubscriptions()
  │       └─→ Collect active subscriber user IDs

  ├─→ For each digest target group:
  │   └─→ TargetGroupSubscriptionQueryService::findSubscribers(targetGroupId)
  │       └─→ Return subscriber user IDs

  ├─→ Add initiator user ID (if provided)
  └─→ Deduplicate → DigestRecipientList::create()

Design Patterns:

  • Single Responsibility: Resolver handles only recipient resolution, not delivery
  • Liskov Substitution: TargetGroupSubscriptionQueryServiceInterface allows implementation swaps
  • DRY: Category/target group subscriber collection reused in count methods

Layers involved: 2, 4, 7

FLOW 27: Set User Delivery Preference

POST /api/delivery-preferences
Body: { "userId": "U123", "channel": "EMAIL", "frequency": "DAILY" }

UserDeliveryPreferenceController::setPreference()

UserDeliveryPreferenceService::setPreference($userId, $channel, $frequency)
  ├─→ Validate channel (SLACK, EMAIL, NOTION)
  ├─→ Validate frequency (IMMEDIATE, DAILY, WEEKLY)
  ├─→ UserDeliveryPreferenceRepository::findByUserId() → PostgreSQL
  ├─→ If exists: Update preference
  │   └─→ UserDeliveryPreferenceRepository::save() → PostgreSQL
  └─→ If not exists: Create new preference
      └─→ UserDeliveryPreferenceRepository::save() → PostgreSQL

Return UserDeliveryPreferenceDto (userId, channel, frequency)

Layers involved: 1, 2, 4, 7, 8

FLOW 28: Unsubscribe from Target Group

DELETE /api/target-group-subscriptions/{id}

TargetGroupSubscriptionController::unsubscribe()

TargetGroupSubscriptionOrchestrator::unsubscribeFromTargetGroup()
  ├─→ 1. QUERY: TargetGroupSubscriptionQueryService::findByUserAndTargetGroup()
  ├─→ 2. Validate subscription exists and is active
  ├─→ 3. PERSIST: TargetGroupSubscriptionPersistenceService::update() (deactivate)
  │   └─→ TargetGroupSubscriptionRepository::flush() → PostgreSQL
  └─→ 4. REFRESH: SlackHomeRefreshService::refreshUserHomePage() → Slack API

Return success response

GENERIC DELIVERY LAYER FLOWS

The generic delivery system supports distributing ANY DeliverableInterface to ANY user via their preferred channel. It uses a 4-sub-layer architecture: Distribution → Channel → Formatter → Transport.

Note: This is separate from the digest-specific DigestDistributionService (FLOW 24), which uses SlackDigestDeliveryService directly. The generic DistributionService is used for delivering knowledge items, AI summary notifications, and any other DeliverableInterface.

FLOW 29: Generic Distribution (DeliverableInterface)

DistributionService::distribute($deliverable, $userIds, $targetGroupId)

Sub-layer 1 — DISTRIBUTION ORCHESTRATION:
  ├─→ DistributionService iterates over $userIds
  └─→ For each userId:
      ├─→ UserDeliveryPreferenceService::getEffectiveChannel()
      │   └─→ Resolve user's preferred channel (Slack, Email, Notion)
      ├─→ DeliveryChannelFactory::getChannel($channelName)
      │   └─→ Return appropriate DeliveryChannelInterface implementation
      └─→ Continue to Channel sub-layer

Sub-layer 2 — CHANNEL STRATEGY (Template Method Pattern):
  ├─→ DeliveryChannelInterface::deliver($deliverable, $userId, $settings)
  ├─→ AbstractDeliveryChannel::deliver() [TEMPLATE METHOD]
  │   ├─→ 1. validateSettings() [implemented by subclass]
  │   ├─→ 2. isChannelAvailable()
  │   ├─→ 3. executeDelivery() [implemented by subclass]
  │   └─→ 4. logResult() + succeed()/failWithError()

  └─→ SlackDeliveryChannel::executeDelivery()
      ├─→ Build delivery payload from deliverable data
      └─→ Continue to Formatter sub-layer

Sub-layer 3 — FORMATTING (Content Transformation):
  ├─→ SlackBlockFormatter::formatBlocks($deliverable)
  │   ├─→ Detect deliverable type (digest, knowledge, ai_summaries)
  │   ├─→ Format as Slack Block Kit JSON
  │   └─→ Add action buttons and metadata

  └─→ Return formatted blocks array

Sub-layer 4 — TRANSPORT (Pure I/O):
  ├─→ MessageTransportInterface::send($payload)
  ├─→ SlackMessageTransport::send(TransportPayloadDto)
  │   ├─→ Add auth headers (Bearer token)
  │   ├─→ Build HTTP request to Slack Bot API
  │   ├─→ POST http://slack-bot:3000/slack/send-digest
  │   ├─→ Measure timing + handle errors
  │   └─→ Return TransportResultDto (success, statusCode, durationMs, errorMessage)

  └─→ HTTP Request → Slack Bot → Slack API → Slack DM

Return DistributionResult (totalRecipients, successfulDeliveries, failedDeliveries)

Layers involved: 4 (Domain Services - Delivery), 7 (Data Access), 8 (Storage), 9 (Cross-cutting)

Key Concepts:

  • Template Method Pattern: AbstractDeliveryChannel enforces deliver() workflow
  • Strategy Pattern: DeliveryChannelFactory selects appropriate channel
  • Liskov Substitution: Any DeliverableInterface works with any DeliveryChannelInterface
  • Immutability: TransportPayloadDto and TransportResultDto are read-only
  • No Magic Strings: Constants used throughout (DeliveryChannelConstants, DeliveryConstants)

FLOW 30: Distribute Knowledge Item via Generic System

DistributionService::distribute($knowledge, [$userId])

Sub-layer 1 — DISTRIBUTION ORCHESTRATION:
  ├─→ Resolve user's preferred channel
  └─→ Create appropriate DeliveryChannelInterface

Sub-layer 2 — CHANNEL STRATEGY:
  ├─→ SlackDeliveryChannel::deliver($knowledge, $userId, $settings)
  ├─→ AbstractDeliveryChannel::deliver() [TEMPLATE METHOD]
  │   ├─→ validateSettings() → Check Slack user ID format
  │   ├─→ isChannelAvailable() → Return true
  │   ├─→ executeDelivery() [SlackDeliveryChannel specific]
  │   │   └─→ Build knowledge payload
  │   └─→ logResult()
  └─→ Continue to Formatter

Sub-layer 3 — FORMATTING:
  ├─→ SlackBlockFormatter::formatBlocks($knowledge)
  │   ├─→ Detect type: DELIVERABLE_TYPE_KNOWLEDGE
  │   ├─→ Format as Slack message with:
  │   │   ├─→ Title (as Block Header)
  │   │   ├─→ Content snippet (max 500 chars)
  │   │   ├─→ Tags and metadata
  │   │   ├─→ Category badge
  │   │   └─→ "View in Hub" and "Save" action buttons
  │   └─→ Return Block Kit JSON array

  └─→ Continue to Transport

Sub-layer 4 — TRANSPORT:
  ├─→ SlackMessageTransport::send($payload)
  │   ├─→ POST /slack/send-digest
  │   └─→ Return TransportResultDto

  └─→ Return DeliveryResult

Layers involved: 4 (Domain Services - Delivery), 9 (Cross-cutting)

FLOW 31: Distribute AI Summary Notification

DistributionService::distribute($aiSummaryDeliverable, [$userId])
  [where $aiSummaryDeliverable = AiSummaryDeliverable wrapping knowledge + summaries]

Sub-layer 1 — DISTRIBUTION ORCHESTRATION:
  ├─→ Resolve user's preferred channel
  └─→ Create DeliveryChannelInterface (e.g., SlackDeliveryChannel)

Sub-layer 2 — CHANNEL STRATEGY:
  ├─→ SlackDeliveryChannel::deliver($aiSummaryDeliverable, $userId, $settings)
  ├─→ AbstractDeliveryChannel::deliver() [TEMPLATE METHOD]
  │   ├─→ validateSettings()
  │   ├─→ executeDelivery() [SlackDeliveryChannel]
  │   └─→ logResult()
  └─→ Continue

Sub-layer 3 — FORMATTING:
  ├─→ SlackBlockFormatter::formatBlocks($aiSummaryDeliverable)
  │   ├─→ Detect type: DELIVERABLE_TYPE_AI_SUMMARIES
  │   ├─→ Format Knowledge item header
  │   ├─→ Format AI summary text (with "AI Generated" badge)
  │   ├─→ Add "View Original" action button
  │   └─→ Return Block Kit JSON

  └─→ Continue to Transport

Sub-layer 4 — TRANSPORT:
  ├─→ SlackMessageTransport::send($payload)
  └─→ Return TransportResultDto

Layers involved: 4 (Domain Services - Delivery), 9 (Cross-cutting)

FLOW 32: Deliver via Email Channel (Future)

DistributionService::distribute($deliverable, [$userId])

Sub-layer 1 — DISTRIBUTION ORCHESTRATION:
  ├─→ User preference: EMAIL channel
  └─→ DeliveryChannelFactory::getChannel('EMAIL')
      └─→ Return EmailDeliveryChannel

Sub-layer 2 — CHANNEL STRATEGY:
  ├─→ EmailDeliveryChannel::deliver($deliverable, $userId, $settings)
  ├─→ AbstractDeliveryChannel::deliver() [TEMPLATE METHOD]
  │   ├─→ validateSettings() → Check email address
  │   ├─→ isChannelAvailable() → Future: return false (not implemented)
  │   └─→ Return DeliveryResult::failWithError()
      └─→ "Email delivery not yet implemented"

Layers involved: 4 (Domain Services - Delivery)

Future Implementation: When email transport is added, the abstract Channel will delegate to EmailFormatter → EmailTransport sub-layers.

FLOW 33: Deliver via Notion Channel (Future)

DistributionService::distribute($deliverable, [$userId])

Sub-layer 1 — DISTRIBUTION ORCHESTRATION:
  ├─→ User preference: NOTION channel
  └─→ DeliveryChannelFactory::getChannel('NOTION')
      └─→ Return NotionDeliveryChannel

Sub-layer 2 — CHANNEL STRATEGY:
  ├─→ NotionDeliveryChannel::deliver($deliverable, $userId, $settings)
  ├─→ AbstractDeliveryChannel::deliver() [TEMPLATE METHOD]
  │   ├─→ validateSettings() → Check Notion page ID
  │   ├─→ isChannelAvailable() → Future: return true (pending implementation)
  │   └─→ Continue if available
  └─→ executeDelivery() [NotionDeliveryChannel]
      └─→ Return DeliveryResult::success() or ::failWithError()

Layers involved: 4 (Domain Services - Delivery)

Future Implementation: When Notion transport is added, the abstract Channel will delegate to NotionFormatter → NotionTransport sub-layers.


DELIVERY ARCHITECTURE SUMMARY

Two Distribution Systems

The codebase has two distribution paths:

SystemServiceUsed ForChannel Selection
Digest-specificDigestDistributionServiceDigest entities only (FLOW 24)Uses SlackDigestDeliveryService directly
GenericDistributionServiceAny DeliverableInterface (FLOWs 29-33)Uses DeliveryChannelFactory + channel strategy

4-Sub-layer Delivery Model (Generic System)

Sub-layerPurposeExamplesTestability
1. DistributionOrchestrate multi-user delivery, channel selectionDistributionService, DeliveryChannelFactoryHigh (mock all channels)
2. Channel (Strategy)Implement channel workflow via Template MethodSlackDeliveryChannel, AbstractDeliveryChannelHigh (mock transport + formatter)
3. FormatterTransform DeliverableInterface → channel formatSlackBlockFormatter, SlackBlockFormatterInterfaceHigh (no external deps)
4. TransportPure I/O to external APIsSlackMessageTransport, MessageTransportInterfaceHigh (mock HttpClient)

Extension Points

Adding New Channel (e.g., SMS):

  1. Create SmsDeliveryChannel extends AbstractDeliveryChannel
  2. Create SmsBlockFormatter implements SmsFormatterInterface
  3. Create SmsMessageTransport implements MessageTransportInterface
  4. Register in DeliveryChannelFactory
  5. Zero changes to existing channels (Open/Closed Principle)

Adding New Transport (e.g., AWS SNS):

  1. Create AwsSnsTransport implements MessageTransportInterface
  2. Create AwsSnsFormatter
  3. Inject into appropriate Channel
  4. No changes to existing transport implementations (Liskov Substitution)