File manager - Edit - /home/webapp69.cm.in.th/u69319090028/Shop/app/Services/PaymentService.php
Back
<?php /** * PaymentService * Core Payment Flow: initiate, submit slip, confirm, reject, cancel, expire * Enforces Bank Transfer snapshot, secure slip validation, audit logging, and notifications */ require_once __DIR__ . '/../Models/Payment.php'; require_once __DIR__ . '/../Models/PaymentAttempt.php'; require_once __DIR__ . '/../Models/PaymentAccount.php'; require_once __DIR__ . '/../Models/Order.php'; require_once __DIR__ . '/../Models/AdminAuditLog.php'; require_once __DIR__ . '/../Services/NotificationService.php'; require_once __DIR__ . '/../Helpers/DB.php'; require_once __DIR__ . '/../Helpers/Logger.php'; require_once __DIR__ . '/../../database/Database.php'; class PaymentService { private Payment $paymentModel; private PaymentAttempt $attemptModel; private PaymentAccount $accountModel; private Order $orderModel; private AdminAuditLog $auditLog; private NotificationService $notifService; private PDO $db; /** Max slip attempts before marking payment as failed */ private const MAX_ATTEMPTS = 5; /** Payment validity in minutes */ private const EXPIRY_MINUTES = 60; public function __construct() { $this->paymentModel = new Payment(); $this->attemptModel = new PaymentAttempt(); $this->accountModel = new PaymentAccount(); $this->orderModel = new Order(); $this->auditLog = new AdminAuditLog(); $this->notifService = new NotificationService(); $this->db = Database::getInstance(); } // ── 1. Initiate Payment ──────────────────────────────────────── /** * Create Payment record for a given order. * Snapshot the primary active receiving account at time of creation. * Idempotent — returns existing payment if already created. */ public function initiatePayment(int $orderId, string $method, int $customerId, float $amount): array { // Idempotency: return existing payment if already created $existing = $this->paymentModel->findByOrderId($orderId); if ($existing) { return ['success' => true, 'payment' => $existing, 'existed' => true]; } // COD: confirm immediately — no slip needed $initialStatus = ($method === 'COD') ? 'confirmed' : 'pending'; $confirmedAt = ($method === 'COD') ? date('Y-m-d H:i:s') : null; $expiresAt = date('Y-m-d H:i:s', time() + self::EXPIRY_MINUTES * 60); // Get order details $order = $this->orderModel->findById($orderId); if (!$order) { return ['success' => false, 'message' => 'Order not found']; } // Resolve snapshot receiving account from database for non-COD $accountId = null; if ($method !== 'COD') { $account = $this->accountModel->getActiveByMethod($method); $accountId = $account ? (int)$account['id'] : null; } $idempotencyKey = hash('sha256', "payment_{$orderId}_{$customerId}_" . microtime(true)); return DB::transaction(function($pdo) use ($orderId, $order, $customerId, $amount, $method, $initialStatus, $accountId, $idempotencyKey, $expiresAt, $confirmedAt) { $paymentId = $this->paymentModel->insert([ 'order_id' => $orderId, 'order_no' => $order['order_no'], 'customer_id' => $customerId, 'amount' => $amount, 'payment_method' => $method, 'payment_status' => $initialStatus, 'payment_account_id' => $accountId, 'idempotency_key' => $idempotencyKey, 'expires_at' => $expiresAt, 'confirmed_at' => $confirmedAt, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]); // Link payment_id back to order $pdo->prepare("UPDATE `orders` SET `payment_id` = :pid WHERE `id` = :oid") ->execute([':pid' => $paymentId, ':oid' => $orderId]); // COD: update order payment_status to confirmed and preparing if ($method === 'COD') { $pdo->prepare("UPDATE `orders` SET `payment_status` = 'confirmed', `order_status` = 'preparing' WHERE `id` = :oid") ->execute([':oid' => $orderId]); $this->logHistory($paymentId, 'pending', 'confirmed', $customerId, 'customer', 'COD — auto confirmed'); } else { $this->logHistory($paymentId, '', 'pending', $customerId, 'customer', "Payment initiated: {$method}"); } $payment = $this->paymentModel->findByOrderId($orderId); return ['success' => true, 'payment' => $payment, 'existed' => false]; }); } // ── 2. Submit Slip ───────────────────────────────────────────── /** * Customer uploads a payment slip for a pending/failed payment. * Transitions payment to awaiting_verification, creates attempt and slip record. */ public function submitSlip(int $userId, string $orderNo, array $fileData, ?string $note = null): array { $payment = $this->paymentModel->findByOrderNoForCustomer($orderNo, $userId); if (!$payment) { return ['success' => false, 'message' => 'ไม่พบรายการชำระเงินนี้']; } $allowedStatuses = ['pending', 'failed']; if (!in_array($payment['payment_status'], $allowedStatuses, true)) { return ['success' => false, 'message' => 'ไม่สามารถอัปโหลดสลิปได้ในสถานะนี้']; } if ($payment['payment_status'] === 'pending' && strtotime($payment['expires_at']) < time()) { $this->expireSinglePayment((int)$payment['id']); return ['success' => false, 'message' => 'รายการชำระเงินหมดอายุแล้ว กรุณาสั่งซื้อใหม่']; } if ($this->attemptModel->hasPendingAttempt((int)$payment['id'])) { return ['success' => false, 'message' => 'มีสลิปที่รอการตรวจสอบอยู่แล้ว กรุณารอผลการตรวจสอบ']; } $attemptCount = $this->attemptModel->countAttempts((int)$payment['id']); if ($attemptCount >= self::MAX_ATTEMPTS) { return ['success' => false, 'message' => 'เกินจำนวนครั้งการอัปโหลดสลิปที่กำหนด (สูงสุด 5 ครั้ง)']; } // Validate and save slip image securely $uploadResult = $this->uploadSlipImage($fileData, $userId); if (!$uploadResult['success']) { return $uploadResult; } $order = $this->orderModel->findById((int)$payment['order_id']); try { DB::transaction(function($pdo) use ($payment, $uploadResult, $userId, $note, $order) { $attemptNo = $this->attemptModel->getNextAttemptNo((int)$payment['id']); // 1. Insert into payment_attempts $attemptId = $this->attemptModel->insert([ 'payment_id' => (int)$payment['id'], 'attempt_no' => $attemptNo, 'slip_image' => $uploadResult['filename'], 'slip_uploaded_at' => date('Y-m-d H:i:s'), 'amount_claimed' => (float)$payment['amount'], 'status' => 'pending', 'submitted_by' => $userId, 'note' => $note, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]); // 2. Insert into payment_slips table $pdo->prepare("INSERT INTO `payment_slips` (`order_id`, `file_path`, `ocr_amount`, `admin_status`, `created_at`, `updated_at`) VALUES (:oid, :fp, :amt, 'pending', NOW(), NOW())") ->execute([ ':oid' => (int)$payment['order_id'], ':fp' => 'uploads/slips/' . $uploadResult['filename'], ':amt' => (float)$payment['amount'] ]); // 3. Update payment status to awaiting_verification $prevStatus = $payment['payment_status']; $this->paymentModel->updateStatus((int)$payment['id'], 'awaiting_verification'); // 4. Update order payment status to awaiting_verification $pdo->prepare("UPDATE `orders` SET `payment_status` = 'awaiting_verification', `updated_at` = NOW() WHERE `id` = :oid") ->execute([':oid' => $payment['order_id']]); // 5. Log history $this->logHistory((int)$payment['id'], $prevStatus, 'awaiting_verification', $userId, 'customer', "Slip uploaded (attempt #{$attemptNo})"); }); // 6. Broadcast Notifications $this->notifService->onPaymentSlipSubmitted((int)$payment['id'], $payment['order_no']); $this->notifService->notifyUser( $userId, 'payment_awaiting', 'ได้รับหลักฐานการชำระเงินแล้ว', "ระบบได้รับหลักฐานการชำระเงินสำหรับคำสั่งซื้อ #{$payment['order_no']} แล้วและกำลังตรวจสอบ", 'order', (int)$payment['order_id'], '/payment/' . $payment['order_no'], 'customer' ); if ($order && !empty($order['store_id'])) { $this->notifService->notifyStoreOwner( (int)$order['store_id'], 'order_payment_awaiting', 'ลูกค้าส่งหลักฐานการชำระเงินแล้ว', "คำสั่งซื้อ #{$payment['order_no']} ได้รับหลักฐานการชำระเงินแล้ว อยู่ระหว่างรอผู้ดูแลระบบตรวจสอบ", 'order', (int)$payment['order_id'], '/seller/orders/' . $payment['order_no'] ); } return ['success' => true, 'message' => 'ระบบได้รับหลักฐานการชำระเงินแล้วและกำลังตรวจสอบ']; } catch (Exception $e) { // Remove uploaded file on failure if (!empty($uploadResult['filepath']) && file_exists($uploadResult['filepath'])) { @unlink($uploadResult['filepath']); } Logger::error("Failed to submit slip for order {$orderNo}: " . $e->getMessage()); return ['success' => false, 'message' => 'เกิดข้อผิดพลาดในการบันทึกข้อมูล กรุณาลองใหม่']; } } // ── 3. Admin Confirm / Reject ────────────────────────────────── /** * Admin approves a payment attempt */ public function confirmPayment(int $attemptId, int $adminId, ?string $note = null): array { $attempt = $this->attemptModel->findById($attemptId); if (!$attempt || $attempt['status'] !== 'pending') { return ['success' => false, 'message' => 'ไม่พบ Attempt นี้หรือสถานะไม่ถูกต้อง']; } $payment = $this->paymentModel->findById((int)$attempt['payment_id']); if (!$payment) { return ['success' => false, 'message' => 'ไม่พบรายการชำระเงิน']; } $order = $this->orderModel->findById((int)$payment['order_id']); try { DB::transaction(function($pdo) use ($attemptId, $payment, $adminId, $note) { // 1. Approve attempt $this->attemptModel->updateAttemptStatus($attemptId, 'approved'); // 2. Update payment_slips $pdo->prepare("UPDATE `payment_slips` SET `admin_status` = 'confirmed', `admin_id` = :aid, `admin_note` = :note, `confirmed_at` = NOW(), `updated_at` = NOW() WHERE `order_id` = :oid AND `admin_status` = 'pending'") ->execute([ ':aid' => $adminId, ':note' => $note, ':oid' => (int)$payment['order_id'] ]); // 3. Update payment to confirmed $this->paymentModel->updateStatus((int)$payment['id'], 'confirmed', [ 'confirmed_at' => date('Y-m-d H:i:s'), 'confirmed_by' => $adminId, 'note' => $note, ]); // 4. Update order statuses to confirmed and preparing $pdo->prepare("UPDATE `orders` SET `payment_status` = 'confirmed', `order_status` = 'preparing', `updated_at` = NOW() WHERE `id` = :oid") ->execute([':oid' => $payment['order_id']]); // 5. Log verification & history $this->logVerification((int)$payment['id'], $attemptId, $adminId, 'approved', $note); $this->logHistory((int)$payment['id'], 'awaiting_verification', 'confirmed', $adminId, 'admin', $note ?? 'Admin approved payment'); // 6. Record Audit Log $this->auditLog->record( $adminId, 'payment.confirm', 'payment', 'payment', (int)$payment['id'], "Confirmed payment for Order #{$payment['order_no']} (฿" . number_format((float)$payment['amount'], 2) . ")" ); }); // 7. Broadcast Notifications $this->notifService->onPaymentVerified((int)$payment['customer_id'], $payment['order_no'], 'confirmed', $note); if ($order && !empty($order['store_id'])) { $this->notifService->notifyStoreOwner( (int)$order['store_id'], 'order_payment_confirmed', 'คำสั่งซื้อชำระเงินแล้ว!', "คำสั่งซื้อ #{$payment['order_no']} ได้รับการยืนยันการชำระเงินแล้ว กรุณาเริ่มจัดเตรียมสินค้า", 'order', (int)$payment['order_id'], '/seller/orders/' . $payment['order_no'] ); } return ['success' => true, 'message' => 'ยืนยันการชำระเงินสำเร็จ']; } catch (Exception $e) { Logger::error("Failed to confirm payment attempt {$attemptId}: " . $e->getMessage()); return ['success' => false, 'message' => 'เกิดข้อผิดพลาด: ' . $e->getMessage()]; } } /** * Admin rejects a payment attempt */ public function rejectPayment(int $attemptId, int $adminId, string $reason): array { $attempt = $this->attemptModel->findById($attemptId); if (!$attempt || $attempt['status'] !== 'pending') { return ['success' => false, 'message' => 'ไม่พบ Attempt นี้หรือสถานะไม่ถูกต้อง']; } $payment = $this->paymentModel->findById((int)$attempt['payment_id']); if (!$payment) { return ['success' => false, 'message' => 'ไม่พบรายการชำระเงิน']; } try { DB::transaction(function($pdo) use ($attemptId, $payment, $adminId, $reason) { // 1. Reject attempt $this->attemptModel->updateAttemptStatus($attemptId, 'rejected', $reason); // 2. Update payment_slips $pdo->prepare("UPDATE `payment_slips` SET `admin_status` = 'rejected', `admin_id` = :aid, `admin_note` = :note, `updated_at` = NOW() WHERE `order_id` = :oid AND `admin_status` = 'pending'") ->execute([ ':aid' => $adminId, ':note' => $reason, ':oid' => (int)$payment['order_id'] ]); // 3. Determine new payment status based on total attempts $totalAttempts = $this->attemptModel->countAttempts((int)$payment['id']); $newStatus = ($totalAttempts >= self::MAX_ATTEMPTS) ? 'failed' : 'pending'; $this->paymentModel->updateStatus((int)$payment['id'], $newStatus); $pdo->prepare("UPDATE `orders` SET `payment_status` = :ps, `updated_at` = NOW() WHERE `id` = :oid") ->execute([':ps' => $newStatus, ':oid' => $payment['order_id']]); // 4. Log verification & history $this->logVerification((int)$payment['id'], $attemptId, $adminId, 'rejected', $reason); $this->logHistory((int)$payment['id'], 'awaiting_verification', $newStatus, $adminId, 'admin', "Rejected: {$reason}"); // 5. Record Audit Log $this->auditLog->record( $adminId, 'payment.reject', 'payment', 'payment', (int)$payment['id'], "Rejected slip for Order #{$payment['order_no']} (Reason: {$reason})" ); }); // 6. Broadcast Notification to Customer $this->notifService->onPaymentVerified((int)$payment['customer_id'], $payment['order_no'], 'rejected', $reason); return ['success' => true, 'message' => 'ปฏิเสธสลิปแล้ว ลูกค้าสามารถอัปโหลดสลิปใหม่ได้']; } catch (Exception $e) { Logger::error("Failed to reject payment attempt {$attemptId}: " . $e->getMessage()); return ['success' => false, 'message' => 'เกิดข้อผิดพลาด: ' . $e->getMessage()]; } } /** * Admin directly confirms a payment (with or without specific attempt) */ public function confirmPaymentDirect(int $paymentId, int $adminId, ?string $note = null): array { $payment = $this->paymentModel->findById($paymentId); if (!$payment) { return ['success' => false, 'message' => 'ไม่พบรายการชำระเงิน']; } if (in_array($payment['payment_status'], ['confirmed', 'refunded', 'cancelled'], true)) { return ['success' => false, 'message' => 'ไม่สามารถยืนยันรายการชำระเงินในสถานะนี้ได้']; } $order = $this->orderModel->findById((int)$payment['order_id']); try { DB::transaction(function($pdo) use ($payment, $paymentId, $adminId, $note) { // 1. Approve any pending attempts for this payment $pendingAttempts = $this->attemptModel->fetchAll( "SELECT id FROM `payment_attempts` WHERE payment_id = :pid AND status = 'pending'", [':pid' => $paymentId] ); foreach ($pendingAttempts as $pa) { $this->attemptModel->updateAttemptStatus((int)$pa['id'], 'approved'); $this->logVerification($paymentId, (int)$pa['id'], $adminId, 'approved', $note); } // 2. Update payment_slips $pdo->prepare("UPDATE `payment_slips` SET `admin_status` = 'confirmed', `admin_id` = :aid, `admin_note` = :note, `confirmed_at` = NOW(), `updated_at` = NOW() WHERE `order_id` = :oid AND `admin_status` = 'pending'") ->execute([ ':aid' => $adminId, ':note' => $note, ':oid' => (int)$payment['order_id'] ]); // 3. Update payment to confirmed $this->paymentModel->updateStatus($paymentId, 'confirmed', [ 'confirmed_at' => date('Y-m-d H:i:s'), 'confirmed_by' => $adminId, 'note' => $note, ]); // 4. Update order statuses to confirmed and preparing $pdo->prepare("UPDATE `orders` SET `payment_status` = 'confirmed', `order_status` = 'preparing', `updated_at` = NOW() WHERE `id` = :oid") ->execute([':oid' => $payment['order_id']]); // 5. Log history $this->logHistory($paymentId, $payment['payment_status'], 'confirmed', $adminId, 'admin', $note ?? 'Admin approved payment'); // 6. Record Audit Log $this->auditLog->record( $adminId, 'payment.confirm', 'payment', 'payment', $paymentId, "Confirmed payment for Order #{$payment['order_no']} (฿" . number_format((float)$payment['amount'], 2) . ")" ); }); // 7. Broadcast Notifications $this->notifService->onPaymentVerified((int)$payment['customer_id'], $payment['order_no'], 'confirmed', $note); if ($order && !empty($order['store_id'])) { $this->notifService->notifyStoreOwner( (int)$order['store_id'], 'order_payment_confirmed', 'คำสั่งซื้อชำระเงินแล้ว!', "คำสั่งซื้อ #{$payment['order_no']} ได้รับการยืนยันการชำระเงินแล้ว กรุณาเริ่มจัดเตรียมสินค้า", 'order', (int)$payment['order_id'], '/seller/orders/' . $payment['order_no'] ); } return ['success' => true, 'message' => 'ยืนยันการชำระเงินสำเร็จ']; } catch (Exception $e) { Logger::error("Failed to directly confirm payment {$paymentId}: " . $e->getMessage()); return ['success' => false, 'message' => 'เกิดข้อผิดพลาด: ' . $e->getMessage()]; } } /** * Admin directly rejects a payment (with or without specific attempt) */ public function rejectPaymentDirect(int $paymentId, int $adminId, string $reason): array { $payment = $this->paymentModel->findById($paymentId); if (!$payment) { return ['success' => false, 'message' => 'ไม่พบรายการชำระเงิน']; } try { DB::transaction(function($pdo) use ($payment, $paymentId, $adminId, $reason) { // 1. Reject any pending attempts for this payment $pendingAttempts = $this->attemptModel->fetchAll( "SELECT id FROM `payment_attempts` WHERE payment_id = :pid AND status = 'pending'", [':pid' => $paymentId] ); foreach ($pendingAttempts as $pa) { $this->attemptModel->updateAttemptStatus((int)$pa['id'], 'rejected', $reason); $this->logVerification($paymentId, (int)$pa['id'], $adminId, 'rejected', $reason); } // 2. Update payment_slips $pdo->prepare("UPDATE `payment_slips` SET `admin_status` = 'rejected', `admin_id` = :aid, `admin_note` = :note, `updated_at` = NOW() WHERE `order_id` = :oid AND `admin_status` = 'pending'") ->execute([ ':aid' => $adminId, ':note' => $reason, ':oid' => (int)$payment['order_id'] ]); // 3. Determine new payment status based on total attempts $totalAttempts = $this->attemptModel->countAttempts($paymentId); $newStatus = ($totalAttempts >= self::MAX_ATTEMPTS) ? 'failed' : 'pending'; $this->paymentModel->updateStatus($paymentId, $newStatus); $pdo->prepare("UPDATE `orders` SET `payment_status` = :ps, `updated_at` = NOW() WHERE `id` = :oid") ->execute([':ps' => $newStatus, ':oid' => $payment['order_id']]); // 4. Log history $this->logHistory($paymentId, $payment['payment_status'], $newStatus, $adminId, 'admin', "Rejected: {$reason}"); // 5. Record Audit Log $this->auditLog->record( $adminId, 'payment.reject', 'payment', 'payment', $paymentId, "Rejected slip for Order #{$payment['order_no']} (Reason: {$reason})" ); }); // 6. Broadcast Notification to Customer $this->notifService->onPaymentVerified((int)$payment['customer_id'], $payment['order_no'], 'rejected', $reason); return ['success' => true, 'message' => 'ปฏิเสธสลิปแล้ว ลูกค้าสามารถอัปโหลดสลิปใหม่ได้']; } catch (Exception $e) { Logger::error("Failed to directly reject payment {$paymentId}: " . $e->getMessage()); return ['success' => false, 'message' => 'เกิดข้อผิดพลาด: ' . $e->getMessage()]; } } // ── 4. Cancel Payment ────────────────────────────────────────── public function cancelPayment(int $paymentId, int $userId): array { $payment = $this->paymentModel->findById($paymentId); if (!$payment || (int)$payment['customer_id'] !== $userId) { return ['success' => false, 'message' => 'ไม่พบรายการชำระเงิน']; } if ($payment['payment_status'] !== 'pending') { return ['success' => false, 'message' => 'สามารถยกเลิกได้เฉพาะรายการที่รอชำระเงินเท่านั้น']; } if ($this->attemptModel->hasPendingAttempt($paymentId)) { return ['success' => false, 'message' => 'ไม่สามารถยกเลิกได้เนื่องจากมีสลิปที่รอตรวจสอบอยู่']; } try { DB::transaction(function($pdo) use ($payment, $paymentId, $userId) { $this->paymentModel->updateStatus($paymentId, 'cancelled', [ 'cancelled_at' => date('Y-m-d H:i:s'), ]); // Update order $pdo->prepare("UPDATE `orders` SET `order_status` = 'cancelled', `payment_status` = 'cancelled', `updated_at` = NOW() WHERE `id` = :oid") ->execute([':oid' => $payment['order_id']]); // Restore stock $this->restoreStockForOrder((int)$payment['order_id']); $this->logHistory($paymentId, 'pending', 'cancelled', $userId, 'customer', 'Customer cancelled payment'); }); return ['success' => true, 'message' => 'ยกเลิกการชำระเงินเรียบร้อยแล้ว']; } catch (Exception $e) { return ['success' => false, 'message' => 'เกิดข้อผิดพลาด: ' . $e->getMessage()]; } } // ── 5. Expiry Check ──────────────────────────────────────────── public function expirePendingPayments(): int { $sql = "SELECT id, order_id FROM `payments` WHERE `payment_status` = 'pending' AND `payment_method` != 'COD' AND `expires_at` < NOW()"; $expired = $this->paymentModel->fetchAll($sql); $count = 0; foreach ($expired as $p) { if (!$this->attemptModel->hasPendingAttempt((int)$p['id'])) { $this->expireSinglePayment((int)$p['id']); $count++; } } return $count; } private function expireSinglePayment(int $paymentId): void { $payment = $this->paymentModel->findById($paymentId); if (!$payment || $payment['payment_status'] !== 'pending') return; $prevStatus = $payment['payment_status']; $this->paymentModel->updateStatus($paymentId, 'expired'); $this->db->prepare("UPDATE `orders` SET `order_status` = 'cancelled', `payment_status` = 'expired', `updated_at` = NOW() WHERE `id` = :oid") ->execute([':oid' => $payment['order_id']]); $this->restoreStockForOrder((int)$payment['order_id']); $this->logHistory($paymentId, $prevStatus, 'expired', null, 'system', 'Payment expired — stock restored'); } // ── 6. Getters ───────────────────────────────────────────────── public function getPaymentByOrderNo(string $orderNo, int $userId): ?array { $payment = $this->paymentModel->findByOrderNoForCustomer($orderNo, $userId); if (!$payment) return null; $payment['attempts'] = $this->attemptModel->getAttemptsByPayment((int)$payment['id']); return $payment; } public function getPaymentForAdmin(string $orderNo): ?array { $payment = $this->paymentModel->findByOrderNo($orderNo); if (!$payment) return null; $payment['attempts'] = $this->attemptModel->getAttemptsByPayment((int)$payment['id']); $payment['history'] = $this->getPaymentHistory((int)$payment['id']); return $payment; } // ── Internal helpers ─────────────────────────────────────────── /** * Strict image upload validation with MIME sniffing and Magic Bytes check */ private function uploadSlipImage(array $fileData, int $userId): array { $allowedTypes = ['image/jpeg', 'image/png', 'image/webp']; $maxSize = 5 * 1024 * 1024; // 5MB if (empty($fileData['tmp_name']) || $fileData['error'] !== UPLOAD_ERR_OK) { return ['success' => false, 'message' => 'กรุณาเลือกไฟล์สลิปการโอนเงิน']; } if ($fileData['size'] > $maxSize || $fileData['size'] <= 0) { return ['success' => false, 'message' => 'ขนาดไฟล์ต้องไม่เกิน 5MB']; } if (!is_uploaded_file($fileData['tmp_name']) && php_sapi_name() !== 'cli') { return ['success' => false, 'message' => 'ไฟล์ที่อัปโหลดไม่ถูกต้องตามมาตรฐานความปลอดภัย']; } $finfo = finfo_open(FILEINFO_MIME_TYPE); $mimeType = finfo_file($finfo, $fileData['tmp_name']); finfo_close($finfo); if (!in_array($mimeType, $allowedTypes, true)) { return ['success' => false, 'message' => 'รองรับเฉพาะไฟล์รูปภาพ (JPG, PNG, WEBP) เท่านั้น']; } // Magic bytes validation $handle = @fopen($fileData['tmp_name'], 'rb'); if ($handle) { $header = fread($handle, 12); fclose($handle); $validMagic = false; if ($mimeType === 'image/jpeg' && strncmp($header, "\xFF\xD8\xFF", 3) === 0) { $validMagic = true; } elseif ($mimeType === 'image/png' && strncmp($header, "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A", 8) === 0) { $validMagic = true; } elseif ($mimeType === 'image/webp' && strncmp($header, "RIFF", 4) === 0 && substr($header, 8, 4) === "WEBP") { $validMagic = true; } if (!$validMagic) { return ['success' => false, 'message' => 'โครงสร้างไบนารีของไฟล์ไม่ถูกต้องตามประเภทรูปภาพ']; } } $extMap = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp']; $ext = $extMap[$mimeType] ?? 'jpg'; $randomToken = bin2hex(random_bytes(10)); $filename = sprintf('slip_%d_%d_%s.%s', $userId, time(), $randomToken, $ext); $uploadDir = __DIR__ . '/../../uploads/slips/'; if (!is_dir($uploadDir)) { @mkdir($uploadDir, 0755, true); } $filepath = $uploadDir . $filename; $saved = (php_sapi_name() === 'cli') ? copy($fileData['tmp_name'], $filepath) : move_uploaded_file($fileData['tmp_name'], $filepath); if (!$saved) { return ['success' => false, 'message' => 'ไม่สามารถบันทึกไฟล์สลิปได้ กรุณาลองใหม่อีกครั้ง']; } @chmod($filepath, 0644); return ['success' => true, 'filename' => $filename, 'filepath' => $filepath]; } private function logHistory(int $paymentId, string $fromStatus, string $toStatus, ?int $actorId, string $actorType, ?string $note): void { $this->db->prepare( "INSERT INTO `payment_history` (`payment_id`, `from_status`, `to_status`, `actor_id`, `actor_type`, `note`, `created_at`) VALUES (:pid, :from, :to, :actor, :type, :note, NOW())" )->execute([ ':pid' => $paymentId, ':from' => $fromStatus, ':to' => $toStatus, ':actor' => $actorId, ':type' => $actorType, ':note' => $note, ]); } private function logVerification(int $paymentId, int $attemptId, int $adminId, string $action, ?string $reason): void { $this->db->prepare( "INSERT INTO `payment_verifications` (`payment_id`, `payment_attempt_id`, `verified_by`, `action`, `reason`, `ip_address`, `created_at`) VALUES (:pid, :aid, :admin, :action, :reason, :ip, NOW())" )->execute([ ':pid' => $paymentId, ':aid' => $attemptId, ':admin' => $adminId, ':action' => $action, ':reason' => $reason, ':ip' => $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1', ]); } private function restoreStockForOrder(int $orderId): void { $items = $this->orderModel->getOrderItems($orderId); foreach ($items as $item) { if (!empty($item['sku_id'])) { DB::atomicIncrement('product_skus', 'stock', (int)$item['quantity'], "id = " . (int)$item['sku_id']); } if (!empty($item['product_id'])) { DB::atomicIncrement('products', 'stock', (int)$item['quantity'], "id = " . (int)$item['product_id']); } } } private function getPaymentHistory(int $paymentId): array { $sql = "SELECT ph.*, CONCAT(u.first_name, ' ', u.last_name) as actor_name FROM `payment_history` ph LEFT JOIN `users` u ON u.id = ph.actor_id WHERE ph.payment_id = :pid ORDER BY ph.created_at ASC"; return $this->paymentModel->fetchAll($sql, [':pid' => $paymentId]); } }
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | Generation time: 0.24 |
proxy
|
phpinfo
|
Settings