diff --git a/app/Console/Commands/SendAcceptanceReminder.php b/app/Console/Commands/SendAcceptanceReminder.php index 67efecbb3453..4eb4a5114fe6 100644 --- a/app/Console/Commands/SendAcceptanceReminder.php +++ b/app/Console/Commands/SendAcceptanceReminder.php @@ -3,13 +3,18 @@ namespace App\Console\Commands; use App\Mail\UnacceptedAssetReminderMail; +use App\Models\Accessory; use App\Models\Asset; use App\Models\CheckoutAcceptance; +use App\Models\Component; +use App\Models\Consumable; +use App\Models\LicenseSeat; use App\Models\Setting; use App\Models\User; use App\Notifications\CheckoutAssetNotification; use App\Notifications\CurrentInventory; use Illuminate\Console\Command; +use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Support\Facades\Mail; class SendAcceptanceReminder extends Command @@ -45,19 +50,30 @@ public function __construct() */ public function handle() { - $pending = CheckoutAcceptance::pending()->where('checkoutable_type', 'App\Models\Asset') - ->whereHas('checkoutable', function($query) { - $query->where('accepted_at', null) - ->where('declined_at', null); - }) - ->with(['assignedTo', 'checkoutable.assignedTo', 'checkoutable.model', 'checkoutable.adminuser']) - ->get(); + $pending = CheckoutAcceptance::query() + ->with([ + 'checkoutable' => function (MorphTo $morph) { + $morph->morphWith([ + Asset::class => ['model.category', 'assignedTo', 'adminuser', 'company', 'checkouts'], + Accessory::class => ['category', 'company', 'checkouts'], + LicenseSeat::class => ['user', 'license', 'checkouts'], + Component::class => ['assignedTo', 'company', 'checkouts'], + Consumable::class => ['company', 'checkouts'], + ]); + }, + 'assignedTo', + ]) + ->whereHasMorph( + 'checkoutable', + [Asset::class, Accessory::class, LicenseSeat::class, Component::class, Consumable::class], + fn ($q) => $q->whereNull('accepted_at') + ->whereNull('declined_at') + ) + ->pending() + ->get(); $count = 0; $unacceptedAssetGroups = $pending - ->filter(function($acceptance) { - return $acceptance->checkoutable_type == 'App\Models\Asset'; - }) ->map(function($acceptance) { return ['assetItem' => $acceptance->checkoutable, 'acceptance' => $acceptance]; }) diff --git a/app/Http/Controllers/ReportsController.php b/app/Http/Controllers/ReportsController.php index 1eccf4886ff7..d06e779a40e7 100644 --- a/app/Http/Controllers/ReportsController.php +++ b/app/Http/Controllers/ReportsController.php @@ -3,12 +3,21 @@ namespace App\Http\Controllers; use App\Helpers\Helper; +use App\Mail\CheckoutAccessoryMail; use App\Mail\CheckoutAssetMail; +use App\Mail\CheckoutComponentMail; +use App\Mail\CheckoutConsumableMail; +use App\Mail\CheckoutLicenseMail; use App\Models\Accessory; +use App\Models\AccessoryCheckout; use App\Models\Actionlog; use App\Models\Asset; use App\Models\AssetModel; use App\Models\Category; +use App\Models\Checkoutable; +use App\Models\Component; +use App\Models\Consumable; +use App\Models\LicenseSeat; use App\Models\Maintenance; use App\Models\CheckoutAcceptance; use App\Models\Company; @@ -18,9 +27,11 @@ use App\Models\ReportTemplate; use App\Models\Setting; use Carbon\Carbon; +use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Http\Request; use Illuminate\Http\Response; +use Illuminate\Mail\Mailable; use Illuminate\Support\Facades\Mail; use \Illuminate\Contracts\View\View; use League\Csv\Reader; @@ -1111,33 +1122,29 @@ public function getAssetAcceptanceReport($deleted = false) : View $showDeleted = $deleted == 'deleted'; $query = CheckoutAcceptance::pending() - ->where('checkoutable_type', 'App\Models\Asset') ->with([ 'checkoutable' => function (MorphTo $query) { $query->morphWith([ - AssetModel::class => ['model'], - Company::class => ['company'], - Asset::class => ['assignedTo'], - ])->with('model.category'); + Asset::class => ['model.category', 'assignedTo', 'company'], + Accessory::class => ['category','checkouts', 'company'], + LicenseSeat::class => ['user', 'license'], + Component::class => ['assignedTo', 'company'], + Consumable::class => ['company'], + ]); }, 'assignedTo' => function($query){ $query->withTrashed(); } - ]); + ])->orderByDesc('checkout_acceptances.created_at'); + if ($showDeleted) { $query->withTrashed(); } - $assetsForReport = $query->get() - ->map(function ($acceptance) { - return [ - 'assetItem' => $acceptance->checkoutable, - 'acceptance' => $acceptance, - ]; - }); + $itemsForReport = $query->get()->map(fn ($unaccepted) => Checkoutable::fromAcceptance($unaccepted)); - return view('reports/unaccepted_assets', compact('assetsForReport','showDeleted' )); + return view('reports/unaccepted_assets', compact('itemsForReport','showDeleted' )); } /** @@ -1149,41 +1156,77 @@ public function getAssetAcceptanceReport($deleted = false) : View public function sentAssetAcceptanceReminder(Request $request) : RedirectResponse { $this->authorize('reports.view'); - - if (!$acceptance = CheckoutAcceptance::pending()->find($request->input('acceptance_id'))) { + $id = $request->input('acceptance_id'); + $query = CheckoutAcceptance::query() + ->with([ + 'checkoutable' => function (MorphTo $query) { + $query->morphWith([ + Asset::class => ['model.category', 'assignedTo', 'company', 'checkouts'], + Accessory::class => ['category', 'company', 'checkouts'], + LicenseSeat::class => ['user', 'license', 'checkouts'], + Component::class => ['assignedTo', 'company', 'checkouts'], + Consumable::class => ['company', 'checkouts'], + ]); + }, + 'assignedTo' => fn ($q) => $q->withTrashed(), + ]) + ->pending(); + $acceptance = $query->find($id); + if (!$acceptance) { Log::debug('No pending acceptances'); // Redirect to the unaccepted assets report page with error return redirect()->route('reports/unaccepted_assets')->with('error', trans('general.bad_data')); } + $item = $acceptance->checkoutable; + $assignee = $acceptance->assignedTo ?? $item->assignedTo ?? null; + $email = $assignee?->email; + $locale = $assignee?->locale; - $assetItem = $acceptance->checkoutable; - - Log::debug(print_r($assetItem, true)); + Log::debug(print_r($acceptance, true)); if (is_null($acceptance->created_at)){ Log::debug('No acceptance created_at'); return redirect()->route('reports/unaccepted_assets')->with('error', trans('general.bad_data')); } else { - $logItem_res = $assetItem->checkouts()->where('created_at', '=', $acceptance->created_at)->get(); - + if($item instanceof LicenseSeat){ + $logItem_res = $item->license->checkouts()->with('adminuser')->where('created_at', '=', $acceptance->created_at)->get(); + } + else{ + $logItem_res = $item->checkouts()->with('adminuser')->where('created_at', '=', $acceptance->created_at)->get(); + } if ($logItem_res->isEmpty()){ Log::debug('Acceptance date mismatch'); return redirect()->route('reports/unaccepted_assets')->with('error', trans('general.bad_data')); } $logItem = $logItem_res[0]; } - $email = $assetItem->assignedTo?->email; - $locale = $assetItem->assignedTo?->locale; if (is_null($email) || $email === '') { return redirect()->route('reports/unaccepted_assets')->with('error', trans('general.no_email')); } - - Mail::to($email)->send((new CheckoutAssetMail($assetItem, $assetItem->assignedTo, $logItem->user, $acceptance, $logItem->note, firstTimeSending: false))->locale($locale)); + $mailable = $this->getCheckoutMailType($acceptance, $logItem); + Mail::to($email)->send($mailable->locale($locale)); return redirect()->route('reports/unaccepted_assets')->with('success', trans('admin/reports/general.reminder_sent')); } + private function getCheckoutMailType(CheckoutAcceptance $acceptance, $logItem) : Mailable + { + $lookup = [ + Accessory::class => CheckoutAccessoryMail::class, + Asset::class => CheckoutAssetMail::class, + LicenseSeat::class => CheckoutLicenseMail::class, + Consumable::class => CheckoutConsumableMail::class, + Component::class => CheckoutComponentMail::class, + ]; + $mailable= $lookup[get_class($acceptance->checkoutable)]; + return new $mailable($acceptance->checkoutable, + $acceptance->checkedOutTo ?? $acceptance->assignedTo, + $logItem->adminuser, + $acceptance, + $acceptance->note); + + } /** * sentAssetAcceptanceReminder * @@ -1215,31 +1258,41 @@ public function deleteAssetAcceptance($acceptanceId = null) : RedirectResponse public function postAssetAcceptanceReport($deleted = false) : Response { $this->authorize('reports.view'); - $showDeleted = $deleted == 'deleted'; + $showDeleted = request('deleted') === 'deleted';; /** * Get all assets with pending checkout acceptances */ - if($showDeleted) { - $acceptances = CheckoutAcceptance::pending()->where('checkoutable_type', 'App\Models\Asset')->withTrashed()->with(['assignedTo', 'checkoutable.assignedTo', 'checkoutable.model'])->get(); - } else { - $acceptances = CheckoutAcceptance::pending()->where('checkoutable_type', 'App\Models\Asset')->with(['assignedTo', 'checkoutable.assignedTo', 'checkoutable.model'])->get(); - } - $assetsForReport = $acceptances - ->filter(function($acceptance) { - return $acceptance->checkoutable_type == 'App\Models\Asset'; - }) - ->map(function($acceptance) { - return ['assetItem' => $acceptance->checkoutable, 'acceptance' => $acceptance]; - }); + $acceptances = CheckoutAcceptance::pending() + ->with([ + 'checkoutable' => function (MorphTo $acceptance) { + $acceptance->morphWith([ + Asset::class => ['model.category', 'assignedTo', 'company'], + Accessory::class => ['category','checkouts', 'company'], + LicenseSeat::class => ['user', 'license'], + Component::class => ['assignedTo', 'company'], + Consumable::class => ['company'], + ]); + }, + 'assignedTo', + ])->orderByDesc('checkout_acceptances.created_at'); + + if ($showDeleted) { + $acceptances->withTrashed(); + } + + $itemsForReport = $acceptances->get()->map(fn ($unaccepted) => Checkoutable::fromAcceptance($unaccepted)); $rows = []; $header = [ + trans('general.date'), + trans('general.type'), + trans('admin/companies/table.title'), trans('general.category'), trans('admin/hardware/form.model'), - trans('admin/hardware/form.name'), + trans('general.name'), trans('admin/hardware/table.asset_tag'), trans('admin/hardware/table.checkoutto'), ]; @@ -1247,16 +1300,19 @@ public function postAssetAcceptanceReport($deleted = false) : Response $header = array_map('trim', $header); $rows[] = implode(',', $header); - foreach ($assetsForReport as $item) { + foreach ($itemsForReport as $item) { - if ($item['assetItem'] != null){ + if ($item != null){ $row = [ ]; - $row[] = str_replace(',', '', e($item['assetItem']->model->category->name)); - $row[] = str_replace(',', '', e($item['assetItem']->model->name)); - $row[] = str_replace(',', '', e($item['assetItem']->name)); - $row[] = str_replace(',', '', e($item['assetItem']->asset_tag)); - $row[] = str_replace(',', '', e(($item['acceptance']->assignedTo) ? $item['acceptance']->assignedTo->display_name : trans('admin/reports/general.deleted_user'))); + $row[] = str_replace(',', '', $item->acceptance->created_at); + $row[] = str_replace(',', '', $item->type); + $row[] = str_replace(',', '', $item->plain_text_company); + $row[] = str_replace(',', '', $item->plain_text_category); + $row[] = str_replace(',', '', $item->plain_text_model); + $row[] = str_replace(',', '', $item->plain_text_name); + $row[] = str_replace(',', '', $item->asset_tag); + $row[] = str_replace(',', '', ($item->acceptance->assignedto) ? $item->acceptance->assignedto->display_name : trans('admin/reports/general.deleted_user')); $rows[] = implode(',', $row); } } diff --git a/app/Models/Checkoutable.php b/app/Models/Checkoutable.php new file mode 100644 index 000000000000..9bf091c15f58 --- /dev/null +++ b/app/Models/Checkoutable.php @@ -0,0 +1,79 @@ +checkoutable; + $acceptance = $unaccepted; + + $assignee = $acceptance->assignedTo; + $company = optional($unaccepted_row->company)->present()?->nameUrl() ?? ''; + $category = $model = $name = $tag = ''; + $type = $acceptance->checkoutable_item_type ?? ''; + + + if($unaccepted_row instanceof Asset){ + $category = optional($unaccepted_row->model?->category?->present())->nameUrl() ?? ''; + $model = optional($unaccepted_row->present())->modelUrl() ?? ''; + $name = optional($unaccepted_row->present())->nameUrl() ?? ''; + $tag = (string) ($unaccepted_row->asset_tag ?? ''); + } + if($unaccepted_row instanceof Accessory){ + $category = optional($unaccepted_row->category?->present())->nameUrl() ?? ''; + $model = $unaccepted_row->model_number ?? ''; + $name = optional($unaccepted_row->present())->nameUrl() ?? ''; + } + if($unaccepted_row instanceof LicenseSeat){ + $category = optional($unaccepted_row->license->category?->present())->nameUrl() ?? ''; + $company = optional($unaccepted_row->license->company?->present())?->nameUrl() ?? ''; + $model = ''; + $name = $unaccepted_row->license->present()->nameUrl() ?? ''; + } + if($unaccepted_row instanceof Consumable){ + $category = optional($unaccepted_row->category?->present())->nameUrl() ?? ''; + $model = $unaccepted_row->model_number ?? ''; + $name = $unaccepted_row?->present()?->nameUrl() ?? ''; + } + if($unaccepted_row instanceof Component){ + $category = optional($unaccepted_row->category?->present())->nameUrl() ?? ''; + $model = $unaccepted_row->model_number ?? ''; + $name = $unaccepted_row?->present()?->nameUrl() ?? ''; + } + + return new self( + acceptance_id: $acceptance->id, + company: $company, + category: $category, + model: $model, + asset_tag: $tag, + name: $name, + type: $type, + acceptance: $acceptance, + assignee: $assignee, + //plain text for CSVs + plain_text_category: ($unaccepted_row->model?->category?->name ?? $unaccepted_row->license->category?->name ?? $unaccepted_row->category?->name ?? ''), + plain_text_model: ($unaccepted_row->model?->name ?? $unaccepted_row->model_number ?? ''), + plain_text_name: ($unaccepted_row->name ?? $unaccepted_row->license?->name ?? ''), + plain_text_company: ($unaccepted_row->company)->name ?? $unaccepted_row->license->company?->name ?? '', + ); + } +} diff --git a/app/Models/Component.php b/app/Models/Component.php index 6352981418ed..c7eb0f91e0fe 100644 --- a/app/Models/Component.php +++ b/app/Models/Component.php @@ -274,7 +274,19 @@ public function checkin_email() { return $this->category?->checkin_email; } - + /** + * Get the list of checkouts for this License + * + * @author [A. Gianotto] [] + * @since [v2.0] + * @return \Illuminate\Database\Eloquent\Relations\Relation + */ + public function checkouts() + { + return $this->assetlog()->where('action_type', '=', 'checkout') + ->orderBy('created_at', 'desc') + ->withTrashed(); + } /** * Check how many items within a component are remaining diff --git a/app/Models/Consumable.php b/app/Models/Consumable.php index 806b10d31788..ed40ffc3ded0 100644 --- a/app/Models/Consumable.php +++ b/app/Models/Consumable.php @@ -316,6 +316,20 @@ public function totalCostSum() { return $this->purchase_cost !== null ? $this->qty * $this->purchase_cost : null; } + /** + * Get the list of checkouts for this consumable + * + * @author [A. Gianotto] [] + * @since [v2.0] + * @return \Illuminate\Database\Eloquent\Relations\Relation + */ + public function checkouts() + { + return $this->assetlog()->where('action_type', '=', 'checkout') + ->orderBy('created_at', 'desc') + ->withTrashed(); + } + /** * ----------------------------------------------- * BEGIN MUTATORS diff --git a/app/Models/License.php b/app/Models/License.php index 8630080d144f..6ceadf654f1e 100755 --- a/app/Models/License.php +++ b/app/Models/License.php @@ -391,7 +391,12 @@ public function checkin_email() } return false; } - + public function checkouts() + { + return $this->assetlog()->where('action_type', '=', 'checkout') + ->orderBy('created_at', 'desc') + ->withTrashed(); + } /** * Determine whether the user should be required to accept the license * diff --git a/app/Models/LicenseSeat.php b/app/Models/LicenseSeat.php index 39cb53f9d824..bf10c050a467 100755 --- a/app/Models/LicenseSeat.php +++ b/app/Models/LicenseSeat.php @@ -134,7 +134,31 @@ public function location() return false; } + /** + * Get the list of checkouts for this License + * + * @author [A. Gianotto] [] + * @since [v2.0] + * @return \Illuminate\Database\Eloquent\Relations\Relation + */ + public function checkouts() + { + return $this->assetlog()->where('action_type', '=', 'checkout') + ->orderBy('created_at', 'desc') + ->withTrashed(); + } + /** + * Establishes the license -> action logs relationship + * + * @author [A. Gianotto] [] + * @since [v3.0] + * @return \Illuminate\Database\Eloquent\Relations\Relation + */ + public function assetlog() + { + return $this->hasMany(Actionlog::class, 'item_id')->where('item_type', self::class)->orderBy('created_at', 'desc')->withTrashed(); + } /** * Query builder scope to order on department * diff --git a/database/factories/CheckoutAcceptanceFactory.php b/database/factories/CheckoutAcceptanceFactory.php index bb56ab2b4a1f..1cd139176e56 100644 --- a/database/factories/CheckoutAcceptanceFactory.php +++ b/database/factories/CheckoutAcceptanceFactory.php @@ -23,23 +23,39 @@ public function definition() 'assigned_to_id' => User::factory(), ]; } + protected static bool $skipActionLog = false; + + public function withoutActionLog(): static + { + // turn off for this create() call + static::$skipActionLog = true; + + // ensure it turns back on AFTER creating + return $this->afterCreating(function () { + static::$skipActionLog = false; + }); + } public function configure(): static { return $this->afterCreating(function (CheckoutAcceptance $acceptance) { + if (static::$skipActionLog) { + return; // short-circuit + } if ($acceptance->checkoutable instanceof Asset) { $this->createdAssociatedActionLogEntry($acceptance); } if ($acceptance->checkoutable instanceof Asset && $acceptance->assignedTo instanceof User) { $acceptance->checkoutable->update([ - 'assigned_to' => $acceptance->assigned_to_id, - 'assigned_type' => get_class($acceptance->assignedTo), + 'assigned_to' => $acceptance->assigned_to_id, + 'assigned_type'=> get_class($acceptance->assignedTo), ]); } }); } + public function forAccessory() { return $this->state([ diff --git a/resources/views/reports/unaccepted_assets.blade.php b/resources/views/reports/unaccepted_assets.blade.php index f0d93c0bed47..5dbefbc8e157 100644 --- a/resources/views/reports/unaccepted_assets.blade.php +++ b/resources/views/reports/unaccepted_assets.blade.php @@ -48,54 +48,81 @@ class="table table-striped snipe-table" }'> - {{ trans('general.date') }} + {{ trans('general.date') }} + {{ trans('general.type') }} {{ trans('admin/companies/table.title') }} {{ trans('general.category') }} {{ trans('admin/hardware/form.model') }} - {{ trans('admin/hardware/form.name') }} + {{ trans('general.name') }} {{ trans('admin/hardware/table.asset_tag') }} {{ trans('admin/hardware/table.checkoutto') }} {{ trans('table.actions') }} - @if ($assetsForReport) - @foreach ($assetsForReport as $item) - @if ($item['assetItem']) - trashed()) style="text-decoration: line-through" @endif> - {{ Helper::getFormattedDateObject($item['acceptance']->created_at, 'datetime', false) }} - {{ ($item['assetItem']->company) ? $item['assetItem']->company->name : '' }} - {!! $item['assetItem']->model->category->present()->nameUrl() !!} - {!! $item['assetItem']->present()->modelUrl() !!} - {!! $item['assetItem']->present()->nameUrl() !!} - {{ $item['assetItem']->asset_tag }} - assignedTo === null || $item['acceptance']->assignedTo->trashed()) style="text-decoration: line-through" @endif>{!! ($item['acceptance']->assignedTo) ? $item['acceptance']->assignedTo->present()->nameUrl() : trans('admin/reports/general.deleted_user') !!} - - - @if(!$item['acceptance']->trashed()) -
- @if (($item['acceptance']->assignedTo) && ($item['acceptance']->assignedTo->email)) - @csrf - - - @else - - - - - - @endif - -
- @endif + @if ($itemsForReport) + @foreach ($itemsForReport as $item) + acceptance->trashed()) style="text-decoration: line-through" @endif> + {{-- Created date --}} + + {{ Helper::getFormattedDateObject($item->acceptance->created_at, 'datetime', false) }} + + {{-- Item Type --}} + {{ $item->type }} + {{-- Company name --}} + {!! $item->company !!} -
- - - @endif - @endforeach + {{-- Category --}} + {!! $item->category !!} + + {{-- Model --}} + {!! $item->model !!} + + {{-- Name --}} + {!! $item->name !!} + + {{-- Asset tag or blank --}} + {{ $item->asset_tag }} + + {{-- Assigned To (with soft-delete strike if needed) --}} + assignee || (method_exists($item->assignee, 'trashed') && $item->assignee->trashed())) style="text-decoration: line-through" @endif> + {!! $item->assignee + ? optional($item->assignee->present())->nameUrl() ?? e($item->assignee->name) + : trans('admin/reports/general.deleted_user') !!} + + + {{-- Actions: send reminder / delete --}} + + + @unless($item->acceptance->trashed()) +
+ @csrf + + @if ($item->assignee && $item->assignee->email) + + @else + + + + + + @endif + + + +
+ @endunless + + + @endforeach @endif diff --git a/tests/Feature/Notifications/Email/AssetAcceptanceReminderTest.php b/tests/Feature/Notifications/Email/AssetAcceptanceReminderTest.php index deb3e07d2c01..04081241c90a 100644 --- a/tests/Feature/Notifications/Email/AssetAcceptanceReminderTest.php +++ b/tests/Feature/Notifications/Email/AssetAcceptanceReminderTest.php @@ -2,8 +2,19 @@ namespace Tests\Feature\Notifications\Email; +use App\Mail\CheckoutAccessoryMail; use App\Mail\CheckoutAssetMail; +use App\Mail\CheckoutComponentMail; +use App\Mail\CheckoutConsumableMail; +use App\Mail\CheckoutLicenseMail; +use App\Models\Accessory; +use App\Models\Actionlog; +use App\Models\Asset; use App\Models\CheckoutAcceptance; +use App\Models\Component; +use App\Models\Consumable; +use App\Models\License; +use App\Models\LicenseSeat; use App\Models\User; use Illuminate\Support\Facades\Mail; use PHPUnit\Framework\Attributes\DataProvider; @@ -86,16 +97,52 @@ public function testUserWithoutEmailAddressHandledGracefully($callback) public function testReminderIsSentToUser() { - $checkoutAcceptance = CheckoutAcceptance::factory()->pending()->create(); + $checkedOutBy = User::factory()->canViewReports()->create(); + + $checkoutTypes = [ + Asset::class => CheckoutAssetMail::class, + Accessory::class => CheckoutAccessoryMail::class, + LicenseSeat::class => CheckoutLicenseMail::class, + Consumable::class => CheckoutConsumableMail::class, + //for the future its setup for components, but we dont send reminders for components at the moment. +// Component::class => CheckoutComponentMail::class, + ]; - $this->actingAs(User::factory()->canViewReports()->create()) - ->post($this->routeFor($checkoutAcceptance)) + $assignee = User::factory()->create(['email' => 'test@example.com']); + foreach ($checkoutTypes as $modelClass => $mailable) { + + $item = $modelClass::factory()->create(); + $acceptance = CheckoutAcceptance::factory()->withoutActionLog()->pending()->create([ + 'checkoutable_id' => $item->id, + 'checkoutable_type' => $modelClass, + 'assigned_to_id' => $assignee->id, + ]); + + if ($modelClass === LicenseSeat::class) { + $logType = License::class; + $logId = $item->license->id; + } else { + $logType = $modelClass; + $logId = $item->id; + } + + Actionlog::factory()->create([ + 'action_type' => 'checkout', + 'created_by' => $checkedOutBy->id, + 'target_id' => $assignee->id, + 'item_type' => $logType, + 'item_id' => $logId, + 'created_at' => $acceptance->created_at, + ]); + + $this->actingAs($checkedOutBy) + ->post($this->routeFor($acceptance)) ->assertRedirect(route('reports/unaccepted_assets')); + } - Mail::assertSent(CheckoutAssetMail::class, 1); - Mail::assertSent(CheckoutAssetMail::class, function (CheckoutAssetMail $mail) use ($checkoutAcceptance) { - return $mail->hasTo($checkoutAcceptance->assignedTo->email) - && $mail->hasSubject(trans('mail.unaccepted_asset_reminder')); + Mail::assertSent($mailable, 1); + Mail::assertSent($mailable, function ($mail) use ($assignee) { + return $mail->hasTo($assignee->email); }); }