Contacts Overview
Contacts are the core of Miranda’s people management. A single Contact record represents one person known to a foundation. Contacts can hold multiple relationship types simultaneously — a person can be a donor, a scholarship applicant, and a newsletter subscriber all at once.
Contact model
Section titled “Contact model”Every contact is scoped to a foundation via foundation_id.
| Field | Type | Description |
|---|---|---|
id | UUID | Primary key |
foundation_id | UUID | Owning foundation |
first_name | string | Required |
last_name | string? | Optional |
email | string? | Used for deduplication and email campaigns |
phone | string? | Used for SMS campaigns |
address | JSON? | Structured address (street, city, state, zip) |
photo_path | string? | Profile photo in Supabase storage |
communication_preferences | JSON? | Opt-in flags (email_opt_in, sms_opt_in) |
first_contact_at | datetime? | When this person was first known to the foundation |
Computed attributes
Section titled “Computed attributes”full_name— Returns"{first_name} {last_name}"trimmedrelationship_types— Array of active relationship type values, appended to every JSON response
Relationship types
Section titled “Relationship types”Contacts are connected to foundations through ContactRelationship records. A contact can hold multiple relationship types at once.
| Type | Value | Description |
|---|---|---|
| Donor | donor | Has made or is linked to a donation. Metadata tracks lifetime_giving, gift_count, last_gift_at |
| Applicant | applicant | Has submitted a scholarship application |
| Subscriber | subscriber | Has opted into newsletters or communications |
| General | general | Imported or manually added without a specific role |
ContactRelationship model
Section titled “ContactRelationship model”| Field | Type | Description |
|---|---|---|
contact_id | UUID | The contact |
foundation_id | UUID | The foundation |
type | ContactRelationshipType | One of: donor, applicant, subscriber, general |
metadata | JSON? | Type-specific data (e.g., giving stats for donors) |
assigned_at | datetime | When this relationship was established |
Adding relationships
Section titled “Adding relationships”Relationships are assigned idempotently via the addRelationship() method on the Contact model:
// Assigns donor relationship with metadata (creates if not exists)$contact->addRelationship( ContactRelationshipType::Donor, ['lifetime_giving' => 0, 'gift_count' => 0, 'last_gift_at' => null]);The hasRelationship() method checks whether a contact has a specific type:
if ($contact->hasRelationship(ContactRelationshipType::Donor)) { // Contact is a donor}Communication preferences
Section titled “Communication preferences”The communication_preferences JSON column stores opt-in flags:
{ "email_opt_in": true, "sms_opt_in": false}These preferences are enforced by the EmailService and SmsService — messages are silently skipped for contacts who are not opted in.
Contact relationships
Section titled “Contact relationships”Contacts connect to other models through Eloquent relationships:
| Relationship | Type | Target |
|---|---|---|
foundation() | BelongsTo | Foundation |
relationships() | HasMany | ContactRelationship |
tags() | BelongsToMany | Tag (via contact_tag pivot) |
activities() | HasMany | Activity |
notes() | HasMany | Note |
donations() | HasMany | Donation |
Contact creation
Section titled “Contact creation”Contacts are created through several pathways:
- Manual creation —
POST /api/contactsfrom the admin dashboard - CSV import — Via the import workflow
- Donation —
DonationService::findOrCreateContact()matches by email or creates a new contact when a donation is processed - Scholarship application —
ScholarshipService::createApplication()finds or creates a contact by email - Stripe import —
ImportService::importFromStripe()pulls customers and charge history from a connected Stripe account - API ingest —
POST /api/import/ingestaccepts a JSON array of contacts
Deduplication
Section titled “Deduplication”Email is the primary deduplication key. When creating contacts through any pathway, Miranda checks for an existing contact with the same email within the foundation before creating a new record.
API endpoints
Section titled “API endpoints”| Method | Path | Description |
|---|---|---|
| GET | /api/contacts | List contacts (paginated) |
| POST | /api/contacts | Create a contact |
| GET | /api/contacts/{id} | Show a contact |
| PUT | /api/contacts/{id} | Update a contact |
| DELETE | /api/contacts/{id} | Delete a contact |
See Foundation Endpoints for full request/response details.