Donor identity
Donor identity is platform infrastructure, not a plugin. A donor remains available when Fundraising or any other plugin is disabled or purged.
Data model
users: authentication identity. Organization staff keep email/password login. Donors use unique normalizedphone_e164, password, andphone_verified_at; email is nullable.donors: one global profile per user through uniqueuser_id. It has noorganization_idand must never use tenant scope.phone_verification_challenges: short-lived hashed OTP challenges with purpose, attempt limit, expiry, consumption timestamp, and hashed request IP.
A user may be both an organization member and a donor. Organization membership and donor identity are independent relationships.
Routes
GET|POST /donor/register—donor.register,donor.register.store.GET|POST /donor/login—donor.login,donor.login.store.GET /donor/verify-phone—donor.verification.notice.POST /donor/verify-phone/send—donor.verification.send.POST /donor/verify-phone—donor.verification.verify.GET /donor/dashboard—donor.dashboard, guarded byauth,donor, andphone.verified.POST /donor/logout—donor.logout.
The existing /login and /register remain the staff and organization onboarding flows.
Phone and OTP contracts
PhoneNumber::normalize() converts local Sudan numbers using DONOR_DEFAULT_CALLING_CODE and stores E.164. Never compare raw request strings.
PhoneVerificationProvider::send(string $phoneE164, string $code): void is the provider boundary. The default LogPhoneVerificationProvider is development/testing only and refuses production use. A production SMS provider must implement the same contract.
PhoneVerificationService:
- enforces resend cooldown;
- consumes previous active challenges;
- hashes the code before storage;
- enforces expiry and attempt limits under a database lock;
- marks the user's phone verified;
- dispatches
DonorPhoneVerified.
DonorAccountService owns normalization, global phone uniqueness, the user/profile transaction, registration event, and initial OTP challenge. DonorAuthenticationService owns phone normalization and password/profile authentication. Controllers keep only request validation, session rotation, and redirects; Blade files use donor layout components and contain no domain decisions.
Current Fundraising submission links a donation when the normalized phone already belongs to a verified donor. There is no retroactive DonorPhoneVerified listener yet, so a guest donation created before account verification remains unlinked. Retroactive claiming is roadmap work and must require successful verification; matching a raw phone alone is forbidden.
Privacy boundary
The current donor dashboard shows identity and verification state; cross-organization donation history is not implemented yet. An organization may query only donations scoped to that organization and may not read the donor's activity elsewhere. Phone numbers and proof files are never public.