feat: bật/tắt yêu cầu hoàn tất hồ sơ, xoá dịch vụ kèm đơn/hoá đơn, sync tên VM đã gán
- setting.require_complete_profile (mặc định bật), trả qua GET v1/setting - bulkDeleteServices xoá mềm invoice nâng cấp + order mồ côi (ServiceOrderRecorder::discardOrder) - createService trỏ dịch vụ sang order mới thì bỏ order cũ - SyncVirtualMachine cập nhật name + gắn virtual_machine_id cho module_vmware Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
bf78f3b810
commit
0bd15f6018
@@ -67,6 +67,7 @@ class SettingController extends Controller
|
||||
$validator = Validator::make($request->all(), [
|
||||
'setting' => 'sometimes|array',
|
||||
'setting.api_inet' => 'sometimes|string|nullable',
|
||||
'setting.require_complete_profile' => 'sometimes|boolean',
|
||||
'smtp' => 'sometimes|array',
|
||||
'smtp.host' => 'sometimes|string',
|
||||
'smtp.port' => 'sometimes|integer',
|
||||
|
||||
@@ -24,7 +24,7 @@ class SettingController extends Controller
|
||||
* operationId="getSetting",
|
||||
* tags={"Settings"},
|
||||
* summary="Get application settings",
|
||||
* description="Returns the public application settings (title, company, logo, favi_icon, brand_color)",
|
||||
* description="Returns the public application settings (title, company, logo, favi_icon, brand_color, require_complete_profile)",
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="Successful operation",
|
||||
@@ -39,7 +39,9 @@ class SettingController extends Controller
|
||||
public function getSetting(): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => PublicSetting::values(),
|
||||
'data' => PublicSetting::values() + [
|
||||
'require_complete_profile' => PublicSetting::requireCompleteProfile(),
|
||||
],
|
||||
'message' => 'Get setting successfully',
|
||||
'error' => false
|
||||
]);
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Http\Services\Telegram;
|
||||
use App\Http\Services\vCenter as ServiceVCenter;
|
||||
use App\Models\ModuleDatacenter;
|
||||
use App\Models\ModuleHost;
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\VirtualMachines;
|
||||
use App\Models\vCenter;
|
||||
use Illuminate\Bus\Queueable;
|
||||
@@ -178,12 +179,26 @@ class SyncVirtualMachine implements ShouldQueue
|
||||
$vmObj->last_synced_at = now();
|
||||
|
||||
$vmObj->save();
|
||||
$this->syncAssignedVm($vmObj);
|
||||
$syncedVmIds[] = $vmData['vm'];
|
||||
$count++;
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
private function syncAssignedVm(VirtualMachines $vm): void
|
||||
{
|
||||
ModuleVmware::query()
|
||||
->where(fn ($query) => $query
|
||||
->where('virtual_machine_id', $vm->id)
|
||||
->orWhere(fn ($legacy) => $legacy->whereNull('virtual_machine_id')->where('vmware_id', $vm->vm_id)))
|
||||
->where(fn ($query) => $query
|
||||
->whereNull('virtual_machine_id')
|
||||
->orWhereNull('name')
|
||||
->orWhere('name', '!=', $vm->name))
|
||||
->update(['name' => $vm->name, 'virtual_machine_id' => $vm->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete VMs in DB that belong to the given vCenter but no longer exist on vCenter.
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Services\Customer;
|
||||
use App\Helpers\OrderHelper;
|
||||
use App\Mail\ServiceRenewalReminderMail;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\Order;
|
||||
use App\Models\Product;
|
||||
@@ -54,15 +55,21 @@ class CustomerServiceProvisioningService
|
||||
return $services;
|
||||
}
|
||||
|
||||
/**
|
||||
* Xoá nhiều dịch vụ của khách hàng. Chỉ xoá bản ghi trong bảng `services`,
|
||||
* không xoá VM (module_vmware / virtual_machines) liên kết.
|
||||
*/
|
||||
public function bulkDeleteServices(string $uuid, array $ids): int
|
||||
{
|
||||
$customer = $this->resolveCustomer($uuid);
|
||||
|
||||
return Service::where('customer_id', $customer->id)->whereIn('id', $ids)->delete();
|
||||
return DB::transaction(function () use ($customer, $ids) {
|
||||
$services = Service::where('customer_id', $customer->id)->whereIn('id', $ids)->get();
|
||||
|
||||
foreach ($services as $service) {
|
||||
$service->delete();
|
||||
Invoice::where('service_id', $service->id)->delete();
|
||||
$this->orderRecorder->discardOrder($service->order_item_id);
|
||||
}
|
||||
|
||||
return $services->count();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -286,6 +293,8 @@ class CustomerServiceProvisioningService
|
||||
'server_name' => $data['server_name'] ?? null,
|
||||
];
|
||||
|
||||
$replacedOrderItemId = Service::where($serviceLookup)->value('order_item_id');
|
||||
|
||||
$service = Service::updateOrCreate($serviceLookup, [
|
||||
'customer_id' => $customer->id,
|
||||
'product_id' => $product->id,
|
||||
@@ -300,6 +309,8 @@ class CustomerServiceProvisioningService
|
||||
'note' => $data['note'] ?? null,
|
||||
]);
|
||||
|
||||
$this->orderRecorder->discardOrder($replacedOrderItemId);
|
||||
|
||||
// module_vmware.service_id chỉ biết được sau khi Service đã tồn tại.
|
||||
if ($vmId) {
|
||||
ModuleVmware::where('id', $vmId)->update(['service_id' => $service->id]);
|
||||
|
||||
@@ -143,6 +143,21 @@ class ServiceOrderRecorder
|
||||
$invoice->order?->update(['status' => OrderStatus::CANCELLED->value]);
|
||||
}
|
||||
|
||||
public function discardOrder(?int $orderItemId): void
|
||||
{
|
||||
$order = $orderItemId ? OrderItem::find($orderItemId)?->order : null;
|
||||
if (! $order) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Service::whereIn('order_item_id', $order->orderItems()->pluck('id'))->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order->invoices()->delete();
|
||||
$order->delete();
|
||||
}
|
||||
|
||||
public function listPrice(?Product $product, string $billingCycle, ?string $serverName): ?int
|
||||
{
|
||||
if ($product?->product_type === 'domain') {
|
||||
|
||||
@@ -13,8 +13,21 @@ final class PublicSetting
|
||||
*/
|
||||
public static function values(): array
|
||||
{
|
||||
$value = json_decode((string) Setting::where('key', 'setting')->value('value'), true) ?: [];
|
||||
$value = self::raw();
|
||||
|
||||
return array_map(fn (string $field) => (string) ($value[$field] ?? ''), array_combine(self::FIELDS, self::FIELDS));
|
||||
}
|
||||
|
||||
public static function requireCompleteProfile(): bool
|
||||
{
|
||||
return (bool) (self::raw()['require_complete_profile'] ?? true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function raw(): array
|
||||
{
|
||||
return json_decode((string) Setting::where('key', 'setting')->value('value'), true) ?: [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api\Admin\Customer;
|
||||
|
||||
use App\Models\Customer;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Order;
|
||||
use App\Models\Product;
|
||||
use App\Models\Service;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DeleteCustomerServiceOrdersTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private User $admin;
|
||||
|
||||
private Customer $customer;
|
||||
|
||||
private Product $product;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Role::firstOrCreate(['name' => 'Full Administrator', 'guard_name' => 'admin']);
|
||||
$this->admin = User::factory()->create();
|
||||
$this->admin->assignRole('Full Administrator');
|
||||
$this->customer = Customer::factory()->create();
|
||||
$this->product = Product::factory()->create(['product_type' => 'domain']);
|
||||
}
|
||||
|
||||
private function createService(string $serverName): Service
|
||||
{
|
||||
$this->actingAs($this->admin, 'admin')->postJson(
|
||||
"/api/v1/administrator/customers/{$this->customer->uuid}/services",
|
||||
['product_id' => $this->product->id, 'payment_method' => 'transfer', 'server_name' => $serverName, 'amount_paid' => 0],
|
||||
)->assertCreated();
|
||||
|
||||
return Service::where('server_name', $serverName)->firstOrFail();
|
||||
}
|
||||
|
||||
public function test_deleting_service_hides_its_order_and_invoices(): void
|
||||
{
|
||||
$kept = $this->createService('kept.vn');
|
||||
$removed = $this->createService('removed.vn');
|
||||
$removedOrderId = $removed->orderItem->order_id;
|
||||
|
||||
$this->actingAs($this->admin, 'admin')->postJson(
|
||||
"/api/v1/administrator/customers/{$this->customer->uuid}/services/bulk-delete",
|
||||
['ids' => [$removed->id]],
|
||||
)->assertOk();
|
||||
|
||||
$this->assertSoftDeleted('orders', ['id' => $removedOrderId]);
|
||||
$this->assertSoftDeleted('invoices', ['order_id' => $removedOrderId]);
|
||||
$this->assertSame([$kept->orderItem->order_id], Order::pluck('id')->all());
|
||||
$this->assertSame(1, Invoice::count());
|
||||
}
|
||||
|
||||
public function test_recreating_same_service_discards_previous_order(): void
|
||||
{
|
||||
$first = $this->createService('same.vn');
|
||||
$firstOrderId = $first->orderItem->order_id;
|
||||
|
||||
$second = $this->createService('same.vn');
|
||||
|
||||
$this->assertSame($first->id, $second->id);
|
||||
$this->assertSoftDeleted('orders', ['id' => $firstOrderId]);
|
||||
$this->assertSame([$second->orderItem->order_id], Order::pluck('id')->all());
|
||||
$this->assertSame(1, Invoice::count());
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,8 @@ class PublicSettingTest extends TestCase
|
||||
->getJson('/v1/setting')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.title', 'Portal')
|
||||
->assertJsonPath('data.brand_color', '#EF6824');
|
||||
->assertJsonPath('data.brand_color', '#EF6824')
|
||||
->assertJsonPath('data.require_complete_profile', true);
|
||||
|
||||
$admin = $this->getJson('/api/v1/settings/public')->assertOk();
|
||||
|
||||
@@ -39,4 +40,14 @@ class PublicSettingTest extends TestCase
|
||||
$this->assertStringNotContainsString('admin@example.com', $body);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_client_setting_reflects_disabled_profile_completion(): void
|
||||
{
|
||||
Setting::updateOrCreate(['key' => 'setting'], ['value' => json_encode(['require_complete_profile' => false])]);
|
||||
|
||||
$this->withHeaders(['x-access-token' => $this->buildClientAccessToken()])
|
||||
->getJson('/v1/setting')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.require_complete_profile', false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Jobs;
|
||||
|
||||
use App\Jobs\VirtualMachine\Sync\SyncVirtualMachine;
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\VirtualMachines;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SyncVirtualMachineTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private function sync(array $vms): void
|
||||
{
|
||||
$synced = [];
|
||||
$failed = false;
|
||||
(new \ReflectionMethod(SyncVirtualMachine::class, 'processVMs'))
|
||||
->invokeArgs(new SyncVirtualMachine, [$vms, null, 1, 1, &$synced, &$failed]);
|
||||
}
|
||||
|
||||
public function test_renamed_vm_updates_assigned_module_vmware_name(): void
|
||||
{
|
||||
Schema::disableForeignKeyConstraints();
|
||||
$vm = VirtualMachines::create(['uuid' => (string) Str::uuid(), 'vm_id' => 'vm-1', 'name' => 'old-name', 'datacenter_id' => 1, 'host_id' => 1]);
|
||||
$linked = ModuleVmware::create(['name' => 'old-name', 'vmware_id' => 'vm-1', 'virtual_machine_id' => $vm->id, 'hostname' => 'customer-host']);
|
||||
$legacy = ModuleVmware::create(['name' => 'old-name', 'vmware_id' => 'vm-1']);
|
||||
$other = ModuleVmware::create(['name' => 'untouched', 'vmware_id' => 'vm-2']);
|
||||
|
||||
$this->sync([['vm' => 'vm-1', 'name' => 'new-name', 'power_state' => 'POWERED_ON', 'cpu_count' => 2, 'memory_size_MiB' => 2048]]);
|
||||
|
||||
$this->assertSame('new-name', $vm->fresh()->name);
|
||||
$this->assertSame('new-name', $linked->fresh()->name);
|
||||
$this->assertSame('customer-host', $linked->fresh()->hostname);
|
||||
$this->assertSame('new-name', $legacy->fresh()->name);
|
||||
$this->assertSame($vm->id, (int) $legacy->fresh()->virtual_machine_id);
|
||||
$this->assertSame('untouched', $other->fresh()->name);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user