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
@@ -215,8 +215,10 @@ trait ManagesCustomerServices
'expires_at' => 'nullable|date',
'note' => 'nullable|string',
'billing_cycle' => ['nullable', Rule::in(BillingCycleCatalog::codes())],
'payment_method' => 'required|in:transfer,balance',
'amount_paid' => 'nullable|numeric|min:0',
'price' => 'nullable|numeric|min:0',
'payment_status' => 'nullable|in:paid,partial,unpaid',
'payment_method' => 'required_unless:payment_status,unpaid|nullable|in:transfer,balance',
'amount_paid' => 'required_if:payment_status,partial|nullable|numeric|gt:0',
'proof_images' => 'nullable|array',
'proof_images.*' => 'file|image|max:5120',
'os_id' => 'nullable|integer|exists:os,id',
@@ -230,8 +232,12 @@ trait ManagesCustomerServices
'vm_port' => 'nullable|integer|min:1|max:65535',
]);
// Upload proof images (chuyển khoản) — I/O ở tầng HTTP, service chỉ nhận đường dẫn đã lưu.
if ($data['payment_method'] === 'transfer' && $request->hasFile('proof_images')) {
$data['payment_status'] ??= 'paid';
if ($data['payment_status'] === 'unpaid') {
$data['payment_method'] = null;
}
if (($data['payment_method'] ?? null) === 'transfer' && $request->hasFile('proof_images')) {
$data['_proof_paths'] = collect($request->file('proof_images'))
->map(fn($file) => $file->store('payment-proofs', 'public'))
->all();
@@ -28,7 +28,7 @@ trait HandlesServiceAttachments
public function uploadAttachment(Request $request, int $id): JsonResponse
{
Service::findOrFail($id);
$request->validate(['file' => InvoiceAttachment::UPLOAD_RULE]);
$request->validate(['file' => ServiceAttachment::UPLOAD_RULE]);
$file = $request->file('file');
$attachment = ServiceAttachment::create([
@@ -8,6 +8,7 @@ use App\Http\Controllers\Controller;
use App\Jobs\VirtualMachine\Provision\ResendVmCredentialsEmailJob;
use App\Models\ActivityLog;
use App\Models\DomainPricing;
use App\Support\Billing\InvoicePaymentHistory;
use App\Models\Invoice;
use App\Models\ModuleVmware;
use App\Models\Service;
@@ -93,7 +94,10 @@ class ServiceController extends Controller
ModuleVmware::resolveVirtualMachineIds([$service->vmware]);
$service->setAttribute('linked_invoices', $this->linkedInvoices(collect([$service])));
$invoices = $this->linkedInvoices(collect([$service]));
$payments = InvoicePaymentHistory::forInvoices($invoices);
$invoices->each(fn (Invoice $invoice) => $invoice->setAttribute('payments', $payments[$invoice->id]));
$service->setAttribute('linked_invoices', $invoices);
$service->setAttribute('activity_logs', ActivityLog::where('subject_type', Service::class)
->where('subject_id', $service->id)
->latest('id')
@@ -109,7 +113,7 @@ class ServiceController extends Controller
return Invoice::ofServices($services)
->with('order:id,order_num')
->latest('id')
->get(['id', 'order_id', 'service_id', 'invoice_number', 'invoice_type', 'status', 'total', 'created_at']);
->get(['id', 'customer_id', 'order_id', 'service_id', 'invoice_number', 'invoice_type', 'status', 'total', 'amount_paid', 'due_date', 'created_at']);
}
public function bulkAction(Request $request): JsonResponse