feat(admin): star/tags marks, service product-type filter, campaign bulk actions, SMTP test
- Star (is_starred) and colored tags for services, customers, orders, invoices:
Markable trait, tags/taggables tables, PATCH administrator/marks/{type}/{key},
tag CRUD, list filters starred / tag_ids[]
- GET administrator/services accepts product_type (falls back to product type
for legacy rows without services.type)
- Mail campaigns: bulk cancel / retry failed / delete; cancelled campaigns are
skipped by SendMailCampaignRecipient
- POST administrator/settings/test-smtp sends a test email through the saved
mail transport
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
46aa138eec
commit
40bf6b54f9
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api\Admin;
|
||||
|
||||
use App\Models\Customer;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Order;
|
||||
use App\Models\Product;
|
||||
use App\Models\Service;
|
||||
use App\Models\Tag;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MarkTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private function admin(string ...$permissions): User
|
||||
{
|
||||
$role = Role::create(['name' => 'role-' . uniqid(), 'guard_name' => 'admin']);
|
||||
foreach ($permissions as $permission) {
|
||||
$role->givePermissionTo(Permission::firstOrCreate(['name' => $permission, 'guard_name' => 'admin']));
|
||||
}
|
||||
$admin = User::factory()->create();
|
||||
$admin->assignRole($role);
|
||||
|
||||
return $admin;
|
||||
}
|
||||
|
||||
public function test_stars_and_tags_customer_then_filters_list(): void
|
||||
{
|
||||
$admin = $this->admin('customer.view', 'customer.edit');
|
||||
$customer = Customer::factory()->create();
|
||||
Customer::factory()->create();
|
||||
|
||||
$tagId = $this->actingAs($admin, 'admin')
|
||||
->postJson('/api/v1/administrator/tags', ['title' => 'VIP', 'color' => '#ff0000'])
|
||||
->assertCreated()
|
||||
->json('data.id');
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->patchJson("/api/v1/administrator/marks/customers/{$customer->uuid}", [
|
||||
'is_starred' => true,
|
||||
'tag_ids' => [$tagId],
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('data.is_starred', true)
|
||||
->assertJsonPath('data.tags.0.title', 'VIP');
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson('/api/v1/administrator/customers?starred=1')
|
||||
->assertOk()
|
||||
->assertJsonCount(1, 'data.items')
|
||||
->assertJsonPath('data.items.0.uuid', $customer->uuid)
|
||||
->assertJsonPath('data.items.0.tags.0.color', '#ff0000');
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson("/api/v1/administrator/customers?tag_ids[]={$tagId}")
|
||||
->assertJsonCount(1, 'data.items');
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->patchJson("/api/v1/administrator/marks/customers/{$customer->uuid}", ['tag_ids' => []])
|
||||
->assertOk()
|
||||
->assertJsonCount(0, 'data.tags');
|
||||
|
||||
$this->assertTrue($customer->fresh()->is_starred);
|
||||
}
|
||||
|
||||
public function test_requires_module_edit_permission_and_valid_input(): void
|
||||
{
|
||||
Permission::firstOrCreate(['name' => 'customer.edit', 'guard_name' => 'admin']);
|
||||
$customer = Customer::factory()->create();
|
||||
|
||||
$this->actingAs($this->admin('order.edit'), 'admin')
|
||||
->patchJson("/api/v1/administrator/marks/customers/{$customer->uuid}", ['is_starred' => true])
|
||||
->assertForbidden();
|
||||
|
||||
$admin = $this->admin('customer.edit');
|
||||
$this->actingAs($admin, 'admin')
|
||||
->patchJson("/api/v1/administrator/marks/customers/{$customer->uuid}", ['tag_ids' => [999]])
|
||||
->assertUnprocessable();
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson('/api/v1/administrator/tags', ['title' => 'X', 'color' => 'red;'])
|
||||
->assertUnprocessable();
|
||||
}
|
||||
|
||||
public function test_deleting_tag_detaches_it(): void
|
||||
{
|
||||
$customer = Customer::factory()->create();
|
||||
$tag = Tag::create(['title' => 'Nợ', 'color' => '#000000']);
|
||||
$customer->tags()->attach($tag);
|
||||
|
||||
$this->actingAs($this->admin('setting.manager'), 'admin')
|
||||
->deleteJson("/api/v1/administrator/tags/{$tag->id}")
|
||||
->assertOk();
|
||||
|
||||
$this->assertDatabaseCount('taggables', 0);
|
||||
}
|
||||
|
||||
public static function listEndpoints(): array
|
||||
{
|
||||
return [
|
||||
'services' => ['services', Service::class, '/api/v1/administrator/services', 'service'],
|
||||
'orders' => ['orders', Order::class, '/api/v1/administrator/orders', 'domain'],
|
||||
'invoices' => ['invoices', Invoice::class, '/api/v1/administrator/invoices', 'domain'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<\Illuminate\Database\Eloquent\Model> $model
|
||||
*/
|
||||
#[DataProvider('listEndpoints')]
|
||||
public function test_marks_and_filters_billing_lists(string $type, string $model, string $listUrl, string $productType): void
|
||||
{
|
||||
Role::firstOrCreate(['name' => 'Full Administrator', 'guard_name' => 'admin']);
|
||||
$admin = User::factory()->create();
|
||||
$admin->assignRole('Full Administrator');
|
||||
$customer = Customer::factory()->create();
|
||||
$product = Product::factory()->create(['product_type' => $productType]);
|
||||
foreach (['a.vn', 'b.vn'] as $serverName) {
|
||||
$this->actingAs($admin, 'admin')->postJson(
|
||||
"/api/v1/administrator/customers/{$customer->uuid}/services",
|
||||
['product_id' => $product->id, 'payment_method' => 'transfer', 'server_name' => $serverName, 'amount_paid' => 0],
|
||||
)->assertCreated();
|
||||
}
|
||||
$tag = Tag::create(['title' => 'Gấp', 'color' => '#fa8c16']);
|
||||
$target = $model::query()->latest('id')->firstOrFail();
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->patchJson("/api/v1/administrator/marks/{$type}/{$target->id}", ['is_starred' => true, 'tag_ids' => [$tag->id]])
|
||||
->assertOk();
|
||||
|
||||
$items = $this->actingAs($admin, 'admin')
|
||||
->getJson("{$listUrl}?starred=1&tag_ids[]={$tag->id}")
|
||||
->assertOk()
|
||||
->json('data.items');
|
||||
|
||||
$this->assertSame([$target->id], array_column($items, 'id'));
|
||||
$this->assertTrue($items[0]['is_starred']);
|
||||
$this->assertSame('Gấp', $items[0]['tags'][0]['title']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user