warmbox/docs/03-domain-model.md

123 lines
3.7 KiB
Markdown

# Domain Model (Target)
Evolution from flat `Inbox` to campaign-centric model. Implement incrementally; not all entities ship in phase 1.
## Entity relationship (target)
```mermaid
erDiagram
Workspace ||--o{ Mailbox : owns
Workspace ||--o{ WarmupCampaign : has
Mailbox ||--o{ WarmupCampaign : "primary"
WarmupCampaign ||--o{ CampaignSatellite : assigns
Mailbox ||--o{ CampaignSatellite : "satellite"
WarmupCampaign ||--o{ DailySchedule : plans
WarmupCampaign ||--o{ DailyStat : tracks
WarmupCampaign ||--o{ WarmupLog : generates
Mailbox ||--o{ WarmupLog : sends
Mailbox ||--o{ WarmupLog : receives
```
## Mailbox (evolved Inbox)
| Field | Type | Notes |
|-------|------|-------|
| id | cuid | |
| workspaceId | cuid | Future multi-tenant; nullable for self-hosted |
| email | string | Unique per workspace |
| role | enum | `primary` \| `satellite` \| `unassigned` |
| smtpHost, smtpPort, imapHost, imapPort | | Connection |
| username, password | | Encrypted |
| provider | enum | `gmail` \| `yahoo` \| `outlook` \| `custom` |
| status | enum | `active` \| `paused` \| `error` \| `warming` \| `completed` |
| industry | string | AI context |
| dnsStatus | json | SPF/DKIM/DMARC check results (future) |
| lastError, errorAt | | |
**Rule:** A mailbox can be primary in one campaign and satellite in another only if explicitly allowed (default: one role per campaign assignment).
## WarmupCampaign
One campaign = one primary mailbox warming with one recipe.
| Field | Type | Notes |
|-------|------|-------|
| id | cuid | |
| workspaceId | cuid | |
| primaryMailboxId | cuid | FK Mailbox |
| name | string | e.g. "FillCare hello@" |
| recipe | enum | `grow` \| `flat` \| `randomized` \| `custom` |
| status | enum | `draft` \| `active` \| `paused` \| `completed` \| `failed` |
| durationDays | int | Default 30, max 45 |
| startedAt | datetime | |
| endsAt | datetime | Computed |
| minEmailsPerDay | int | Default 1 |
| maxEmailsPerDay | int | Default 40, max 50 |
| replyRatePercent | int | Default 30, max 45 |
| customSchedule | json | For `custom` recipe: `[{ day, sends }]` |
| timezone | string | Send window TZ |
| sendWindowStart | time | e.g. 08:00 |
| sendWindowEnd | time | e.g. 18:00 |
| currentDay | int | Day index since start |
| rampState | json | Recipe-specific state |
## CampaignSatellite (join)
| Field | Type |
|-------|------|
| campaignId | cuid |
| satelliteMailboxId | cuid |
| weight | int | Optional load balancing (default 1) |
| active | boolean |
## DailySchedule
Pre-computed plan for a campaign day (generated at campaign start or nightly).
| Field | Type |
|-------|------|
| campaignId | cuid |
| date | date |
| dayIndex | int |
| plannedSends | int |
| plannedReplies | int |
| actualSends | int |
| actualReplies | int |
| spamDetected | int |
| spamRescued | int |
## WarmupLog (evolved)
Add `campaignId`, `placement` (`inbox` \| `spam` \| `unknown`), `scheduledAt`, `sentAt`, `repliedAt`.
## DailyStat (materialized)
Rollup for fast dashboard queries.
| Field | Type |
|-------|------|
| campaignId | cuid |
| date | date |
| sent, received, replied, spamDetected, spamRescued | int |
| replyRateActual | float |
| inboxPlacementRate | float |
## Setting (global / workspace)
Move cron and AI config to workspace scope over time.
## Migration from current schema
| Current | Target |
|---------|--------|
| `Inbox` | `Mailbox` + `role` |
| Per-inbox `dailyLimit`, `currentRamp` | `WarmupCampaign` fields |
| Flat dispatcher | Campaign-aware dispatcher |
| `WarmupLog` | Add `campaignId` |
### Phase 1 migration (minimal)
1. Add `role` to `Inbox` (`primary` \| `satellite`)
2. Add `WarmupCampaign` table
3. Add `campaignId` to `WarmupLog`
4. Keep backward compat: inboxes without campaign use legacy flat mode (deprecated)