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\Enums\PaymentStatusEnum;
|
||||||
use App\Models\Payment;
|
use App\Models\Payment;
|
||||||
use App\Models\PaymentStatus;
|
use App\Models\PaymentStatus;
|
||||||
|
use DB;
|
||||||
|
use Log;
|
||||||
use Symfony\Component\Translation\Exception\NotFoundResourceException;
|
use Symfony\Component\Translation\Exception\NotFoundResourceException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
final readonly class MarkPaymentAsPaidAction
|
final readonly class MarkPaymentAsPaidAction
|
||||||
{
|
{
|
||||||
|
public function __construct(private MarkCartAsConvertedAction $cartAsConvertedAction) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the action.
|
* Execute the action.
|
||||||
*
|
*
|
||||||
* @throws NotFoundResourceException
|
* @throws NotFoundResourceException|Throwable
|
||||||
*/
|
*/
|
||||||
public function execute(Payment $payment): bool
|
public function execute(Payment $payment): bool
|
||||||
{
|
{
|
||||||
$status = PaymentStatus::whereName(PaymentStatusEnum::Paid->value)->value('id');
|
try {
|
||||||
if (! $status) {
|
|
||||||
throw new NotFoundResourceException('Paid Status not found');
|
|
||||||
}
|
|
||||||
$payment->payment_status_id = $status;
|
|
||||||
|
|
||||||
return $payment->save();
|
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;
|
||||||
|
|
||||||
|
$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
|
final readonly class ProcessOrderPaymentAction
|
||||||
{
|
{
|
||||||
public function __construct(private PaymentGatewayFactory $paymentGatewayFactory) {}
|
public function __construct(
|
||||||
|
private PaymentGatewayFactory $paymentGatewayFactory,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the action.
|
* 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;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Actions\ProcessOrderPaymentAction;
|
use App\Actions\ProcessOrderPaymentAction;
|
||||||
|
use App\Actions\VerifyStripeSessionAction;
|
||||||
use App\Enums\PaymentModes;
|
use App\Enums\PaymentModes;
|
||||||
use App\Http\Requests\PaymentRequest;
|
use App\Http\Requests\PaymentRequest;
|
||||||
|
use App\Http\Requests\VerifyPaymentRequest;
|
||||||
use App\Http\Resources\PaymentResource;
|
use App\Http\Resources\PaymentResource;
|
||||||
use App\Models\Order;
|
use App\Models\Order;
|
||||||
use App\Models\Payment;
|
use App\Models\Payment;
|
||||||
@ -23,6 +25,11 @@ public function store(PaymentRequest $request, Order $order, ProcessOrderPayment
|
|||||||
return new PaymentResource($response);
|
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)
|
public function show(Payment $payment)
|
||||||
{
|
{
|
||||||
return new PaymentResource($payment);
|
return new PaymentResource($payment);
|
||||||
@ -34,11 +41,4 @@ public function update(PaymentRequest $request, Payment $payment)
|
|||||||
|
|
||||||
return new PaymentResource($payment);
|
return new PaymentResource($payment);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function destroy(Payment $payment)
|
|
||||||
{
|
|
||||||
$payment->delete();
|
|
||||||
|
|
||||||
return response()->json();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,9 +15,7 @@
|
|||||||
|
|
||||||
class StripeWebhookController extends Controller
|
class StripeWebhookController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(private readonly MarkPaymentAsPaidAction $paidAction)
|
public function __construct(private readonly MarkPaymentAsPaidAction $paidAction) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __invoke(Request $request)
|
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;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
@ -44,4 +45,11 @@ public function payments(): HasMany
|
|||||||
{
|
{
|
||||||
return $this->hasMany(Payment::class);
|
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;
|
namespace App\Providers;
|
||||||
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Stripe\StripeClient;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@ -11,7 +12,7 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function register(): void
|
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('user.addresses', UserAddressController::class)->shallow();
|
||||||
Route::apiResource('users.orders', OrderController::class)->shallow();
|
Route::apiResource('users.orders', OrderController::class)->shallow();
|
||||||
Route::post('orders/{order}/payments', [PaymentController::class, 'store']);
|
Route::post('orders/{order}/payments', [PaymentController::class, 'store']);
|
||||||
|
Route::post('payments/verify', [PaymentController::class, 'verify']);
|
||||||
});
|
});
|
||||||
Route::get('/categories', [ProductCategoryController::class, 'index']);
|
Route::get('/categories', [ProductCategoryController::class, 'index']);
|
||||||
Route::apiResource('products', ProductController::class);
|
Route::apiResource('products', ProductController::class);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user