feature: implement payment verification, make cart as converted after successful payment
This commit is contained in:
parent
8e1fe1336e
commit
ca0aaaa84b
20
backend/app/Actions/MarkCartAsConvertedAction.php
Normal file
20
backend/app/Actions/MarkCartAsConvertedAction.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use App\Enums\CartStatus;
|
||||
use App\Models\Cart;
|
||||
|
||||
final readonly class MarkCartAsConvertedAction
|
||||
{
|
||||
/**
|
||||
* Execute the action.
|
||||
*/
|
||||
public function execute(Cart $cart): Cart
|
||||
{
|
||||
$cart->status = CartStatus::Converted;
|
||||
$cart->save();
|
||||
|
||||
return $cart;
|
||||
}
|
||||
}
|
||||
@ -5,23 +5,45 @@
|
||||
use App\Enums\PaymentStatusEnum;
|
||||
use App\Models\Payment;
|
||||
use App\Models\PaymentStatus;
|
||||
use DB;
|
||||
use Log;
|
||||
use Symfony\Component\Translation\Exception\NotFoundResourceException;
|
||||
use Throwable;
|
||||
|
||||
final readonly class MarkPaymentAsPaidAction
|
||||
{
|
||||
public function __construct(private MarkCartAsConvertedAction $cartAsConvertedAction) {}
|
||||
|
||||
/**
|
||||
* Execute the action.
|
||||
*
|
||||
* @throws NotFoundResourceException
|
||||
* @throws NotFoundResourceException|Throwable
|
||||
*/
|
||||
public function execute(Payment $payment): bool
|
||||
{
|
||||
try {
|
||||
|
||||
DB::beginTransaction();
|
||||
// get the cart and make the status to converted
|
||||
$cart = $payment->order->cart;
|
||||
$this->cartAsConvertedAction->execute($cart);
|
||||
|
||||
$status = PaymentStatus::whereName(PaymentStatusEnum::Paid->value)->value('id');
|
||||
if (! $status) {
|
||||
throw new NotFoundResourceException('Paid Status not found');
|
||||
}
|
||||
$payment->payment_status_id = $status;
|
||||
|
||||
return $payment->save();
|
||||
$payment->save();
|
||||
DB::commit();
|
||||
|
||||
return true;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
Log::error('Cannot mark order payment as paid', [$e->getMessage()]);
|
||||
DB::rollBack();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,9 @@
|
||||
|
||||
final readonly class ProcessOrderPaymentAction
|
||||
{
|
||||
public function __construct(private PaymentGatewayFactory $paymentGatewayFactory) {}
|
||||
public function __construct(
|
||||
private PaymentGatewayFactory $paymentGatewayFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Execute the action.
|
||||
|
||||
57
backend/app/Actions/VerifyStripeSessionAction.php
Normal file
57
backend/app/Actions/VerifyStripeSessionAction.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use App\Data\VerifiedCheckoutResponseDTO;
|
||||
use App\Models\User;
|
||||
use Exception;
|
||||
use Log;
|
||||
use Stripe\Exception\ApiErrorException;
|
||||
use Stripe\StripeClient;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
|
||||
|
||||
final readonly class VerifyStripeSessionAction
|
||||
{
|
||||
public function __construct(private StripeClient $stripe) {}
|
||||
|
||||
/**
|
||||
* Execute the action.
|
||||
*/
|
||||
public function execute(int $orderId, string $stripeSessionId, User $user): VerifiedCheckoutResponseDTO
|
||||
{
|
||||
/**
|
||||
* Check if the order is actually made by user
|
||||
*/
|
||||
$order = $user->orders()
|
||||
->whereId($orderId)
|
||||
->first();
|
||||
if (! $order) {
|
||||
throw new AccessDeniedHttpException('Order is not made by you');
|
||||
}
|
||||
if (! $order->stripeSession()->where('session_id', $stripeSessionId)->exists()) {
|
||||
throw new AccessDeniedHttpException('Stripe session is not made by you');
|
||||
}
|
||||
|
||||
try {
|
||||
$session = $this->stripe->checkout->sessions->retrieve($stripeSessionId);
|
||||
} catch (ApiErrorException $e) {
|
||||
Log::error('Stripe api is not available: ', [$e->getMessage()]);
|
||||
throw new ServiceUnavailableHttpException('Stripe api is not available');
|
||||
} catch (Exception $e) {
|
||||
throw new NotFoundHttpException('Invalid Stripe session id');
|
||||
}
|
||||
|
||||
if ($session->payment_status !== 'paid' || $session->status !== 'complete') {
|
||||
return VerifiedCheckoutResponseDTO::failure('Payment Unsuccessful');
|
||||
}
|
||||
|
||||
return VerifiedCheckoutResponseDTO::success(
|
||||
message: 'Payment Successful',
|
||||
amount: $session->amount_total,
|
||||
transactionId: $session->payment_intent,
|
||||
mode: $session->payment_method_types[0],
|
||||
);
|
||||
}
|
||||
}
|
||||
41
backend/app/Data/VerifiedCheckoutResponseDTO.php
Normal file
41
backend/app/Data/VerifiedCheckoutResponseDTO.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Data;
|
||||
|
||||
use App\Contracts\OutputDataTransferObject;
|
||||
|
||||
final readonly class VerifiedCheckoutResponseDTO implements OutputDataTransferObject
|
||||
{
|
||||
public function __construct(
|
||||
public bool $isSuccess,
|
||||
public string $message,
|
||||
public ?int $amount = null,
|
||||
public ?string $transactionId = null,
|
||||
public ?string $mode = null
|
||||
) {}
|
||||
|
||||
public static function failure(string $message): VerifiedCheckoutResponseDTO
|
||||
{
|
||||
return new self(isSuccess: false, message: $message);
|
||||
}
|
||||
|
||||
public static function success(string $message, int $amount, string $transactionId, string $mode): VerifiedCheckoutResponseDTO
|
||||
{
|
||||
return new self(isSuccess: true, message: $message, amount: $amount, transactionId: $transactionId,
|
||||
mode: $mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'isSuccess' => $this->isSuccess,
|
||||
'message' => $this->message,
|
||||
'amount' => $this->amount,
|
||||
'transactionId' => $this->transactionId,
|
||||
'paymentMethod' => $this->mode,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -3,8 +3,10 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\ProcessOrderPaymentAction;
|
||||
use App\Actions\VerifyStripeSessionAction;
|
||||
use App\Enums\PaymentModes;
|
||||
use App\Http\Requests\PaymentRequest;
|
||||
use App\Http\Requests\VerifyPaymentRequest;
|
||||
use App\Http\Resources\PaymentResource;
|
||||
use App\Models\Order;
|
||||
use App\Models\Payment;
|
||||
@ -23,6 +25,11 @@ public function store(PaymentRequest $request, Order $order, ProcessOrderPayment
|
||||
return new PaymentResource($response);
|
||||
}
|
||||
|
||||
public function verify(VerifyPaymentRequest $request, VerifyStripeSessionAction $action)
|
||||
{
|
||||
return $action->execute($request->orderId, $request->sessionId, $request->user())->toArray();
|
||||
}
|
||||
|
||||
public function show(Payment $payment)
|
||||
{
|
||||
return new PaymentResource($payment);
|
||||
@ -34,11 +41,4 @@ public function update(PaymentRequest $request, Payment $payment)
|
||||
|
||||
return new PaymentResource($payment);
|
||||
}
|
||||
|
||||
public function destroy(Payment $payment)
|
||||
{
|
||||
$payment->delete();
|
||||
|
||||
return response()->json();
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,9 +15,7 @@
|
||||
|
||||
class StripeWebhookController extends Controller
|
||||
{
|
||||
public function __construct(private readonly MarkPaymentAsPaidAction $paidAction)
|
||||
{
|
||||
}
|
||||
public function __construct(private readonly MarkPaymentAsPaidAction $paidAction) {}
|
||||
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
|
||||
30
backend/app/Http/Requests/VerifyPaymentRequest.php
Normal file
30
backend/app/Http/Requests/VerifyPaymentRequest.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class VerifyPaymentRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'sessionId' => 'required|exists:stripe_sessions,session_id',
|
||||
'orderId' => 'required|exists:orders,id',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
@ -44,4 +45,11 @@ public function payments(): HasMany
|
||||
{
|
||||
return $this->hasMany(Payment::class);
|
||||
}
|
||||
|
||||
protected function totalAmount(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
fn () => $this->cart?->products->sum(fn ($product) => $product->pivot->price * $product->pivot->quantity) ?? 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Stripe\StripeClient;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
@ -11,7 +12,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
$this->app->singleton(StripeClient::class, fn () => new StripeClient(config('services.stripe.secret')));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
Route::apiResource('user.addresses', UserAddressController::class)->shallow();
|
||||
Route::apiResource('users.orders', OrderController::class)->shallow();
|
||||
Route::post('orders/{order}/payments', [PaymentController::class, 'store']);
|
||||
Route::post('payments/verify', [PaymentController::class, 'verify']);
|
||||
});
|
||||
Route::get('/categories', [ProductCategoryController::class, 'index']);
|
||||
Route::apiResource('products', ProductController::class);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user