P3 (intake and contracts) of the qbit-crm 0.2.0 design:
Public package com.kingsrook.qbits.crm.api, the only supported surface:
CrmIntakeApi:
submitForm: stores the submission, links or creates the contact, sets first- and last-touch UTM attribution, records consent, logs an activity, auto-assigns the owner and sends a crm.lead.received notification
subscribe, confirm, unsubscribe and unsubscribeByContact, with double opt-in by default
CrmMarketingApi: the contract agreed with qbit-marketing: resolveRecipients, checkEmailEligibility, listFormSubmissions, recordActivity(kind), recordUnsubscribe, buildUnsubscribeUrl and recipientScopeOptions
CrmApi:isInstalled and readiness
Consent rules:
A contact form never grants marketing consent unless the visitor explicitly opts in, and the exact wording is stored as evidence.
An explicit re-subscribe clears an earlier global unsubscribe. Bounce and complaint suppression (doNotEmail) is never cleared.
Write permissions: intake runs as trusted host work after the host validates the public request, so permission enforcement is skipped and the reason is recorded. The marketing API runs with the caller's permissions.
CrmEmailTransport: qbit-email's EmailApi.sendOnce first (an optional dependency allowed by G1), then core SendMessageAction.
CrmTokens: HMAC-signed confirm and unsubscribe tokens that expire after 90 days. Only token hashes are stored.
Hook routes:/qqq/hooks/crm/confirm/{token} and /qqq/hooks/crm/unsubscribe/{token}. A GET shows a page; only a POST button acts, so mail scanners cannot unsubscribe anyone.
Schema:
attribution fields on contacts
list keys and double opt-in fields
consent evidence
activity-type codes
no raw IP addresses or user agents are stored
Alerts:crm.lead.received, crm.sequence.failing and crm.email.unavailable.
Codex implemented this; Claude reviewed it. Review fixes: trusted intake writes; eligibility no longer blocked by another list's status; enum ids instead of magic numbers; re-subscribe; last-touch only when the request carries attribution; auto-assign.
## Summary
P3 (intake and contracts) of the qbit-crm 0.2.0 design:
- **Public package `com.kingsrook.qbits.crm.api`**, the only supported surface:
- **`CrmIntakeApi`:**
- `submitForm`: stores the submission, links or creates the contact, sets first- and last-touch UTM attribution, records consent, logs an activity, auto-assigns the owner and sends a `crm.lead.received` notification
- `subscribe`, `confirm`, `unsubscribe` and `unsubscribeByContact`, with double opt-in by default
- **`CrmMarketingApi`:** the contract agreed with qbit-marketing: `resolveRecipients`, `checkEmailEligibility`, `listFormSubmissions`, `recordActivity(kind)`, `recordUnsubscribe`, `buildUnsubscribeUrl` and `recipientScopeOptions`
- **`CrmApi`:** `isInstalled` and `readiness`
- **Consent rules:**
- A contact form never grants marketing consent unless the visitor explicitly opts in, and the exact wording is stored as evidence.
- An explicit re-subscribe clears an earlier global unsubscribe. Bounce and complaint suppression (`doNotEmail`) is never cleared.
- **Write permissions:** intake runs as trusted host work after the host validates the public request, so permission enforcement is skipped and the reason is recorded. The marketing API runs with the caller's permissions.
- **`CrmEmailTransport`:** qbit-email's `EmailApi.sendOnce` first (an optional dependency allowed by G1), then core `SendMessageAction`.
- **`CrmTokens`:** HMAC-signed confirm and unsubscribe tokens that expire after 90 days. Only token hashes are stored.
- **Hook routes:** `/qqq/hooks/crm/confirm/{token}` and `/qqq/hooks/crm/unsubscribe/{token}`. A GET shows a page; only a POST button acts, so mail scanners cannot unsubscribe anyone.
- **Schema:**
- attribution fields on contacts
- list keys and double opt-in fields
- consent evidence
- activity-type codes
- no raw IP addresses or user agents are stored
- **Alerts:** `crm.lead.received`, `crm.sequence.failing` and `crm.email.unavailable`.
Codex implemented this; Claude reviewed it. Review fixes: trusted intake writes; eligibility no longer blocked by another list's status; enum ids instead of magic numbers; re-subscribe; last-touch only when the request carries attribution; auto-assign.
## Test plan
- [x] `./mvnw verify`: 412 tests, 0 failures; checkstyle and guardrails pass; JaCoCo 91% instructions
- [x] Eligibility matrix, attribution, double opt-in end to end, token tampering and expiry, and Javalin route tests
🤖 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
P3 (intake and contracts) of the qbit-crm 0.2.0 design:
com.kingsrook.qbits.crm.api, the only supported surface:CrmIntakeApi:submitForm: stores the submission, links or creates the contact, sets first- and last-touch UTM attribution, records consent, logs an activity, auto-assigns the owner and sends acrm.lead.receivednotificationsubscribe,confirm,unsubscribeandunsubscribeByContact, with double opt-in by defaultCrmMarketingApi: the contract agreed with qbit-marketing:resolveRecipients,checkEmailEligibility,listFormSubmissions,recordActivity(kind),recordUnsubscribe,buildUnsubscribeUrlandrecipientScopeOptionsCrmApi:isInstalledandreadinessdoNotEmail) is never cleared.CrmEmailTransport: qbit-email'sEmailApi.sendOncefirst (an optional dependency allowed by G1), then coreSendMessageAction.CrmTokens: HMAC-signed confirm and unsubscribe tokens that expire after 90 days. Only token hashes are stored./qqq/hooks/crm/confirm/{token}and/qqq/hooks/crm/unsubscribe/{token}. A GET shows a page; only a POST button acts, so mail scanners cannot unsubscribe anyone.crm.lead.received,crm.sequence.failingandcrm.email.unavailable.Codex implemented this; Claude reviewed it. Review fixes: trusted intake writes; eligibility no longer blocked by another list's status; enum ids instead of magic numbers; re-subscribe; last-touch only when the request carries attribution; auto-assign.
Test plan
./mvnw verify: 412 tests, 0 failures; checkstyle and guardrails pass; JaCoCo 91% instructions🤖 Generated with Claude Code