feat: nạp tiền thủ công mã 30 phút, gia hạn trừ số dư, đồng bộ domain từ Plesk

- Nạp tiền: mã thanh toán sống 30 phút (ManualDepositDraft), khôi phục/huỷ mã, lệnh dọn mã hết hạn; total_pending chỉ tính giao dịch đã nộp chứng từ, admin duyệt/từ chối/xoá giao dịch
- Gia hạn dịch vụ: phát hành invoice gia hạn trên order gốc và trừ số dư ngay
- Đồng bộ domain từ Plesk theo server, ServiceOrderRecorder tạo order/invoice cho dịch vụ admin dựng
- Danh sách dịch vụ trả field theo loại (service_plan, dns_records_count), đếm dịch vụ theo loại

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
baodev0
2026-10-11 23:07:51 +07:00
co-authored by Claude Opus 5.5
parent 10af41c5cf
commit c5115b52dd
53 changed files with 1887 additions and 613 deletions
@@ -12,6 +12,7 @@ use App\Models\Log;
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;
@@ -77,28 +78,17 @@ class ManualDepositController extends Controller
], 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();
});
}
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 {
Transaction::create([
$draft = Transaction::create([
'uuid' => $uuid,
'public_code' => $publicCode,
'trans' => 'MAN' . time(),
@@ -108,9 +98,6 @@ class ManualDepositController extends Controller
'status' => TransactionStatus::PENDING->value,
]);
$user->total_pending += $request->amount;
$user->save();
DB::commit();
} catch (\Exception) {
DB::rollBack();
@@ -126,13 +113,43 @@ class ManualDepositController extends Controller
return response()->json([
'error' => false,
'data' => [
'uuid' => $uuid,
'public_code' => $publicCode,
],
'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).
@@ -142,21 +159,15 @@ class ManualDepositController extends Controller
{
$user = $request->user();
$transaction = Transaction::where('uuid', $uuid)
$transaction = ManualDepositDraft::query()
->where('uuid', $uuid)
->where('customer_id', $user->id)
->where('type', 'manual')
->where('status', TransactionStatus::PENDING->value)
->whereNull('proof_image')
->firstOrFail();
$amount = $transaction->amount;
$publicCode = $transaction->public_code;
DB::transaction(function () use ($transaction, $user) {
$user->total_pending = max(0, $user->total_pending - $transaction->amount);
$user->save();
$transaction->forceDelete();
});
ManualDepositDraft::discard($transaction);
$this->activityLogger->log(
event: 'deposit_cancel',
@@ -194,22 +205,30 @@ class ManualDepositController extends Controller
], 422);
}
$transaction = Transaction::where('uuid', $request->uuid)
->where('customer_id', $user->id)
->where('type', 'manual')
->where('status', TransactionStatus::PENDING->value)
->whereNull('proof_image')
->first();
$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' => 'Giao dịch không hợp lệ hoặc đã được xử lý',
'message' => 'Mã thanh toán đã hết hạn hoặc đã được xử lý, vui lòng tạo mã mới',
], 422);
}
$transaction->update(['proof_image' => $request->proof_image]);
try {
(new Telegram())->SendManualDepositPendingApproval($user, $transaction);
$this->sendManualDepositAdminEmail($user, $transaction, $request);