invoiceRepository->find($id); if (!$invoice) { return response()->json([ 'error' => true, 'message' => "Can't find invoice by ID #" . $id ], 404); } // Bỏ trống = thu đủ hoá đơn. Nhập số nhỏ hơn khi khách mới chuyển được một phần — // hoá đơn ghi nhận trả một phần và chưa kích hoạt dịch vụ. $validated = $request->validate([ 'received_amount' => 'nullable|integer|min:1', ]); try { DB::beginTransaction(); $invoice->load(['order', 'customer']); $invoice = $this->invoicePaymentService->markPaidByAdmin( $invoice, auth()->id(), receivedAmount: $validated['received_amount'] ?? null, ); DB::commit(); $outstanding = InvoicePaymentService::outstanding($invoice); return response()->json([ 'error' => false, 'message' => $outstanding > 0 ? 'Đã ghi nhận thanh toán một phần. Còn thiếu ' . number_format($outstanding) . ' VNĐ.' : 'Invoice marked as paid', 'data' => [ 'amount_paid' => (int) $invoice->amount_paid, 'outstanding' => $outstanding, ], ]); } catch (\InvalidArgumentException $e) { DB::rollBack(); return response()->json([ 'error' => true, 'message' => $e->getMessage() ], 422); } catch (\Exception $e) { DB::rollBack(); return response()->json([ 'error' => true, 'message' => $e->getMessage() ], 500); } } public function cancel(int $id): JsonResponse { $invoice = $this->invoiceRepository->find($id); if (!$invoice) { return response()->json([ 'error' => true, 'message' => "Can't find invoice by ID #" . $id ], 404); } try { $this->assertCancellable($invoice); DB::beginTransaction(); $invoice->update([ 'status' => InvoiceStatus::CANCELLED->value, 'paid_at' => null ]); DB::commit(); return response()->json([ 'error' => false, 'message' => 'Invoice cancelled' ]); } catch (\InvalidArgumentException $e) { DB::rollBack(); return response()->json([ 'error' => true, 'message' => $e->getMessage() ], 422); } catch (\Exception $e) { DB::rollBack(); return response()->json([ 'error' => true, 'message' => $e->getMessage() ], 500); } } /** * Hoàn tiền hoá đơn đã thu — cộng lại số dư khách và ghi Transaction âm, thay cho việc * huỷ hoá đơn đã thanh toán (nay đã bị chặn) hoặc xử lý tay ngoài hệ thống. */ public function refund(Request $request, int $id): JsonResponse { $invoice = $this->invoiceRepository->find($id); if (!$invoice) { return response()->json([ 'error' => true, 'message' => "Can't find invoice by ID #" . $id ], 404); } $validated = $request->validate([ 'amount' => 'nullable|integer|min:1', 'reason' => 'nullable|string|max:500', ]); try { $invoice = $this->invoicePaymentService->refund( $invoice, auth()->id(), $validated['amount'] ?? null, $validated['reason'] ?? '', ); } catch (\InvalidArgumentException $e) { return response()->json(['error' => true, 'message' => $e->getMessage()], 422); } catch (\Throwable $e) { Log::error('InvoiceController::refund error: ' . $e->getMessage(), ['invoice_id' => $id]); return response()->json(['error' => true, 'message' => 'Không thể hoàn tiền, vui lòng thử lại.'], 500); } return response()->json([ 'error' => false, 'message' => 'Đã hoàn tiền vào số dư khách hàng.', 'data' => ['invoice' => new InvoiceResource($invoice)], ]); } /** * Huỷ hoá đơn đã thu tiền sẽ xoá paid_at nhưng không hoàn số dư, không revert order/service, * trong khi Transaction vẫn còn — sổ sách lệch im lặng. Chặn ở đây, đảo giao dịch phải đi * qua luồng hoàn tiền. */ private function assertCancellable(Invoice $invoice): void { if (in_array((int) $invoice->status, [InvoiceStatus::PAID->value, InvoiceStatus::REFUNDED->value], true)) { throw new \InvalidArgumentException( 'Không thể huỷ hoá đơn đã thanh toán hoặc đã hoàn tiền. Hãy dùng chức năng hoàn tiền nếu cần đảo giao dịch.' ); } } }