Files
HRM-System/app/Http/Controllers/PayfastPaymentController.php
2026-04-13 08:16:56 +08:00

225 lines
8.4 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Plan;
use App\Models\PlanOrder;
use Illuminate\Http\Request;
class PayfastPaymentController extends Controller
{
public function processPayment(Request $request)
{
$validated = $request->validate([
'plan_id' => 'required|exists:plans,id',
'billing_cycle' => 'required|in:monthly,yearly',
'coupon_code' => 'nullable|string',
'customer_details' => 'required|array',
'customer_details.firstName' => 'required|string',
'customer_details.lastName' => 'required|string',
'customer_details.email' => 'required|email',
]);
try {
$settings = getPaymentMethodConfig('payfast');
$isLive = ($settings['mode'] ?? 'sandbox') === 'live';
if (!$settings['merchant_id'] || !$settings['merchant_key']) {
return response()->json(['success' => false, 'error' => __('PayFast not configured')]);
}
$plan = Plan::findOrFail($validated['plan_id']);
$pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null);
if ($pricing['final_price'] < 5.00) {
return response()->json(['success' => false, 'error' => __('Minimum amount is R5.00')]);
}
$paymentId = 'pf_' . $plan->id . '_' . time() . '_' . uniqid();
createPlanOrder([
'user_id' => auth()->id(),
'plan_id' => $validated['plan_id'],
'billing_cycle' => $validated['billing_cycle'],
'payment_method' => 'payfast',
'coupon_code' => $validated['coupon_code'] ?? null,
'payment_id' => $paymentId,
'status' => 'pending'
]);
$data = [
'merchant_id' => $settings['merchant_id'],
'merchant_key' => $settings['merchant_key'],
'return_url' => route('payfast.success'),
'cancel_url' => route('plans.index'),
'notify_url' => route('payfast.callback'),
'name_first' => $validated['customer_details']['firstName'],
'name_last' => $validated['customer_details']['lastName'],
'email_address' => $validated['customer_details']['email'],
'm_payment_id' => $paymentId,
'amount' => number_format($pricing['final_price'], 2, '.', ''),
'item_name' => $plan->name,
];
$passphrase = $settings['passphrase'] ?? '';
$signature = $this->generateSignature($data, $passphrase);
$data['signature'] = $signature;
$htmlForm = '';
foreach ($data as $name => $value) {
$htmlForm .= '<input name="' . $name . '" type="hidden" value="' . $value . '" />';
}
$endpoint = $isLive
? 'https://www.payfast.co.za/eng/process'
: 'https://sandbox.payfast.co.za/eng/process';
return response()->json([
'success' => true,
'inputs' => $htmlForm,
'action' => $endpoint
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'error' => __('Payment failed')]);
}
}
public function generateSignature($data, $passPhrase = null)
{
$pfOutput = '';
foreach ($data as $key => $val) {
if ($val !== '') {
$pfOutput .= $key . '=' . urlencode(trim($val)) . '&';
}
}
$getString = substr($pfOutput, 0, -1);
if ($passPhrase !== null) {
$getString .= '&passphrase=' . urlencode(trim($passPhrase));
}
return md5($getString);
}
public function callback(Request $request)
{
try {
// Validate IP address (only for live mode)
$settings = getPaymentMethodConfig('payfast');
// Get callback data
$pfData = $request->all();
$paymentId = $pfData['m_payment_id'] ?? null;
$paymentStatus = $pfData['payment_status'] ?? null;
if (!$paymentId) {
return response(__('Missing payment ID'), 400);
}
// Find the plan order
$planOrder = PlanOrder::where('payment_id', $paymentId)->first();
if (!$planOrder) {
return response(__('Order not found'), 404);
}
// Verify signature
if (!$this->verifyPayfastSignature($pfData, $settings['passphrase'] ?? '')) {
return response(__('Invalid signature'), 400);
}
// Verify amount
if (!$this->verifyAmount($pfData, $planOrder)) {
return response(__('Amount mismatch'), 400);
}
// Process payment based on status
if ($paymentStatus === 'COMPLETE') {
if ($planOrder->status === 'pending') {
// Update order status
$planOrder->update([
'status' => 'approved',
'processed_at' => now()
]);
// Assign plan to user
$user = $planOrder->user;
$plan = $planOrder->plan;
$expiresAt = $planOrder->billing_cycle === 'yearly' ? now()->addYear() : now()->addMonth();
$user->update([
'plan_id' => $plan->id,
'plan_expires_at' => $expiresAt,
]);
}
} else {
if (in_array($paymentStatus, ['CANCELLED', 'FAILED'])) {
$planOrder->update(['status' => 'rejected']);
}
}
return response('OK', 200);
} catch (\Exception $e) {
return response('ERROR', 500);
}
}
private function verifyPayfastSignature($pfData, $passphrase = '')
{
$signature = $pfData['signature'] ?? '';
unset($pfData['signature']);
$expectedSignature = $this->generateSignature($pfData, $passphrase);
return hash_equals($expectedSignature, $signature);
}
private function verifyAmount($pfData, $planOrder)
{
$receivedAmount = floatval($pfData['amount_gross'] ?? 0);
$expectedAmount = floatval($planOrder->final_price);
// Allow small floating point differences
return abs($receivedAmount - $expectedAmount) < 0.01;
}
public function success(Request $request)
{
// Try different parameter names PayFast might use
$paymentId = $request->get('m_payment_id') ?? $request->get('pf_payment_id') ?? $request->get('payment_id');
if (!$paymentId && auth()->check()) {
// If no payment ID, find the most recent pending order for this user
$planOrder = PlanOrder::where('user_id', auth()->id())
->where('payment_method', 'payfast')
->where('status', 'pending')
->orderBy('created_at', 'desc')
->first();
} else {
$planOrder = PlanOrder::where('payment_id', $paymentId)->first();
}
if ($planOrder) {
// Always process the payment on success return
$planOrder->update([
'status' => 'approved',
'processed_at' => now()
]);
// Assign plan to user
$user = $planOrder->user;
$plan = $planOrder->plan;
$expiresAt = $planOrder->billing_cycle === 'yearly' ? now()->addYear() : now()->addMonth();
$user->update([
'plan_id' => $plan->id,
'plan_expires_at' => $expiresAt,
]);
return redirect()->route('plans.index')->with('success', __('Payment completed and plan activated!'));
}
return redirect()->route('plans.index')->with('error', __('Payment verification failed'));
}
}