Domain Model
Both entities are defined in
@nx/coreunder theoutreachPostgreSQL schema and re-exported into this package. There are no foreign keys between them — each is an independent capture table.
1. Full ERD
2. Entities
Inquiry
| Property | Value |
|---|---|
| Table | outreach.Inquiry |
| Source | packages/core/src/models/schemas/outreach/inquiry/schema.ts |
| Soft-delete | yes (deletedAt) |
| Owner ID column | — (global, not merchant-scoped) |
| Hidden properties | deletedAt |
Fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | text | ✓ | Snowflake | Primary key |
type | text | ✓ | 000_CONSULT | Source channel — see enum |
status | text | ✓ | NEW | Pipeline state — see enum |
firstName | text | ✓ | — | Contact first name |
lastName | text | — | Contact last name | |
email | text | ✓ | — | Contact email (indexed) |
phone | text | ✓ | — | Contact phone |
businessName | text | — | Lead's business | |
businessType | text | — | Business category | |
locationCount | text | — | Lead qualification | |
estimatedRevenue | text | — | Lead qualification | |
subject | text | — | Message subject | |
message | text | — | Free-text body | |
assignedTo | text | — | Owning admin/user id | |
repliedAt | timestamptz | — | First-reply timestamp | |
repliedBy | text | — | Replying user id | |
convertedAt | timestamptz | — | Conversion timestamp | |
lostReason | text | — | Lost-deal reason | |
note | text | — | Internal note | |
createdAt | timestamptz | ✓ | now() | — |
Type enum (InquiryTypes):
| Value | Description |
|---|---|
000_CONSULT | General consultation / unknown source (default) |
100_CONTACT | Contact form (/contact) |
200_SALES | Sales inquiry (/contact-sales) |
300_DEMO | Demo request (/demo) |
400_PARTNER | Partner application (/partners) |
Status enum (InquiryStatuses, IGNIS Statuses):
| Value | Description |
|---|---|
NEW | Just submitted |
PROCESSING | Picked up by admin |
COMPLETED | Converted |
CLOSED | Resolved without conversion |
CANCELLED | Spam / invalid / abandoned |
Indexes & constraints:
| Name | Columns | Type |
|---|---|---|
PK_Inquiry | id | Primary key |
IDX_Inquiry_type | type | Btree |
IDX_Inquiry_status | status | Btree |
IDX_Inquiry_email | email | Btree |
Relations: none.
Subscriber
| Property | Value |
|---|---|
| Table | outreach.Subscriber |
| Source | packages/core/src/models/schemas/outreach/subscriber/schema.ts |
| Soft-delete | yes (deletedAt) |
| Owner ID column | — (global) |
| Hidden properties | deletedAt, unsubscribeToken (never returned by the model API) |
Fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | text | ✓ | Snowflake | Primary key |
email | text | ✓ | — | Unique (partial, where not deleted) |
locale | text | ✓ | vi | Newsletter language (vi / en) |
topics | jsonb (string[]) | ✓ | ['all'] | Topic preferences |
status | text | ✓ | ACTIVATED | See enum |
unsubscribeToken | text | ✓ | Snowflake ($defaultFn) | Token for unsubscribe link; hidden from API |
subscribedAt | timestamptz | ✓ | now() | First/last subscribe time |
unsubscribedAt | timestamptz | — | Set on unsubscribe, cleared on re-subscribe | |
createdAt | timestamptz | ✓ | now() | — |
Status enum (SubscriberStatuses, IGNIS Statuses):
| Value | Description |
|---|---|
ACTIVATED | Active subscription |
DEACTIVATED | Unsubscribed |
ARCHIVED | Admin-archived (valid status, not produced by the service flow) |
Indexes & constraints:
| Name | Columns | Type |
|---|---|---|
PK_Subscriber | id | Primary key |
UPI_Subscriber_email | email (where deletedAt IS NULL) | Unique partial index |
IDX_Subscriber_status | status | Btree |
IDX_Subscriber_unsubscribeToken | unsubscribeToken | Btree |
Relations: none.
3. Cross-entity Invariants
| Invariant | Enforcement |
|---|---|
One live Subscriber per email | Unique partial index UPI_Subscriber_email (where deletedAt IS NULL) + SubscriberService.subscribe() find-then-reactivate |
unsubscribeToken unique enough to be unguessable | Snowflake $defaultFn (64-bit, worker 10) — see ADR-0001 |
Re-subscribe clears unsubscribedAt | SubscriberService.subscribe() sets unsubscribedAt: null on reactivate |
4. Soft-delete Behavior
| Behavior | Detail |
|---|---|
| Read default | deletedAt IS NULL (model defaultFilter) |
| Hard-delete | Never by default; admin CRUD deleteById performs soft-delete |
| Restore | Not exposed via API |
| Email uniqueness | Enforced only over non-deleted rows (partial unique index) |