Tasks 1-2 complete (AWS SDK + schema migration, types + credential factory).
Task 3 is a blocking human-verify checkpoint requiring BWS/AWS credential
confirmation and live docker/DNS-egress verification — not fabricated, not
run unilaterally. Execution stops here pending developer response.
- lib/services/route53-factory.ts: isRoute53Configured() / getRoute53Client()
/ resetRoute53Client(), following the veeam-factory.ts singleton shape
- No explicit credentials option passed to Route53Client — relies on the AWS
SDK's default credential chain reading AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY
from process.env, exactly how BWS injects them at the container entrypoint
- CLAUDE.md: document the AWS_* env-prefix exception in the integration table
- All 7 route53-factory.test.ts assertions pass; npx tsc --noEmit clean
Tier 3 is the only tier automatic (webhook-triggered) grouping ever
reaches, since grouping runs before message parsing. It was scoped to
reports.requester_contact_id, so the same campaign reported by
different employees at the same company never consolidated into one
campaign — each report's evidence/blast-radius view silently
under-reported the campaign's true recipients.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Add mimecast_tenants routing branch to stageQueries and company_id to
report fixtures
- New test: enabled mimecast_tenants row -> getBlastRadius called with
{ client, cacheScope: companyId } and the tenant SQL is issued
- New parameterized test: null companyId and companyId-with-no-enabled-row
-> getBlastRadius called with no second argument (global fallback)
- Thread company_id through reports query and CampaignReportSummary
- Mirror the route's Bug 2 (D-05) tenant-resolution block: query
mimecast_tenants for an enabled row, build a tenant-scoped client via
getMimecastClientForTenant, and pass { client, cacheScope } to
getBlastRadius() when one exists
- Preserve global env fallback unchanged when no companyId or no
enabled tenant row is present
- Updated the getHeldMessages window test to assert no `recipient` key and
added an explicit no-`to` assertion for searchDeliveredMessages
- Added a new test driving searchDeliveredMessages with 3 distinct `to`
addresses, asserting all appear in perRecipient and count toward
matched/delivered
- searchDeliveredMessages now called with from+subject+start+end only
(no `to`) so it returns every delivered/rejected message matching the
campaign across all recipients, not just the reporter's mailbox
- getHeldMessages now called with start+end only (no `recipient`) —
domainsMatch() post-filter is the sole scoping mechanism for held rows
- Updated inline comments to document the tenant-wide fan-out and the
per-recipient merge behavior it now produces
- Add migration 101: guarded UPDATE moving mimecast-sync from 0 2 * * * to
45 4 * * * (minute 45 is unused by any other schedule row)
- Guarded on the stale cron value so it's a no-op if already moved and won't
clobber an admin's manual schedule change
- Applied the same UPDATE directly to the live pulse-postgres container
(migrations only auto-apply on first volume boot, per CLAUDE.md)
- Eliminates the 3-way 2 AM collision with qbo-sync-2am and veeam-full
- Extend ScheduleConfig.sync_type union with 'mimecast-sync'
- Add mimecast-sync branch calling runMimecastIncrementalSync() behind
isMimecastConfigured(), mirroring the engagement/zoom configured-gate pattern
- Add qbo branch calling getQboSyncService().incrementalSync('scheduled')
behind an integration_settings disabled check, mirroring the pax8-daily
disable-check pattern
- Both branches previously fell through to the generic Autotask fullSync()
catch-all, which also contended for the SyncService singleton mutex
Records the plan, execution summary, and STATE.md quick-task log entry
for the QBO_INTEGRATION_HANDOFF.md doc committed in ea8a36b.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6RuWdiUiXrPK6FLBHjtpY
Documents Pulse's QuickBooks Online integration (OAuth2 authorization-code
flow, token storage/refresh, sandbox vs production API base URLs, scopes,
minor version, and gotchas learned from the AR reconciliation/soft-delete
work) so a new app's team can build their own QBO connection without
access to the Pulse codebase.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6RuWdiUiXrPK6FLBHjtpY
Bundles several in-progress efforts that were sitting uncommitted:
- User queue-preferences (migration 087, API route, popover component)
- QBO invoice soft-delete (migration 088) and AR diagnostics route
- Dashboard/mobile engagement route and page adjustments
- Docker Compose log-rotation config
- One-off ticket/RMM investigation scripts (scripts/)
- Planning docs: phase verification/pattern notes, mobile shell design spec
- .gitignore: exclude local scratch financial/inventory data and Claude Code
worktree/local-settings runtime state (never meant for version control)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6RuWdiUiXrPK6FLBHjtpY
- blast-radius: getHeldMessages called with same start/end window as
searchDeliveredMessages
- blast-radius: unrelated-sender held row excluded from held/matched and
perRecipient; matching-sender held row still counts and overrides
- client: getHeldMessages threads start/end into POST body data[0] when
provided, omits them when not
- getHeldMessages() accepts optional start/end, threaded into data[0] as
siblings of admin/searchBy (backward compatible when omitted; 403
fallback body inherits them automatically via the existing spread)
- getBlastRadius() passes the same startStr/endStr window already
computed for searchDeliveredMessages into getHeldMessages()
- Added domainsMatch() sender-relevance guard: held rows whose sender
domain doesn't match input.sender (exact-or-proper-subdomain) are
filtered out before counting/merging, so unrelated same-window holds
never inflate held/matched or override a delivered recipient