Automating Mentorship Contract Lifecycle in Laravel: From Notification to Re-Enrollment
Managing ongoing subscriptions and contracts can be a delicate balance between providing a seamless user experience and ensuring robust system automation. How do you gracefully handle contract expirations, notify users proactively, and facilitate re-enrollment without extensive manual oversight? In the landing project, we recently tackled this challenge for our 6-month mentorship contracts.
The Challenge: Manual Contract Management
Previously, managing the lifecycle of these 6-month mentorship contracts involved a degree of manual intervention, especially around expiration and re-enrollment. Without clear automated processes, there was a risk of mentees missing renewal deadlines, leading to unexpected service interruptions or an unclear path to continue their mentorship. Our goal was to build a fully automated system that handles notifications, expiration, and ensures a smooth re-enrollment path.
The Automated Solution: Scheduled Commands to the Rescue
Our solution leverages Laravel's robust scheduling capabilities to manage the entire contract lifecycle automatically. This involves two key scheduled commands and updates to the user interface and database schema.
7-Day Reminder
One week before a mentee's 6-month contract is set to expire, a daily scheduled command, mentorship:notify-contract-ending, springs into action. This command identifies all paid subscriptions ending in the next 7 days and sends a clear re-enrollment call-to-action via email. To ensure idempotency and prevent duplicate notifications, a new ending_notice_sent_at column tracks when this reminder has been issued.
Expiration Logic
On the actual contract end date, another daily scheduled command, mentorship:expire-contracts, takes over. Running in the early hours, this job performs several critical tasks:
- Marks Subscription as Cancelled: The mentee's subscription status is updated to
Cancelled. - Cancels Future Bookings: Any future mentorship bookings associated with the now-expired contract are automatically cancelled.
- Sends Expiration Email: A final email is sent to the mentee, confirming the contract's expiration and reiterating the re-enrollment opportunity.
- Ignores Free Subscriptions: Crucially, free subscriptions (those without a defined
contract_ends_atdate) are intentionally bypassed by this command, as they operate on a different lifecycle.
User Experience: Dashboard Banners
To keep mentees informed, the bookings dashboard now displays prominent banners for contracts that are either ending soon or recently expired. This provides immediate visual cues, guiding users towards the next steps.
Seamless Re-Enrollment Flow
Post-expiration, the system is designed to allow mentees to re-enroll seamlessly. Since their old subscription is marked Cancelled, the existing enrollment flow correctly identifies them as eligible to start a fresh 6-month contract, bypassing any "already enrolled" gates.
Architectural Overview
This system integrates several components:
- Scheduled Commands: Two new Artisan commands (
mentorship:notify-contract-endingandmentorship:expire-contracts) handle the core logic. - Mailables: Dedicated
Mailableclasses (MentorCycleContractEndingNotificationandMentorCycleContractExpired) manage the content and sending of notification emails. - Controller Updates: The
MentorBookingController::indexmethod was extended to query and surface the contract status to the dashboard view. - Database Schema: New columns (
contract_expired_at,ending_notice_sent_at) were added to thementor_subscriptionstable for precise tracking.
Here's a simplified example of how a scheduled command might be defined in Laravel:
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\MentorSubscription;
use App\Mail\MentorCycleContractEndingNotification;
use Illuminate\Support\Facades\Mail;
use Carbon\Carbon;
class NotifyContractEnding extends Command
{
protected $signature = 'mentorship:notify-contract-ending';
protected $description = 'Notifies mentees about contracts ending in 7 days.';
public function handle()
{
$sevenDaysFromNow = Carbon::now()->addDays(7)->startOfDay();
MentorSubscription::whereNotNull('contract_ends_at')
->whereDate('contract_ends_at', $sevenDaysFromNow)
->whereNull('ending_notice_sent_at')
->chunk(100, function ($subscriptions) {
foreach ($subscriptions as $subscription) {
Mail::to($subscription->mentee->email)
->send(new MentorCycleContractEndingNotification($subscription));
$subscription->update(['ending_notice_sent_at' => Carbon::now()]);
$this->info("Notified mentee {$subscription->mentee->id} about ending contract.");
}
});
$this->info('Contract ending notifications processed.');
}
}
Key Takeaways
Automating contract lifecycle management provides significant benefits. It reduces operational overhead, ensures timely communication with users, and creates a smoother experience for both mentees and administrators. By leveraging scheduled tasks and careful state management, we've built a robust system that handles the complexities of subscription renewals with minimal human intervention.