Hintergründe zum EventInvitePage Layout Customizer hinzugefügt. Badge und CTA entfernt, Textfelder zu Textareas gemacht. Geschenkgutscheine verbessert, E-Mail-Versand ergänzt + Resend + Confirmationseite mit Code-Copy und Link zur Package-Seite, die den Code als URL-Parameter enthält.
This commit is contained in:
@@ -6,13 +6,17 @@ use App\Filament\Resources\GiftVoucherResource\Pages;
|
||||
use App\Models\GiftVoucher;
|
||||
use App\Services\GiftVouchers\GiftVoucherService;
|
||||
use BackedEnum;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\BadgeColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables\Actions\ExportAction;
|
||||
use UnitEnum;
|
||||
|
||||
class GiftVoucherResource extends Resource
|
||||
@@ -92,6 +96,45 @@ class GiftVoucherResource extends Resource
|
||||
$service->refund($record, 'customer_request');
|
||||
})
|
||||
->successNotificationTitle('Gutschein erstattet'),
|
||||
Action::make('resend')
|
||||
->label('E-Mails erneut senden')
|
||||
->icon('heroicon-o-paper-airplane')
|
||||
->action(fn (GiftVoucher $record, GiftVoucherService $service) => $service->resend($record))
|
||||
->successNotificationTitle('E-Mails werden erneut versendet'),
|
||||
Action::make('schedule_delivery')
|
||||
->label('Versand terminieren')
|
||||
->icon('heroicon-o-clock')
|
||||
->form([
|
||||
DateTimePicker::make('recipient_delivery_scheduled_at')
|
||||
->label('Versenden am')
|
||||
->required()
|
||||
->minDate(now()->addMinutes(10)),
|
||||
])
|
||||
->action(function (GiftVoucher $record, array $data, GiftVoucherService $service): void {
|
||||
$service->scheduleRecipientDelivery(
|
||||
$record,
|
||||
Carbon::parse($data['recipient_delivery_scheduled_at'])
|
||||
);
|
||||
})
|
||||
->visible(fn (GiftVoucher $record): bool => ! empty($record->recipient_email)),
|
||||
Action::make('mark_redeemed')
|
||||
->label('Als eingelöst markieren')
|
||||
->color('success')
|
||||
->visible(fn (GiftVoucher $record): bool => $record->canBeRedeemed())
|
||||
->form([
|
||||
Textarea::make('note')->label('Notiz')->maxLength(250),
|
||||
])
|
||||
->action(function (GiftVoucher $record, array $data): void {
|
||||
$record->forceFill([
|
||||
'status' => GiftVoucher::STATUS_REDEEMED,
|
||||
'redeemed_at' => now(),
|
||||
'metadata' => array_merge($record->metadata ?? [], [
|
||||
'manual_note' => $data['note'] ?? null,
|
||||
'manual_marked' => true,
|
||||
]),
|
||||
])->save();
|
||||
})
|
||||
->successNotificationTitle('Als eingelöst markiert'),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -106,4 +149,12 @@ class GiftVoucherResource extends Resource
|
||||
'index' => Pages\ListGiftVouchers::route('/'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function tableActions(): array
|
||||
{
|
||||
return [
|
||||
ExportAction::make()
|
||||
->label('Exportieren'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
namespace App\Filament\Resources\GiftVoucherResource\Pages;
|
||||
|
||||
use App\Filament\Resources\GiftVoucherResource;
|
||||
use App\Services\GiftVouchers\GiftVoucherService;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ListGiftVouchers extends ListRecords
|
||||
{
|
||||
@@ -11,6 +16,55 @@ class ListGiftVouchers extends ListRecords
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [];
|
||||
return [
|
||||
Action::make('issue')
|
||||
->label('Gutschein ausstellen')
|
||||
->icon('heroicon-o-plus')
|
||||
->form([
|
||||
TextInput::make('amount')
|
||||
->label('Betrag')
|
||||
->numeric()
|
||||
->required(),
|
||||
TextInput::make('currency')
|
||||
->label('Währung')
|
||||
->default('EUR')
|
||||
->maxLength(3)
|
||||
->required(),
|
||||
TextInput::make('purchaser_email')->label('E-Mail Käufer')->required(),
|
||||
TextInput::make('recipient_email')->label('E-Mail Empfänger'),
|
||||
TextInput::make('recipient_name')->label('Name Empfänger'),
|
||||
TextInput::make('message')
|
||||
->label('Nachricht')
|
||||
->maxLength(500),
|
||||
TextInput::make('code')
|
||||
->label('Code (optional)')
|
||||
->placeholder('GIFT-'.Str::upper(Str::random(8))),
|
||||
Placeholder::make('code_hint')
|
||||
->label('')
|
||||
->content('Wenn kein Code eingetragen wird, erzeugen wir automatisch einen eindeutigen Gutscheincode.'),
|
||||
])
|
||||
->action(function (array $data, GiftVoucherService $service): void {
|
||||
$payload = [
|
||||
'id' => null,
|
||||
'metadata' => [
|
||||
'type' => 'gift_voucher',
|
||||
'purchaser_email' => $data['purchaser_email'],
|
||||
'recipient_email' => $data['recipient_email'] ?? null,
|
||||
'recipient_name' => $data['recipient_name'] ?? null,
|
||||
'message' => $data['message'] ?? null,
|
||||
'gift_code' => $data['code'] ?? null,
|
||||
],
|
||||
'currency_code' => $data['currency'] ?? 'EUR',
|
||||
'totals' => [
|
||||
'grand_total' => [
|
||||
'amount' => (float) $data['amount'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$service->issueFromPaddle($payload);
|
||||
})
|
||||
->modalHeading('Geschenkgutschein ausstellen'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Api\Marketing;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\GiftVoucher;
|
||||
use App\Services\GiftVouchers\GiftVoucherCheckoutService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -41,4 +42,44 @@ class GiftVoucherCheckoutController extends Controller
|
||||
|
||||
return response()->json($checkout);
|
||||
}
|
||||
|
||||
public function show(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'checkout_id' => ['nullable', 'string', 'required_without_all:transaction_id,code'],
|
||||
'transaction_id' => ['nullable', 'string', 'required_without_all:checkout_id,code'],
|
||||
'code' => ['nullable', 'string', 'required_without_all:checkout_id,transaction_id'],
|
||||
]);
|
||||
|
||||
$voucherQuery = GiftVoucher::query();
|
||||
|
||||
if (! empty($data['checkout_id'])) {
|
||||
$voucherQuery->where('paddle_checkout_id', $data['checkout_id']);
|
||||
}
|
||||
|
||||
if (! empty($data['transaction_id'])) {
|
||||
$voucherQuery->orWhere('paddle_transaction_id', $data['transaction_id']);
|
||||
}
|
||||
|
||||
if (! empty($data['code'])) {
|
||||
$voucherQuery->orWhere('code', strtoupper($data['code']));
|
||||
}
|
||||
|
||||
$voucher = $voucherQuery->latest()->firstOrFail();
|
||||
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'code' => $voucher->code,
|
||||
'amount' => (float) $voucher->amount,
|
||||
'currency' => $voucher->currency,
|
||||
'expires_at' => optional($voucher->expires_at)->toIso8601String(),
|
||||
'recipient_name' => $voucher->recipient_name,
|
||||
'recipient_email' => $voucher->recipient_email,
|
||||
'purchaser_email' => $voucher->purchaser_email,
|
||||
'status' => $voucher->status,
|
||||
'redeemed_at' => optional($voucher->redeemed_at)->toIso8601String(),
|
||||
'refunded_at' => optional($voucher->refunded_at)->toIso8601String(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Marketing;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\GiftVoucher;
|
||||
use App\Services\GiftVouchers\GiftVoucherService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class GiftVoucherResendController extends Controller
|
||||
{
|
||||
public function __construct(private readonly GiftVoucherService $vouchers) {}
|
||||
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'code' => ['required', 'string'],
|
||||
'recipient_only' => ['sometimes', 'boolean'],
|
||||
'locale' => ['nullable', 'string'],
|
||||
'schedule_at' => ['nullable', 'date'],
|
||||
]);
|
||||
|
||||
$voucher = GiftVoucher::query()
|
||||
->where('code', strtoupper($data['code']))
|
||||
->first();
|
||||
|
||||
if (! $voucher) {
|
||||
throw ValidationException::withMessages([
|
||||
'code' => __('Voucher not found.'),
|
||||
]);
|
||||
}
|
||||
|
||||
if (! empty($data['schedule_at'])) {
|
||||
$this->vouchers->scheduleRecipientDelivery($voucher, now()->parse($data['schedule_at']), $data['locale'] ?? app()->getLocale());
|
||||
} else {
|
||||
$this->vouchers->resend($voucher, $data['locale'] ?? app()->getLocale(), $data['recipient_only'] ?? null);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'ok',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Marketing;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\GiftVoucher;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class GiftVoucherPrintController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, GiftVoucher $voucher)
|
||||
{
|
||||
if ($voucher->code !== strtoupper($request->query('code'))) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return view('marketing.gift-voucher-print', [
|
||||
'voucher' => $voucher,
|
||||
]);
|
||||
}
|
||||
}
|
||||
40
app/Jobs/NotifyGiftVoucherReminder.php
Normal file
40
app/Jobs/NotifyGiftVoucherReminder.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Mail\GiftVoucherIssued;
|
||||
use App\Models\GiftVoucher;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class NotifyGiftVoucherReminder implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct(public GiftVoucher $voucher, public bool $expiry = false) {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$voucher = $this->voucher->fresh();
|
||||
|
||||
if (! $voucher || $voucher->isRedeemed() || $voucher->isRefunded() || $voucher->isExpired()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$recipients = collect([$voucher->purchaser_email, $voucher->recipient_email])
|
||||
->filter()
|
||||
->unique()
|
||||
->all();
|
||||
|
||||
foreach ($recipients as $email) {
|
||||
Mail::to($email)->queue((new GiftVoucherIssued($voucher, $email === $voucher->recipient_email))->with([
|
||||
'isReminder' => true,
|
||||
'isExpiry' => $this->expiry,
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
58
app/Mail/GiftVoucherIssued.php
Normal file
58
app/Mail/GiftVoucherIssued.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\GiftVoucher;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
|
||||
class GiftVoucherIssued extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public GiftVoucher $voucher,
|
||||
public bool $forRecipient = false
|
||||
) {}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
$amount = number_format((float) $this->voucher->amount, 2);
|
||||
|
||||
return new Envelope(
|
||||
subject: $this->forRecipient
|
||||
? __('emails.gift_voucher.recipient.subject', ['amount' => $amount, 'currency' => $this->voucher->currency])
|
||||
: __('emails.gift_voucher.purchaser.subject', ['amount' => $amount, 'currency' => $this->voucher->currency]),
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
$amount = number_format((float) $this->voucher->amount, 2);
|
||||
$printUrl = URL::signedRoute('marketing.gift-vouchers.print', [
|
||||
'locale' => app()->getLocale(),
|
||||
'voucher' => $this->voucher->id,
|
||||
'code' => $this->voucher->code,
|
||||
]);
|
||||
|
||||
return new Content(
|
||||
view: 'emails.gift-voucher',
|
||||
with: [
|
||||
'voucher' => $this->voucher,
|
||||
'amount' => $amount,
|
||||
'currency' => $this->voucher->currency,
|
||||
'forRecipient' => $this->forRecipient,
|
||||
'printUrl' => $printUrl,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,8 @@ class GiftVoucher extends Model
|
||||
'expires_at',
|
||||
'redeemed_at',
|
||||
'refunded_at',
|
||||
'recipient_delivery_scheduled_at',
|
||||
'recipient_delivery_sent_at',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
@@ -47,6 +49,8 @@ class GiftVoucher extends Model
|
||||
'expires_at' => 'datetime',
|
||||
'redeemed_at' => 'datetime',
|
||||
'refunded_at' => 'datetime',
|
||||
'recipient_delivery_scheduled_at' => 'datetime',
|
||||
'recipient_delivery_sent_at' => 'datetime',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
|
||||
|
||||
@@ -178,6 +178,26 @@ class AppServiceProvider extends ServiceProvider
|
||||
];
|
||||
});
|
||||
|
||||
RateLimiter::for('gift-lookup', function (Request $request) {
|
||||
$code = strtoupper((string) $request->query('code'));
|
||||
$ip = $request->ip() ?? 'unknown';
|
||||
|
||||
return [
|
||||
Limit::perMinute(30)->by('gift-lookup:ip:'.$ip),
|
||||
Limit::perMinute(10)->by('gift-lookup:code:'.($code ?: $ip)),
|
||||
];
|
||||
});
|
||||
|
||||
RateLimiter::for('gift-resend', function (Request $request) {
|
||||
$code = strtoupper((string) $request->input('code'));
|
||||
$ip = $request->ip() ?? 'unknown';
|
||||
|
||||
return [
|
||||
Limit::perMinute(10)->by('gift-resend:ip:'.$ip),
|
||||
Limit::perHour(5)->by('gift-resend:code:'.($code ?: $ip)),
|
||||
];
|
||||
});
|
||||
|
||||
Inertia::share('locale', fn () => app()->getLocale());
|
||||
Inertia::share('analytics', static function () {
|
||||
$config = config('services.matomo');
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace App\Services\GiftVouchers;
|
||||
use App\Enums\CouponStatus;
|
||||
use App\Enums\CouponType;
|
||||
use App\Jobs\SyncCouponToPaddle;
|
||||
use App\Mail\GiftVoucherIssued;
|
||||
use App\Jobs\NotifyGiftVoucherReminder;
|
||||
use App\Models\Coupon;
|
||||
use App\Models\GiftVoucher;
|
||||
use App\Models\Package;
|
||||
@@ -12,8 +14,10 @@ use App\Services\Paddle\PaddleTransactionService;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class GiftVoucherService
|
||||
{
|
||||
@@ -28,9 +32,19 @@ class GiftVoucherService
|
||||
$priceId = $this->resolvePriceId($payload);
|
||||
$amount = $this->resolveAmount($payload);
|
||||
$currency = Str::upper($this->resolveCurrency($payload));
|
||||
$locale = $metadata['app_locale'] ?? app()->getLocale();
|
||||
$existing = null;
|
||||
|
||||
$expiresAt = now()->addYears((int) config('gift-vouchers.default_valid_years', 5));
|
||||
|
||||
if (! empty($payload['id'])) {
|
||||
$existing = GiftVoucher::query()
|
||||
->where('paddle_transaction_id', $payload['id'])
|
||||
->first();
|
||||
}
|
||||
|
||||
$mergedMetadata = array_merge($existing?->metadata ?? [], $metadata);
|
||||
|
||||
$voucher = GiftVoucher::query()->updateOrCreate(
|
||||
[
|
||||
'paddle_transaction_id' => $payload['id'] ?? null,
|
||||
@@ -46,7 +60,7 @@ class GiftVoucherService
|
||||
'message' => $metadata['message'] ?? null,
|
||||
'paddle_checkout_id' => $payload['checkout_id'] ?? Arr::get($payload, 'details.checkout_id'),
|
||||
'paddle_price_id' => $priceId,
|
||||
'metadata' => $metadata,
|
||||
'metadata' => $mergedMetadata,
|
||||
'expires_at' => $expiresAt,
|
||||
'refunded_at' => null,
|
||||
'redeemed_at' => null,
|
||||
@@ -59,9 +73,29 @@ class GiftVoucherService
|
||||
SyncCouponToPaddle::dispatch($coupon);
|
||||
}
|
||||
|
||||
$notificationsSent = (bool) Arr::get($voucher->metadata ?? [], 'notifications_sent', false);
|
||||
|
||||
if (! $notificationsSent) {
|
||||
$this->sendNotifications($voucher, locale: $locale);
|
||||
}
|
||||
|
||||
return $voucher;
|
||||
}
|
||||
|
||||
public function resend(GiftVoucher $voucher, ?string $locale = null, ?bool $recipientOnly = null): void
|
||||
{
|
||||
$this->sendNotifications($voucher, force: true, locale: $locale, recipientOnly: $recipientOnly);
|
||||
}
|
||||
|
||||
public function scheduleRecipientDelivery(GiftVoucher $voucher, Carbon $when, ?string $locale = null): void
|
||||
{
|
||||
$voucher->forceFill([
|
||||
'recipient_delivery_scheduled_at' => $when,
|
||||
])->save();
|
||||
|
||||
$this->sendNotifications($voucher, force: true, when: $when, locale: $locale, recipientOnly: true);
|
||||
}
|
||||
|
||||
public function markRedeemed(?Coupon $coupon, ?string $transactionId = null): void
|
||||
{
|
||||
if (! $coupon?->giftVoucher) {
|
||||
@@ -212,4 +246,65 @@ class GiftVoucherService
|
||||
{
|
||||
return 'GIFT-'.Str::upper(Str::random(8));
|
||||
}
|
||||
|
||||
protected function sendNotifications(
|
||||
GiftVoucher $voucher,
|
||||
bool $force = false,
|
||||
?Carbon $when = null,
|
||||
?string $locale = null,
|
||||
?bool $recipientOnly = null
|
||||
): void {
|
||||
$alreadySent = (bool) Arr::get($voucher->metadata ?? [], 'notifications_sent', false);
|
||||
|
||||
if ($alreadySent && ! $force) {
|
||||
return;
|
||||
}
|
||||
|
||||
$purchaserMail = $voucher->purchaser_email ? Mail::to($voucher->purchaser_email) : null;
|
||||
$recipientMail = $voucher->recipient_email && $voucher->recipient_email !== $voucher->purchaser_email
|
||||
? Mail::to($voucher->recipient_email)
|
||||
: null;
|
||||
|
||||
if (! $recipientOnly && $purchaserMail) {
|
||||
$mailable = (new GiftVoucherIssued($voucher, false))->locale($locale);
|
||||
$when ? $purchaserMail->later($when, $mailable) : $purchaserMail->queue($mailable);
|
||||
}
|
||||
|
||||
if ($recipientMail) {
|
||||
$mailable = (new GiftVoucherIssued($voucher, true))->locale($locale);
|
||||
$when ? $recipientMail->later($when, $mailable) : $recipientMail->queue($mailable);
|
||||
}
|
||||
|
||||
$metadata = $voucher->metadata ?? [];
|
||||
if (! $recipientOnly) {
|
||||
$metadata['notifications_sent'] = true;
|
||||
}
|
||||
$voucher->forceFill([
|
||||
'metadata' => $metadata,
|
||||
'recipient_delivery_sent_at' => $when ? $voucher->recipient_delivery_sent_at : ($recipientMail ? now() : $voucher->recipient_delivery_sent_at),
|
||||
])->save();
|
||||
|
||||
$this->scheduleReminders($voucher);
|
||||
}
|
||||
|
||||
protected function scheduleReminders(GiftVoucher $voucher): void
|
||||
{
|
||||
if ($voucher->isRedeemed() || $voucher->isRefunded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$reminderDays = (int) config('gift-vouchers.reminder_days', 7);
|
||||
$expiryReminderDays = (int) config('gift-vouchers.expiry_reminder_days', 14);
|
||||
|
||||
if ($reminderDays > 0) {
|
||||
NotifyGiftVoucherReminder::dispatch($voucher)->delay(now()->addDays($reminderDays));
|
||||
}
|
||||
|
||||
if ($voucher->expires_at && $expiryReminderDays > 0) {
|
||||
$when = $voucher->expires_at->copy()->subDays($expiryReminderDays);
|
||||
if ($when->isFuture()) {
|
||||
NotifyGiftVoucherReminder::dispatch($voucher, true)->delay($when);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user