Files
portal-api/app/Http/Controllers/Api/Transaction/ManualDepositController.php

356 lines
14 KiB
PHP

<?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\Mail\Admin\ManualDepositPendingNotification;
use App\Mail\Customer\ManualDepositStatus;
use App\Models\Log;
use App\Models\Notification;
use App\Models\Setting;
use App\Models\Transaction;
use App\Services\ActivityLogger;
use App\Support\ManualDepositDraft;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class ManualDepositController extends Controller
{
public function __construct(protected ActivityLogger $activityLogger) {}
/**
* 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);
}
ManualDepositDraft::query()
->where('customer_id', $user->id)
->get()
->each(fn (Transaction $draft) => ManualDepositDraft::discard($draft));
$uuid = Str::uuid()->toString();
$publicCode = Transaction::newPublicCode('MAN');
DB::beginTransaction();
try {
$draft = Transaction::create([
'uuid' => $uuid,
'public_code' => $publicCode,
'trans' => 'MAN' . time(),
'amount' => $request->amount,
'customer_id' => $user->id,
'type' => 'manual',
'status' => TransactionStatus::PENDING->value,
]);
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);
}
$this->activityLogger->log(
event: 'deposit_prepare',
logName: 'Transaction',
description: "Customer {$user->email} tạo yêu cầu nạp tiền thủ công: " . number_format($request->amount) . ' VNĐ - Mã: ' . $publicCode,
newValues: ['public_code' => $publicCode, 'amount' => $request->amount, 'type' => 'manual'],
);
return response()->json([
'error' => false,
'data' => $this->draftPayload($draft),
]);
}
/**
* GET /v1/transaction/manual-deposit/{uuid}
*/
public function show(Request $request, string $uuid): JsonResponse
{
$draft = ManualDepositDraft::query()
->where('uuid', $uuid)
->where('customer_id', $request->user()->id)
->first();
if (! $draft) {
return response()->json(['error' => true, 'message' => 'Mã thanh toán không tồn tại'], 404);
}
if (ManualDepositDraft::isExpired($draft)) {
ManualDepositDraft::discard($draft);
return response()->json(['error' => true, 'message' => 'Mã thanh toán đã hết hạn, vui lòng tạo mã mới'], 410);
}
return response()->json(['error' => false, 'data' => $this->draftPayload($draft)]);
}
private function draftPayload(Transaction $draft): array
{
return [
'uuid' => $draft->uuid,
'public_code' => $draft->public_code,
'amount' => (int) $draft->amount,
'expires_at' => ManualDepositDraft::expiresAt($draft)->toISOString(),
];
}
/**
* 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 = ManualDepositDraft::query()
->where('uuid', $uuid)
->where('customer_id', $user->id)
->firstOrFail();
$amount = $transaction->amount;
$publicCode = $transaction->public_code;
ManualDepositDraft::discard($transaction);
$this->activityLogger->log(
event: 'deposit_cancel',
logName: 'Transaction',
description: "Customer {$user->email} huỷ yêu cầu nạp tiền thủ công: " . number_format($amount) . ' VNĐ - Mã: ' . $publicCode,
newValues: ['public_code' => $publicCode, 'amount' => $amount],
);
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();
if (
!str_starts_with($request->proof_image, "deposit-proofs/{$user->id}/")
|| str_contains($request->proof_image, '..')
|| !Storage::disk('local')->exists($request->proof_image)
|| Transaction::where('proof_image', $request->proof_image)->exists()
) {
return response()->json([
'error' => true,
'message' => 'Chứng từ không hợp lệ',
], 422);
}
$transaction = DB::transaction(function () use ($request, $user) {
$draft = ManualDepositDraft::query()
->where('uuid', $request->uuid)
->where('customer_id', $user->id)
->lockForUpdate()
->first();
if (! $draft || ManualDepositDraft::isExpired($draft)) {
return null;
}
$draft->update(['proof_image' => $request->proof_image]);
$user->newQuery()->whereKey($user->getKey())->increment('total_pending', $draft->amount);
return $draft;
});
if (!$transaction) {
return response()->json([
'error' => true,
'message' => 'Mã thanh toán đã hết hạn hoặc đã được xử lý, vui lòng tạo mã mới',
], 422);
}
Notification::notifyAdmins([
'type' => 'manual_deposit_pending',
'title' => 'Có yêu cầu nạp tiền chờ duyệt',
'body' => sprintf(
'%s vừa gửi chứng từ nạp %s VNĐ (%s).',
$user->fullname,
number_format((float) $transaction->amount, 0, ',', '.'),
$transaction->public_code,
),
'link' => '/transactions',
'data' => [
'transaction_uuid' => $transaction->uuid,
'transaction_code' => $transaction->public_code,
'customer_id' => $user->id,
'amount' => (int) $transaction->amount,
],
]);
try {
(new Telegram())->SendManualDepositPendingApproval($user, $transaction);
} catch (\Throwable $e) {
\Log::error('Không thể gửi Telegram thông báo nạp tiền chờ duyệt: ' . $e->getMessage(), [
'transaction_code' => $transaction->public_code,
]);
}
$this->sendManualDepositAdminEmail($user, $transaction, $request);
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,
]);
$this->activityLogger->log(
event: 'deposit_confirm',
logName: 'Transaction',
description: "Customer {$user->email} nộp chứng từ nạp tiền thủ công: " . number_format($transaction->amount) . ' VNĐ - Mã: ' . $transaction->public_code,
newValues: ['public_code' => $transaction->public_code, 'amount' => $transaction->amount, 'has_proof' => true],
);
try {
Mail::to($user->email)->send(new ManualDepositStatus([
'customer_id' => $user->id,
'customer_email' => $user->email,
'customer_name' => $user->fullname,
'transaction_code' => $transaction->public_code,
'amount' => $transaction->amount,
'status_label' => 'Đang chờ duyệt',
'status_color' => '#f16723',
'status_bg' => '#fff7ed',
'subject' => 'Yêu cầu nạp tiền đang chờ duyệt - ' . $transaction->public_code,
'status_message' => 'Yêu cầu nạp tiền của bạn đã được ghi nhận. Chúng tôi sẽ kiểm tra chứng từ và phản hồi kết quả trong thời gian sớm nhất.',
'deposit_date' => $transaction->created_at?->format('d/m/Y H:i:s') ?? now()->format('d/m/Y H:i:s'),
]));
} catch (\Throwable $e) {
\Log::error('Không thể gửi email trạng thái nạp tiền thủ công: ' . $e->getMessage(), [
'transaction_code' => $transaction->public_code,
'customer_email' => $user->email,
]);
}
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,webp|max:5120',
]);
$user = $request->user();
$path = $request->file('file')->store('deposit-proofs/' . $user->id, 'local');
return response()->json([
'error' => false,
'path' => $path,
]);
}
private function sendManualDepositAdminEmail($customer, Transaction $transaction, Request $request): void
{
try {
$setting = Setting::where('key', 'setting')->first();
$settingValue = $setting ? json_decode((string) $setting->value, true) : [];
$adminEmail = $settingValue['admin_email'] ?? null;
if (empty($adminEmail)) {
\Log::warning('Manual deposit admin email not sent: admin_email not configured.', [
'transaction_code' => $transaction->public_code,
]);
return;
}
Mail::to($adminEmail)->send(new ManualDepositPendingNotification([
'company_name' => $settingValue['company'] ?? config('app.name', 'Portal Admin'),
'customer_name' => (string) $customer->fullname,
'customer_email' => $customer->email,
'customer_phone' => $customer->phone ?? null,
'client_ip' => $request->ip(),
'transaction_code' => $transaction->public_code,
'amount' => $transaction->amount,
'proof_image_url' => $transaction->proof_image_urls[0] ?? null,
'created_at' => now()->format('d/m/Y H:i:s'),
]));
} catch (\Throwable $e) {
\Log::error('Không thể gửi email admin thông báo nạp tiền chờ duyệt: ' . $e->getMessage(), [
'transaction_code' => $transaction->public_code,
'customer_email' => $customer->email ?? null,
]);
}
}
}