From 7136bec265c33265687ae4013b7d5e9f849777a9 Mon Sep 17 00:00:00 2001 From: baodev0 Date: Wed, 28 Oct 2026 17:26:41 +0700 Subject: [PATCH] =?UTF-8?q?feat(domain):=20ch=E1=BB=89nh=20ng=C3=A0y=20b?= =?UTF-8?q?=E1=BA=AFt=20=C4=91=E1=BA=A7u/h=E1=BA=BFt=20h=E1=BA=A1n=20t?= =?UTF-8?q?=C3=AAn=20mi=E1=BB=81n=20v=C3=A0=20=C4=91i=E1=BB=81n=20h?= =?UTF-8?q?=E1=BA=A1n=20t=E1=BB=AB=20d=E1=BB=AF=20li=E1=BB=87u=20c=C5=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PUT administrator/domains/{id}/term, LegacyDomainTerms và migration backfill hạn cho dịch vụ domain chưa có expires_at. Co-Authored-By: Claude Sonnet 5.5 --- .../Api/Admin/DnsManager/DomainController.php | 37 ++++++++++++++- .../Domain/CustomerDomainServiceSync.php | 44 +++++++++++++++++- app/Support/LegacyDomainTerms.php | 46 +++++++++++++++++++ ...kfill_domain_service_terms_from_legacy.php | 21 +++++++++ routes/api/admin/domains.php | 1 + .../Admin/CustomerDomainServiceSyncTest.php | 24 ++++++++++ 6 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 app/Support/LegacyDomainTerms.php create mode 100644 database/migrations/2026_10_28_000003_backfill_domain_service_terms_from_legacy.php diff --git a/app/Http/Controllers/Api/Admin/DnsManager/DomainController.php b/app/Http/Controllers/Api/Admin/DnsManager/DomainController.php index 11bd14f..8faad5a 100644 --- a/app/Http/Controllers/Api/Admin/DnsManager/DomainController.php +++ b/app/Http/Controllers/Api/Admin/DnsManager/DomainController.php @@ -93,9 +93,13 @@ class DomainController extends Controller return response()->json(['error' => true, 'message' => "Không tìm thấy domain #$id"], 404); } + $summary = DomainServiceSummary::forDomains(collect([$domain])); + return response()->json([ 'error' => false, - 'data' => $this->formatDomain($domain), + 'data' => $this->formatDomain($domain) + [ + 'service' => $summary[DomainServiceSummary::key($domain->domain_name)] ?? null, + ], ]); } @@ -194,6 +198,37 @@ class DomainController extends Controller } } + public function updateTerm(int $id, Request $request): JsonResponse + { + $domain = $this->domainRepository->find($id); + + if (!$domain) { + return response()->json(['error' => true, 'message' => "Không tìm thấy domain #$id"], 404); + } + + $data = $request->validate([ + 'registered_at' => 'nullable|date', + 'expires_at' => 'nullable|date|after_or_equal:registered_at', + 'cycle_years' => 'nullable|integer|min:1|max:10', + ]); + + $updated = $this->customerDomainServiceSync->applyTerm( + $domain, + $data['registered_at'] ?? null, + $data['expires_at'] ?? null, + $data['cycle_years'] ?? null, + ); + + if ($updated === 0) { + return response()->json([ + 'error' => true, + 'message' => 'Tên miền chưa gắn khách hàng nên chưa có dịch vụ để lưu ngày đăng ký/hết hạn.', + ], 422); + } + + return $this->show($id); + } + public function provision(int $id): JsonResponse { $domain = $this->domainRepository->find($id); diff --git a/app/Services/Domain/CustomerDomainServiceSync.php b/app/Services/Domain/CustomerDomainServiceSync.php index a0eafb3..5e3cece 100644 --- a/app/Services/Domain/CustomerDomainServiceSync.php +++ b/app/Services/Domain/CustomerDomainServiceSync.php @@ -2,9 +2,12 @@ namespace App\Services\Domain; +use App\Enums\Product\ProductBillingCycle; use App\Models\DnsDomain; use App\Models\Service; use App\Services\Order\ServiceOrderRecorder; +use App\Support\BillingCycleCatalog; +use App\Support\LegacyDomainTerms; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; @@ -40,7 +43,9 @@ class CustomerDomainServiceSync 'server_name' => $domain->domain_name, 'status' => $this->serviceStatus($domain), 'started_at' => $service->started_at ?? $domain->created_at ?? Carbon::now(), - ])->save(); + ]); + LegacyDomainTerms::fill($service); + $service->save(); if ($service->status !== 'cancelled') { $this->orderRecorder->ensureForService($service); @@ -86,6 +91,43 @@ class CustomerDomainServiceSync return $service; } + public function applyTerm(DnsDomain $domain, ?string $registeredAt, ?string $expiresAt, ?int $cycleYears): int + { + $services = Service::query() + ->where('type', 'domain') + ->where('status', '!=', 'cancelled') + ->whereIn('customer_id', $domain->customer()->pluck('customers.id')) + ->whereRaw('LOWER(TRIM(server_name)) = ?', [$this->domainKey($domain->domain_name)]) + ->with('orderItem') + ->get(); + + DB::transaction(function () use ($services, $registeredAt, $expiresAt, $cycleYears): void { + foreach ($services as $service) { + $attributes = [ + 'expires_at' => $expiresAt + ? Carbon::parse($expiresAt)->setTimeFrom($service->expires_at ?? Carbon::today()) + : null, + ]; + if ($registeredAt) { + $attributes['started_at'] = Carbon::parse($registeredAt); + } + $service->forceFill($attributes)->save(); + + if ($cycleYears && $service->orderItem) { + $months = $cycleYears * 12; + $service->orderItem->update([ + 'months' => $months, + 'billing_cycle' => collect(ProductBillingCycle::cases()) + ->first(fn (ProductBillingCycle $cycle) => $cycle->months() === $months)?->value + ?? BillingCycleCatalog::codeFor($months), + ]); + } + } + }); + + return $services->count(); + } + private function findService(int $customerId, string $oldName, string $currentName): ?Service { return Service::withTrashed() diff --git a/app/Support/LegacyDomainTerms.php b/app/Support/LegacyDomainTerms.php new file mode 100644 index 0000000..7fecca8 --- /dev/null +++ b/app/Support/LegacyDomainTerms.php @@ -0,0 +1,46 @@ +whereRaw('LOWER(TRIM(name)) = ?', [mb_strtolower(trim((string) $service->server_name))]) + ->orderByRaw('customer_id = ? desc', [$service->customer_id]) + ->orderByDesc('next_due_date') + ->first(); + + if (! $legacy) { + return null; + } + + return [ + 'registered_at' => $legacy->order_id ? Order::whereKey($legacy->order_id)->value('created_at') : null, + 'expires_at' => $legacy->next_due_date > 0 ? Carbon::createFromTimestamp($legacy->next_due_date) : null, + ]; + } + + public static function fill(Service $service): void + { + $terms = self::for($service); + + if (! $terms) { + return; + } + + if (! $service->expires_at && $terms['expires_at']) { + $service->expires_at = $terms['expires_at']; + } + + if ($service->note === \App\Services\Domain\CustomerDomainServiceSync::AUTO_CREATED_NOTE && $terms['registered_at']) { + $service->started_at = $terms['registered_at']; + } + } +} diff --git a/database/migrations/2026_10_28_000003_backfill_domain_service_terms_from_legacy.php b/database/migrations/2026_10_28_000003_backfill_domain_service_terms_from_legacy.php new file mode 100644 index 0000000..309647b --- /dev/null +++ b/database/migrations/2026_10_28_000003_backfill_domain_service_terms_from_legacy.php @@ -0,0 +1,21 @@ +where('type', 'domain') + ->whereNull('expires_at') + ->each(function (Service $service): void { + LegacyDomainTerms::fill($service); + $service->save(); + }); + } + + public function down(): void {} +}; diff --git a/routes/api/admin/domains.php b/routes/api/admin/domains.php index 8f9f7b5..d7750fa 100644 --- a/routes/api/admin/domains.php +++ b/routes/api/admin/domains.php @@ -18,6 +18,7 @@ Route::prefix('domains') Route::post('sync-dns', 'syncDns')->middleware('perm:domain.manager'); Route::get('{id}', 'show')->middleware('perm:domain.manager'); Route::put('{id}', 'update')->middleware('perm:domain.manager'); + Route::put('{id}/term', 'updateTerm')->middleware('perm:domain.manager'); Route::post('{id}/provision', 'provision')->middleware('perm:domain.manager'); Route::delete('{id}', 'destroy')->middleware('perm:domain.manager'); // DNS Records diff --git a/tests/Feature/Api/Admin/CustomerDomainServiceSyncTest.php b/tests/Feature/Api/Admin/CustomerDomainServiceSyncTest.php index 9fa01e2..cb0f7e7 100644 --- a/tests/Feature/Api/Admin/CustomerDomainServiceSyncTest.php +++ b/tests/Feature/Api/Admin/CustomerDomainServiceSyncTest.php @@ -69,4 +69,28 @@ class CustomerDomainServiceSyncTest extends TestCase 'user_id' => $customer->id, ]); } + + public function test_apply_term_sets_dates_and_cycle_for_domain_services(): void + { + $customer = Customer::factory()->create(); + $serverId = DB::table('dns_manager_servers')->insertGetId(['ip_address' => '10.0.0.1', 'password' => 'x']); + $domain = DnsDomain::create([ + 'domain_name' => 'devro-tech.com', + 'dns_manager_server_id' => $serverId, + 'is_created' => DnsDomain::STATUS_CREATED, + ]); + Product::factory()->create(['product_type' => 'domain', 'is_active' => true]); + $sync = app(CustomerDomainServiceSync::class); + $sync->syncCustomers($domain, [$customer->id]); + + $updated = $sync->applyTerm($domain, '2025-06-17', '2027-06-17', 2); + + $service = Service::where('customer_id', $customer->id)->where('type', 'domain')->firstOrFail(); + $this->assertSame(1, $updated); + $this->assertSame('2025-06-17', $service->started_at->toDateString()); + $this->assertSame('2027-06-17', $service->expires_at->toDateString()); + $this->assertSame(24, (int) $service->orderItem->months); + $this->assertSame('biennially', $service->orderItem->billing_cycle); + $this->assertSame(0, $sync->applyTerm(DnsDomain::create(['domain_name' => 'solo.vn', 'dns_manager_server_id' => $serverId]), null, null, 1)); + } }