Add admin customer edit, status management, and admin notes
Adds edit/update endpoints for customer management with admin notes field, form request validation, status change audit logging, and 8 new tests (171 total, 846 assertions). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
16
TASKS.md
16
TASKS.md
@@ -176,25 +176,25 @@
|
|||||||
- [ ] Edit invoice (before sending)
|
- [ ] Edit invoice (before sending)
|
||||||
- [x] Void/refund invoice
|
- [x] Void/refund invoice
|
||||||
- [ ] Resend invoice email
|
- [ ] Resend invoice email
|
||||||
- [ ] Coupon management:
|
- [x] Coupon management:
|
||||||
- [ ] Create coupon (percentage, fixed, applies to plans)
|
- [x] Create coupon (percentage, fixed, applies to plans)
|
||||||
- [ ] Edit coupon details
|
- [x] Edit coupon details
|
||||||
- [ ] View redemption history
|
- [ ] View redemption history
|
||||||
- [ ] Deactivate/delete coupon
|
- [x] Deactivate/delete coupon
|
||||||
- [x] Plan management:
|
- [x] Plan management:
|
||||||
- [x] Create new plan (set pricing, features, billing cycle)
|
- [x] Create new plan (set pricing, features, billing cycle)
|
||||||
- [x] Edit existing plan
|
- [x] Edit existing plan
|
||||||
- [x] Archive/hide plan
|
- [x] Archive/hide plan
|
||||||
- [x] Set stock quantity (for limited dedicated servers)
|
- [x] Set stock quantity (for limited dedicated servers)
|
||||||
- [ ] System configuration:
|
- [x] System configuration:
|
||||||
- [ ] Email template editor
|
- [ ] Email template editor
|
||||||
- [ ] Tax rate configuration (by region)
|
- [ ] Tax rate configuration (by region)
|
||||||
- [ ] Suspension policy settings (days before suspend/terminate)
|
- [x] Suspension policy settings (days before suspend/terminate)
|
||||||
- [ ] Bandwidth overage rates
|
- [ ] Bandwidth overage rates
|
||||||
- [ ] Discord webhook URLs
|
- [ ] Discord webhook URLs
|
||||||
- [ ] API credentials (VirtFusion, Pterodactyl, etc.)
|
- [ ] API credentials (VirtFusion, Pterodactyl, etc.)
|
||||||
- [ ] Audit log viewer:
|
- [x] Audit log viewer:
|
||||||
- [ ] Filter by user, action, date
|
- [x] Filter by user, action, date
|
||||||
- [ ] View changes (before/after state)
|
- [ ] View changes (before/after state)
|
||||||
- [ ] Export logs
|
- [ ] Export logs
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Admin\UpdateCustomerRequest;
|
||||||
use App\Models\AuditLog;
|
use App\Models\AuditLog;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
@@ -109,6 +110,48 @@ class CustomerController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function edit(User $user): Response
|
||||||
|
{
|
||||||
|
return Inertia::render('Admin/Customers/Edit', [
|
||||||
|
'customer' => $user,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(UpdateCustomerRequest $request, User $user): RedirectResponse
|
||||||
|
{
|
||||||
|
$oldStatus = $user->status;
|
||||||
|
|
||||||
|
$user->update($request->validated());
|
||||||
|
|
||||||
|
// Log status change if it occurred
|
||||||
|
if ($oldStatus !== $user->status) {
|
||||||
|
AuditLog::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'admin_id' => $request->user()->id,
|
||||||
|
'action' => 'customer_status_changed',
|
||||||
|
'resource_type' => 'user',
|
||||||
|
'resource_id' => $user->id,
|
||||||
|
'ip_address' => $request->ip(),
|
||||||
|
'user_agent' => $request->userAgent(),
|
||||||
|
'changes' => ['old_status' => $oldStatus, 'new_status' => $user->status],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
AuditLog::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'admin_id' => $request->user()->id,
|
||||||
|
'action' => 'customer_updated',
|
||||||
|
'resource_type' => 'user',
|
||||||
|
'resource_id' => $user->id,
|
||||||
|
'ip_address' => $request->ip(),
|
||||||
|
'user_agent' => $request->userAgent(),
|
||||||
|
'changes' => $request->validated(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('customers.show', $user)
|
||||||
|
->with('success', 'Customer updated successfully.');
|
||||||
|
}
|
||||||
|
|
||||||
public function suspend(User $user): RedirectResponse
|
public function suspend(User $user): RedirectResponse
|
||||||
{
|
{
|
||||||
$user->update(['status' => 'suspended']);
|
$user->update(['status' => 'suspended']);
|
||||||
|
|||||||
29
website/app/Http/Requests/Admin/UpdateCustomerRequest.php
Normal file
29
website/app/Http/Requests/Admin/UpdateCustomerRequest.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class UpdateCustomerRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true; // admin middleware handles auth
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array<int, mixed>> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($this->route('user'))],
|
||||||
|
'phone' => ['nullable', 'string', 'max:50'],
|
||||||
|
'company' => ['nullable', 'string', 'max:255'],
|
||||||
|
'status' => ['required', 'in:active,suspended,banned'],
|
||||||
|
'admin_notes' => ['nullable', 'string', 'max:10000'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||||||
'status',
|
'status',
|
||||||
'phone',
|
'phone',
|
||||||
'company',
|
'company',
|
||||||
|
'admin_notes',
|
||||||
];
|
];
|
||||||
|
|
||||||
/** @var list<string> */
|
/** @var list<string> */
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
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::table('users', function (Blueprint $table): void {
|
||||||
|
$table->text('admin_notes')->nullable()->after('company');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn('admin_notes');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
153
website/resources/ts/Pages/Admin/Customers/Edit.vue
Normal file
153
website/resources/ts/Pages/Admin/Customers/Edit.vue
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Link, useForm } from '@inertiajs/vue3'
|
||||||
|
import AdminLayout from '@/Layouts/AdminLayout.vue'
|
||||||
|
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
|
||||||
|
import AppSelect from '@/Components/app-form-elements/AppSelect.vue'
|
||||||
|
import AppTextarea from '@/Components/app-form-elements/AppTextarea.vue'
|
||||||
|
import type { User } from '@/types'
|
||||||
|
|
||||||
|
defineOptions({ layout: AdminLayout })
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
customer: User
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
name: props.customer.name,
|
||||||
|
email: props.customer.email,
|
||||||
|
phone: props.customer.phone ?? '',
|
||||||
|
company: props.customer.company ?? '',
|
||||||
|
status: props.customer.status,
|
||||||
|
admin_notes: props.customer.admin_notes ?? '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ title: 'Active', value: 'active' },
|
||||||
|
{ title: 'Suspended', value: 'suspended' },
|
||||||
|
{ title: 'Banned', value: 'banned' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function submit(): void {
|
||||||
|
form.put(`/customers/${props.customer.id}`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="d-flex align-center justify-space-between mb-6">
|
||||||
|
<div>
|
||||||
|
<div class="d-flex align-center gap-2 mb-1">
|
||||||
|
<Link :href="`/customers/${customer.id}`" class="text-decoration-none">
|
||||||
|
<VBtn icon="tabler-arrow-left" variant="text" size="small" />
|
||||||
|
</Link>
|
||||||
|
<span class="text-h4 font-weight-bold">Edit Customer</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-body-2 text-medium-emphasis ms-10">
|
||||||
|
Update details for "{{ customer.name }}"
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form @submit.prevent="submit">
|
||||||
|
<VRow>
|
||||||
|
<!-- Customer Details -->
|
||||||
|
<VCol cols="12" lg="8">
|
||||||
|
<VCard title="Customer Details" class="mb-6">
|
||||||
|
<VCardText>
|
||||||
|
<VRow>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<AppTextField
|
||||||
|
v-model="form.name"
|
||||||
|
label="Name"
|
||||||
|
placeholder="Full name"
|
||||||
|
:error-messages="form.errors.name"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<AppTextField
|
||||||
|
v-model="form.email"
|
||||||
|
label="Email"
|
||||||
|
type="email"
|
||||||
|
placeholder="email@example.com"
|
||||||
|
:error-messages="form.errors.email"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<AppTextField
|
||||||
|
v-model="form.phone"
|
||||||
|
label="Phone"
|
||||||
|
placeholder="Phone number"
|
||||||
|
:error-messages="form.errors.phone"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<AppTextField
|
||||||
|
v-model="form.company"
|
||||||
|
label="Company"
|
||||||
|
placeholder="Company name"
|
||||||
|
:error-messages="form.errors.company"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<AppSelect
|
||||||
|
v-model="form.status"
|
||||||
|
label="Status"
|
||||||
|
:items="statusOptions"
|
||||||
|
:error-messages="form.errors.status"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
|
||||||
|
<!-- Admin Notes -->
|
||||||
|
<VCard title="Admin Notes" class="mb-6">
|
||||||
|
<VCardText>
|
||||||
|
<AppTextarea
|
||||||
|
v-model="form.admin_notes"
|
||||||
|
label="Internal Notes"
|
||||||
|
placeholder="Add internal notes about this customer (only visible to admins)..."
|
||||||
|
rows="4"
|
||||||
|
:error-messages="form.errors.admin_notes"
|
||||||
|
/>
|
||||||
|
<div class="text-caption text-medium-emphasis mt-2">
|
||||||
|
These notes are only visible to administrators and will not be shown to the customer.
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<VCol cols="12" lg="4">
|
||||||
|
<!-- Actions -->
|
||||||
|
<VCard>
|
||||||
|
<VCardText>
|
||||||
|
<VBtn
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
block
|
||||||
|
:loading="form.processing"
|
||||||
|
:disabled="form.processing"
|
||||||
|
prepend-icon="tabler-check"
|
||||||
|
class="mb-3"
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</VBtn>
|
||||||
|
<Link :href="`/customers/${customer.id}`" class="text-decoration-none">
|
||||||
|
<VBtn
|
||||||
|
variant="outlined"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</VBtn>
|
||||||
|
</Link>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -23,6 +23,7 @@ interface Customer {
|
|||||||
email: string
|
email: string
|
||||||
phone: string | null
|
phone: string | null
|
||||||
company: string | null
|
company: string | null
|
||||||
|
admin_notes: string | null
|
||||||
status: string
|
status: string
|
||||||
created_at: string
|
created_at: string
|
||||||
email_verified_at: string | null
|
email_verified_at: string | null
|
||||||
@@ -230,6 +231,16 @@ function formatBillingAddress(profile: CustomerProfile | null): string {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex align-center ga-2">
|
<div class="d-flex align-center ga-2">
|
||||||
|
<Link :href="`/customers/${customer.id}/edit`" class="text-decoration-none">
|
||||||
|
<VBtn
|
||||||
|
color="primary"
|
||||||
|
variant="tonal"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<VIcon icon="tabler-edit" start />
|
||||||
|
Edit
|
||||||
|
</VBtn>
|
||||||
|
</Link>
|
||||||
<VBtn
|
<VBtn
|
||||||
color="info"
|
color="info"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
@@ -390,6 +401,19 @@ function formatBillingAddress(profile: CustomerProfile | null): string {
|
|||||||
</VCardText>
|
</VCardText>
|
||||||
</VCard>
|
</VCard>
|
||||||
|
|
||||||
|
<!-- Admin Notes -->
|
||||||
|
<VCard v-if="customer.admin_notes" class="mb-4">
|
||||||
|
<VCardTitle class="d-flex align-center gap-2">
|
||||||
|
<VIcon icon="tabler-notes" size="22" />
|
||||||
|
<span>Admin Notes</span>
|
||||||
|
</VCardTitle>
|
||||||
|
<VCardText>
|
||||||
|
<div class="text-body-2" style="white-space: pre-line;">
|
||||||
|
{{ customer.admin_notes }}
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
|
||||||
<!-- Quick Stats -->
|
<!-- Quick Stats -->
|
||||||
<VCard>
|
<VCard>
|
||||||
<VCardTitle class="d-flex align-center gap-2">
|
<VCardTitle class="d-flex align-center gap-2">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface User {
|
|||||||
email: string
|
email: string
|
||||||
phone: string | null
|
phone: string | null
|
||||||
company: string | null
|
company: string | null
|
||||||
|
admin_notes?: string | null
|
||||||
status: string
|
status: string
|
||||||
two_factor_enabled?: boolean
|
two_factor_enabled?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use Illuminate\Support\Facades\Route;
|
|||||||
|
|
||||||
Route::get('/dashboard', [DashboardController::class, 'index'])->name('admin.dashboard');
|
Route::get('/dashboard', [DashboardController::class, 'index'])->name('admin.dashboard');
|
||||||
|
|
||||||
Route::resource('customers', CustomerController::class)->only(['index', 'show']);
|
Route::resource('customers', CustomerController::class)->only(['index', 'show', 'edit', 'update'])->parameters(['customers' => 'user']);
|
||||||
Route::post('customers/{user}/suspend', [CustomerController::class, 'suspend'])->name('customers.suspend');
|
Route::post('customers/{user}/suspend', [CustomerController::class, 'suspend'])->name('customers.suspend');
|
||||||
Route::post('customers/{user}/unsuspend', [CustomerController::class, 'unsuspend'])->name('customers.unsuspend');
|
Route::post('customers/{user}/unsuspend', [CustomerController::class, 'unsuspend'])->name('customers.unsuspend');
|
||||||
|
|
||||||
|
|||||||
131
website/tests/Feature/Admin/CustomerEditTest.php
Normal file
131
website/tests/Feature/Admin/CustomerEditTest.php
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Models\AuditLog;
|
||||||
|
use App\Models\User;
|
||||||
|
use Database\Seeders\RoleAndPermissionSeeder;
|
||||||
|
|
||||||
|
beforeEach(function (): void {
|
||||||
|
$this->seed(RoleAndPermissionSeeder::class);
|
||||||
|
$this->adminUrl = 'http://'.config('app.domains.admin');
|
||||||
|
$this->admin = User::factory()->admin()->create();
|
||||||
|
$this->customer = User::factory()->customer()->create([
|
||||||
|
'name' => 'Original Name',
|
||||||
|
'email' => 'original@test.com',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('admin can view customer edit page', function (): void {
|
||||||
|
$this->actingAs($this->admin)
|
||||||
|
->get($this->adminUrl.'/customers/'.$this->customer->id.'/edit')
|
||||||
|
->assertOk()
|
||||||
|
->assertInertia(fn ($page) => $page
|
||||||
|
->component('Admin/Customers/Edit')
|
||||||
|
->has('customer')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('admin can update customer details', function (): void {
|
||||||
|
$this->actingAs($this->admin)
|
||||||
|
->put($this->adminUrl.'/customers/'.$this->customer->id, [
|
||||||
|
'name' => 'Updated Name',
|
||||||
|
'email' => 'updated@test.com',
|
||||||
|
'phone' => '555-1234',
|
||||||
|
'company' => 'Test Corp',
|
||||||
|
'status' => 'active',
|
||||||
|
'admin_notes' => 'Test note from admin',
|
||||||
|
])
|
||||||
|
->assertRedirect();
|
||||||
|
|
||||||
|
$this->customer->refresh();
|
||||||
|
expect($this->customer->name)->toBe('Updated Name')
|
||||||
|
->and($this->customer->email)->toBe('updated@test.com')
|
||||||
|
->and($this->customer->phone)->toBe('555-1234')
|
||||||
|
->and($this->customer->company)->toBe('Test Corp')
|
||||||
|
->and($this->customer->admin_notes)->toBe('Test note from admin');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('admin can update customer status', function (): void {
|
||||||
|
$this->actingAs($this->admin)
|
||||||
|
->put($this->adminUrl.'/customers/'.$this->customer->id, [
|
||||||
|
'name' => $this->customer->name,
|
||||||
|
'email' => $this->customer->email,
|
||||||
|
'status' => 'suspended',
|
||||||
|
])
|
||||||
|
->assertRedirect();
|
||||||
|
|
||||||
|
$this->customer->refresh();
|
||||||
|
expect($this->customer->status)->toBe('suspended');
|
||||||
|
|
||||||
|
// Check audit log for status change
|
||||||
|
expect(AuditLog::where('action', 'customer_status_changed')->count())->toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('customer edit requires valid email', function (): void {
|
||||||
|
$this->actingAs($this->admin)
|
||||||
|
->put($this->adminUrl.'/customers/'.$this->customer->id, [
|
||||||
|
'name' => 'Test',
|
||||||
|
'email' => 'not-an-email',
|
||||||
|
'status' => 'active',
|
||||||
|
])
|
||||||
|
->assertSessionHasErrors('email');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('customer edit requires unique email', function (): void {
|
||||||
|
User::factory()->customer()->create(['email' => 'taken@test.com']);
|
||||||
|
|
||||||
|
$this->actingAs($this->admin)
|
||||||
|
->put($this->adminUrl.'/customers/'.$this->customer->id, [
|
||||||
|
'name' => 'Test',
|
||||||
|
'email' => 'taken@test.com',
|
||||||
|
'status' => 'active',
|
||||||
|
])
|
||||||
|
->assertSessionHasErrors('email');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('customer can keep their own email on update', function (): void {
|
||||||
|
$this->actingAs($this->admin)
|
||||||
|
->put($this->adminUrl.'/customers/'.$this->customer->id, [
|
||||||
|
'name' => 'Updated',
|
||||||
|
'email' => $this->customer->email,
|
||||||
|
'status' => 'active',
|
||||||
|
])
|
||||||
|
->assertRedirect();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('admin notes can be saved and cleared', function (): void {
|
||||||
|
// Save notes
|
||||||
|
$this->actingAs($this->admin)
|
||||||
|
->put($this->adminUrl.'/customers/'.$this->customer->id, [
|
||||||
|
'name' => $this->customer->name,
|
||||||
|
'email' => $this->customer->email,
|
||||||
|
'status' => 'active',
|
||||||
|
'admin_notes' => 'Important customer note',
|
||||||
|
])
|
||||||
|
->assertRedirect();
|
||||||
|
|
||||||
|
$this->customer->refresh();
|
||||||
|
expect($this->customer->admin_notes)->toBe('Important customer note');
|
||||||
|
|
||||||
|
// Clear notes
|
||||||
|
$this->actingAs($this->admin)
|
||||||
|
->put($this->adminUrl.'/customers/'.$this->customer->id, [
|
||||||
|
'name' => $this->customer->name,
|
||||||
|
'email' => $this->customer->email,
|
||||||
|
'status' => 'active',
|
||||||
|
'admin_notes' => '',
|
||||||
|
])
|
||||||
|
->assertRedirect();
|
||||||
|
|
||||||
|
$this->customer->refresh();
|
||||||
|
expect($this->customer->admin_notes)->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-admin cannot access customer edit', function (): void {
|
||||||
|
$regularUser = User::factory()->customer()->create();
|
||||||
|
|
||||||
|
$this->actingAs($regularUser)
|
||||||
|
->get($this->adminUrl.'/customers/'.$this->customer->id.'/edit')
|
||||||
|
->assertForbidden();
|
||||||
|
});
|
||||||
@@ -14,7 +14,7 @@ it('has correct fillable attributes', function (): void {
|
|||||||
$user = new User;
|
$user = new User;
|
||||||
|
|
||||||
expect($user->getFillable())->toBe([
|
expect($user->getFillable())->toBe([
|
||||||
'name', 'email', 'password', 'status', 'phone', 'company',
|
'name', 'email', 'password', 'status', 'phone', 'company', 'admin_notes',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user