Skip to content

feat(campaign-packer): paginate audience and publish campaigns.send (EVO-1216)#39

Open
nickoliveira23 wants to merge 1 commit into
developfrom
feat/EVO-1216
Open

feat(campaign-packer): paginate audience and publish campaigns.send (EVO-1216)#39
nickoliveira23 wants to merge 1 commit into
developfrom
feat/EVO-1216

Conversation

@nickoliveira23

Copy link
Copy Markdown

Summary

Implements story 4.2 (Campaign Dispatch Pipeline): extends CampaignPackerService.pack() (from 4.1 / EVO-1215) to paginate the resolved audience and publish one campaigns.send message per page, enabling the horizontal fan-out of the campaign-sender (4.3).

  • New PaginationService: pure, O(n) split(contactIds, batchSize) into 1-based pages (page ≤ totalPages).
  • pack() now: loads the campaign (with templates), computes audience, loads the persisted (PENDING) contactIds, then:
    • empty audience → publishes one campaigns.tracked with {page:0, sentCount:0, failedCount:0, completed:true} + logs campaign has no contacts (so CampaignWorkflow closes instead of hanging);
    • non-empty → resolves templateId (A-variant) + channelType, splits, and publishes one campaigns.send per page.
  • channelType mapping: CRM stores the Rails STI name (Channel::Email); the broker contract uses the transport id (email). Bridged at the publish boundary.
  • CAMPAIGN_PACKER_BATCH_SIZE env (default 1000), clamped against 0/NaN.
  • CampaignNotConfiguredError extends TerminalError for a campaign missing channel type or message template (drops, no requeue loop).
  • correlationId propagated from the incoming campaigns.pack payload to every published message.

Security

  • Internal broker consumer (no auth surface, single-account).
  • Misconfigured campaign (no channel/template) → TerminalError → terminal drop, never a redelivery loop.

Test plan

  • evo-flow: npm run typecheck — clean
  • evo-flow: npx jest src/runners/campaign-packer/services/pagination.service.spec.ts src/runners/campaign-packer/services/campaign-packer.service.spec.ts — 15 examples, 0 failures
  • evo-flow: npx eslint <changed .ts> — production files clean (specs follow the existing campaign-packer spec baseline)

Covers ACs: split math at 2500→[1000,1000,500] / exact-multiple (AC1/AC3), 1500→2 pages (AC3), empty→tracked+warn (AC2), payload validated against the strict campaigns.send zod contract + channelType mapping (AC4).

Notes

  • At-least-once semantics: a publish failure mid-loop nacks → redelivery re-publishes all pages; combined with clearAudience on re-pack this opens a double-send window. By design the campaign-sender (4.3) dedups via CampaignContact.status; broker-level redelivery backstop is tracked in EVO-1677. Out of scope here.
  • Audience materialization: loadContactIds reads the full PENDING audience via find({ select: { contactId } }); at ~1M contacts a getRawMany projection would avoid entity hydration — optional perf follow-up, in-memory split is the approach the card prescribes.
  • Pagination metrics are intentionally out of scope (story 5.2); a campaign.paginated structured log was added for observability.

Changed Files

  • src/runners/campaign-packer/services/pagination.service.ts (new)
  • src/runners/campaign-packer/services/pagination.service.spec.ts (new)
  • src/runners/campaign-packer/services/campaign-packer.service.ts
  • src/runners/campaign-packer/services/campaign-packer.service.spec.ts
  • src/runners/campaign-packer/errors/campaign-not-configured.error.ts (new)
  • src/runners/campaign-packer/campaign-packer.module.ts

Linked Issue

  • EVO-1216

…EVO-1216)

Implements story 4.2: extends CampaignPackerService.pack() to paginate the
resolved audience into batches and publish one `campaigns.send` message per
page. Empty audiences publish a single `campaigns.tracked` with completed:true
so the CampaignWorkflow closes instead of hanging.

- PaginationService: pure, O(n) split into 1-based pages (page ≤ totalPages)
- CampaignPackerService: load contactIds, branch empty vs paginate+publish;
  map Campaign channelType (Channel::*) to the send-contract transport id;
  resolve the A-variant message template; CAMPAIGN_PACKER_BATCH_SIZE (default 1000)
- CampaignNotConfiguredError (TerminalError) for a campaign missing channel/template
- specs: pagination unit + packer integration with a broker mock (15 examples)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @nickoliveira23, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant