feat: add domain registration form and email notifications
- Create a new Blade view for domain registration (`registrant-tochuc.blade.php`) to capture organization and manager details. - Implement email templates for new domain orders for both admin (`domain-order-new.blade.php`) and customers (`domain-order-received.blade.php`). - Add CSV exports for client and domain data. - Introduce tests for customer mass assignment, password reset, login throttling, and permission gating. - Ensure service renewals use current pricing and validate ownership for virtual machines. - Validate mass assignment behavior in service model.
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\Report;
|
||||
|
||||
use App\Enums\Invoice\InvoiceStatus;
|
||||
use App\Enums\TransactionStatus;
|
||||
use App\Exports\Reports\ReportRowsExport;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Customer;
|
||||
use App\Models\DnsDomain;
|
||||
use App\Models\ExportJob;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Service;
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Maatwebsite\Excel\Excel as ExcelWriter;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
|
||||
class ExportController extends Controller
|
||||
{
|
||||
private const PREVIEW_LIMIT = 20;
|
||||
|
||||
public function preview(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'type' => 'required|string',
|
||||
'filters' => 'array',
|
||||
'fields' => 'array',
|
||||
]);
|
||||
|
||||
$query = $this->baseQuery($data['type'], $data['filters'] ?? []);
|
||||
$total = (clone $query)->count();
|
||||
$items = $query->limit(self::PREVIEW_LIMIT)->get()
|
||||
->map(fn ($model) => $this->rowValues($model, $data['type']));
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => ['items' => $items, 'total' => $total],
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'type' => 'required|string',
|
||||
'format' => 'required|in:csv,xlsx',
|
||||
'filters' => 'array',
|
||||
'fields' => 'array',
|
||||
]);
|
||||
|
||||
$labels = $this->fieldLabels($data['type']);
|
||||
$fields = !empty($data['fields']) ? $data['fields'] : array_keys($labels);
|
||||
|
||||
$rows = $this->baseQuery($data['type'], $data['filters'] ?? [])->get()
|
||||
->map(fn ($model) => $this->rowValues($model, $data['type']));
|
||||
|
||||
$uuid = (string) Str::uuid();
|
||||
$filename = "exports/{$data['type']}_{$uuid}.{$data['format']}";
|
||||
$writerType = $data['format'] === 'xlsx' ? ExcelWriter::XLSX : ExcelWriter::CSV;
|
||||
|
||||
Excel::store(new ReportRowsExport($rows, $fields, $labels), $filename, 'local', $writerType);
|
||||
|
||||
$job = ExportJob::create([
|
||||
'uuid' => $uuid,
|
||||
'type' => $data['type'],
|
||||
'format' => $data['format'],
|
||||
'filters' => $data['filters'] ?? [],
|
||||
'fields' => $fields,
|
||||
'status' => 'completed',
|
||||
'file_path' => $filename,
|
||||
'total_rows' => $rows->count(),
|
||||
'created_by' => auth('admin')->id(),
|
||||
'completed_at' => now(),
|
||||
]);
|
||||
|
||||
return response()->json(['error' => false, 'data' => ['id' => $job->uuid]]);
|
||||
}
|
||||
|
||||
public function show(string $uuid): JsonResponse
|
||||
{
|
||||
$job = ExportJob::where('uuid', $uuid)->firstOrFail();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'id' => $job->uuid,
|
||||
'type' => $job->type,
|
||||
'format' => $job->format,
|
||||
'status' => $job->status,
|
||||
'progress' => $job->status === 'completed' ? 100 : 0,
|
||||
'processed' => $job->total_rows ?? 0,
|
||||
'total' => $job->total_rows ?? 0,
|
||||
'error' => $job->error,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function history(Request $request): JsonResponse
|
||||
{
|
||||
$perPage = min((int) $request->get('per_page', 15), 100);
|
||||
|
||||
$query = ExportJob::query()->with('creator')->latest();
|
||||
|
||||
if ($request->filled('filter')) {
|
||||
$query->where('type', 'like', '%' . $request->get('filter') . '%');
|
||||
}
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->get('status'));
|
||||
}
|
||||
|
||||
$paginator = $query->paginate($perPage);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'items' => $paginator->getCollection()->map(fn (ExportJob $job) => [
|
||||
'id' => $job->uuid,
|
||||
'type' => $job->type,
|
||||
'format' => $job->format,
|
||||
'records' => $job->total_rows ?? 0,
|
||||
'created_by' => $job->creator?->username ?? '—',
|
||||
'created_at' => $job->created_at?->toIso8601String(),
|
||||
'status' => $job->status,
|
||||
]),
|
||||
'total' => $paginator->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function download(string $uuid)
|
||||
{
|
||||
$job = ExportJob::where('uuid', $uuid)->firstOrFail();
|
||||
abort_unless($job->status === 'completed' && $job->file_path, 404);
|
||||
|
||||
return Storage::disk('local')->download($job->file_path, basename($job->file_path));
|
||||
}
|
||||
|
||||
private function baseQuery(string $type, array $filters): Builder
|
||||
{
|
||||
return match ($type) {
|
||||
'client', 'clients' => Customer::query()
|
||||
->when($type === 'client', fn (Builder $q) => $q->where('id', $filters['client_id'] ?? 0))
|
||||
->when(!empty($filters['date_from']), fn (Builder $q) => $q->whereDate('created_at', '>=', $filters['date_from']))
|
||||
->when(!empty($filters['date_to']), fn (Builder $q) => $q->whereDate('created_at', '<=', $filters['date_to'])),
|
||||
|
||||
'domains' => DnsDomain::query()
|
||||
->with(['customer', 'server', 'ns'])
|
||||
->when(!empty($filters['date_from']), fn (Builder $q) => $q->whereDate('created_at', '>=', $filters['date_from']))
|
||||
->when(!empty($filters['date_to']), fn (Builder $q) => $q->whereDate('created_at', '<=', $filters['date_to'])),
|
||||
|
||||
'invoices' => Invoice::query()
|
||||
->when(!empty($filters['date_from']), fn (Builder $q) => $q->whereDate('created_at', '>=', $filters['date_from']))
|
||||
->when(!empty($filters['date_to']), fn (Builder $q) => $q->whereDate('created_at', '<=', $filters['date_to']))
|
||||
->when(!empty($filters['invoice_status']), fn (Builder $q) => $q->where(
|
||||
'status',
|
||||
$filters['invoice_status'] === 'paid' ? InvoiceStatus::PAID->value : InvoiceStatus::UNPAID->value
|
||||
)),
|
||||
|
||||
'services' => Service::query()
|
||||
->with(['product', 'orderItem'])
|
||||
->when(!empty($filters['date_from']), fn (Builder $q) => $q->whereDate('created_at', '>=', $filters['date_from']))
|
||||
->when(!empty($filters['date_to']), fn (Builder $q) => $q->whereDate('created_at', '<=', $filters['date_to'])),
|
||||
|
||||
'transactions' => Transaction::query()
|
||||
->when(!empty($filters['date_from']), fn (Builder $q) => $q->whereDate('created_at', '>=', $filters['date_from']))
|
||||
->when(!empty($filters['date_to']), fn (Builder $q) => $q->whereDate('created_at', '<=', $filters['date_to'])),
|
||||
|
||||
default => throw new \InvalidArgumentException("Loại xuất dữ liệu không được hỗ trợ: {$type}"),
|
||||
};
|
||||
}
|
||||
|
||||
private function rowValues($model, string $type): array
|
||||
{
|
||||
return match ($type) {
|
||||
'client', 'clients' => [
|
||||
'id' => $model->id,
|
||||
'name' => $model->fullname,
|
||||
'company' => $model->company,
|
||||
'email' => $model->email,
|
||||
'phone' => $model->phone,
|
||||
'address' => $model->address,
|
||||
'city' => $model->city,
|
||||
'state' => $model->state,
|
||||
'postcode' => $model->postcode,
|
||||
'country' => $model->country,
|
||||
'status' => match ((int) $model->status) {
|
||||
1 => 'Actived',
|
||||
2 => 'De-actived',
|
||||
default => 'Banned',
|
||||
},
|
||||
'created_at' => optional($model->created_at)->format('Y-m-d H:i'),
|
||||
],
|
||||
|
||||
'domains' => [
|
||||
'domain' => $model->domain_name,
|
||||
'customer' => $model->customer
|
||||
->map(fn ($c) => $c->email ? "{$c->fullname} ({$c->email})" : $c->fullname)
|
||||
->filter()
|
||||
->implode(', '),
|
||||
'server' => $model->server?->name,
|
||||
'nameservers' => $model->ns->pluck('value')->filter()->implode(', '),
|
||||
'status' => match ((int) $model->is_created) {
|
||||
1 => 'Đã khởi tạo',
|
||||
2 => 'Không hoạt động',
|
||||
default => 'Chưa khởi tạo',
|
||||
},
|
||||
'created_at' => optional($model->created_at)->format('Y-m-d H:i'),
|
||||
],
|
||||
|
||||
'invoices' => [
|
||||
'id' => $model->id,
|
||||
'client_id' => $model->customer_id,
|
||||
'invoice_number' => $model->invoice_number,
|
||||
'date' => optional($model->created_at)->format('Y-m-d'),
|
||||
'due_date' => optional($model->due_date)->format('Y-m-d'),
|
||||
'subtotal' => $model->sub_total,
|
||||
'tax' => $model->tax,
|
||||
'total' => $model->total,
|
||||
'status' => InvoiceStatus::tryFrom((int) $model->status)?->label() ?? $model->status,
|
||||
'payment_method' => $model->payment_method,
|
||||
],
|
||||
|
||||
'services' => [
|
||||
'id' => $model->id,
|
||||
'client_id' => $model->customer_id,
|
||||
'product_id' => $model->product_id,
|
||||
'product_name' => $model->product?->name,
|
||||
'domain' => $model->server_name,
|
||||
'billing_cycle' => $model->orderItem?->billing_cycle,
|
||||
'amount' => $model->orderItem?->price,
|
||||
'next_due_date' => optional($model->expires_at)->format('Y-m-d'),
|
||||
'status' => $model->status,
|
||||
],
|
||||
|
||||
'transactions' => [
|
||||
'id' => $model->id,
|
||||
'client_id' => $model->customer_id,
|
||||
'date' => optional($model->created_at)->format('Y-m-d H:i'),
|
||||
'description' => $model->trans,
|
||||
'amount' => $model->amount,
|
||||
'gateway' => $model->type,
|
||||
'status' => TransactionStatus::tryFrom((int) $model->status)?->label() ?? $model->status,
|
||||
],
|
||||
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
private function fieldLabels(string $type): array
|
||||
{
|
||||
return match ($type) {
|
||||
'client', 'clients' => [
|
||||
'id' => 'Client ID',
|
||||
'name' => 'Name',
|
||||
'company' => 'Company',
|
||||
'email' => 'Email',
|
||||
'phone' => 'Phone',
|
||||
'address' => 'Address',
|
||||
'city' => 'City',
|
||||
'state' => 'State',
|
||||
'postcode' => 'Postcode',
|
||||
'country' => 'Country',
|
||||
'status' => 'Status',
|
||||
'created_at' => 'Created At',
|
||||
],
|
||||
'domains' => [
|
||||
'domain' => 'Domain',
|
||||
'customer' => 'Khách hàng',
|
||||
'server' => 'DNS Server',
|
||||
'nameservers' => 'NS',
|
||||
'status' => 'Trạng thái',
|
||||
'created_at' => 'Ngày tạo',
|
||||
],
|
||||
'invoices' => [
|
||||
'id' => 'Invoice ID',
|
||||
'client_id' => 'Client ID',
|
||||
'invoice_number' => 'Invoice Number',
|
||||
'date' => 'Date',
|
||||
'due_date' => 'Due Date',
|
||||
'subtotal' => 'Subtotal',
|
||||
'tax' => 'Tax',
|
||||
'total' => 'Total',
|
||||
'status' => 'Status',
|
||||
'payment_method' => 'Payment Method',
|
||||
],
|
||||
'services' => [
|
||||
'id' => 'Service ID',
|
||||
'client_id' => 'Client ID',
|
||||
'product_id' => 'Product ID',
|
||||
'product_name' => 'Product Name',
|
||||
'domain' => 'Domain',
|
||||
'billing_cycle' => 'Billing Cycle',
|
||||
'amount' => 'Amount',
|
||||
'next_due_date' => 'Next Due Date',
|
||||
'status' => 'Status',
|
||||
],
|
||||
'transactions' => [
|
||||
'id' => 'Transaction ID',
|
||||
'client_id' => 'Client ID',
|
||||
'date' => 'Date',
|
||||
'description' => 'Description',
|
||||
'amount' => 'Amount',
|
||||
'gateway' => 'Gateway',
|
||||
'status' => 'Status',
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user