feat(setup preprod env and mailing interceptor)

This commit is contained in:
2026-04-10 11:47:04 +02:00
parent 203a40c713
commit 59017a2c9b
112 changed files with 6038 additions and 160 deletions

View File

@@ -2,10 +2,10 @@
namespace App\Filament\Actions;
use App\Models\Member;
use App\Models\Membership;
use Filament\Actions\Action;
use Illuminate\Support\Facades\Bus;
use App\Models\Member;
class ServiceToggleAction extends Action
{
@@ -16,7 +16,7 @@ class ServiceToggleAction extends Action
*/
public static function forService(string $serviceIdentifier): static
{
return static::make('toggle_' . $serviceIdentifier)
return static::make('toggle_'.$serviceIdentifier)
->configureForService($serviceIdentifier);
}
@@ -28,50 +28,48 @@ class ServiceToggleAction extends Action
$this->serviceIdentifier = $serviceIdentifier;
return $this
->label('Service actif')
->icon(fn (Member|Membership $record) =>
$this->getMember($record)?->hasService($serviceIdentifier)
? 'heroicon-o-check-circle'
: 'heroicon-o-x-circle'
->label(fn (Member|Membership $record) => $this->getMember($record)?->hasService($serviceIdentifier)
? 'Service actif'
: 'Activer le service'
)
->color(fn (Member|Membership $record) =>
$this->getMember($record)?->hasService($serviceIdentifier)
? 'success'
: 'gray'
->icon(fn (Member|Membership $record) => $this->getMember($record)?->hasService($serviceIdentifier)
? 'heroicon-o-check-circle'
: 'heroicon-o-x-circle'
)
->color(fn (Member|Membership $record) => $this->getMember($record)?->hasService($serviceIdentifier)
? 'success'
: 'warning'
)
->requiresConfirmation()
->modalHeading(fn (Member|Membership $record) =>
$this->getMember($record)?->hasService($serviceIdentifier)
->modalHeading(fn (Member|Membership $record) => $this->getMember($record)?->hasService($serviceIdentifier)
? 'Désactiver le service'
: 'Activer le service'
)
->modalDescription(fn (Member|Membership $record) =>
$this->getMember($record)?->hasService($serviceIdentifier)
->modalDescription(fn (Member|Membership $record) => $this->getMember($record)?->hasService($serviceIdentifier)
? 'Êtes-vous sûr·e de vouloir désactiver ce service pour ce membre ?'
: 'Êtes-vous sûr·e de vouloir activer ce service pour ce membre ?'
)
->modalSubmitActionLabel(fn (Member|Membership $record) =>
$this->getMember($record)?->hasService($serviceIdentifier)
->modalSubmitActionLabel(fn (Member|Membership $record) => $this->getMember($record)?->hasService($serviceIdentifier)
? 'Désactiver'
: 'Activer'
)
->action(function (Member|Membership $record) use ($serviceIdentifier) {
->action(function (Member|Membership $record) {
$member = $this->getMember($record);
if (!$member) {
if (! $member) {
return;
}
// @todo à discuter
/* if ($record->hasService($serviceIdentifier)) {
Bus::dispatch(
new \App\Jobs\DisableServiceJob($record, $serviceIdentifier)
);
} else {
Bus::dispatch(
new \App\Jobs\EnableServiceJob($record, $serviceIdentifier)
);
}*/
/* if ($record->hasService($serviceIdentifier)) {
Bus::dispatch(
new \App\Jobs\DisableServiceJob($record, $serviceIdentifier)
);
} else {
Bus::dispatch(
new \App\Jobs\EnableServiceJob($record, $serviceIdentifier)
);
}*/
});
}

View File

@@ -4,7 +4,11 @@ namespace App\Filament\Resources\Members\Schemas;
use App\Enums\IspconfigType;
use App\Filament\Actions\ServiceToggleAction;
use App\Filament\Resources\Memberships\MembershipResource;
use App\Models\ListmonkMember;
use App\Models\Member;
use App\Models\Membership;
use App\Models\Package;
use Filament\Actions\Action;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select;
@@ -159,11 +163,7 @@ class MemberForm
->columnSpanFull(),
])
->columns(2),
])
->visible(fn (?Member $record) => $record?->ispconfigs()
->where('type', IspconfigType::MAIL)
->exists()
),
]),
Section::make(__('members.sections.ispconfig_web'))
->afterHeader([
@@ -202,11 +202,7 @@ class MemberForm
])
->columns(3),
])
->visible(fn (?Member $record) => $record?->ispconfigs()
->where('type', IspconfigType::WEB)
->exists()
),
]),
Section::make(__('members.sections.nextcloud'))
->afterHeader([
@@ -244,10 +240,34 @@ class MemberForm
->columnSpanFull(),
])
->columns(3),
]),
Section::make(__('members.sections.listmonk'))
->afterHeader([
ServiceToggleAction::forService('listmonk'),
])
->visible(fn (?Member $record) => $record?->nextcloudAccounts()
->exists()
),
->collapsible()
->schema([
RepeatableEntry::make('listmonk_accounts')
->label(__('members.ispconfig.listmonk_data'))
->state(fn (?Member $record) => $record?->listmonkMembers()
->get()
->map(fn (ListmonkMember $lm) => $lm->toArray())
->all()
)
->schema([
TextEntry::make('listmonk_user_id')
->label(__('members.ispconfig.listmonk_id')),
ViewEntry::make('data')
->label('JSON')
->view('filament.components.json-viewer')
->viewData(fn ($state) => [
'data' => $state,
])
->columnSpanFull(),
])
->columns(2),
]),
]),
])
->contained(false),
@@ -285,9 +305,59 @@ class MemberForm
Section::make(__('members.sections.actions'))
->collapsible()
->schema([
Action::make('create-membership')
->label(__('members.actions.create_membership'))
->icon('heroicon-o-plus-circle')
->color('primary')
->modalHeading(__('members.actions.create_membership'))
->modalSubmitActionLabel(__('members.actions.create_membership_submit'))
->form([
Select::make('package_id')
->label(Membership::getAttributeLabel('package_id'))
->options(fn () => Package::all()->pluck('name', 'id'))
->searchable()
->required(),
Select::make('status')
->label(Membership::getAttributeLabel('status'))
->options([
'pending' => Membership::getAttributeLabel('pending'),
'active' => Membership::getAttributeLabel('active'),
])
->default('pending')
->required(),
Select::make('payment_status')
->label(Membership::getAttributeLabel('payment_status'))
->options([
'paid' => Membership::getAttributeLabel('paid'),
'unpaid' => Membership::getAttributeLabel('unpaid'),
'partial' => Membership::getAttributeLabel('partial'),
])
->default('unpaid')
->required(),
TextInput::make('amount')
->label(Membership::getAttributeLabel('amount'))
->numeric()
->default(0)
->required(),
DatePicker::make('start_date')
->label(Membership::getAttributeLabel('start_date'))
->default(now()),
DatePicker::make('end_date')
->label(Membership::getAttributeLabel('end_date')),
])
->action(function (array $data, Member $record) {
$membership = $record->memberships()->create([
'admin_id' => auth()->id(),
...$data,
]);
return redirect(MembershipResource::getUrl('edit', ['record' => $membership->id]));
}),
Action::make('send-payment-mail')
->label(__('members.actions.send_payment_mail'))
->icon('heroicon-o-envelope')
->color('primary')
->action(function () {
// Mail de paiement pour nouvelle inscription (Job)
}),
@@ -295,6 +365,7 @@ class MemberForm
Action::make('send-renewal-mail')
->label(__('members.actions.send_renewal_mail'))
->icon('heroicon-o-envelope')
->color('primary')
->action(function () {
// Mail de relance à créer (Job)
}),

View File

@@ -4,6 +4,7 @@ namespace App\Filament\Resources\Memberships\Schemas;
use App\Enums\IspconfigType;
use App\Filament\Actions\ServiceToggleAction;
use App\Models\ListmonkMember;
use App\Models\Membership;
use Filament\Actions\Action;
use Filament\Forms\Components\DatePicker;
@@ -127,11 +128,7 @@ class MembershipForm
->columnSpanFull(),
])
->columns(2),
])
->visible(fn (?Membership $record) => $record?->member?->ispconfigs()
->where('type', IspconfigType::MAIL)
->exists() ?? false
),
]),
Section::make(__('memberships.sections.ispconfig_web'))
->afterHeader([
@@ -170,11 +167,7 @@ class MembershipForm
->columnSpanFull(),
])
->columns(3),
])
->visible(fn (?Membership $record) => $record?->member?->ispconfigs()
->where('type', IspconfigType::WEB)
->exists() ?? false
),
]),
Section::make(__('memberships.sections.nextcloud'))
->afterHeader([
@@ -212,10 +205,34 @@ class MembershipForm
->columnSpanFull(),
])
->columns(3),
]),
Section::make(__('memberships.sections.listmonk'))
->afterHeader([
ServiceToggleAction::forService('listmonk'),
])
->visible(fn (?Membership $record) => $record?->member?->nextcloudAccounts()
->exists() ?? false
),
->collapsible()
->schema([
RepeatableEntry::make('listmonk_accounts')
->label(__('members.ispconfig.listmonk_data'))
->state(fn (?Membership $record) => $record?->member?->listmonkMembers()
->get()
->map(fn (ListmonkMember $lm) => $lm->toArray())
->all()
)
->schema([
TextEntry::make('listmonk_user_id')
->label(__('members.ispconfig.listmonk_id')),
ViewEntry::make('data')
->label('JSON')
->view('filament.components.json-viewer')
->viewData(fn ($state) => [
'data' => $state,
])
->columnSpanFull(),
])
->columns(2),
]),
]),
])
->contained(false),

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Listeners;
use App\Models\User;
use Illuminate\Mail\Events\MessageSending;
use Symfony\Component\Mime\Address;
class PreprodMailInterceptor
{
public function handle(MessageSending $event): void
{
if (! config('preprod.enabled')) {
return;
}
$message = $event->message;
$adminEmail = config('app.admin_email');
$originalRecipients = array_map(
fn (Address $address) => $address->getAddress(),
$message->getTo()
);
$isAdminMail = collect($originalRecipients)->contains(
fn (string $email) => $email === $adminEmail
|| User::where('email', $email)->exists()
);
$configKey = $isAdminMail ? 'preprod.admin_mails' : 'preprod.test_mails';
$emails = collect(explode(',', config($configKey, '')))
->map(fn (string $email) => trim($email))
->filter()
->values()
->all();
if (empty($emails)) {
return;
}
// Clear all recipient headers before redirecting
foreach (['To', 'Cc', 'Bcc'] as $header) {
while ($message->getHeaders()->has($header)) {
$message->getHeaders()->remove($header);
}
}
$message->to(...$emails);
$subject = $message->getSubject() ?? '';
if (! str_starts_with($subject, '[PREPROD]')) {
$message->subject('[PREPROD] '.$subject);
}
}
}

View File

@@ -153,6 +153,11 @@ class Member extends Model
return $this->hasMany(NextCloudMember::class, 'member_id');
}
public function listmonkMembers(): HasMany
{
return $this->hasMany(ListmonkMember::class, 'member_id');
}
public function lastActiveMembership(): HasOne
{
return $this->hasOne(Membership::class)->where('status', 'active')->latest();

View File

@@ -2,7 +2,10 @@
namespace App\Providers;
use App\Listeners\PreprodMailInterceptor;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Mail\Events\MessageSending;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@@ -20,7 +23,8 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
// Disable wrapping for all JSON responses
JsonResource::withoutWrapping();
Event::listen(MessageSending::class, PreprodMailInterceptor::class);
}
}