P4 part A (platform reuse) of the qbit-crm 0.2.0 design:
Send Email (contact action):
Merge fields: contact, company, owner and the unsubscribe link.
Sends through CrmEmailTransport (qbit-email first, then core messaging) with an idempotency key.
Records a crm_email_message row and an EMAIL activity.
Skips, with a warning line, contacts marked do-not-email, contacts with no address, and sends when no transport is configured.
Sequences send for real:
Only active sequences run, so Pause is honored.
EMAIL steps go through the MARKETING category, and each enrollment is gated on checkEmailEligibility.
The crm.sequence.failing and crm.email.unavailable alerts raise and resolve.
New background processes:
crm_sync_email_delivery pulls qbit-email status. Bounces and complaints set do-not-email and record a withdrawn consent.
crm_expire_consents expires unconfirmed double opt-ins after 14 days.
Schedules: sequences every 5 min, scoring hourly, delivery sync every 15 min, and consent expiry daily. Hosts control them with withSchedulerName and withSchedulesEnabled.
Lead scoring:
The DAYS_AGO_GT and DAYS_AGO_LT decay operators are implemented.
Recalculation is incremental since the last run, with a full pass daily.
Company reads are batched, removing the N+1 queries.
Auto-assign runs on contact and deal insert and sends a crm.lead.assigned notification.
Deal notifications:crm.deal.won and crm.deal.rotting, deduplicated per stage.
Collaboration: core RecordFeaturesMetaData declarations (tags, comments, attachments, follow, timeline, recents, favorites, tasks, assignment) and ownership on ownerUserId.
Not in RC.6, so deferred to RC.7: the DurableProcessMetaData and DurableJobsMetaData declarations. The scheduled runs stay plain processes until then. qbit-email RC.3 exposes no open, click or bounce timestamps, so those dates stay empty.
## Summary
P4 part A (platform reuse) of the qbit-crm 0.2.0 design:
- **Send Email (contact action):**
- Merge fields: contact, company, owner and the unsubscribe link.
- Sends through `CrmEmailTransport` (qbit-email first, then core messaging) with an idempotency key.
- Records a crm_email_message row and an EMAIL activity.
- Skips, with a warning line, contacts marked do-not-email, contacts with no address, and sends when no transport is configured.
- **Sequences send for real:**
- Only active sequences run, so Pause is honored.
- EMAIL steps go through the MARKETING category, and each enrollment is gated on `checkEmailEligibility`.
- The `crm.sequence.failing` and `crm.email.unavailable` alerts raise and resolve.
- **New background processes:**
- `crm_sync_email_delivery` pulls qbit-email status. Bounces and complaints set do-not-email and record a withdrawn consent.
- `crm_expire_consents` expires unconfirmed double opt-ins after 14 days.
- **Schedules:** sequences every 5 min, scoring hourly, delivery sync every 15 min, and consent expiry daily. Hosts control them with `withSchedulerName` and `withSchedulesEnabled`.
- **Lead scoring:**
- The `DAYS_AGO_GT` and `DAYS_AGO_LT` decay operators are implemented.
- Recalculation is incremental since the last run, with a full pass daily.
- Company reads are batched, removing the N+1 queries.
- **Auto-assign** runs on contact and deal insert and sends a `crm.lead.assigned` notification.
- **Deal notifications:** `crm.deal.won` and `crm.deal.rotting`, deduplicated per stage.
- **Collaboration:** core `RecordFeaturesMetaData` declarations (tags, comments, attachments, follow, timeline, recents, favorites, tasks, assignment) and ownership on `ownerUserId`.
Not in RC.6, so deferred to RC.7: the `DurableProcessMetaData` and `DurableJobsMetaData` declarations. The scheduled runs stay plain processes until then. qbit-email RC.3 exposes no open, click or bounce timestamps, so those dates stay empty.
Codex implemented this; Claude reviewed it.
## Test plan
- [x] `./mvnw verify`: 430 tests, 0 failures; JaCoCo 91.5%; checkstyle and guardrails pass
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
P4 part A (platform reuse) of the qbit-crm 0.2.0 design:
CrmEmailTransport(qbit-email first, then core messaging) with an idempotency key.checkEmailEligibility.crm.sequence.failingandcrm.email.unavailablealerts raise and resolve.crm_sync_email_deliverypulls qbit-email status. Bounces and complaints set do-not-email and record a withdrawn consent.crm_expire_consentsexpires unconfirmed double opt-ins after 14 days.withSchedulerNameandwithSchedulesEnabled.DAYS_AGO_GTandDAYS_AGO_LTdecay operators are implemented.crm.lead.assignednotification.crm.deal.wonandcrm.deal.rotting, deduplicated per stage.RecordFeaturesMetaDatadeclarations (tags, comments, attachments, follow, timeline, recents, favorites, tasks, assignment) and ownership onownerUserId.Not in RC.6, so deferred to RC.7: the
DurableProcessMetaDataandDurableJobsMetaDatadeclarations. The scheduled runs stay plain processes until then. qbit-email RC.3 exposes no open, click or bounce timestamps, so those dates stay empty.Codex implemented this; Claude reviewed it.
Test plan
./mvnw verify: 430 tests, 0 failures; JaCoCo 91.5%; checkstyle and guardrails pass🤖 Generated with Claude Code