# SOLUTION: Duplicate SMS Fix

**Date:** 2025-01-30
**Status:** ✅ IMPLEMENTED - Ready for QA
**Commit:** `03d6a77`

---

## Problem

Leads were receiving duplicate SMS messages because they could be enrolled in **multiple sequences simultaneously**. The enrollment deduplication only checked the *same* sequence, not *any* active sequence.

**Evidence:** Lead Paddy Fleming Ivers received 2 SMS messages **717 milliseconds apart** from two different sequences.

---

## Solution: Three-Layer Protection

### Layer 1: Enrollment Prevention (Primary Fix)

**File:** `src/routes/sequences.js`

**Change:** Modified the enrollment check from:
```javascript
// OLD - Only checks SAME sequence (BUG!)
.eq('sequence_id', sequenceId)
```
to:
```javascript
// NEW - Checks ANY active sequence
.in('lead_id', leadsToEnroll)
.or(`status.eq.active,enrolled_at.gte.${thirtyDaysAgo}`)
```

Now when enrolling leads:
- ✅ Checks if lead is in ANY active sequence (not just the target sequence)
- ✅ Checks if lead was enrolled in ANY sequence within last 30 days
- ✅ Returns detailed `skipReasons` explaining why leads were skipped
- ✅ Also fixed in `/:id/bulk-enroll` endpoint

### Layer 2: Send-Time Throttle (Critical Safety Net)

**File:** `src/jobs/sequenceProcessor.js`

**Change:** Before sending any SMS via sequence processor, check communications table:

```javascript
// Check if lead received ANY SMS in last 30 minutes
const { data: recentSms } = await db
  .from('communications')
  .select('id, created_at')
  .eq('lead_id', lead.id)
  .eq('type', 'sms_outbound')
  .gte('created_at', thirtyMinutesAgo)
  .limit(1);

if (recentSms && recentSms.length > 0) {
  // Skip send, reschedule for 1 hour later
}
```

Behavior when throttled:
- ✅ SMS is NOT sent
- ✅ Enrollment is rescheduled 1 hour later (not failed)
- ✅ Clear log message: `[SequenceProcessor] THROTTLED: Lead X received SMS Y minutes ago`
- ✅ Does NOT count as a failed attempt (won't pause after 3)

### Layer 3: UI Warning Support

**File:** `src/routes/sequences.js`

**New Endpoint:** `GET /api/sequences/check-enrollments?lead_ids=a,b,c`

Returns:
```json
{
  "activeEnrollments": [
    { "lead_id": "...", "sequence_id": "...", "sequences": { "name": "..." } }
  ],
  "leadCount": 3,
  "enrolledCount": 1
}
```

Frontend can use this to warn users before enrollment.

---

## Files Changed

| File | Changes |
|------|---------|
| `src/routes/sequences.js` | Fixed enroll + bulk-enroll checks, added check-enrollments endpoint |
| `src/jobs/sequenceProcessor.js` | Added 30-minute SMS throttle before sends |

---

## Testing Checklist

### Manual Tests:
- [ ] Enroll lead in Sequence A → should succeed
- [ ] Enroll SAME lead in Sequence B → should fail/skip (Layer 1)
- [ ] Trigger sequence send for lead who got SMS 10 min ago → should throttle (Layer 2)
- [ ] Check `/api/sequences/check-enrollments` returns active enrollments

### Edge Cases:
- [ ] Bulk enroll 10 leads, 3 already enrolled → should enroll 7, skip 3 with reasons
- [ ] Lead gets throttled → should be rescheduled 1 hour later, NOT paused
- [ ] Lead in completed sequence (>30 days) → should be enrollable in new sequence

---

## Rollout

1. **Immediate:** Deploy this commit (Layer 2 protects even without Layer 1)
2. **Monitor:** Watch logs for `THROTTLED` messages - should decrease over time
3. **Cleanup:** Run SQL to find/pause duplicate enrollments (see `sms-duplicate-fix-plan.md`)

---

## Monitoring

After deploy, check for:
```
[SequenceProcessor] THROTTLED: Lead ...
```

If you see many throttle messages, it means existing duplicate enrollments are being caught. Once duplicates are cleaned up, throttle should rarely trigger.

---

## Related Files

- `sms-duplicate-fix-plan.md` - Original fix plan with database cleanup SQL
- `INVESTIGATION.md` - Root cause investigation
