feat(services): partial/unpaid payment on admin create, payment history per invoice, zip attachments

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
baodev0
2026-10-22 22:22:09 +07:00
co-authored by Claude Opus 5.5
parent 02daefe4c2
commit fe68171aa7
15 changed files with 281 additions and 40 deletions
@@ -204,38 +204,49 @@ class CustomerServiceProvisioningService
$billingCycle = $data['billing_cycle'] ?? 'monthly';
$months = OrderHelper::cycleToMonths($billingCycle);
// 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)
$listPrice = $this->orderRecorder->listPrice($product, $billingCycle, $data['server_name'] ?? null);
abort_if(
!isset($data['amount_paid']) && $listPrice === null && $product->product_type === 'domain',
!isset($data['price']) && $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;
$basePrice = (int) round((float) ($data['price'] ?? $listPrice));
$paymentMethod = $data['payment_method'];
// Đối với balance: kiểm tra số dư trước khi vào transaction
$paymentStatus = $data['payment_status'] ?? 'paid';
$paidAmount = match ($paymentStatus) {
'unpaid' => 0,
'partial' => (int) round((float) $data['amount_paid']),
default => $basePrice,
};
abort_if(
$paymentMethod === 'balance' && (int) $customer->total < $basePrice,
$paymentStatus === 'partial' && $paidAmount >= $basePrice,
422,
'Số tiền trả trước phải nhỏ hơn giá dịch vụ. Khách trả đủ thì chọn "Đã thanh toán".',
);
$paymentMethod = $paymentStatus === 'unpaid' ? null : $data['payment_method'];
abort_if(
$paymentMethod === 'balance' && (int) $customer->total < $paidAmount,
422,
sprintf(
'Số dư khách không đủ. Hiện có: %s VNĐ, cần: %s VNĐ.',
number_format((int) $customer->total),
number_format($basePrice),
number_format($paidAmount),
),
);
return DB::transaction(function () use ($customer, $product, $data, $billingCycle, $months, $basePrice, $paymentMethod, $clientIp) {
return DB::transaction(function () use ($customer, $product, $data, $billingCycle, $months, $basePrice, $paidAmount, $paymentStatus, $paymentMethod, $clientIp) {
['order' => $order, 'orderItem' => $orderItem, 'invoice' => $invoice] = $this->orderRecorder->create(
$customer->id,
$product,
$billingCycle,
$basePrice,
$data['server_name'] ?? null,
paid: true,
paid: $paymentStatus === 'paid',
paymentMethod: $paymentMethod,
clientIp: $clientIp,
amountPaid: $paidAmount,
);
// Ngày bắt đầu/hết hạn dùng chung cho cả module_vmware (next_due_date/next_invoice) và services.
@@ -246,28 +257,32 @@ class CustomerServiceProvisioningService
// resolve VM qua service->orderItem->order->vmWare, không qua services.vm_id.
$vmId = $this->createModuleVmwareIfNeeded($data, $customer, $order, $basePrice, $expiresAt);
// 4) Payment processing
if ($paymentMethod === 'balance') {
$customer->total = (int) $customer->total - $basePrice;
$customer->total = (int) $customer->total - $paidAmount;
$customer->save();
}
$proofPaths = $data['_proof_paths'] ?? [];
Transaction::create([
'uuid' => (string) Str::uuid(),
'public_code' => Transaction::newPublicCode('SVC'),
'customer_id' => $customer->id,
'user_id' => Auth::id(),
'trans' => $order->order_num,
'amount' => $basePrice,
'type' => $paymentMethod === 'transfer' ? 'transfer' : 'balance',
'status' => 1, // paid
'note' => $paymentMethod === 'transfer'
if ($paidAmount > 0) {
$proofPaths = $data['_proof_paths'] ?? [];
$paymentNote = $paymentMethod === 'transfer'
? 'Admin tạo dịch vụ - thanh toán chuyển khoản'
: 'Admin tạo dịch vụ - trừ số dư khách hàng',
'proof_image' => !empty($proofPaths) ? json_encode($proofPaths) : null,
]);
: 'Admin tạo dịch vụ - trừ số dư khách hàng';
Transaction::create([
'uuid' => (string) Str::uuid(),
'public_code' => Transaction::newPublicCode('SVC'),
'customer_id' => $customer->id,
'user_id' => Auth::id(),
'trans' => $order->order_num,
'amount' => $paidAmount,
'type' => $paymentMethod === 'transfer' ? 'transfer' : 'balance',
'status' => 1,
'note' => $paymentStatus === 'partial'
? sprintf('%s (trả trước %s/%s VNĐ)', $paymentNote, number_format($paidAmount), number_format($basePrice))
: $paymentNote,
'proof_image' => !empty($proofPaths) ? json_encode($proofPaths) : null,
]);
}
// 5) Service — updateOrCreate để tránh trùng khi cùng VM được gán lại
$serviceLookup = $vmId