feat(Reminder process): add 4 reminder phases for membership (Retzien)
All checks were successful
Deploy Roxane to Preprod / deploy (push) Successful in 1m21s

This commit is contained in:
2026-09-06 17:03:14 +02:00
parent 98893924ac
commit ba11ee7120
61 changed files with 967 additions and 162 deletions

0
- Normal file
View File

View File

@@ -0,0 +1,150 @@
<?php
namespace App\Console\Commands;
use App\Models\Member;
use App\Services\MemberRenewalService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Symfony\Component\Console\Command\Command as CommandAlias;
class MemberRenewalProcessCommand extends Command
{
protected $signature = 'members:renewal-process
{--phase=auto : Phase à exécuter (auto|reminder_1|reminder_2|reminder_3|deactivate)}
{--year= : Année de renouvellement (par défaut : année suivante)}
{--dry-run : Mode simulation}
{email? : Email d\'un adhérent spécifique}';
protected $description = 'Processus de relances pour renouvellement adhésion';
public function __construct(
protected MemberRenewalService $renewalService
) {
parent::__construct();
}
public function handle(): int
{
$dryRun = $this->option('dry-run');
$emailFilter = $this->argument('email');
$phaseOption = $this->option('phase');
$year = $this->option('year') ?? now()->addYear()->year;
$this->info(
$dryRun
? 'DRY-RUN activé – aucune modification ne sera effectuée'
: 'Mode réel activé'
);
$phase = $phaseOption === 'auto' ? $this->determinePhase() : $phaseOption;
if ($phase === null) {
$this->warn('Mode auto : aucune phase détectée pour la date actuelle');
return CommandAlias::SUCCESS;
}
$this->info("Phase détectée : {$phase}");
$this->info("Année de renouvellement : {$year}");
if ($emailFilter) {
$this->warn("Mode utilisateur unique : {$emailFilter}");
}
$members = $this->getEligibleMembers($emailFilter);
if ($members->isEmpty()) {
$this->info('Aucun adhérent éligible');
return CommandAlias::SUCCESS;
}
$this->info("{$members->count()} adhérent(s) à traiter");
foreach ($members as $member) {
try {
$this->processMember($member, $phase, $year, $dryRun);
} catch (\Throwable $e) {
Log::error('Erreur traitement adhérent relance', [
'member_id' => $member->id,
'phase' => $phase,
'error' => $e->getMessage(),
]);
$this->error("Erreur pour {$member->email} : {$e->getMessage()}");
}
}
$this->info(
$dryRun
? 'DRY-RUN terminé – aucune action effectuée'
: 'Traitement terminé'
);
return CommandAlias::SUCCESS;
}
protected function determinePhase(): ?string
{
$today = now();
if ($today->month === 12 && $today->day >= 1 && $today->day <= 10) {
return 'reminder_1';
}
if ($today->month === 12 && $today->day >= 15 && $today->day <= 20) {
return 'reminder_2';
}
if ($today->month === 1 && $today->day >= 4 && $today->day <= 6) {
return 'reminder_3';
}
if ($today->month === 1 && $today->day >= 25 && $today->day <= 31) {
return 'deactivate';
}
return null;
}
protected function getEligibleMembers(?string $emailFilter): \Illuminate\Support\Collection
{
$query = Member::query()
->where('status', 'valid')
->whereHas('memberships', function ($q) {
$q->where('status', 'active');
});
if ($emailFilter) {
$query->where('email', $emailFilter)
->orWhere('retzien_email', $emailFilter);
}
return $query->get();
}
protected function processMember(Member $member, string $phase, int $year, bool $dryRun): void
{
$this->line("• {$member->id} - {$member->email}");
if ($dryRun) {
$this->info("[DRY-RUN] Phase {$phase}");
return;
}
$sent = match ($phase) {
'reminder_1' => $this->renewalService->sendReminder1($member, $year),
'reminder_2' => $this->renewalService->sendReminder2($member, $year),
'reminder_3' => $this->renewalService->sendReminder3($member, $year),
'deactivate' => $this->renewalService->deactivateMember($member, $year),
default => false,
};
if ($sent) {
$this->info(" ✓ Relance envoyée");
} else {
$this->warn(" → Aucune relance envoyée (Membre non concerné / mail de relance déjà envoyé / adhésion active pour {$year})");
}
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Console\Commands;
use App\Models\Member;
use App\Models\Service;
use Illuminate\Console\Command;
use function Laravel\Prompts\progress;
class SyncServicesMembers extends Command

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Filament\Actions;
use App\Models\Membership;
use App\Services\MemberRenewalService;
use Filament\Actions\Action;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
class SendRenewalReminderAction extends Action
{
public static function make(?string $name = 'send_renewal_reminder'): static
{
return parent::make($name)
->label('Envoyer une relance')
->icon('heroicon-o-envelope')
->color('primary')
->disabled(function () {
$today = now();
return ! ($today->month === 12 || ($today->month === 1 && $today->day <= 31));
})
->requiresConfirmation()
->modalHeading('Envoyer une relance de renouvellement')
->modalDescription(fn (Membership $record) => "Envoyer une relance à {$record->member->full_name} ({$record->member->email}) ?")
->form([
Select::make('reminder_type')
->label('Type de relance')
->options([
'reminder_1' => '1ère relance (début décembre)',
'reminder_2' => '2ème relance (mi-décembre)',
'reminder_3' => '3ème relance (début janvier)',
])
->required()
->default('reminder_1'),
TextInput::make('year')
->label('Année de renouvellement')
->default(now()->addYear()->year)
->numeric()
->required(),
])
->action(function (Membership $record, array $data) {
$service = app(MemberRenewalService::class);
$member = $record->member;
$year = (int) $data['year'];
match ($data['reminder_type']) {
'reminder_1' => $service->sendReminder1($member, $year),
'reminder_2' => $service->sendReminder2($member, $year),
'reminder_3' => $service->sendReminder3($member, $year),
};
Notification::make()
->title('Relance envoyée')
->success()
->send();
});
}
}

View File

@@ -5,7 +5,6 @@ namespace App\Filament\Resources\MemberGroups;
use App\Filament\Resources\MemberGroups\Pages\CreateMemberGroup;
use App\Filament\Resources\MemberGroups\Pages\EditMemberGroup;
use App\Filament\Resources\MemberGroups\Pages\ListMemberGroups;
use App\Filament\Resources\MemberGroups\RelationManagers\MemberRelationManager;
use App\Filament\Resources\MemberGroups\RelationManagers\MembersRelationManager;
use App\Filament\Resources\MemberGroups\Schemas\MemberGroupForm;
use App\Filament\Resources\MemberGroups\Tables\MemberGroupsTable;

View File

@@ -23,8 +23,7 @@ class MemberGroupForm
->action(function (MemberGroup $record) {
return redirect()->route('filament.admin.resources.members.index', ['filters[group][name][values][0]' => $record->id]);
})
->visible(fn(?MemberGroup $record) => $record !== null)
->visible(fn (?MemberGroup $record) => $record !== null),
])
->schema([

View File

@@ -6,11 +6,9 @@ use App\Models\Member;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Filters\SelectFilter;
use Illuminate\Database\Eloquent\Builder;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\Filter;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
class MembersTable
@@ -83,7 +81,7 @@ class MembersTable
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
])
]),
]);
}
}

View File

@@ -16,7 +16,7 @@ class MemberCount extends StatsOverviewWidget
->description(Member::getAttributeLabel('widgets.stats.description'))
->descriptionIcon('heroicon-o-user-group', IconPosition::Before)
->chart([7, 2, 10, 3, 15, 4, 17])
->color('primary')
->color('primary'),
];
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Filament\Resources\Memberships\Schemas;
use App\Enums\IspconfigType;
use App\Filament\Actions\SendRenewalReminderAction;
use App\Filament\Actions\ServiceToggleAction;
use App\Models\ListmonkMember;
use App\Models\Membership;
@@ -261,6 +262,12 @@ class MembershipForm
->label(Membership::getAttributeLabel('end_date')),
])
->extraAttributes(['class' => 'sticky top-4 h-fit']),
Section::make('Relances')
->schema([
SendRenewalReminderAction::make(),
])
->extraAttributes(['class' => 'sticky top-4 h-fit']),
])
->columnSpan(1),
])

View File

@@ -34,7 +34,7 @@ class PackageForm
Textarea::make('description')
->label(Package::getAttributeLabel('description'))
->default(null),
])
]),
]);
}

View File

@@ -31,7 +31,7 @@ class ServiceForm
TextInput::make('icon')
->label(Service::getAttributeLabel('icon'))
->default(null),
])
]),
]);
}
}

View File

@@ -19,6 +19,7 @@ class UserResource extends Resource
protected static ?string $model = User::class;
protected static string|null|\UnitEnum $navigationGroup = 'Paramètres';
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
public static function form(Schema $schema): Schema
@@ -51,6 +52,7 @@ class UserResource extends Resource
{
return User::getAttributeLabel('user');
}
public static function getPluralModelLabel(): string
{
return User::getAttributeLabel('users');

View File

@@ -25,7 +25,7 @@ class MembershipFormController extends Controller
->where('is_active', true)
->select('id', 'identifier', 'name', 'price', 'description')
->get()
->map(function (Package $p) use ($remainingMonths, $currentYear) {
->map(function (Package $p) use ($remainingMonths) {
$months = match ($p->identifier) {
'custom' => $remainingMonths,
'one-year' => $remainingMonths + 12,

View File

@@ -45,6 +45,7 @@ class Contact extends Model
{
return __("contacts.fields.$attribute");
}
public function getFullNameAttribute(): string
{
return "{$this->firstname} {$this->lastname}";

View File

@@ -13,7 +13,15 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Member $member
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|ListmonkMember newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|ListmonkMember newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|ListmonkMember query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|ListmonkMember whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ListmonkMember whereData($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ListmonkMember whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ListmonkMember whereListmonkUserId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ListmonkMember whereMemberId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|ListmonkMember whereUpdatedAt($value)
* @mixin \Eloquent
*/
class ListmonkMember extends Model

View File

@@ -19,10 +19,10 @@ use Illuminate\Notifications\Notifiable;
* @property string $nature
* @property int|null $type_id
* @property int|null $group_id
* @property string|null $lastname
* @property string|null $firstname
* @property string|null $lastname
* @property string $email
* @property string $retzien_email
* @property string|null $retzien_email
* @property string|null $company
* @property string|null $date_of_birth
* @property string|null $address
@@ -40,17 +40,23 @@ use Illuminate\Notifications\Notifiable;
* @property-read \App\Models\MemberGroup|null $group
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\IspconfigMember> $ispconfigs
* @property-read int|null $ispconfigs_count
* @property-read \App\Models\Membership|null $lastActiveMembership
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ListmonkMember> $listmonkMembers
* @property-read int|null $listmonk_members_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Membership> $memberships
* @property-read int|null $memberships_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\NextCloudMember> $nextcloudAccounts
* @property-read int|null $nextcloud_accounts_count
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection<int, \Illuminate\Notifications\DatabaseNotification> $notifications
* @property-read int|null $notifications_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\MemberRenewalReminder> $renewalReminders
* @property-read int|null $renewal_reminders_count
* @property-read \App\Models\MemberType|null $type
* @property-read \App\Models\User|null $user
*
* @method static \Database\Factories\MemberFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member whereAddress($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member whereCity($value)
@@ -77,7 +83,8 @@ use Illuminate\Notifications\Notifiable;
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member whereUserId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member whereWebsiteUrl($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member whereZipcode($value)
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member withTrashed(bool $withTrashed = true)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member withoutTrashed()
* @mixin \Eloquent
*/
class Member extends Model
@@ -163,6 +170,11 @@ class Member extends Model
return $this->hasMany(ListmonkMember::class, 'member_id');
}
public function renewalReminders(): HasMany
{
return $this->hasMany(MemberRenewalReminder::class);
}
public function lastActiveMembership(): HasOne
{
return $this->hasOne(Membership::class)->where('status', 'active')->latest();

View File

@@ -32,7 +32,7 @@ class MemberGroup extends Model
protected $fillable = [
'name',
'description',
'identifier'
'identifier',
];
public static function getAttributeLabel(string $attribute): string

View File

@@ -0,0 +1,107 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property int $member_id
* @property int $renewal_year
* @property bool $reminder_1_sent
* @property bool $reminder_2_sent
* @property bool $reminder_3_sent
* @property bool $deactivation_sent
* @property \Illuminate\Support\Carbon|null $reminder_1_sent_at
* @property \Illuminate\Support\Carbon|null $reminder_2_sent_at
* @property \Illuminate\Support\Carbon|null $reminder_3_sent_at
* @property \Illuminate\Support\Carbon|null $deactivated_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\Member $member
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereDeactivatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereDeactivationSent($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereMemberId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereReminder1Sent($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereReminder1SentAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereReminder2Sent($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereReminder2SentAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereReminder3Sent($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereReminder3SentAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereRenewalYear($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberRenewalReminder whereUpdatedAt($value)
* @mixin \Eloquent
*/
class MemberRenewalReminder extends Model
{
protected $fillable = [
'member_id',
'renewal_year',
'reminder_1_sent',
'reminder_2_sent',
'reminder_3_sent',
'deactivation_sent',
'reminder_1_sent_at',
'reminder_2_sent_at',
'reminder_3_sent_at',
'deactivated_at',
];
protected function casts(): array
{
return [
'reminder_1_sent' => 'boolean',
'reminder_2_sent' => 'boolean',
'reminder_3_sent' => 'boolean',
'deactivation_sent' => 'boolean',
'reminder_1_sent_at' => 'datetime',
'reminder_2_sent_at' => 'datetime',
'reminder_3_sent_at' => 'datetime',
'deactivated_at' => 'datetime',
'renewal_year' => 'integer',
];
}
public function member(): BelongsTo
{
return $this->belongsTo(Member::class);
}
public function markAsReminder1Sent(): void
{
$this->update([
'reminder_1_sent' => true,
'reminder_1_sent_at' => now(),
]);
}
public function markAsReminder2Sent(): void
{
$this->update([
'reminder_2_sent' => true,
'reminder_2_sent_at' => now(),
]);
}
public function markAsReminder3Sent(): void
{
$this->update([
'reminder_3_sent' => true,
'reminder_3_sent_at' => now(),
]);
}
public function markAsDeactivated(): void
{
$this->update([
'deactivation_sent' => true,
'deactivated_at' => now(),
]);
}
}

View File

@@ -5,6 +5,29 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property int $id
* @property string $identifier
* @property string $name
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType whereDescription($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType whereIdentifier($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType withTrashed(bool $withTrashed = true)
* @method static \Illuminate\Database\Eloquent\Builder<static>|MemberType withoutTrashed()
* @mixin \Eloquent
*/
class MemberType extends Model
{
use SoftDeletes;

View File

@@ -16,7 +16,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
* @property string $status
* @property string|null $validation_date
* @property string|null $payment_method
* @property string $amount
* @property numeric $amount
* @property string $payment_status
* @property string|null $note_public
* @property string|null $note_private
@@ -30,7 +30,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
* @property-read \App\Models\Package $package
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Service> $services
* @property-read int|null $services_count
*
* @method static \Illuminate\Database\Eloquent\Builder<static>|Membership newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Membership newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Membership query()
@@ -52,7 +51,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
* @method static \Illuminate\Database\Eloquent\Builder<static>|Membership whereStatus($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Membership whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Membership whereValidationDate($value)
*
* @mixin \Eloquent
*/
class Membership extends Model

View File

@@ -22,7 +22,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @method static \Illuminate\Database\Eloquent\Builder<static>|NextCloudMember whereMemberId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|NextCloudMember whereNextcloudUserId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|NextCloudMember whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|NextCloudMember whereUsername($value)
* @mixin \Eloquent
*/
class NextCloudMember extends Model

View File

@@ -6,6 +6,36 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property int $id
* @property string $identifier
* @property string $name
* @property string $subject
* @property string $body
* @property array<array-key, mixed>|null $variables
* @property bool $is_active
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
* @method static \Database\Factories\NotificationTemplateFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate onlyTrashed()
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate whereBody($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate whereDeletedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate whereIdentifier($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate whereIsActive($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate whereSubject($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate whereVariables($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate withTrashed(bool $withTrashed = true)
* @method static \Illuminate\Database\Eloquent\Builder<static>|NotificationTemplate withoutTrashed()
* @mixin \Eloquent
*/
class NotificationTemplate extends Model
{
use HasFactory, SoftDeletes;

View File

@@ -3,14 +3,13 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property int $id
* @property string $identifier
* @property string $name
* @property string|null $description
* @property string $price
* @property numeric $price
* @property int $is_active
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
@@ -42,5 +41,4 @@ class Package extends Model
{
return __('packages.fields.'.$attribute);
}
}

View File

@@ -30,7 +30,6 @@ use Spatie\Permission\Traits\HasRoles;
* @property-read int|null $permissions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Spatie\Permission\Models\Role> $roles
* @property-read int|null $roles_count
*
* @method static \Database\Factories\UserFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newQuery()
@@ -47,7 +46,6 @@ use Spatie\Permission\Traits\HasRoles;
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User withoutPermission($permissions)
* @method static \Illuminate\Database\Eloquent\Builder<static>|User withoutRole($roles, $guard = null)
*
* @mixin \Eloquent
*/
class User extends Authenticatable implements FilamentUser

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Notifications;
use App\Models\Member;
use App\Models\NotificationTemplate;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class MemberAccountDeactivatedNotification extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(
public readonly Member $member,
) {}
/**
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['mail'];
}
public function toMail(object $notifiable): MailMessage
{
$template = NotificationTemplate::findByIdentifier('member_account_deactivated');
$vars = [
'member_name' => $this->member->full_name,
'app_name' => config('app.front_name'),
];
return (new MailMessage)
->subject($template->renderSubject($vars))
->view('notifications.mail-template', [
'body' => $template->renderBody($vars),
]);
}
/**
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [];
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Notifications;
use App\Models\Member;
use App\Models\NotificationTemplate;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class MemberRenewalReminder2Notification extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(
public readonly Member $member,
) {}
/**
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['mail'];
}
public function toMail(object $notifiable): MailMessage
{
$template = NotificationTemplate::findByIdentifier('member_renewal_reminder_2');
$vars = [
'member_name' => $this->member->full_name,
'app_name' => config('app.front_name'),
];
return (new MailMessage)
->subject($template->renderSubject($vars))
->view('notifications.mail-template', [
'body' => $template->renderBody($vars),
]);
}
/**
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [];
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Notifications;
use App\Models\Member;
use App\Models\NotificationTemplate;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class MemberRenewalReminder3Notification extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(
public readonly Member $member,
) {}
/**
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['mail'];
}
public function toMail(object $notifiable): MailMessage
{
$template = NotificationTemplate::findByIdentifier('member_renewal_reminder_3');
$vars = [
'member_name' => $this->member->full_name,
'app_name' => config('app.front_name'),
];
return (new MailMessage)
->subject($template->renderSubject($vars))
->view('notifications.mail-template', [
'body' => $template->renderBody($vars),
]);
}
/**
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [];
}
}

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace App\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use App\Models\MemberGroup;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Foundation\Auth\User as AuthUser;
class MemberGroupPolicy
{
@@ -66,5 +66,4 @@ class MemberGroupPolicy
{
return $authUser->can('Reorder:MemberGroup');
}
}

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace App\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use App\Models\Member;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Foundation\Auth\User as AuthUser;
class MemberPolicy
{
@@ -66,5 +66,4 @@ class MemberPolicy
{
return $authUser->can('Reorder:Member');
}
}

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace App\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use App\Models\Membership;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Foundation\Auth\User as AuthUser;
class MembershipPolicy
{
@@ -66,5 +66,4 @@ class MembershipPolicy
{
return $authUser->can('Reorder:Membership');
}
}

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace App\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use App\Models\Package;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Foundation\Auth\User as AuthUser;
class PackagePolicy
{
@@ -66,5 +66,4 @@ class PackagePolicy
{
return $authUser->can('Reorder:Package');
}
}

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace App\Policies;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Foundation\Auth\User as AuthUser;
use Spatie\Permission\Models\Role;
use Illuminate\Auth\Access\HandlesAuthorization;
class RolePolicy
{
@@ -66,5 +66,4 @@ class RolePolicy
{
return $authUser->can('Reorder:Role');
}
}

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace App\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use App\Models\Service;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Foundation\Auth\User as AuthUser;
class ServicePolicy
{
@@ -66,5 +66,4 @@ class ServicePolicy
{
return $authUser->can('Reorder:Service');
}
}

View File

@@ -2,8 +2,8 @@
namespace App\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Foundation\Auth\User as AuthUser;
class UserPolicy
{
@@ -63,5 +63,4 @@ class UserPolicy
{
return $authUser->can('Reorder:User');
}
}

View File

@@ -9,9 +9,13 @@ use Illuminate\Support\Facades\Http;
class DolibarrService
{
protected string $baseUrl;
protected string $htaccessUrl;
protected string $username;
protected string $password;
protected string $apiKey;
public function __construct()
@@ -37,6 +41,7 @@ class DolibarrService
/**
* Get all members
*
* @throws ConnectionException
*/
public function getAllMembers(int $limit = 400, string $sortField = 't.rowid', string $sortOrder = 'ASC'): array
@@ -52,6 +57,7 @@ class DolibarrService
/**
* Get member subscriptions
*
* @throws ConnectionException
*/
public function getMemberSubscriptions(int|string $id): array
@@ -66,6 +72,7 @@ class DolibarrService
*
* @param int|string $id The Dolibarr member ID (rowid)
* @param array $data Array of attributes to update (e.g. ['email' => 'new@email.com', 'array_options' => ['options_custom' => 'val']])
*
* @throws ConnectionException
*/
public function updateMember(int|string $id, array $data): bool
@@ -77,6 +84,4 @@ class DolibarrService
return $response->successful();
}
}

View File

@@ -18,7 +18,7 @@ class ISPConfigMailService extends ISPConfigService
public function getAllMailDomains(): array
{
return Cache::remember(
"ispconfig.mail.domains.all",
'ispconfig.mail.domains.all',
config('services.ispconfig.cache_ttl'),
fn () => $this->call('mail_domain_get', ['primary_id' => -1])
);
@@ -30,7 +30,7 @@ class ISPConfigMailService extends ISPConfigService
public function getAllMailUsers(): array
{
return Cache::remember(
"ispconfig.mail.users.all",
'ispconfig.mail.users.all',
config('services.ispconfig.cache_ttl'),
fn () => $this->call('mail_user_get', ['primary_id' => -1])
);
@@ -105,7 +105,7 @@ class ISPConfigMailService extends ISPConfigService
// On récupère l'enregistrement COMPLET (OBLIGATOIRE)
$mailUserRecord = $this->call('mail_user_get', [
$mailuserId
$mailuserId,
]);
if (! is_array($mailUserRecord)) {
@@ -121,11 +121,9 @@ class ISPConfigMailService extends ISPConfigService
$result = $this->call('mail_user_update', [
0, // client_id (ADMIN)
$mailuserId, // primary_id
$mailUserRecord // FULL RECORD
$mailUserRecord, // FULL RECORD
]);
return (bool) $result;
}
}

View File

@@ -4,15 +4,18 @@ namespace App\Services\ISPConfig;
use Exception;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use SoapClient;
use SoapFault;
use Illuminate\Support\Facades\Log;
class ISPConfigService
{
protected ?SoapClient $client = null;
protected ?string $sessionId = null;
protected array $config;
protected string $serverType;
/**
@@ -44,8 +47,8 @@ class ISPConfigService
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
]
])
],
]),
]);
$this->sessionId = $this->client->login(
@@ -53,17 +56,17 @@ class ISPConfigService
$this->config['password']
);
Log::info("ISPConfig connected", [
Log::info('ISPConfig connected', [
'server' => $this->serverType,
'session_id' => $this->sessionId
'session_id' => $this->sessionId,
]);
} catch (SoapFault $e) {
Log::error("ISPConfig connection failed", [
Log::error('ISPConfig connection failed', [
'server' => $this->serverType,
'error' => $e->getMessage()
'error' => $e->getMessage(),
]);
throw new Exception("Failed to connect to ISPConfig: " . $e->getMessage());
throw new Exception('Failed to connect to ISPConfig: '.$e->getMessage());
}
}
@@ -72,11 +75,11 @@ class ISPConfigService
if ($this->client && $this->sessionId) {
try {
$this->client->logout($this->sessionId);
Log::info("ISPConfig disconnected", ['server' => $this->serverType]);
Log::info('ISPConfig disconnected', ['server' => $this->serverType]);
} catch (SoapFault $e) {
Log::warning("ISPConfig logout failed", [
Log::warning('ISPConfig logout failed', [
'server' => $this->serverType,
'error' => $e->getMessage()
'error' => $e->getMessage(),
]);
}
@@ -97,21 +100,21 @@ class ISPConfigService
$result = $this->client->__soapCall($method, $params);
Log::debug("ISPConfig API call", [
Log::debug('ISPConfig API call', [
'method' => $method,
'server' => $this->serverType,
'success' => true
'success' => true,
]);
return $result;
} catch (SoapFault $e) {
Log::error("ISPConfig API call failed", [
Log::error('ISPConfig API call failed', [
'method' => $method,
'server' => $this->serverType,
'error' => $e->getMessage()
'error' => $e->getMessage(),
]);
throw new Exception("ISPConfig API call failed: " . $e->getMessage());
throw new Exception('ISPConfig API call failed: '.$e->getMessage());
}
}
@@ -126,7 +129,7 @@ class ISPConfigService
public function getAllClients(): array
{
return Cache::remember(
"ispconfig.mail.clients.all",
'ispconfig.mail.clients.all',
config('services.ispconfig.cache_ttl'),
fn () => $this->call('client_get_all')
);

View File

@@ -0,0 +1,152 @@
<?php
namespace App\Services;
use App\Models\Member;
use App\Models\MemberRenewalReminder;
use App\Notifications\MemberAccountDeactivatedNotification;
use App\Notifications\MemberRenewalReminder2Notification;
use App\Notifications\MemberRenewalReminder3Notification;
use App\Notifications\MemberRenewalReminderNotification;
use App\Services\Dolibarr\DolibarrService;
class MemberRenewalService
{
public function __construct(
protected DolibarrService $dolibarr,
protected MemberService $memberService
) {}
public function sendReminder1(Member $member, int $year): bool
{
$reminder = MemberRenewalReminder::firstOrCreate(
['member_id' => $member->id, 'renewal_year' => $year],
);
if ($reminder->reminder_1_sent) {
return false;
}
if ($this->hasActiveMembershipForYear($member, $year)) {
return false;
}
if (! $this->shouldSendRenewalForYear($member, $year)) {
return false;
}
$member->notify(new MemberRenewalReminderNotification($member));
$reminder->markAsReminder1Sent();
return true;
}
public function sendReminder2(Member $member, int $year): bool
{
$reminder = MemberRenewalReminder::firstOrCreate(
['member_id' => $member->id, 'renewal_year' => $year],
);
if ($reminder->reminder_2_sent) {
return false;
}
if ($this->hasActiveMembershipForYear($member, $year)) {
return false;
}
if (! $this->shouldSendRenewalForYear($member, $year)) {
return false;
}
$member->notify(new MemberRenewalReminder2Notification($member));
$reminder->markAsReminder2Sent();
return true;
}
public function sendReminder3(Member $member, int $year): bool
{
$reminder = MemberRenewalReminder::firstOrCreate(
['member_id' => $member->id, 'renewal_year' => $year],
);
if ($reminder->reminder_3_sent) {
return false;
}
if ($this->hasActiveMembershipForYear($member, $year)) {
return false;
}
if (! $this->shouldSendRenewalForYear($member, $year)) {
return false;
}
$member->notify(new MemberRenewalReminder3Notification($member));
$reminder->markAsReminder3Sent();
return true;
}
public function deactivateMember(Member $member, int $year): bool
{
$reminder = MemberRenewalReminder::firstOrCreate(
['member_id' => $member->id, 'renewal_year' => $year],
);
if ($reminder->deactivation_sent) {
return false;
}
if ($this->hasActiveMembershipForYear($member, $year)) {
return false;
}
$this->memberService->deactivateMember($member);
$member->notify(new MemberAccountDeactivatedNotification($member));
$reminder->markAsDeactivated();
return true;
}
protected function hasActiveMembershipForYear(Member $member, int $year): bool
{
$yearStart = "{$year}-01-01";
$yearEnd = "{$year}-12-31";
return $member->memberships()
->where('status', 'active')
->where(function ($query) use ($yearStart, $yearEnd) {
$query->where(function ($q) use ($yearStart, $yearEnd) {
$q->where('start_date', '<=', $yearEnd)
->where(function ($q2) use ($yearStart) {
$q2->whereNull('end_date')
->orWhere('end_date', '>=', $yearStart);
});
});
})
->exists();
}
protected function shouldSendRenewalForYear(Member $member, int $year): bool
{
$currentMembership = $member->memberships()
->where('status', 'active')
->orderBy('end_date', 'desc')
->first();
if (! $currentMembership || ! $currentMembership->end_date) {
return false;
}
$expirationYear = (int) date('Y', strtotime($currentMembership->end_date));
return $expirationYear === ($year - 1);
}
}

View File

@@ -27,6 +27,7 @@ class NextcloudService
/**
* Disable user by email
*
* @throws ConnectionException
*/
public function disableUserByEmail(string $email): void
@@ -34,7 +35,8 @@ class NextcloudService
$userId = $this->findUserIdByEmail($email);
if (! $userId) {
Log::warning("Utilisateur Nextcloud introuvable", ['email' => $email]);
Log::warning('Utilisateur Nextcloud introuvable', ['email' => $email]);
return;
}
@@ -47,6 +49,7 @@ class NextcloudService
/**
* Desable user by id
*
* @throws ConnectionException
*/
public function disableUserById(string $userId): void
@@ -110,6 +113,4 @@ class NextcloudService
->get("/cloud/users/{$userId}")
->json('ocs.data') ?? [];
}
}

View File

@@ -29,7 +29,7 @@ return new class extends Migration
'valid',
'pending',
'cancelled',
'excluded'
'excluded',
])->default('draft');
// Nature

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('member_renewal_reminders', function (Blueprint $table) {
$table->id();
$table->foreignId('member_id')->constrained('members')->onDelete('cascade');
$table->year('renewal_year');
$table->boolean('reminder_1_sent')->default(false);
$table->boolean('reminder_2_sent')->default(false);
$table->boolean('reminder_3_sent')->default(false);
$table->boolean('deactivation_sent')->default(false);
$table->timestamp('reminder_1_sent_at')->nullable();
$table->timestamp('reminder_2_sent_at')->nullable();
$table->timestamp('reminder_3_sent_at')->nullable();
$table->timestamp('deactivated_at')->nullable();
$table->timestamps();
$table->unique(['member_id', 'renewal_year']);
});
}
public function down(): void
{
Schema::dropIfExists('member_renewal_reminders');
}
};

View File

@@ -172,13 +172,67 @@ class NotificationTemplateSeeder extends Seeder
NotificationTemplate::updateOrCreate(
['identifier' => 'member_renewal_reminder'],
[
'name' => 'Relance adhésion — générique',
'subject' => 'Rappel concernant votre adhésion — {app_name}',
'name' => 'Relance renouvellement — 1ère relance (début décembre)',
'subject' => 'Renouvellement de votre adhésion pour l\'année à venir — {app_name}',
'body' => '<p>Bonjour {member_name},</p>'
.'<p>Nous vous contactons au sujet de votre adhésion au sein de notre association.</p>'
.'<p>Si votre adhésion est arrivée à expiration ou si vous n\'avez pas encore finalisé votre inscription, '
.'nous vous invitons à régulariser votre situation afin de continuer à bénéficier de l\'ensemble de nos services.</p>'
.'<p>Pour toute question ou besoin d\'assistance, n\'hésitez pas à nous contacter.</p>'
.'<p>L\'année touche à sa fin et nous vous invitons à renouveler votre adhésion pour l\'année prochaine.</p>'
.'<p>Cela vous permettra de continuer à bénéficier de l\'ensemble de nos services sans interruption.</p>'
.'<p>Nous restons à votre disposition pour toute question.</p>'
.'<p>Cordialement,<br>L\'équipe {app_name}</p>',
'variables' => [
'member_name' => 'Nom complet du membre',
'app_name' => 'Nom de l\'application',
],
'is_active' => true,
]
);
NotificationTemplate::updateOrCreate(
['identifier' => 'member_renewal_reminder_2'],
[
'name' => 'Relance renouvellement — 2ème relance (mi-décembre)',
'subject' => 'Pensez à renouveler votre adhésion — {app_name}',
'body' => '<p>Bonjour {member_name},</p>'
.'<p>Nous vous avons récemment contacté concernant le renouvellement de votre adhésion pour l\'année prochaine.</p>'
.'<p>Si vous n\'avez pas encore renouvelé, nous vous encourageons à le faire rapidement pour garantir la continuité de vos services.</p>'
.'<p>N\'hésitez pas à nous contacter si vous avez des questions.</p>'
.'<p>Cordialement,<br>L\'équipe {app_name}</p>',
'variables' => [
'member_name' => 'Nom complet du membre',
'app_name' => 'Nom de l\'application',
],
'is_active' => true,
]
);
NotificationTemplate::updateOrCreate(
['identifier' => 'member_renewal_reminder_3'],
[
'name' => 'Relance renouvellement — 3ème relance (début janvier)',
'subject' => 'Dernière relance : renouvellement de votre adhésion — {app_name}',
'body' => '<p>Bonjour {member_name},</p>'
.'<p>Nous vous contactons une dernière fois concernant le renouvellement de votre adhésion.</p>'
.'<p><strong>Sans action de votre part d\'ici la fin janvier, votre compte et vos services seront automatiquement désactivés.</strong></p>'
.'<p>Pour éviter toute interruption, nous vous invitons à renouveler dès maintenant.</p>'
.'<p>Pour toute question, n\'hésitez pas à nous contacter.</p>'
.'<p>Cordialement,<br>L\'équipe {app_name}</p>',
'variables' => [
'member_name' => 'Nom complet du membre',
'app_name' => 'Nom de l\'application',
],
'is_active' => true,
]
);
NotificationTemplate::updateOrCreate(
['identifier' => 'member_account_deactivated'],
[
'name' => 'Compte désactivé pour non-renouvellement',
'subject' => 'Votre compte {app_name} a été désactivé',
'body' => '<p>Bonjour {member_name},</p>'
.'<p>Faute de renouvellement de votre adhésion, votre compte et vos services ont été désactivés.</p>'
.'<p>Si vous souhaitez réactiver votre compte, n\'hésitez pas à nous contacter.</p>'
.'<p>Nous vous remercions pour la confiance que vous nous avez accordée.</p>'
.'<p>Cordialement,<br>L\'équipe {app_name}</p>',
'variables' => [
'member_name' => 'Nom complet du membre',

View File

@@ -1,4 +1,5 @@
<?php
namespace Deployer;
require 'recipe/laravel.php';

View File

@@ -8,5 +8,5 @@ return [
'responses' => [
'success' => 'Your message has been sent successfully',
'error' => 'An error occurred while sending the message',
]
],
];

View File

@@ -9,6 +9,6 @@ return [
'description' => 'Description',
'created_at' => 'Created at',
'Updated at' => 'Updated at',
'view_members' => 'View members of this group'
'view_members' => 'View members of this group',
],
];

View File

@@ -8,5 +8,5 @@ return [
'name' => 'Name',
'description' => 'Description',
'is_active' => 'Active',
]
],
];

View File

@@ -9,5 +9,5 @@ return [
'description' => 'Description',
'url' => 'URL',
'icon' => 'Icon',
]
],
];

View File

@@ -8,5 +8,5 @@ return [
'responses' => [
'success' => 'Votre message a été envoyé avec succès. Nous vous recontacterons dans les plus brefs délais.',
'error' => 'Une erreur est survenue lors de l\'envoi du message',
]
],
];

View File

@@ -9,6 +9,6 @@ return [
'description' => 'Description',
'created_at' => 'Créé le',
'updated_at' => 'Mis à jour le',
'view_members' => 'Voir les membres de ce groupe'
'view_members' => 'Voir les membres de ce groupe',
],
];

View File

@@ -8,5 +8,5 @@ return [
'name' => 'Nom',
'description' => 'Description',
'is_active' => 'Actif',
]
],
];

0
memberships Normal file
View File

View File

@@ -2,7 +2,24 @@
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');
Schedule::command('members:renewal-process --phase=reminder_1')
->yearlyOn(12, 1, '09:00')
->description('1ère relance renouvellement adhésion');
Schedule::command('members:renewal-process --phase=reminder_2')
->yearlyOn(12, 15, '09:00')
->description('2ème relance renouvellement adhésion');
Schedule::command('members:renewal-process --phase=reminder_3')
->yearlyOn(1, 5, '09:00')
->description('3ème relance renouvellement adhésion');
Schedule::command('members:renewal-process --phase=deactivate')
->yearlyOn(1, 28, '09:00')
->description('Désactivation comptes non renouvelés');