Skip to content

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.

Every contact is scoped to a foundation via foundation_id.

FieldTypeDescription
idUUIDPrimary key
foundation_idUUIDOwning foundation
first_namestringRequired
last_namestring?Optional
emailstring?Used for deduplication and email campaigns
phonestring?Used for SMS campaigns
addressJSON?Structured address (street, city, state, zip)
photo_pathstring?Profile photo in Supabase storage
communication_preferencesJSON?Opt-in flags (email_opt_in, sms_opt_in)
first_contact_atdatetime?When this person was first known to the foundation
  • full_name — Returns "{first_name} {last_name}" trimmed
  • relationship_types — Array of active relationship type values, appended to every JSON response

Contacts are connected to foundations through ContactRelationship records. A contact can hold multiple relationship types at once.

TypeValueDescription
DonordonorHas made or is linked to a donation. Metadata tracks lifetime_giving, gift_count, last_gift_at
ApplicantapplicantHas submitted a scholarship application
SubscribersubscriberHas opted into newsletters or communications
GeneralgeneralImported or manually added without a specific role
FieldTypeDescription
contact_idUUIDThe contact
foundation_idUUIDThe foundation
typeContactRelationshipTypeOne of: donor, applicant, subscriber, general
metadataJSON?Type-specific data (e.g., giving stats for donors)
assigned_atdatetimeWhen this relationship was established

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
}

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.

Contacts connect to other models through Eloquent relationships:

RelationshipTypeTarget
foundation()BelongsToFoundation
relationships()HasManyContactRelationship
tags()BelongsToManyTag (via contact_tag pivot)
activities()HasManyActivity
notes()HasManyNote
donations()HasManyDonation

Contacts are created through several pathways:

  1. Manual creationPOST /api/contacts from the admin dashboard
  2. CSV import — Via the import workflow
  3. DonationDonationService::findOrCreateContact() matches by email or creates a new contact when a donation is processed
  4. Scholarship applicationScholarshipService::createApplication() finds or creates a contact by email
  5. Stripe importImportService::importFromStripe() pulls customers and charge history from a connected Stripe account
  6. API ingestPOST /api/import/ingest accepts a JSON array of contacts

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.

MethodPathDescription
GET/api/contactsList contacts (paginated)
POST/api/contactsCreate 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.