feat: add GeneratesUniqueSlug class for unique slug generation

feat: create PostCategoryFactory for generating post category data

feat: create PostFactory for generating post data

feat: add retry_requested_at column to services table

feat: add vcenter provisioning fields to products table

feat: add module_host_id to os table

feat: create email_notification_templates table for email templates

feat: add scenario and format fields to template table

feat: add admin fields to transactions table

fix: update ticket table columns and types

feat: add uuid to ticket table and backfill existing records

fix: make ticket_reply user_id nullable

feat: create notifications table for user notifications

fix: make ticket uuid nullable

feat: create post_categories table for blog categories

feat: create posts table for blog posts

feat: add invoice email template for PDF generation
This commit is contained in:
baodev0
2026-05-25 15:39:42 +07:00
parent a31d238135
commit 520f21cb11
73 changed files with 4682 additions and 440 deletions
@@ -4,7 +4,9 @@ namespace App\Http\Controllers\Api\Admin\Product;
use App\Http\Controllers\Controller;
use App\Http\Resources\Api\Admin\ProductResource;
use App\Http\Services\NextjsRevalidationService;
use App\Models\Product;
use App\Models\ProductPricings;
use App\Repositories\Admin\Product\AdminProductRepositoryInterface;
use App\Models\Os;
use Illuminate\Http\JsonResponse;
@@ -15,6 +17,7 @@ class ProductController extends Controller
{
public function __construct(
private readonly AdminProductRepositoryInterface $products,
private readonly NextjsRevalidationService $revalidation,
) {}
public function index(Request $request): JsonResponse
@@ -67,10 +70,19 @@ class ProductController extends Controller
'ram' => 'nullable|numeric|min:0',
'disk' => 'nullable|numeric|min:0',
'bandwidth' => 'nullable|numeric|min:0',
'billing_type' => 'nullable|string|in:recurring,onetime',
'currency' => 'nullable|string|max:10',
'price_monthly' => 'nullable|numeric|min:0',
'price_quarterly' => 'nullable|numeric|min:0',
'price_semiannually' => 'nullable|numeric|min:0',
'price_annually' => 'nullable|numeric|min:0',
'price_biennially' => 'nullable|numeric|min:0',
'price_triennially' => 'nullable|numeric|min:0',
'price_onetime' => 'nullable|numeric|min:0',
]);
$productType = $validated['product_type'] ?? null;
if (!$productType && !empty($validated['product_categories_id'])) {
$category = \App\Models\ProductCategories::find($validated['product_categories_id']);
$productType = $category?->type;
@@ -89,6 +101,9 @@ class ProductController extends Controller
'bandwidth' => $validated['bandwidth'] ?? null,
]);
$this->savePricings($product, $validated);
$this->revalidation->revalidateProducts();
return response()->json([
'error' => false,
'message' => 'Tạo sản phẩm thành công.',
@@ -116,6 +131,17 @@ class ProductController extends Controller
'bandwidth' => 'nullable|numeric|min:0',
'os_ids' => 'nullable|array',
'os_ids.*' => 'integer|exists:os,id',
'module_host_id' => 'nullable|integer|exists:module_host,id',
'module_datastore_id' => 'nullable|integer|exists:module_datastore,id',
'billing_type' => 'nullable|string|in:recurring,onetime',
'currency' => 'nullable|string|max:10',
'price_monthly' => 'nullable|numeric|min:0',
'price_quarterly' => 'nullable|numeric|min:0',
'price_semiannually' => 'nullable|numeric|min:0',
'price_annually' => 'nullable|numeric|min:0',
'price_biennially' => 'nullable|numeric|min:0',
'price_triennially' => 'nullable|numeric|min:0',
'price_onetime' => 'nullable|numeric|min:0',
]);
if (isset($validated['name']) && $validated['name'] !== $product->name) {
@@ -138,13 +164,49 @@ class ProductController extends Controller
$product->operatingSystems()->sync($osIds);
}
$this->savePricings($product, $validated);
$this->revalidation->revalidateProducts();
return response()->json([
'error' => false,
'message' => 'Cập nhật sản phẩm thành công.',
'data' => new ProductResource($product->fresh(['pricings', 'category'])),
'data' => new ProductResource($product->fresh(['pricings', 'category', 'operatingSystems'])),
]);
}
private function savePricings(Product $product, array $data): void
{
$currency = $data['currency'] ?? 'VND';
$cycles = [
'monthly' => 'price_monthly',
'quarterly' => 'price_quarterly',
'semiannually' => 'price_semiannually',
'annually' => 'price_annually',
'biennially' => 'price_biennially',
'triennially' => 'price_triennially',
'onetime' => 'price_onetime',
];
foreach ($cycles as $cycle => $field) {
if (!array_key_exists($field, $data)) {
continue;
}
$price = $data[$field];
if ($price !== null && $price !== '') {
ProductPricings::updateOrCreate(
['product_id' => $product->id, 'billing_cycle' => $cycle],
['price' => $price, 'currency' => $currency, 'is_active' => true]
);
} else {
ProductPricings::where('product_id', $product->id)
->where('billing_cycle', $cycle)
->delete();
}
}
}
public function destroy(int $id): JsonResponse
{
$product = Product::find($id);
@@ -154,6 +216,7 @@ class ProductController extends Controller
}
$product->delete();
$this->revalidation->revalidateProducts();
return response()->json([
'error' => false,
@@ -248,15 +311,19 @@ class ProductController extends Controller
public function getOsList(): JsonResponse
{
$osList = Os::with('osGroup')
$osList = Os::with(['osGroup', 'host.datacenter.vCenter'])
->where('status', 1)
->get()
->map(fn($os) => [
'id' => $os->id,
'name' => $os->name,
'version' => $os->version,
'os_group_name' => $os->osGroup?->name,
'template_id' => $os->template_id,
'id' => $os->id,
'name' => $os->name,
'version' => $os->version,
'os_group_name' => $os->osGroup?->name,
'template_id' => $os->template_id,
'module_host_id' => $os->module_host_id,
'host_name' => $os->host?->name,
'datacenter_name' => $os->host?->datacenter?->name,
'vcenter_name' => $os->host?->datacenter?->vCenter?->host,
]);
return response()->json([