feat: add manual deposit system, implement Bank model, remove Docker configs, and initialize comprehensive Laravel best practice documentation for AI agents.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\Bank;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Bank;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class BankController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$banks = Bank::withTrashed()->orderBy('sort_order')->orderBy('id')->get();
|
||||
|
||||
return response()->json(['error' => false, 'data' => $banks]);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'bank_code' => 'required|string|max:20',
|
||||
'bank_name' => 'required|string|max:100',
|
||||
'account_no' => 'required|string|max:50',
|
||||
'account_name' => 'required|string|max:200',
|
||||
'is_active' => 'boolean',
|
||||
'sort_order' => 'integer|min:0',
|
||||
]);
|
||||
|
||||
$bank = Bank::create($data);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Thêm ngân hàng thành công',
|
||||
'data' => $bank,
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$bank = Bank::withTrashed()->findOrFail($id);
|
||||
|
||||
$data = $request->validate([
|
||||
'bank_code' => 'sometimes|string|max:20',
|
||||
'bank_name' => 'sometimes|string|max:100',
|
||||
'account_no' => 'sometimes|string|max:50',
|
||||
'account_name' => 'sometimes|string|max:200',
|
||||
'is_active' => 'boolean',
|
||||
'sort_order' => 'integer|min:0',
|
||||
]);
|
||||
|
||||
$bank->update($data);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Cập nhật ngân hàng thành công',
|
||||
'data' => $bank->fresh(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$bank = Bank::findOrFail($id);
|
||||
|
||||
DB::transaction(fn () => $bank->delete());
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Đã xoá ngân hàng',
|
||||
]);
|
||||
}
|
||||
|
||||
public function restore(int $id): JsonResponse
|
||||
{
|
||||
$bank = Bank::onlyTrashed()->findOrFail($id);
|
||||
$bank->restore();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Đã khôi phục ngân hàng',
|
||||
'data' => $bank,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\Dashboard;
|
||||
|
||||
use App\Enums\TransactionStatus;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function stats(): JsonResponse
|
||||
{
|
||||
$total_customers = Customer::count();
|
||||
|
||||
$total_deposits = (float) Transaction::where('type', 'manual')
|
||||
->where('status', TransactionStatus::COMPLETED->value)
|
||||
->sum('amount');
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'total_customers' => $total_customers,
|
||||
'total_deposits' => $total_deposits,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function monthlyStats(Request $request): JsonResponse
|
||||
{
|
||||
$year = $request->integer('year', now()->year);
|
||||
|
||||
$deposits = Transaction::where('type', 'manual')
|
||||
->where('status', TransactionStatus::COMPLETED->value)
|
||||
->whereYear('created_at', $year)
|
||||
->selectRaw('MONTH(created_at) as month, SUM(amount) as total_deposits')
|
||||
->groupBy('month')
|
||||
->pluck('total_deposits', 'month');
|
||||
|
||||
$newCustomers = Customer::whereYear('created_at', $year)
|
||||
->selectRaw('MONTH(created_at) as month, COUNT(*) as new_customers')
|
||||
->groupBy('month')
|
||||
->pluck('new_customers', 'month');
|
||||
|
||||
$months = ['T1', 'T2', 'T3', 'T4', 'T5', 'T6', 'T7', 'T8', 'T9', 'T10', 'T11', 'T12'];
|
||||
|
||||
$data = collect(range(1, 12))->map(function ($m) use ($months, $deposits, $newCustomers) {
|
||||
return [
|
||||
'month' => $months[$m - 1],
|
||||
'deposits' => (float) ($deposits[$m] ?? 0),
|
||||
'new_customers' => (int) ($newCustomers[$m] ?? 0),
|
||||
];
|
||||
})->values();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
public function latestCustomers(): JsonResponse
|
||||
{
|
||||
$customers = Customer::orderByDesc('created_at')
|
||||
->limit(5)
|
||||
->get(['id', 'uuid', 'fullname', 'email', 'total', 'created_at']);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'items' => $customers->map(fn(Customer $c) => [
|
||||
'id' => $c->id,
|
||||
'uuid' => $c->uuid,
|
||||
'fullname' => $c->fullname,
|
||||
'email' => $c->email,
|
||||
'total' => (float) $c->total,
|
||||
'created_at' => $c->created_at?->toISOString(),
|
||||
])->values(),
|
||||
'total' => Customer::count(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\Transaction;
|
||||
|
||||
use App\Enums\TransactionStatus;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Log;
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log as FacadesLog;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class AdminTransactionController extends Controller
|
||||
{
|
||||
public function pendingCount(): JsonResponse
|
||||
{
|
||||
$count = Transaction::where('type', 'manual')
|
||||
->where('status', TransactionStatus::PENDING->value)
|
||||
->whereNotNull('proof_image')
|
||||
->count();
|
||||
|
||||
return response()->json(['error' => false, 'data' => ['count' => $count]]);
|
||||
}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = Transaction::with('customer')
|
||||
->where('type', 'manual')
|
||||
->orderByDesc('created_at');
|
||||
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->integer('status'));
|
||||
}
|
||||
|
||||
if ($request->filled('filter')) {
|
||||
$search = $request->string('filter')->trim();
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('public_code', 'like', "%{$search}%")
|
||||
->orWhereHas('customer', fn($c) => $c
|
||||
->where('fullname', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%"));
|
||||
});
|
||||
}
|
||||
|
||||
$perPage = $request->integer('per_page', 15);
|
||||
$paginated = $query->paginate($perPage);
|
||||
|
||||
$items = $paginated->map(fn(Transaction $t) => [
|
||||
'id' => $t->id,
|
||||
'uuid' => $t->uuid,
|
||||
'public_code' => $t->public_code,
|
||||
'trans' => $t->trans,
|
||||
'amount' => $t->amount,
|
||||
'type' => $t->type,
|
||||
'status' => $t->status,
|
||||
'status_label' => TransactionStatus::from((int) $t->status)->label(),
|
||||
'proof_image' => $t->proof_image ? Storage::disk('public')->url($t->proof_image) : null,
|
||||
'created_at' => $t->created_at?->toISOString(),
|
||||
'customer' => $t->customer ? [
|
||||
'uuid' => $t->customer->uuid,
|
||||
'fullname' => $t->customer->fullname,
|
||||
'email' => $t->customer->email,
|
||||
'phone' => $t->customer->phone,
|
||||
'total' => $t->customer->total,
|
||||
'total_pending' => $t->customer->total_pending,
|
||||
] : null,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'items' => $items,
|
||||
'total' => $paginated->total(),
|
||||
'per_page' => $paginated->perPage(),
|
||||
'current_page' => $paginated->currentPage(),
|
||||
'last_page' => $paginated->lastPage(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function approve(string $uuid): JsonResponse
|
||||
{
|
||||
$transaction = Transaction::with('customer')
|
||||
->where('uuid', $uuid)
|
||||
->where('type', 'manual')
|
||||
->where('status', TransactionStatus::PENDING->value)
|
||||
->first();
|
||||
|
||||
if (!$transaction) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Giao dịch không tồn tại hoặc không thể duyệt',
|
||||
], 404);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$transaction->update(['status' => TransactionStatus::COMPLETED->value]);
|
||||
|
||||
$customer = $transaction->customer;
|
||||
$customer->total += $transaction->amount;
|
||||
$customer->credit += (int) floor($transaction->amount / 100);
|
||||
$customer->total_pending = max(0, $customer->total_pending - $transaction->amount);
|
||||
$customer->save();
|
||||
|
||||
Log::create([
|
||||
'agent' => 'admin',
|
||||
'client_ip' => request()->ip(),
|
||||
'description' => 'Admin duyệt nạp tiền thủ công: ' . number_format($transaction->amount) . ' VNĐ (+' . floor($transaction->amount / 100) . ' credit)',
|
||||
'customer_id' => $customer->id,
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
FacadesLog::error('AdminTransactionController::approve error: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Có lỗi xảy ra, vui lòng thử lại',
|
||||
], 500);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Đã duyệt giao dịch thành công',
|
||||
]);
|
||||
}
|
||||
|
||||
public function reject(string $uuid): JsonResponse
|
||||
{
|
||||
$transaction = Transaction::with('customer')
|
||||
->where('uuid', $uuid)
|
||||
->where('type', 'manual')
|
||||
->where('status', TransactionStatus::PENDING->value)
|
||||
->first();
|
||||
|
||||
if (!$transaction) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Giao dịch không tồn tại hoặc không thể từ chối',
|
||||
], 404);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$transaction->update(['status' => TransactionStatus::FAILED->value]);
|
||||
|
||||
$customer = $transaction->customer;
|
||||
$customer->total_pending = max(0, $customer->total_pending - $transaction->amount);
|
||||
$customer->save();
|
||||
|
||||
Log::create([
|
||||
'agent' => 'admin',
|
||||
'client_ip' => request()->ip(),
|
||||
'description' => 'Admin từ chối nạp tiền thủ công: ' . number_format($transaction->amount) . ' VNĐ',
|
||||
'customer_id' => $customer->id,
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
FacadesLog::error('AdminTransactionController::reject error: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Có lỗi xảy ra, vui lòng thử lại',
|
||||
], 500);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Đã từ chối giao dịch',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Client;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Bank;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class BankController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$banks = Bank::active()->get(['id', 'bank_code', 'bank_name', 'account_no', 'account_name']);
|
||||
|
||||
return response()->json(['error' => false, 'data' => $banks]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Transaction;
|
||||
|
||||
use App\Enums\TransactionStatus;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\DepositRequest;
|
||||
use App\Http\Services\Telegram;
|
||||
use App\Models\Log;
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ManualDepositController extends Controller
|
||||
{
|
||||
/**
|
||||
* GET /v1/transaction/manual-deposit/pending
|
||||
* Kiểm tra xem user có GD manual đang chờ duyệt (đã submit proof) không.
|
||||
*/
|
||||
public function pending(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$transaction = Transaction::where('customer_id', $user->id)
|
||||
->where('type', 'manual')
|
||||
->where('status', TransactionStatus::PENDING->value)
|
||||
->whereNotNull('proof_image')
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if (!$transaction) {
|
||||
return response()->json(['error' => false, 'data' => ['has_pending' => false]]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'has_pending' => true,
|
||||
'transaction' => [
|
||||
'public_code' => $transaction->public_code,
|
||||
'amount' => $transaction->amount,
|
||||
'created_at' => $transaction->created_at?->toISOString(),
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/transaction/manual-deposit/prepare
|
||||
* Tạo transaction PENDING và trả về public_code để show trong QR / nội dung CK.
|
||||
* Client gọi khi chuyển sang bước chọn ngân hàng.
|
||||
*/
|
||||
public function prepare(DepositRequest $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
// Chặn nếu đã có GD đã submit đang chờ admin duyệt
|
||||
$submitted = Transaction::where('customer_id', $user->id)
|
||||
->where('type', 'manual')
|
||||
->where('status', TransactionStatus::PENDING->value)
|
||||
->whereNotNull('proof_image')
|
||||
->exists();
|
||||
|
||||
if ($submitted) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Bạn đang có yêu cầu nạp tiền chờ duyệt. Vui lòng chờ admin xác nhận trước khi tạo yêu cầu mới.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Huỷ transaction prepare cũ (chưa có proof) nếu còn tồn tại
|
||||
$old = Transaction::where('customer_id', $user->id)
|
||||
->where('type', 'manual')
|
||||
->where('status', TransactionStatus::PENDING->value)
|
||||
->whereNull('proof_image')
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if ($old) {
|
||||
DB::transaction(function () use ($old, $user) {
|
||||
$user->total_pending = max(0, $user->total_pending - $old->amount);
|
||||
$user->save();
|
||||
$old->forceDelete();
|
||||
});
|
||||
}
|
||||
|
||||
$uuid = Str::uuid()->toString();
|
||||
$publicCode = 'MAN-' . now()->format('Ymd') . '-' . strtoupper(Str::random(6));
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
Transaction::create([
|
||||
'uuid' => $uuid,
|
||||
'public_code' => $publicCode,
|
||||
'trans' => 'MAN' . time(),
|
||||
'amount' => $request->amount,
|
||||
'customer_id' => $user->id,
|
||||
'type' => 'manual',
|
||||
'status' => TransactionStatus::PENDING->value,
|
||||
]);
|
||||
|
||||
$user->total_pending += $request->amount;
|
||||
$user->save();
|
||||
|
||||
DB::commit();
|
||||
} catch (\Exception) {
|
||||
DB::rollBack();
|
||||
return response()->json(['error' => true, 'message' => 'Có lỗi xảy ra, vui lòng thử lại'], 500);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'uuid' => $uuid,
|
||||
'public_code' => $publicCode,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /v1/transaction/manual-deposit/{uuid}/cancel
|
||||
* Huỷ transaction đang prepare (chưa có proof, chưa submit).
|
||||
* Client gọi khi user quay lại step 1 để đổi số tiền.
|
||||
*/
|
||||
public function cancel(Request $request, string $uuid): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$transaction = Transaction::where('uuid', $uuid)
|
||||
->where('customer_id', $user->id)
|
||||
->where('type', 'manual')
|
||||
->where('status', TransactionStatus::PENDING->value)
|
||||
->whereNull('proof_image')
|
||||
->firstOrFail();
|
||||
|
||||
DB::transaction(function () use ($transaction, $user) {
|
||||
$user->total_pending = max(0, $user->total_pending - $transaction->amount);
|
||||
$user->save();
|
||||
$transaction->forceDelete();
|
||||
});
|
||||
|
||||
return response()->json(['error' => false, 'message' => 'Đã huỷ yêu cầu']);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/transaction/manual-deposit/confirm
|
||||
* Attach proof image vào transaction đã prepare rồi đánh dấu đã submit.
|
||||
* Client gọi sau khi upload proof ở step 3.
|
||||
*/
|
||||
public function confirm(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'uuid' => 'required|string|uuid',
|
||||
'proof_image' => 'required|string|max:500',
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
$transaction = Transaction::where('uuid', $request->uuid)
|
||||
->where('customer_id', $user->id)
|
||||
->where('type', 'manual')
|
||||
->where('status', TransactionStatus::PENDING->value)
|
||||
->whereNull('proof_image')
|
||||
->first();
|
||||
|
||||
if (!$transaction) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Giao dịch không hợp lệ hoặc đã được xử lý',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$transaction->update(['proof_image' => $request->proof_image]);
|
||||
|
||||
try {
|
||||
(new Telegram())->SendManualDepositPendingApproval($user, $transaction);
|
||||
} catch (\Exception) {
|
||||
// Không để lỗi Telegram ảnh hưởng response
|
||||
}
|
||||
|
||||
Log::create([
|
||||
'agent' => 'api',
|
||||
'client_ip' => $request->ip(),
|
||||
'description' => 'Gửi yêu cầu nạp tiền thủ công: ' . number_format($transaction->amount) . ' VNĐ (chờ duyệt)',
|
||||
'customer_id' => $user->id,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Yêu cầu nạp tiền đã được ghi nhận, vui lòng chờ admin xác nhận',
|
||||
'data' => ['public_code' => $transaction->public_code],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/transaction/manual-deposit/upload-proof
|
||||
* Upload ảnh chứng từ, trả về path để dùng ở bước confirm.
|
||||
*/
|
||||
public function uploadProof(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'file' => 'required|image|mimes:jpeg,jpg,png|max:5120',
|
||||
]);
|
||||
|
||||
$path = $request->file('file')->store('deposit-proofs', 'public');
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'url' => Storage::disk('public')->url($path),
|
||||
'path' => $path,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ class ClientResource extends JsonResource
|
||||
'credit' => [
|
||||
'credit' => $this->credit,
|
||||
'total' => $this->total,
|
||||
'total_pending' => $this->total_pending ?? 0,
|
||||
'point' => $this->point,
|
||||
'point_used' => 0,
|
||||
'point_total' => $this->point,
|
||||
|
||||
@@ -84,6 +84,21 @@ class Telegram
|
||||
return $this->SendMessage($msg);
|
||||
}
|
||||
|
||||
public function SendManualDepositPendingApproval($customer, $transaction)
|
||||
{
|
||||
$msg = "🔔 Yêu cầu nạp tiền chờ duyệt!\n";
|
||||
$msg .= "--------------------------------\n";
|
||||
$msg .= "👤 Khách: {$customer->firstname} {$customer->lastname}\n";
|
||||
$msg .= "📧 Email: {$customer->email}\n";
|
||||
$msg .= "💵 Số tiền: " . number_format($transaction->amount, 0, ',', '.') . " VNĐ\n";
|
||||
$msg .= "🧾 Mã GD: {$transaction->public_code}\n";
|
||||
$msg .= "🕒 Thời gian: " . Carbon::now()->format('d/m/Y H:i:s') . "\n";
|
||||
$msg .= "--------------------------------\n";
|
||||
$msg .= "⚠️ Vui lòng vào admin để xác nhận giao dịch.";
|
||||
|
||||
return $this->SendMessage($msg);
|
||||
}
|
||||
|
||||
public function SendCustomerRecharge($customer, $amount)
|
||||
{
|
||||
$msg = "💰 Khách hàng vừa nạp tiền vào tài khoản\n";
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace App\Jobs\Mail;
|
||||
|
||||
use App\Http\Services\Telegram;
|
||||
use App\Mail\CreateVirtualMachine;
|
||||
use App\Models\Invoice;
|
||||
use App\Services\InvoicePdfService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
@@ -19,50 +21,71 @@ class SendInformationVirtualMachine implements ShouldQueue
|
||||
|
||||
public $data = [];
|
||||
public $customer;
|
||||
public ?int $invoiceId;
|
||||
|
||||
protected ?string $invoicePdf;
|
||||
protected ?string $invoicePdfFileName;
|
||||
|
||||
public function __construct($data = [], $customer = [], ?string $invoicePdf = null, ?string $invoicePdfFileName = null)
|
||||
public function __construct($data = [], $customer = [], ?int $invoiceId = null)
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->customer = $customer;
|
||||
$this->invoicePdf = $invoicePdf;
|
||||
$this->invoicePdfFileName = $invoicePdfFileName;
|
||||
$this->data = $data;
|
||||
$this->customer = $customer;
|
||||
$this->invoiceId = $invoiceId;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
// Generate PDF inside the job — never pass binary blobs through queue payload
|
||||
$invoicePdf = null;
|
||||
$invoicePdfFileName = null;
|
||||
|
||||
$email = new CreateVirtualMachine(2, $this->data, $this->invoicePdf, $this->invoicePdfFileName);
|
||||
if ($this->invoiceId) {
|
||||
try {
|
||||
$invoice = Invoice::with(['invoiceItems.product', 'customer', 'order.orderItems'])
|
||||
->find($this->invoiceId);
|
||||
|
||||
if ($invoice) {
|
||||
$pdfService = app(InvoicePdfService::class);
|
||||
$invoicePdf = $pdfService->generate($invoice);
|
||||
$invoicePdfFileName = $pdfService->generateFileName($invoice);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('SendInformationVirtualMachine: PDF generation failed, sending without attachment', [
|
||||
'invoice_id' => $this->invoiceId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$email = new CreateVirtualMachine(2, $this->data, $invoicePdf, $invoicePdfFileName);
|
||||
|
||||
Mail::to($this->customer->email)->send($email);
|
||||
|
||||
\App\Models\Mail::create([
|
||||
'template_id' => 2,
|
||||
'subject' => $email->envelope()->subject,
|
||||
'to' => $this->customer->email,
|
||||
'subject' => $email->envelope()->subject,
|
||||
'to' => $this->customer->email,
|
||||
'customer_id' => $this->customer->id,
|
||||
'message' => $email->render()
|
||||
'message' => $email->render(),
|
||||
]);
|
||||
|
||||
} catch (\Exception $exception)
|
||||
{
|
||||
throw new \Exception( $exception->getMessage());
|
||||
} catch (\Throwable $exception) {
|
||||
throw new \Exception($exception->getMessage(), 0, $exception);
|
||||
}
|
||||
}
|
||||
|
||||
public function failed(\Throwable $exception, Telegram $telegram)
|
||||
public function failed(\Throwable $exception): void
|
||||
{
|
||||
Log::channel('Custom')->error("SendInformationVirtualMachine failed at " . Carbon::now() . ": " . $exception->getMessage());
|
||||
|
||||
Log::channel('Custom')->info("=================================");
|
||||
Log::channel('Custom')->info("Thông báo lỗi lúc: " .Carbon::now());
|
||||
Log::channel('Custom')->info("Notification Send Information VirtualMachine: ");
|
||||
Log::channel('Custom')->info("========");
|
||||
Log::channel('Custom')->info($exception->getMessage());
|
||||
Log::channel('Custom')->info("=================================");
|
||||
|
||||
$telegram->SendErrorMessage(json_decode($exception->getMessage()));
|
||||
try {
|
||||
app(Telegram::class)->SendErrorMessage([
|
||||
'job' => 'SendInformationVirtualMachine',
|
||||
'email' => $this->customer->email ?? 'unknown',
|
||||
'error' => $exception->getMessage(),
|
||||
'file' => $exception->getFile(),
|
||||
'line' => $exception->getLine(),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('SendInformationVirtualMachine: could not send Telegram notification: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Jobs\VirtualMachine\Provision;
|
||||
|
||||
use App\Http\Services\Telegram;
|
||||
use App\Jobs\Mail\Admin\AdminOrderNotificationJob;
|
||||
use App\Jobs\Mail\SendInformationVirtualMachine;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\Setting;
|
||||
use App\Services\InvoicePdfService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
@@ -37,23 +37,23 @@ class SendVmCredentialsEmailJob implements ShouldQueue
|
||||
public readonly int $moduleVmwareId,
|
||||
) {}
|
||||
|
||||
public function handle(): void
|
||||
public function handle(Telegram $telegram): void
|
||||
{
|
||||
$vm = ModuleVmware::with(['customer', 'os.osGroup', 'order.orderItems'])->find($this->moduleVmwareId);
|
||||
|
||||
if (!$vm) {
|
||||
Log::error('SendVmCredentialsEmailJob: ModuleVmware not found', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
]);
|
||||
$msg = "SendVmCredentialsEmailJob: ModuleVmware #{$this->moduleVmwareId} not found";
|
||||
Log::error($msg);
|
||||
$telegram->SendErrorMessage($msg);
|
||||
return;
|
||||
}
|
||||
|
||||
$customer = $vm->customer;
|
||||
|
||||
if (!$customer?->email) {
|
||||
Log::error('SendVmCredentialsEmailJob: customer or email missing', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
]);
|
||||
$msg = "SendVmCredentialsEmailJob: customer or email missing for ModuleVmware #{$this->moduleVmwareId}";
|
||||
Log::error($msg, ['module_vmware_id' => $this->moduleVmwareId]);
|
||||
$telegram->SendErrorMessage($msg);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,35 +87,17 @@ class SendVmCredentialsEmailJob implements ShouldQueue
|
||||
: 'N/A',
|
||||
];
|
||||
|
||||
// Generate invoice PDF attachment (synchronous, no queue)
|
||||
$invoicePdf = null;
|
||||
$invoicePdfFileName = null;
|
||||
// Resolve invoice ID — PDF is generated inside SendInformationVirtualMachine::handle()
|
||||
// to avoid passing large binary blobs through the queue payload
|
||||
$invoice = Invoice::where('order_id', $vm->order_id)->first();
|
||||
|
||||
$invoice = Invoice::where('order_id', $vm->order_id)
|
||||
->with(['invoiceItems.product', 'customer', 'order.orderItems'])
|
||||
->first();
|
||||
|
||||
if ($invoice) {
|
||||
try {
|
||||
$pdfService = app(InvoicePdfService::class);
|
||||
$invoicePdf = $pdfService->generate($invoice);
|
||||
$invoicePdfFileName = $pdfService->generateFileName($invoice);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('SendVmCredentialsEmailJob: failed to generate invoice PDF', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'invoice_id' => $invoice->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
SendInformationVirtualMachine::dispatch($emailData, $customer, $invoicePdf, $invoicePdfFileName);
|
||||
SendInformationVirtualMachine::dispatch($emailData, $customer, $invoice?->id);
|
||||
|
||||
Log::info('SendVmCredentialsEmailJob: email dispatched', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'customer_email' => $customer->email,
|
||||
'ip' => $vm->ip,
|
||||
'invoice_attached' => $invoicePdf !== null,
|
||||
'invoice_id' => $invoice?->id,
|
||||
]);
|
||||
|
||||
// Notify admin with VM credentials after successful provisioning
|
||||
@@ -146,6 +128,18 @@ class SendVmCredentialsEmailJob implements ShouldQueue
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
try {
|
||||
app(Telegram::class)->SendErrorMessage([
|
||||
'job' => 'SendVmCredentialsEmailJob',
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'error' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
]);
|
||||
} catch (\Throwable $te) {
|
||||
Log::error('SendVmCredentialsEmailJob: could not send Telegram notification: ' . $te->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -30,13 +30,21 @@ class CreateVirtualMachine extends Mailable
|
||||
|
||||
public function envelope()
|
||||
{
|
||||
if (!$this->template) {
|
||||
throw new \RuntimeException('Email template ID=2 (CreateVirtualMachine) not found in database.');
|
||||
}
|
||||
|
||||
return new Envelope(
|
||||
subject: str_replace("{\$os_name}", $this->data['os_name'], $this->template->subject),
|
||||
subject: str_replace("{\$os_name}", $this->data['os_name'] ?? '', $this->template->subject),
|
||||
);
|
||||
}
|
||||
|
||||
public function content()
|
||||
{
|
||||
if (!$this->template) {
|
||||
throw new \RuntimeException('Email template ID=2 (CreateVirtualMachine) not found in database.');
|
||||
}
|
||||
|
||||
return new Content(
|
||||
view: 'emails.template',
|
||||
with: [
|
||||
@@ -79,7 +87,7 @@ class CreateVirtualMachine extends Mailable
|
||||
if ($this->invoicePdf !== null && $this->invoicePdfFileName !== null) {
|
||||
return [
|
||||
Attachment::fromData(fn () => $this->invoicePdf, $this->invoicePdfFileName)
|
||||
->mime('application/pdf'),
|
||||
->withMime('application/pdf'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Bank extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $table = 'banks';
|
||||
|
||||
protected $fillable = [
|
||||
'bank_code',
|
||||
'bank_name',
|
||||
'account_no',
|
||||
'account_name',
|
||||
'is_active',
|
||||
'sort_order',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true)->orderBy('sort_order')->orderBy('id');
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ class Customer extends Authenticatable implements JWTSubject
|
||||
'password',
|
||||
'credit',
|
||||
'total',
|
||||
'total_pending',
|
||||
'remember_token',
|
||||
'status',
|
||||
'gender',
|
||||
|
||||
@@ -18,7 +18,9 @@ class Transaction extends Model
|
||||
'trans',
|
||||
'amount',
|
||||
'customer_id',
|
||||
'status'
|
||||
'status',
|
||||
'type',
|
||||
'proof_image',
|
||||
];
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user