Phone Resolution Service
The resolve_sender_phone service in src/mods/messaging/services/resolve_sender_phone_service.rs determines which phone number a member uses to send outbound SMS. It replaced duplicated resolution logic that previously lived in both the assistant SMS tool and the plan execution service.
Resolution Order
Section titled “Resolution Order”The service follows a strict priority chain:
- Default assigned phone — the member’s assignment with
is_default = true - Any assigned phone — falls back to the first assignment if no default is set
- Org fallback (owners only) — org owners with no assignments use any org phone number
- Error (non-owners) — non-owners with no assignments get a clear error
pub async fn resolve_sender_phone( db: &DatabaseConnection, member_id: Uuid, organization_id: Uuid, is_owner: bool,) -> Result<phone_number::Model, AppError>Multi-Tenancy Check
Section titled “Multi-Tenancy Check”Before looking up assignments, the service verifies the member belongs to the given organization. If the member doesn’t belong, it returns AppError::Forbidden.
let member_exists = schemas::member::Entity::find_by_id(member_id) .filter(schemas::member::Column::OrganizationId.eq(organization_id)) .one(db) .await? .is_some();Where It’s Used
Section titled “Where It’s Used”| Caller | File | Behavior |
|---|---|---|
| Assistant SMS tool | ai_send_sms_tool.rs | Resolves member’s phone; validates assignment if a specific phone is requested |
| Plan SMS execution | execute_send_sms_service.rs | Uses the template’s sender_phone_number_id directly (no resolution needed) |
| Compose bar (UI) | messaging_contact_view.rs | Uses the same priority: member’s default phone → first available phone |
The assistant tool calls resolve_sender_phone when the user doesn’t specify a phone number. When the user explicitly picks a phone, the tool validates the member is assigned to it before sending.
The compose bar in MessagingContactView follows the same resolution order — selecting the member’s default phone first, then falling back to the first available phone. If the member has no allowed phones (non-owner with no assignments), the compose bar does not render.
Mandatory Sender on Plan Templates
Section titled “Mandatory Sender on Plan Templates”sender_phone_number_id is now a required Uuid field on both PlanTemplate and PlanTemplateData (previously Option<Uuid>). The UI no longer shows a “Default (auto-detect)” option in the phone dropdown — you must select a phone number when creating or editing a template.
Migration
Section titled “Migration”Migration m20260327_120625 handles the schema change:
- Backfills NULLs — sets
sender_phone_number_idto the org’s first phone number for any template missing it - Adds NOT NULL —
ALTER TABLE plan_template ALTER COLUMN sender_phone_number_id SET NOT NULL - Switches FK to RESTRICT — replaces
ON DELETE SET NULLwithON DELETE RESTRICT(you can’t delete a phone number that’s referenced by a template)
Type Changes
Section titled “Type Changes”// Beforepub struct PlanTemplateData { pub sender_phone_number_id: Option<Uuid>,}
// Afterpub struct PlanTemplateData { pub sender_phone_number_id: Uuid,}The PlanTemplate type mirrors this change. Plan creation services wrap the template’s phone ID in Some() when writing to the plan table, which keeps sender_phone_number_id nullable on plans for historical data.
Error Messages
Section titled “Error Messages”| Scenario | Error |
|---|---|
| Member not in org | Forbidden: "Member does not belong to this organization." |
| Non-owner, no assignments | InvalidInput: "No phone number assigned. Ask an owner to assign you a phone number." |
| Owner, no org phones | InvalidInput: "Organization has no phone number configured." |
| Assigned phone not found | NotFound: "Assigned phone number" |