Last active
April 17, 2025 07:37
-
-
Save umutyerebakmaz/10bf005b84360e14065d6a53ba38dbbc to your computer and use it in GitHub Desktop.
Laravel model for managing payment requests independently from user sessions. Useful for handling asynchronous 3D Secure payment callbacks and order fulfillment.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\Http\Controllers; | |
use App\Classes\Payfor3D; | |
use App\Enums\PaymentRequestStatus; | |
use App\Mail\NewOrderReceived; | |
use App\Mail\YourOrderHasBeenConfirmed; | |
use App\Models\OrderDetail; | |
use App\Models\PaymentRequest; | |
use App\Models\ProductStock; | |
use App\Models\ShoppingSession; | |
use DB; | |
use Illuminate\Http\Request; | |
use App\Classes\Page; | |
use App\Models\UserAddress; | |
use Illuminate\Http\RedirectResponse; | |
use Illuminate\Support\Carbon; | |
use Illuminate\Support\Facades\Log; | |
use Illuminate\Support\Facades\Mail; | |
use Illuminate\View\View; | |
use Faker\Generator; | |
use Illuminate\Container\Container; | |
use Ramsey\Uuid\Uuid; | |
use App\Models\ProductStockReservation; | |
class PaymentController extends Controller | |
{ | |
public function checkout(Request $request): View | RedirectResponse | |
{ | |
$order_id = Uuid::uuid4()->toString(); | |
$user = $request->user(); | |
if (!$user) { | |
return redirect()->route('login'); | |
} | |
$shoppingSession = ShoppingSession::where('user_id', $user->id)->first(); | |
if (!$shoppingSession) { | |
return redirect()->route('home')->with('error', 'Alışveriş Oturumu süresi doldu.'); | |
} | |
if ($shoppingSession->shoppingCart->isEmpty()) { | |
return redirect()->route('home')->with('error', 'Sepetiniz boş.'); | |
} | |
$delivery = UserAddress::where('user_id', $user->id)->where('current_delivery', 1)->first(); | |
if (!$delivery) { | |
return redirect()->route('user_address.index')->with('error', 'Teslimat adresi seçilmemiş.'); | |
} | |
$billing = UserAddress::where('user_id', $user->id)->where('current_billing', 1)->first(); | |
if (!$billing) { | |
return redirect()->route('user_address.index')->with('error', 'Fatura adresi seçilmemiş.'); | |
} | |
ProductStockReservation::where('user_id', $user->id) | |
->where('expires_at', '>', now()) | |
->delete(); | |
foreach ($shoppingSession->shoppingCart as $item) { | |
$productStock = ProductStock::where('product_id', $item->product_id)->first(); | |
$reservedQty = ProductStockReservation::where('product_id', $item->product_id) | |
->where('expires_at', '>', now()) | |
->sum('quantity'); | |
$availableQty = $productStock->quantity - $reservedQty; | |
$productName = $item->product->title ?? 'Bilinmeyen ürün'; | |
if ($productStock->quantity < $item->quantity) { | |
// Kullanıcı sepete zaten fazla ürün eklemiş | |
return redirect()->route('sepetim')->with('error', "Üzgünüz, '{$productName}' ürününden yalnızca {$productStock->quantity} adet stokta bulunmaktadır. Lütfen sepetinizi güncelleyin."); | |
} | |
if ($availableQty < $item->quantity) { | |
// Ürün başka kullanıcılar tarafından rezerve edilmiş | |
return redirect()->route('sepetim')->with('error', "Üzgünüz, '{$productName}' ürünü şu anda başka kullanıcılar tarafından rezerve edilmiş. Lütfen daha sonra tekrar deneyin."); | |
} | |
ProductStockReservation::create([ | |
'product_id' => $item->product_id, | |
'user_id' => $user->id, | |
'quantity' => $item->quantity, | |
'order_id' => $order_id, | |
'expires_at' => Carbon::now()->addMinutes(15), | |
]); | |
} | |
$payfor3D = new Payfor3D(); | |
$payfor3D->setTest(true); | |
$payfor3D->setOrderId($order_id); | |
$payfor3D->setPurchAmount($shoppingSession->grand_total); | |
$payfor3D->setHash(); | |
PaymentRequest::create([ | |
'order_id' => $order_id, | |
'user_id' => $user->id, | |
'sub_total' => $shoppingSession->sub_total, | |
'grand_total' => $shoppingSession->grand_total, | |
'tax_total' => $shoppingSession->tax_total, | |
'status' => PaymentRequestStatus::Pending, | |
'cart_snapshot' => $shoppingSession->shoppingCart->map(function ($item) { | |
return [ | |
'product' => $item | |
]; | |
}), | |
'bank_payload' => $payfor3D->getPayload(), | |
]); | |
$page = new Page; | |
$page->title = 'Ödeme'; | |
$page->metaTitle = 'Ödeme'; | |
return view('pages.select-payment', compact('page', 'shoppingSession', 'payfor3D')); | |
} | |
public function callback(Request $request): View | RedirectResponse | |
{ | |
$PaymentStatus = $request->input('3DStatus'); | |
$ProcReturnCode = $request->input('ProcReturnCode'); | |
$OrderId = $request->input('OrderId'); | |
$responseData = $request->all(); | |
$paymentRequest = PaymentRequest::where('order_id', $OrderId)->first(); | |
$user = $paymentRequest->user; | |
if (!$user) { | |
$paymentRequest->status = PaymentRequestStatus::Failed; | |
$paymentRequest->save(); | |
return redirect()->route('home')->with('error', 'Kullanıcı bilgilerine ulaşılamadı.'); | |
} | |
if (!$paymentRequest) { | |
Log::warning("PaymentRequest bulunamadı! OrderId: {$OrderId}"); | |
return redirect()->route('checkout')->with('error', 'Ödeme işlemi bulunamadı.'); | |
} | |
// Banka yanıtını sakla | |
$paymentRequest->bank_response = $responseData; | |
$shoppingSession = ShoppingSession::where('user_id', $paymentRequest->user_id)->first(); | |
if (!$shoppingSession) { | |
$paymentRequest->status = PaymentRequestStatus::NoShoppingSession; | |
$paymentRequest->save(); | |
return redirect()->route('home')->with('error', 'Alışveriş oturumu bulunamadı.'); | |
} | |
$delivery = UserAddress::where('user_id', $paymentRequest->user_id)->where('current_delivery', 1)->first(); | |
if (!$delivery) { | |
$paymentRequest->status = PaymentRequestStatus::NoDelivery; | |
$paymentRequest->save(); | |
return redirect()->route('user_address.index')->with('error', 'Teslimat adresi seçilmemiş.'); | |
} | |
$billing = UserAddress::where('user_id', $paymentRequest->user_id)->where('current_billing', 1)->first(); | |
if (!$billing) { | |
$paymentRequest->status = PaymentRequestStatus::NoBilling; | |
$paymentRequest->save(); | |
return redirect()->route('user_address.index')->with('error', 'Fatura adresi seçilmemiş.'); | |
} | |
// ✅ Sadece başarılı ödeme durumunu işleyelim | |
// ✅ her iki durumda da odeme denemesi sonrasi rezervasyon silinmeli. | |
ProductStockReservation::where('order_id', $OrderId)->delete(); | |
if ($PaymentStatus == 1 || $ProcReturnCode === '00') { | |
//basarili islemde | |
// 🚨 1. Adım: STOK KONTROLÜ | |
foreach ($shoppingSession->shoppingCart as $item) { | |
$stock = ProductStock::where('product_id', $item->product_id)->first(); | |
if (!$stock || $stock->quantity < $item->quantity) { | |
$paymentRequest->status = PaymentRequestStatus::OutOfStock; | |
$paymentRequest->save(); | |
$productName = $item->product->title ?? 'Bilinmeyen ürün'; | |
return redirect()->route('sepetim') | |
->with('error', "Ödeme başarılı ancak '{$productName}' ürünü stokta kalmamış. Sipariş oluşturulamadı."); | |
} | |
} | |
// ✅ 2. Adım: Sipariş oluşturma işlemi başlasın | |
try { | |
DB::beginTransaction(); | |
$faker = Container::getInstance()->make(Generator::class); | |
$orderCode = $faker->numerify('###-###-###'); | |
$orderDetail = OrderDetail::create([ | |
'user_id' => $paymentRequest->user_id, | |
'order_code' => $orderCode, | |
'delivery_address_id' => $delivery->id, | |
'billing_address_id' => $billing->id, | |
'sub_total' => $shoppingSession->sub_total, | |
'grand_total' => $shoppingSession->grand_total, | |
'tax_total' => $shoppingSession->tax_total, | |
'state' => "isleme alindi" | |
]); | |
// ✅ 3. Adım: Ürünleri siparişe ekle ve stoktan düş | |
foreach ($shoppingSession->shoppingCart as $item) { | |
$affected = ProductStock::where('product_id', $item->product_id) | |
->where('quantity', '>=', $item->quantity) | |
->decrement('quantity', $item->quantity); | |
if ($affected === 0) { | |
throw new \Exception("Stok yetersizliği tespit edildi: " . ($item->product->title ?? 'Bilinmeyen ürün')); | |
} | |
$orderDetail->orderItems()->create([ | |
'product_id' => $item->product_id, | |
'quantity' => $item->quantity | |
]); | |
} | |
// ✅ 4. Adım: Ödeme statüsünü güncelle | |
$paymentRequest->status = PaymentRequestStatus::Completed; | |
$paymentRequest->save(); | |
// ✅ 5. Adım: Sepeti sil | |
$shoppingSession->delete(); | |
// ✅ 6. Adım: Mailleri gönder | |
Mail::to($user->email)->queue(new YourOrderHasBeenConfirmed($orderDetail)); | |
Mail::to(['[email protected]', '[email protected]'])->queue(new NewOrderReceived($orderDetail)); | |
DB::commit(); | |
return redirect() | |
->route('order.summary', $orderDetail->id) | |
->with('success', 'Siparişiniz başarıyla tamamlandı. Detayları eposta kutunuzda bulabilirsiniz.'); | |
} catch (\Throwable $e) { | |
DB::rollBack(); | |
Log::error("callback error: " . $e->getMessage()); | |
$paymentRequest->status = PaymentRequestStatus::Failed; | |
$paymentRequest->save(); | |
return redirect()->route('sepetim') | |
->with('error', 'Ödemeniz alındı fakat sistemsel bir hata nedeniyle siparişiniz oluşturulamadı. Lütfen bizimle iletişime geçin.'); | |
} | |
} else { | |
// ❌ Banka ödemeyi reddetti | |
$paymentRequest->status = PaymentRequestStatus::Failed; | |
$paymentRequest->save(); | |
return redirect()->route('checkout') | |
->with('error', 'Ödeme gerçekleştirilemedi. Lütfen bilgilerinizi kontrol edin.'); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment