feat: nhắc gia hạn QR theo dịch vụ, failover server hosting, CCCD admin, log cấp phát dịch vụ

- Mail campaign: mẫu nhắc gia hạn thanh toán QR, chọn dịch vụ + chu kỳ từng dòng (renewal_services)
- Hosting: nhiều server Plesk cho một gói (service_plan_servers), chuyển server khi server chết
- Admin xem/sửa giấy tờ định danh khách, tuỳ chọn gia hạn cho khách, nhật ký cấp phát dịch vụ
- Đánh dấu mẫu email dịch vụ là non-marketing

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
baodev0
2026-10-30 18:36:41 +07:00
co-authored by Claude Sonnet 5.5
parent 047c23beff
commit 55a834a2f8
53 changed files with 1463 additions and 119 deletions
@@ -34,7 +34,8 @@ class ServicePlanRequest extends FormRequest
'features.*' => 'string|distinct|max:100',
'is_active' => 'boolean',
'sort_order' => 'nullable|integer|min:0',
'dns_manager_server_id' => 'nullable|integer|exists:dns_manager_servers,id',
'server_ids' => 'nullable|array|max:10',
'server_ids.*' => 'integer|distinct|exists:dns_manager_servers,id',
'plesk_plan_name' => 'nullable|string|max:255',
'note' => 'nullable|string',
];
@@ -48,10 +49,17 @@ class ServicePlanRequest extends FormRequest
$validated = $this->validated();
return [
...collect($validated)->except(['plesk_plan_name', 'note'])->all(),
'dns_manager_server_id' => $validated['dns_manager_server_id'] ?? null,
...collect($validated)->except(['plesk_plan_name', 'note', 'server_ids'])->all(),
'sort_order' => $validated['sort_order'] ?? 0,
'is_active' => $validated['is_active'] ?? true,
];
}
/**
* @return array<int, int>
*/
public function serverIds(): array
{
return $this->validated()['server_ids'] ?? [];
}
}
@@ -14,9 +14,9 @@ class ClientUpdateCccdRequest extends FormRequest
public function rules(): array
{
return [
'cmnd' => 'required|string|max:50',
'cmnd_before' => 'required|mimes:jpg,jpeg,png,webp|max:5120',
'cmnd_after' => 'required|mimes:jpg,jpeg,png,webp|max:5120',
'cmnd' => 'required|string|regex:/^\d{9}(\d{3})?$/',
'cmnd_before' => 'required|file|mimetypes:image/webp|mimes:webp|max:5120|dimensions:min_width=300,min_height=300,max_width=2000,max_height=2000',
'cmnd_after' => 'required|file|mimetypes:image/webp|mimes:webp|max:5120|dimensions:min_width=300,min_height=300,max_width=2000,max_height=2000',
];
}
@@ -24,10 +24,17 @@ class ClientUpdateCccdRequest extends FormRequest
{
return [
'cmnd.required' => 'Số CMT/CCCD là bắt buộc',
'cmnd.regex' => 'Số CMT/CCCD phải gồm 9 hoặc 12 chữ số',
'cmnd_before.required' => 'Ảnh mặt trước là bắt buộc',
'cmnd_before.image' => 'Ảnh mặt trước phải là hình ảnh',
'cmnd_before.mimetypes' => 'Ảnh mặt trước phải là ảnh WebP',
'cmnd_before.mimes' => 'Ảnh mặt trước phải là ảnh WebP',
'cmnd_before.max' => 'Ảnh mặt trước tối đa 5MB',
'cmnd_before.dimensions' => 'Ảnh mặt trước phải từ 300px đến 2000px mỗi cạnh',
'cmnd_after.required' => 'Ảnh mặt sau là bắt buộc',
'cmnd_after.image' => 'Ảnh mặt sau phải là hình ảnh',
'cmnd_after.mimetypes' => 'Ảnh mặt sau phải là ảnh WebP',
'cmnd_after.mimes' => 'Ảnh mặt sau phải là ảnh WebP',
'cmnd_after.max' => 'Ảnh mặt sau tối đa 5MB',
'cmnd_after.dimensions' => 'Ảnh mặt sau phải từ 300px đến 2000px mỗi cạnh',
];
}
}