# Story 2.5: Deployment Target Assignment ## Status Completed - All acceptance criteria met, 33/33 tests passing (includes tier1-only constraint) ## Story **As a developer**, I want to assign deployment targets (site_deployment_id) to generated content during the content generation process based on job configuration, so that specific articles are assigned to specific sites while others remain unassigned for random template selection. ## Context This story only assigns `site_deployment_id` to GeneratedContent records. Template selection is handled entirely by Story 2.4's existing logic: - If `site_deployment_id` is set → Story 2.4 uses mapped/random template for that site - If `site_deployment_id` is null → Story 2.4 uses random template (no config persistence) ## Acceptance Criteria - The job configuration file supports an optional `deployment_targets` array containing site custom_hostnames - **Only tier1 articles are assigned to deployment targets** - tier2, tier3, etc. always get `site_deployment_id = null` - During tier1 content generation, each article is assigned a `site_deployment_id` based on its index: - If `deployment_targets` has N sites, articles 0 through N-1 get assigned round-robin - Articles N and beyond get `site_deployment_id = null` - If `deployment_targets` is not specified, all articles get `site_deployment_id = null` - The `site_deployment_id` is stored in the `GeneratedContent` record at creation time - Invalid hostnames in `deployment_targets` cause graceful errors with clear messages - Valid hostnames that don't exist in the database cause graceful errors ## Tasks / Subtasks ### 1. Update Job Configuration Schema **Effort:** 1 story point - [x] Add `deployment_targets` field (optional array of strings) to job config schema - [x] Update job config validation to check deployment_targets is an array of strings - [x] Update example job file in `jobs/` directory with the new field ### 2. Implement Target Resolution **Effort:** 2 story points - [x] Add `resolve_hostname_to_id(hostname: str) -> Optional[int]` helper function - Query SiteDeployment table by custom_hostname - Return site_deployment_id if found, None if not found - [x] Add `validate_and_resolve_targets(hostnames: List[str]) -> dict` function - Pre-validate all hostnames at job start (fail fast) - Return dict mapping hostname → site_deployment_id - Raise clear error if any hostname is invalid/not found ### 3. Implement Round-Robin Assignment **Effort:** 2 story points - [x] Add `assign_site_for_article(article_index: int, resolved_targets: dict) -> Optional[int]` function - [x] If resolved_targets is empty: return None - [x] If article_index < len(resolved_targets): return targets[article_index] - [x] If article_index >= len(resolved_targets): return None ### 4. Integration with Content Generation Service **Effort:** 2 story points - [x] Update `src/generation/batch_processor.py` to parse `deployment_targets` from job config - [x] Call validation function at job start (before generating any content) - [x] For each article in batch: - Call assignment function to get site_deployment_id - Pass site_deployment_id to repository when creating GeneratedContent - [x] Log assignment decisions at INFO level ### 5. Unit Tests **Effort:** 2 story points - [x] Test hostname resolution with valid hostnames - [x] Test hostname resolution with invalid hostnames - [x] Test round-robin assignment with 3 targets, 10 articles - [x] Test assignment with no deployment_targets (all null) - [x] Test validation errors for non-existent hostnames - [x] Achieve >80% code coverage (100% achieved with 13 unit tests) ### 6. Integration Tests **Effort:** 2 story points - [x] Test full generation flow with deployment_targets specified - [x] Test 10 articles with 3 targets: verify first 3 assigned, remaining 7 are null - [x] Test with deployment_targets = null (all articles get null site_deployment_id) - [x] Test error handling for invalid deployment targets - [x] Verify site_deployment_id persisted correctly in database (9 integration tests) ## Dev Notes ### Example Job Config ```json { "job_name": "Multi-Site Launch", "project_id": 2, "deployment_targets": [ "www.domain1.com", "www.domain2.com", "www.domain3.com" ], "tiers": [ { "tier": 1, "article_count": 10 } ] } ``` ### Assignment Example Job with tier1 (10 articles) and tier2 (100 articles), 3 deployment targets: **Tier1 articles:** - Article 0 → www.domain1.com (site_deployment_id = 5) - Article 1 → www.domain2.com (site_deployment_id = 8) - Article 2 → www.domain3.com (site_deployment_id = 12) - Articles 3-9 → null **Tier2 articles:** - All 100 articles → null (tier2+ never get deployment targets) ### Technical Decisions 1. **Tier restriction:** Only tier1 articles can be assigned to deployment targets; tier2/tier3 always get null 2. **Target identifier:** Only support custom_hostname (not numeric IDs) 3. **Validation timing:** Validate all targets at job start (fail fast) 4. **Overflow handling:** Simple - just assign null after targets exhausted 5. **Null handling:** No deployment_targets = all articles get null ### Dependencies - **Story 1.6:** SiteDeployment table must exist - **Story 2.3:** Content generation service must be functional - **Story 2.4:** Template selection logic already handles null site_deployment_id ### Database Changes Required None - `site_deployment_id` field already exists in GeneratedContent model (added in Story 2.4) ### Total Effort 11 story points