monicahq/monica · error · ModelNotFoundException

The user does not belong to the vault's account.

Error message

The user does not belong to the vault's account.

What it means

ScheduleContactReminderForUser is an internal service (never called from HTTP clients) that schedules a reminder notification for one specific user of a vault. It loads the ContactReminder and User by id, then asserts user.account_id equals the reminder's contact vault account_id; a mismatch aborts with ModelNotFoundException, which Laravel renders as a 404.

Source

Thrown at app/Domains/Contact/ManageReminders/Services/ScheduleContactReminderForUser.php:47

            'user_id' => 'required|uuid|exists:users,id',
        ];
    }

    /**
     * Schedule a contact reminder for the given user, on his timezone.
     * For each user in the vault, a scheduled reminder is created.
     * This service SHOULD NOT BE CALLED FROM THE CLIENTS, ever.
     * It is called by other services.
     */
    public function execute(array $data): void
    {
        $this->validateRules($data);
        $this->data = $data;

        $this->contactReminder = ContactReminder::findOrFail($this->data['contact_reminder_id']);
        $this->user = User::findOrFail($this->data['user_id']);
        if ($this->user->account_id != $this->contactReminder->contact->vault->account_id) {
            throw new ModelNotFoundException('The user does not belong to the vault\'s account.');
        }

        $this->getDate();
        $this->schedule();
    }

    /**
     * A ContactReminder can be either a complete date, or only a day/month.
     * If it is only a day/month, we need to add a fake year so we can still
     * manipulate the date as a Carbon object.
     */
    private function getDate(): void
    {
        if (! $this->contactReminder->year) {
            $this->upcomingDate = Carbon::parse('1900-'.$this->contactReminder->month.'-'.$this->contactReminder->day);
        } else {
            $this->upcomingDate = Carbon::parse($this->contactReminder->year.'-'.$this->contactReminder->month.'-'.$this->contactReminder->day);
        }

View on GitHub (pinned to e08e917341)

Solutions

  1. Pass a user belonging to the same account as the reminder's contact vault: derive candidates from $reminder->contact->vault->users
  2. Never feed Auth::id() into this service without verifying the authenticated user is a member of that vault
  3. In tests, create account, vault, contact, reminder and user under one account so account_id matches

Example fix

// before
$data = [
    'contact_reminder_id' => $reminder->id,
    'user_id' => Auth::id(), // may belong to another account -> throws
];

// after
$vault = $reminder->contact->vault;
abort_unless($vault->users->contains(Auth::id()), 403, 'User is not a member of this vault.');
$data = [
    'contact_reminder_id' => $reminder->id,
    'user_id' => Auth::id(), // now guaranteed same account
];
Defensive patterns

Strategy: validation

Validate before calling

// Validate before calling: user must belong to the reminder's vault (same account)
$reminder = ContactReminder::with('contact.vault')->findOrFail($data['contact_reminder_id']);
$vault = $reminder->contact->vault;

if (! $vault->users()->where('users.id', $data['user_id'])->exists()) {
    throw new InvalidArgumentException('user_id does not belong to the vault of contact_reminder_id.');
}

Type guard

function isValidReminderRecipient(string $userId, ContactReminder $reminder): bool
{
    return $reminder->contact->vault
        ->users()
        ->where('users.id', $userId)
        ->exists(); // membership implies same account
}

Try / catch

use Illuminate\Database\Eloquent\ModelNotFoundException;

try {
    app(ScheduleContactReminderForUser::class)->execute($data);
} catch (ModelNotFoundException $e) {
    // 404-shaped: one of the ids is wrong or crosses accounts — log and re-dispatch without that user
    report($e);
}

Prevention

When it happens

Trigger: Calling execute() with a user_id whose account differs from the account owning contact_reminder_id — e.g. passing the currently authenticated user instead of a member of the vault, or ids produced by factories/seeds belonging to two different accounts.

Common situations: Calling code deriving user_id from Auth::user() instead of the vault's member list, test data created across two accounts, or stale ids after account restructuring.

Related errors


AI-assisted analysis of monicahq/monica@e08e917341 (2026-08-17). Data as JSON: /api/errors/d58c3e3fb3a7014c. Report an issue: GitHub.