feat(service): huỷ dịch vụ, SSL, đổi mật khẩu hosting, lịch sử thanh toán theo hình thức

- Lịch sử thanh toán hoá đơn trả thêm `method` (tự động gia hạn / số dư / admin / chuyển khoản / hoàn tiền), bỏ đuôi `(method)` khỏi ghi chú
- `DnsServer::panelUrl()` = https://<domain>/ dùng cho email cấp hosting và nút Plesk của khách
- Order item trả thêm `uuid`/`type` của dịch vụ để FE dẫn tới trang quản lý
- Yêu cầu huỷ dịch vụ, chứng chỉ SSL, lịch sử gia hạn, đổi mật khẩu hosting cho khách

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
baodev0
2026-10-28 16:40:24 +07:00
co-authored by Claude Sonnet 5.5
parent a6b641015e
commit b44a88bb52
72 changed files with 1701 additions and 231 deletions
-85
View File
@@ -1,85 +0,0 @@
<?php
namespace Tests\Unit;
use App\Http\Middleware\SealResponse;
use App\Support\ResponseCipher;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Tests\TestCase;
class ResponseCipherTest extends TestCase
{
private function key(): string
{
return str_repeat('k', 32);
}
private function request(array $body = [], bool $optIn = false, ?string $key = null): Request
{
$request = Request::create('/x', 'POST', [], [], [], ['CONTENT_TYPE' => 'application/json'], json_encode($body));
$request->headers->set('x-seal-key', base64_encode($key ?? $this->key()));
if ($optIn) {
$request->headers->set('x-seal', '1');
}
return $request;
}
public function test_always_mode_seals_with_session_key_and_is_reversible(): void
{
$response = (new SealResponse())->handle(
$this->request(),
fn () => new JsonResponse(['data' => ['password' => 'Cloud226*()']]),
'always',
);
$body = $response->getData(true);
$this->assertArrayNotHasKey('data', $body);
$this->assertStringNotContainsString('Cloud226', $response->getContent());
$this->assertSame(
['data' => ['password' => 'Cloud226*()']],
json_decode(ResponseCipher::open($body['enc'], $this->key()), true),
);
$this->assertNull(ResponseCipher::open($body['enc'], str_repeat('x', 32)));
}
public function test_passthrough_without_key_or_opt_in(): void
{
$noKey = Request::create('/x');
$noKey->headers->set('x-seal', '1');
$plain = (new SealResponse())->handle($noKey, fn () => new JsonResponse(['a' => 1]));
$this->assertSame(['a' => 1], $plain->getData(true));
$noOptIn = (new SealResponse())->handle($this->request(), fn () => new JsonResponse(['a' => 1]));
$this->assertSame(['a' => 1], $noOptIn->getData(true));
}
public function test_opt_in_seals_errors_and_opens_request_body(): void
{
$sealed = ['enc' => ResponseCipher::seal(json_encode(['name' => 'An']), $this->key())];
$response = (new SealResponse())->handle(
$this->request($sealed, optIn: true),
fn (Request $r) => new JsonResponse(['got' => $r->input('name')], 422),
);
$this->assertSame(422, $response->getStatusCode());
$this->assertSame(
['got' => 'An'],
json_decode(ResponseCipher::open($response->getData(true)['enc'], $this->key()), true),
);
}
public function test_request_sealed_with_stale_key_gets_428(): void
{
$sealed = ['enc' => ResponseCipher::seal('{"a":1}', str_repeat('o', 32))];
$response = (new SealResponse())->handle(
$this->request($sealed, optIn: true),
fn () => new JsonResponse(['ok' => true]),
);
$this->assertSame(428, $response->getStatusCode());
}
}