feat: nạp tiền thủ công mã 30 phút, gia hạn trừ số dư, đồng bộ domain từ Plesk
- Nạp tiền: mã thanh toán sống 30 phút (ManualDepositDraft), khôi phục/huỷ mã, lệnh dọn mã hết hạn; total_pending chỉ tính giao dịch đã nộp chứng từ, admin duyệt/từ chối/xoá giao dịch - Gia hạn dịch vụ: phát hành invoice gia hạn trên order gốc và trừ số dư ngay - Đồng bộ domain từ Plesk theo server, ServiceOrderRecorder tạo order/invoice cho dịch vụ admin dựng - Danh sách dịch vụ trả field theo loại (service_plan, dns_records_count), đếm dịch vụ theo loại Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
10af41c5cf
commit
c5115b52dd
@@ -2,16 +2,11 @@
|
||||
|
||||
namespace App\Services\Customer;
|
||||
|
||||
use App\Enums\Invoice\InvoiceStatus;
|
||||
use App\Enums\Order\OrderStatus;
|
||||
use App\Helpers\OrderHelper;
|
||||
use App\Mail\ServiceRenewalReminderMail;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\InvoiceItem;
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\Order;
|
||||
use App\Models\OrderItem;
|
||||
use App\Models\Product;
|
||||
use App\Models\Service;
|
||||
use App\Models\Transaction;
|
||||
@@ -21,6 +16,7 @@ use App\Repositories\Module\Hosting\HostingRepositoryInterface as ModuleHostingR
|
||||
use App\Repositories\Module\Vmware\VmwareRepositoryInterface;
|
||||
use App\Repositories\Service\ServiceRepositoryInterface;
|
||||
use App\Services\Customer\Concerns\ResolvesCustomer;
|
||||
use App\Services\Order\ServiceOrderRecorder;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@@ -37,6 +33,7 @@ class CustomerServiceProvisioningService
|
||||
private readonly ModuleHostingRepositoryInterface $moduleHostingRepository,
|
||||
private readonly VmwareRepositoryInterface $vmwareRepository,
|
||||
private readonly ServiceRepositoryInterface $serviceRepository,
|
||||
private readonly ServiceOrderRecorder $orderRecorder,
|
||||
) {}
|
||||
|
||||
public function getServices(string $uuid): Collection
|
||||
@@ -89,8 +86,11 @@ class CustomerServiceProvisioningService
|
||||
$this->assignVirtualMachine($service, $customer, (int) $data['virtual_machine_id']);
|
||||
}
|
||||
|
||||
if (array_key_exists('price', $data) && $data['price'] !== null && $service->orderItem) {
|
||||
$service->orderItem->update(['price' => $data['price']]);
|
||||
$price = isset($data['price']) ? (int) $data['price'] : null;
|
||||
|
||||
if (!$service->order_item_id && ($price !== null || !empty($data['billing_cycle']))) {
|
||||
$this->orderRecorder->ensureForService($service, $price, $data['billing_cycle'] ?? null);
|
||||
$service->refresh();
|
||||
}
|
||||
|
||||
if (!empty($data['billing_cycle']) && $service->orderItem) {
|
||||
@@ -100,6 +100,11 @@ class CustomerServiceProvisioningService
|
||||
]);
|
||||
}
|
||||
|
||||
if ($price !== null && $service->orderItem) {
|
||||
$service->orderItem->update(['price' => $price]);
|
||||
$this->orderRecorder->syncUnpaidInvoice($service->refresh(), $price);
|
||||
}
|
||||
|
||||
// Đồng bộ sang module_vmware nếu có gán VM
|
||||
if ($service->vm_id) {
|
||||
$vmData = array_filter([
|
||||
@@ -196,7 +201,7 @@ class CustomerServiceProvisioningService
|
||||
{
|
||||
$customer = $this->resolveCustomer($uuid);
|
||||
|
||||
$product = Product::with('pricings')->find($data['product_id']);
|
||||
$product = Product::find($data['product_id']);
|
||||
abort_if(
|
||||
$product->product_type === 'vps' && empty($data['virtual_machine_id']),
|
||||
422,
|
||||
@@ -206,9 +211,14 @@ class CustomerServiceProvisioningService
|
||||
$billingCycle = $data['billing_cycle'] ?? 'monthly';
|
||||
$months = OrderHelper::cycleToMonths($billingCycle);
|
||||
|
||||
$pricing = $product->pricings->where('billing_cycle', $billingCycle)->first();
|
||||
// Số tiền đã thanh toán: admin nhập tay để khớp với thực tế (có thể lệch giá niêm yết do khuyến mãi/thoả thuận)
|
||||
$basePrice = isset($data['amount_paid']) ? (int) $data['amount_paid'] : (int) ($pricing->price ?? 0);
|
||||
$listPrice = $this->orderRecorder->listPrice($product, $billingCycle, $data['server_name'] ?? null);
|
||||
abort_if(
|
||||
!isset($data['amount_paid']) && $listPrice === null && $product->product_type === 'domain',
|
||||
422,
|
||||
'Tên miền không có giá trong bảng giá tên miền, vui lòng nhập giá.',
|
||||
);
|
||||
$basePrice = isset($data['amount_paid']) ? (int) $data['amount_paid'] : (int) $listPrice;
|
||||
|
||||
$paymentMethod = $data['payment_method'];
|
||||
|
||||
@@ -224,66 +234,25 @@ class CustomerServiceProvisioningService
|
||||
);
|
||||
|
||||
return DB::transaction(function () use ($customer, $product, $data, $billingCycle, $months, $basePrice, $paymentMethod, $clientIp) {
|
||||
// 1) Order + OrderItem (mô phỏng flow customer đặt hàng)
|
||||
$order = Order::create([
|
||||
'order_num' => OrderHelper::generateOrderNumber(),
|
||||
'customer_id' => $customer->id,
|
||||
'date' => now()->toDateString(),
|
||||
'sub_total' => $basePrice,
|
||||
'discount_amount' => 0,
|
||||
'total' => $basePrice,
|
||||
'status' => OrderStatus::ACTIVE->value,
|
||||
'payment_method' => $paymentMethod,
|
||||
'client_ip' => $clientIp,
|
||||
]);
|
||||
|
||||
$orderItem = OrderItem::create([
|
||||
'order_id' => $order->id,
|
||||
'product_id' => $product->id,
|
||||
'billing_cycle' => $billingCycle,
|
||||
'months' => $months,
|
||||
'server_name' => $data['server_name'] ?? null,
|
||||
'quantity' => 1,
|
||||
'price' => $basePrice,
|
||||
]);
|
||||
['order' => $order, 'orderItem' => $orderItem, 'invoice' => $invoice] = $this->orderRecorder->create(
|
||||
$customer->id,
|
||||
$product,
|
||||
$billingCycle,
|
||||
$basePrice,
|
||||
$data['server_name'] ?? null,
|
||||
paid: true,
|
||||
paymentMethod: $paymentMethod,
|
||||
clientIp: $clientIp,
|
||||
);
|
||||
|
||||
// Ngày bắt đầu/hết hạn dùng chung cho cả module_vmware (next_due_date/next_invoice) và services.
|
||||
$startedAt = $data['started_at'] ?? now();
|
||||
$expiresAt = $data['expires_at'] ?? Carbon::parse($startedAt)->addMonths($months);
|
||||
|
||||
// 2) ModuleVmware: snapshot từ virtual_machines. `order_id` phải trỏ về
|
||||
// order vừa tạo — client console-ticket/rebuild resolve VM qua
|
||||
// service->orderItem->order->vmWare, không qua services.vm_id.
|
||||
// module_vmware.order_id phải trỏ về order vừa tạo — client console-ticket/rebuild
|
||||
// resolve VM qua service->orderItem->order->vmWare, không qua services.vm_id.
|
||||
$vmId = $this->createModuleVmwareIfNeeded($data, $customer, $order, $basePrice, $expiresAt);
|
||||
|
||||
// 3) Invoice — đánh dấu PAID luôn vì admin đứng ra tạo & xác nhận thanh toán
|
||||
$invoice = Invoice::create([
|
||||
'invoice_number' => OrderHelper::generateOrderNumber('INV'),
|
||||
'customer_id' => $customer->id,
|
||||
'order_id' => $order->id,
|
||||
'invoice_type' => 1,
|
||||
'date' => now()->toDateString(),
|
||||
'sub_total' => $basePrice,
|
||||
'discount_amount' => 0,
|
||||
'tax' => 0,
|
||||
'total' => $basePrice,
|
||||
'status' => InvoiceStatus::PAID->value,
|
||||
'payment_method' => $paymentMethod,
|
||||
'paid_at' => now(),
|
||||
'due_date' => now()->addMonths($months)->toDateString(),
|
||||
]);
|
||||
|
||||
InvoiceItem::create([
|
||||
'invoice_id' => $invoice->id,
|
||||
'product_id' => $product->id,
|
||||
'type' => 'service',
|
||||
'description' => $product->name ?? 'Dịch vụ',
|
||||
'months' => $months,
|
||||
'quantity' => 1,
|
||||
'unit_price' => $basePrice,
|
||||
'total_price' => $basePrice,
|
||||
]);
|
||||
|
||||
// 4) Payment processing
|
||||
if ($paymentMethod === 'balance') {
|
||||
$customer->total = (int) $customer->total - $basePrice;
|
||||
|
||||
Reference in New Issue
Block a user