LAYERED ARCHITECTURE — FLOW REFERENCE
Table of Contents
- Core Flows (1-9) — Knowledge CRUD, AI Summary, Sync, Digest, Category
- Slack Input Flows (10-13) — Share, Home, URL Detection, Commands
- Knowledge Management Flows (14-17) — CRUD, Search, Filter
- AI Processing Flows (18-20) — Generate, Regenerate, Batch
- Digest Generation & Distribution (21-24) — Generate, Create Entity, Publish, Distribute
- Subscription & Preference Flows (25-28) — Subscribe, Unsubscribe, Preferences
- Generic Delivery Flows (29-33) — Distribution, Channel Strategy, Formatter, Transport
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 DatabaseFLOW 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 APIFLOW 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 APIFLOW 4: Sync All Knowledge from Notion
NotionSyncOrchestrator::syncAllFromNotion()
↓
NotionSyncKnowledge::syncFromNotion()
├─→ NotionKnowledgeRepository::findAll() → Notion API
└─→ KnowledgePersistenceService::upsert() → LocalKnowledgeRepository → PostgreSQLFLOW 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/APIFLOW 6: Sync AI Summaries from Notion
SyncAiSummariesFromNotionCommand::execute()
↓
NotionAiSummaryService::syncFromNotion()
├─→ NotionAiSummaryService::fetchFromNotion() → Notion API
└─→ SummaryPersistenceService::upsert() → AiSummaryRepository → PostgreSQLFLOW 7: Generate Digest with AI Summaries
DigestOrchestrator::generateDigest()
↓
DigestSummaryOrchestrator::ensureAllSummariesExist()
├─→ PromptBuilderService::buildPrompt()
└─→ SymfonyAiProvider::generateCompletion() → AI Provider Stack
↓
SummaryPersistenceService::create() → AiSummaryRepository → PostgreSQLFLOW 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 → PostgreSQLFLOW 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 → PostgreSQLINPUT 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" buttonLayers 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 itemLayers 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 APILayers 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 APIUpdate:
PUT /api/knowledge/{id}
↓
KnowledgeController::update()
↓
KnowledgeOrchestrator::updateKnowledge()
├─→ KnowledgePersistenceService::update() → LocalKnowledgeRepository → PostgreSQL
└─→ NotionSyncKnowledge::syncToNotion() → NotionKnowledgeRepository → Notion APIDelete:
DELETE /api/knowledge/{id}
↓
KnowledgeController::delete()
↓
KnowledgeOrchestrator::deleteKnowledge()
├─→ KnowledgePersistenceService::delete() → LocalKnowledgeRepository → PostgreSQL
└─→ NotionSyncKnowledge::deleteFromNotion() → NotionKnowledgeRepository → Notion APILayers 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 APILayers 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 resultsLayers 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 resultsLayers 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 → PostgreSQLLayers 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 DigestGenerationResultDtoLayers involved: 1, 2, 4, 5, 6, 7, 8, 9
FLOW 22: Generate Digest with Template (API Variant)
Same as FLOW 21 but with explicit
templateIdfor prompt template selection. Both useDigestOrchestrator::generateDigest(categoryId, dateRange, templateId). The only difference is whethertemplateIdis 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 → PostgreSQLLayers 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
└─→ PostgreSQLLayers 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
DigestDistributionServicewhich routes directly throughSlackDigestDeliveryService. For generic multi-channel delivery of anyDeliverableInterface, see FLOW 29 usingDistributionService.
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:
TargetGroupSubscriptionQueryServiceInterfaceallows 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 responseGENERIC 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 usesSlackDigestDeliveryServicedirectly. The genericDistributionServiceis used for delivering knowledge items, AI summary notifications, and any otherDeliverableInterface.
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 DeliveryResultLayers 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 TransportResultDtoLayers 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:
| System | Service | Used For | Channel Selection |
|---|---|---|---|
| Digest-specific | DigestDistributionService | Digest entities only (FLOW 24) | Uses SlackDigestDeliveryService directly |
| Generic | DistributionService | Any DeliverableInterface (FLOWs 29-33) | Uses DeliveryChannelFactory + channel strategy |
4-Sub-layer Delivery Model (Generic System)
| Sub-layer | Purpose | Examples | Testability |
|---|---|---|---|
| 1. Distribution | Orchestrate multi-user delivery, channel selection | DistributionService, DeliveryChannelFactory | High (mock all channels) |
| 2. Channel (Strategy) | Implement channel workflow via Template Method | SlackDeliveryChannel, AbstractDeliveryChannel | High (mock transport + formatter) |
| 3. Formatter | Transform DeliverableInterface → channel format | SlackBlockFormatter, SlackBlockFormatterInterface | High (no external deps) |
| 4. Transport | Pure I/O to external APIs | SlackMessageTransport, MessageTransportInterface | High (mock HttpClient) |
Extension Points
Adding New Channel (e.g., SMS):
- Create
SmsDeliveryChannel extends AbstractDeliveryChannel - Create
SmsBlockFormatter implements SmsFormatterInterface - Create
SmsMessageTransport implements MessageTransportInterface - Register in
DeliveryChannelFactory - Zero changes to existing channels (Open/Closed Principle)
Adding New Transport (e.g., AWS SNS):
- Create
AwsSnsTransport implements MessageTransportInterface - Create
AwsSnsFormatter - Inject into appropriate Channel
- No changes to existing transport implementations (Liskov Substitution)