225 lines
8.4 KiB
PHP
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'));
|
|
}
|
|
} |