# Design Document: Contact Referred By Field

## Overview

This design adds a "referred by" field to the contacts system, enabling tracking of referral relationships between contacts. The implementation follows existing patterns in the Atomic CRM codebase, using React Admin components and Supabase for data persistence.

The feature allows users to:
- Select an existing contact as the referrer when creating or editing a contact
- Create a new contact on-the-fly if the referrer doesn't exist
- View the referrer's name as a clickable link on contact detail pages
- Update or remove referral relationships
- Safely delete contacts who have referred others

## Architecture

### Database Layer
- Add a `referred_by_id` column to the `contacts` table in Supabase
- Implement a foreign key constraint with `ON DELETE SET NULL` behavior
- Update the `contacts_summary` view to include referrer information

### Type System
- Extend the `Contact` TypeScript interface to include `referred_by_id` field
- Ensure type safety across all components that handle contact data

### UI Components
- Add a ReferenceInput field to `ContactInputs.tsx` for selecting the referrer
- Create an `AutocompleteContactInput` component for contact selection with create capability
- Display the referrer in `ContactShow.tsx` using ReferenceField
- Update `ContactAside.tsx` to show referrer information in the sidebar

## Components and Interfaces

### 1. Database Migration

**File:** `supabase/migrations/[timestamp]_add_referred_by_to_contacts.sql`

```sql
-- Add referred_by_id column to contacts table
ALTER TABLE "public"."contacts" 
ADD COLUMN "referred_by_id" bigint;

-- Add foreign key constraint with ON DELETE SET NULL
ALTER TABLE "public"."contacts" 
ADD CONSTRAINT "contacts_referred_by_id_fkey" 
FOREIGN KEY (referred_by_id) 
REFERENCES contacts(id) 
ON UPDATE CASCADE 
ON DELETE SET NULL;

-- Update contacts_summary view to include referrer name
CREATE OR REPLACE VIEW "public"."contacts_summary"
WITH (security_invoker=on)
AS
SELECT 
    co.*,
    c.name as company_name,
    COUNT(DISTINCT t.id) as nb_tasks,
    ref.first_name || ' ' || ref.last_name as referred_by_name
FROM
    "public"."contacts" co
LEFT JOIN
    "public"."tasks" t ON co.id = t.contact_id
LEFT JOIN
    "public"."companies" c ON co.company_id = c.id
LEFT JOIN
    "public"."contacts" ref ON co.referred_by_id = ref.id
GROUP BY
    co.id, c.name, ref.first_name, ref.last_name;
```

### 2. TypeScript Interface Update

**File:** `src/components/atomic-crm/types.ts`

Add `referred_by_id` to the Contact interface:

```typescript
export type Contact = {
  first_name: string;
  last_name: string;
  title: string;
  company_id: Identifier;
  email_jsonb: EmailAndType[];
  avatar?: Partial<RAFile>;
  linkedin_url?: string | null;
  first_seen: string;
  last_seen: string;
  has_newsletter: boolean;
  tags: Identifier[];
  gender: string;
  sales_id: Identifier;
  status: string;
  background: string;
  phone_jsonb: PhoneNumberAndType[];
  nb_tasks?: number;
  company_name?: string;
  referred_by_id?: Identifier | null;  // NEW FIELD
  referred_by_name?: string;           // NEW FIELD (from view)
} & Pick<RaRecord, "id">;
```

### 3. AutocompleteContactInput Component

**File:** `src/components/atomic-crm/contacts/AutocompleteContactInput.tsx`

Create a new component for contact selection with inline creation capability:

```typescript
import { useCreate, useGetIdentity, useNotify } from "ra-core";
import { AutocompleteInput } from "@/components/admin/autocomplete-input";

export const AutocompleteContactInput = () => {
  const [create] = useCreate();
  const { identity } = useGetIdentity();
  const notify = useNotify();
  
  const handleCreateContact = async (name?: string) => {
    if (!name) return;
    
    // Parse name into first and last name
    const nameParts = name.trim().split(" ");
    const first_name = nameParts[0] || "";
    const last_name = nameParts.slice(1).join(" ") || "";
    
    try {
      const newContact = await create(
        "contacts",
        {
          data: {
            first_name,
            last_name,
            sales_id: identity?.id,
            first_seen: new Date().toISOString(),
            last_seen: new Date().toISOString(),
            tags: [],
            email_jsonb: [],
            phone_jsonb: [],
          },
        },
        { returnPromise: true },
      );
      return newContact;
    } catch {
      notify("An error occurred while creating the contact", {
        type: "error",
      });
    }
  };

  const optionRenderer = (choice: any) => 
    `${choice.first_name} ${choice.last_name}`;

  return (
    <AutocompleteInput
      optionText={optionRenderer}
      helperText={false}
      onCreate={handleCreateContact}
      createItemLabel="Create contact %{item}"
      filterToQuery={(searchText) => ({ q: searchText })}
    />
  );
};
```

### 4. ContactInputs Update

**File:** `src/components/atomic-crm/contacts/ContactInputs.tsx`

Add the referred by field to the `ContactMiscInputs` section:

```typescript
import { AutocompleteContactInput } from "./AutocompleteContactInput";

const ContactMiscInputs = () => {
  const { getValues } = useFormContext();
  
  return (
    <div className="flex flex-col gap-4">
      <h6 className="text-lg font-semibold">Misc</h6>
      <TextInput
        source="background"
        label="Background info (bio, how you met, etc)"
        multiline
        helperText={false}
      />
      <BooleanInput source="has_newsletter" helperText={false} />
      <ReferenceInput
        reference="contacts"
        source="referred_by_id"
        sort={{ field: "last_name", order: "ASC" }}
        filter={(formData) => ({
          // Prevent selecting self as referrer
          "id@neq": formData?.id || 0,
        })}
      >
        <AutocompleteContactInput />
      </ReferenceInput>
      <ReferenceInput
        reference="sales"
        source="sales_id"
        sort={{ field: "last_name", order: "ASC" }}
        filter={{
          "disabled@neq": true,
        }}
      >
        <SelectInput
          helperText={false}
          label="Account manager"
          optionText={saleOptionRenderer}
          validate={required()}
        />
      </ReferenceInput>
    </div>
  );
};
```

### 5. ContactShow Update

**File:** `src/components/atomic-crm/contacts/ContactShow.tsx`

Display the referrer information in the contact details:

```typescript
const ContactShowContent = () => {
  const { record, isPending } = useShowContext<Contact>();
  if (isPending || !record) return null;

  return (
    <div className="mt-2 mb-2 flex gap-8">
      <div className="flex-1">
        <Card>
          <CardContent>
            <div className="flex">
              <Avatar />
              <div className="ml-2 flex-1">
                <h5 className="text-xl font-semibold">
                  {record.first_name} {record.last_name}
                </h5>
                <div className="inline-flex text-sm text-muted-foreground">
                  {record.title}
                  {record.title && record.company_id != null && " at "}
                  {record.company_id != null && (
                    <ReferenceField
                      source="company_id"
                      reference="companies"
                      link="show"
                    >
                      &nbsp;
                      <TextField source="name" />
                    </ReferenceField>
                  )}
                </div>
                {/* NEW: Display referrer */}
                {record.referred_by_id && (
                  <div className="text-sm text-muted-foreground mt-1">
                    Referred by{" "}
                    <ReferenceField
                      source="referred_by_id"
                      reference="contacts"
                      link="show"
                    >
                      <TextField source="first_name" />{" "}
                      <TextField source="last_name" />
                    </ReferenceField>
                  </div>
                )}
              </div>
              <div>
                <ReferenceField
                  source="company_id"
                  reference="companies"
                  link="show"
                  className="no-underline"
                >
                  <CompanyAvatar />
                </ReferenceField>
              </div>
            </div>
            {/* ... rest of component */}
          </CardContent>
        </Card>
      </div>
      <ContactAside />
    </div>
  );
};
```

### 6. ContactAside Update (Optional Enhancement)

**File:** `src/components/atomic-crm/contacts/ContactAside.tsx`

Optionally add referrer information to the sidebar for better visibility.

## Data Models

### Contact Table Schema

```
contacts
├── id (bigint, primary key)
├── first_name (text)
├── last_name (text)
├── ... (existing fields)
├── referred_by_id (bigint, nullable, foreign key -> contacts.id)
└── ... (other existing fields)
```

### Relationships

- **Self-referential relationship**: `contacts.referred_by_id` → `contacts.id`
- **Cascade behavior**: ON DELETE SET NULL (when referrer is deleted, clear the reference)
- **Update behavior**: ON UPDATE CASCADE (if contact ID changes, update references)

## Error Handling

### Validation Rules

1. **Self-referral prevention**: A contact cannot select themselves as their own referrer
   - Implemented via filter in ReferenceInput: `"id@neq": formData?.id`
   
2. **Circular reference prevention**: While not explicitly prevented at the database level, the UI pattern makes circular references unlikely
   - Future enhancement: Add database trigger to prevent circular chains

### Error Scenarios

1. **Referrer not found**: If referred_by_id points to a non-existent contact
   - Database constraint ensures referential integrity
   - UI gracefully handles null values
   
2. **Contact creation failure**: If creating a new contact from autocomplete fails
   - Display error notification to user
   - Allow user to retry or select existing contact
   
3. **Deletion of referrer**: When a contact who has referred others is deleted
   - Database automatically sets referred_by_id to NULL for affected contacts
   - No error displayed to user

## Testing Strategy

### Unit Tests

1. **AutocompleteContactInput Component**
   - Test contact creation from search input
   - Test name parsing (first name / last name split)
   - Test error handling when creation fails
   
2. **Type Safety**
   - Verify Contact interface includes referred_by_id
   - Ensure TypeScript compilation succeeds

### Integration Tests

1. **Contact Creation Flow**
   - Create contact with existing referrer
   - Create contact with new referrer (inline creation)
   - Create contact without referrer
   
2. **Contact Edit Flow**
   - Update referrer to different contact
   - Clear referrer field
   - Verify self-referral is prevented
   
3. **Contact Display**
   - View contact with referrer
   - View contact without referrer
   - Click referrer link and navigate to referrer's page

### Database Tests

1. **Foreign Key Constraint**
   - Verify ON DELETE SET NULL behavior
   - Verify ON UPDATE CASCADE behavior
   - Test that invalid referred_by_id values are rejected
   
2. **View Update**
   - Verify contacts_summary includes referred_by_name
   - Test query performance with referrer joins

## Implementation Notes

### Existing Patterns to Follow

1. **ReferenceInput Pattern**: Use the same pattern as `company_id` and `sales_id` fields
2. **Autocomplete with Create**: Follow the `AutocompleteCompanyInput` pattern
3. **Display Pattern**: Use ReferenceField + TextField like company display
4. **Styling**: Match existing field styling in ContactInputs sections

### Dependencies

- No new npm packages required
- Uses existing React Admin components
- Leverages Supabase foreign key constraints

### Migration Strategy

1. Run database migration to add column and constraint
2. Deploy updated TypeScript types
3. Deploy UI components
4. Existing contacts will have `referred_by_id = null` (no migration needed)
5. Users can gradually populate referrer data as they edit contacts

## Design Decisions and Rationales

### Decision 1: Self-Referential Foreign Key
**Rationale**: Allows flexible referral tracking without creating a separate junction table. Simpler schema for a one-to-many relationship (one referrer, many referred contacts).

### Decision 2: ON DELETE SET NULL
**Rationale**: Preserves referred contacts when referrer is deleted, avoiding cascading deletions. Users can still see that a contact was referred, even if the referrer no longer exists in the system.

### Decision 3: Inline Contact Creation
**Rationale**: Improves user experience by allowing quick referrer creation without leaving the form. Follows existing pattern used for company creation.

### Decision 4: Placement in ContactMiscInputs
**Rationale**: Groups with other relationship fields (account manager) rather than identity or position fields. Keeps the form layout balanced and logical.

### Decision 5: Optional Field
**Rationale**: Not all contacts are referred by existing contacts. Making it optional allows flexibility and doesn't force users to enter data they don't have.
