feature: implement payment gateway
- implement stripe checkout gateway - add payment gateway factory and service
This commit is contained in:
parent
0799965212
commit
2aa76db042
49
backend/app/Actions/ProcessOrderPaymentAction.php
Normal file
49
backend/app/Actions/ProcessOrderPaymentAction.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use App\Data\PaymentResponseDTO;
|
||||
use App\Enums\PaymentModes;
|
||||
use App\Enums\PaymentStatusEnum;
|
||||
use App\Models\Order;
|
||||
use App\Models\PaymentStatus;
|
||||
use App\Services\Payment\PaymentGatewayFactory;
|
||||
use DB;
|
||||
use Log;
|
||||
use Throwable;
|
||||
|
||||
final readonly class ProcessOrderPaymentAction
|
||||
{
|
||||
public function __construct(private PaymentGatewayFactory $paymentGatewayFactory) {}
|
||||
|
||||
/**
|
||||
* Execute the action.
|
||||
*/
|
||||
public function execute(Order $order, PaymentModes $mode): PaymentResponseDTO
|
||||
{
|
||||
$gateway = $this->paymentGatewayFactory->make($mode);
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
$response = $gateway->charge($order);
|
||||
|
||||
if ($response->isSuccess) {
|
||||
$order->payments()->create(
|
||||
[
|
||||
'transaction_id' => $response->transactionId,
|
||||
'amount' => $response->amount,
|
||||
'currency' => $response->currency,
|
||||
'payment_method' => $response->method->value,
|
||||
'payment_status_id' => PaymentStatus::getIdByName(PaymentStatusEnum::Unpaid->value),
|
||||
]
|
||||
);
|
||||
}
|
||||
DB::commit();
|
||||
|
||||
return $response;
|
||||
} catch (Throwable $e) {
|
||||
DB::rollBack();
|
||||
Log::error('Error occurred while processing the payment.', [$e->getMessage()]);
|
||||
abort(500, 'Something went wrong. Please try again or contact us to get in touch with our support team. ');
|
||||
}
|
||||
}
|
||||
}
|
||||
11
backend/app/Contracts/PaymentGateway.php
Normal file
11
backend/app/Contracts/PaymentGateway.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Contracts;
|
||||
|
||||
use App\Data\PaymentResponseDTO;
|
||||
use App\Models\Order;
|
||||
|
||||
interface PaymentGateway
|
||||
{
|
||||
public function charge(Order $order): PaymentResponseDTO;
|
||||
}
|
||||
63
backend/app/Data/PaymentResponseDTO.php
Normal file
63
backend/app/Data/PaymentResponseDTO.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Data;
|
||||
|
||||
use App\Contracts\OutputDataTransferObject;
|
||||
use App\Enums\PaymentModes;
|
||||
|
||||
final readonly class PaymentResponseDTO implements OutputDataTransferObject
|
||||
{
|
||||
public function __construct(
|
||||
public bool $isSuccess,
|
||||
public int $amount,
|
||||
public string $currency,
|
||||
public PaymentModes $method,
|
||||
public ?string $transactionId = null,
|
||||
public ?string $errorMessage = null,
|
||||
public ?string $redirectUrl = null,
|
||||
) {}
|
||||
|
||||
public static function success(
|
||||
string $transactionId,
|
||||
int $amount,
|
||||
string $currency,
|
||||
PaymentModes $method,
|
||||
?string $redirectUrl = null
|
||||
): self {
|
||||
return new self(
|
||||
isSuccess: true,
|
||||
amount: $amount,
|
||||
currency: $currency,
|
||||
method: $method,
|
||||
transactionId: $transactionId,
|
||||
redirectUrl: $redirectUrl,
|
||||
);
|
||||
}
|
||||
|
||||
public static function failure(int $amount, string $currency, PaymentModes $method, string $errorMessage): self
|
||||
{
|
||||
return new self(
|
||||
isSuccess: false,
|
||||
amount: $amount,
|
||||
currency: $currency,
|
||||
method: $method,
|
||||
errorMessage: $errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'isSuccess' => $this->isSuccess,
|
||||
'transactionId' => $this->transactionId,
|
||||
'amount' => $this->amount,
|
||||
'currency' => $this->currency,
|
||||
'method' => $this->method,
|
||||
'redirectUrl' => $this->redirectUrl,
|
||||
'errorMessage' => $this->errorMessage,
|
||||
];
|
||||
}
|
||||
}
|
||||
9
backend/app/Enums/PaymentModes.php
Normal file
9
backend/app/Enums/PaymentModes.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum PaymentModes: string
|
||||
{
|
||||
case StripeCheckout = 'stripeCheckout';
|
||||
case CashOnDelivery = 'cashOnDelivery';
|
||||
}
|
||||
11
backend/app/Enums/PaymentStatusEnum.php
Normal file
11
backend/app/Enums/PaymentStatusEnum.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum PaymentStatusEnum: string
|
||||
{
|
||||
case Unpaid = 'unpaid';
|
||||
case Paid = 'paid';
|
||||
case Refunded = 'refunded';
|
||||
case Failed = 'failed';
|
||||
}
|
||||
44
backend/app/Http/Controllers/PaymentController.php
Normal file
44
backend/app/Http/Controllers/PaymentController.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\ProcessOrderPaymentAction;
|
||||
use App\Enums\PaymentModes;
|
||||
use App\Http\Requests\PaymentRequest;
|
||||
use App\Http\Resources\PaymentResource;
|
||||
use App\Models\Order;
|
||||
use App\Models\Payment;
|
||||
|
||||
class PaymentController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return PaymentResource::collection(Payment::all());
|
||||
}
|
||||
|
||||
public function store(PaymentRequest $request, Order $order, ProcessOrderPaymentAction $action)
|
||||
{
|
||||
$response = $action->execute($order, PaymentModes::tryFrom($request->mode));
|
||||
|
||||
return new PaymentResource($response);
|
||||
}
|
||||
|
||||
public function show(Payment $payment)
|
||||
{
|
||||
return new PaymentResource($payment);
|
||||
}
|
||||
|
||||
public function update(PaymentRequest $request, Payment $payment)
|
||||
{
|
||||
$payment->update($request->validated());
|
||||
|
||||
return new PaymentResource($payment);
|
||||
}
|
||||
|
||||
public function destroy(Payment $payment)
|
||||
{
|
||||
$payment->delete();
|
||||
|
||||
return response()->json();
|
||||
}
|
||||
}
|
||||
22
backend/app/Http/Requests/PaymentRequest.php
Normal file
22
backend/app/Http/Requests/PaymentRequest.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\PaymentModes;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class PaymentRequest extends FormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'mode' => ['required', Rule::enum(PaymentModes::class)],
|
||||
];
|
||||
}
|
||||
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
30
backend/app/Http/Resources/PaymentResource.php
Normal file
30
backend/app/Http/Resources/PaymentResource.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Data\PaymentResponseDTO;
|
||||
use App\Models\Payment;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin Payment
|
||||
*
|
||||
* @property PaymentResponseDTO $resource
|
||||
*/
|
||||
class PaymentResource extends JsonResource
|
||||
{
|
||||
public static $wrap = null;
|
||||
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'success' => $this->resource->isSuccess,
|
||||
'amount' => $this->resource->amount,
|
||||
'currency' => $this->resource->currency,
|
||||
'method' => $this->resource->method,
|
||||
'redirectUrl' => $this->resource->redirectUrl,
|
||||
'errorMessage' => $this->resource->errorMessage,
|
||||
];
|
||||
}
|
||||
}
|
||||
40
backend/app/Models/Payment.php
Normal file
40
backend/app/Models/Payment.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* @mixin IdeHelperPayment
|
||||
*/
|
||||
class Payment extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'order_id',
|
||||
'transaction_id',
|
||||
'amount',
|
||||
'currency',
|
||||
'payment_method',
|
||||
'payment_status_id',
|
||||
'error_message',
|
||||
];
|
||||
|
||||
protected $with = ['paymentStatus'];
|
||||
|
||||
public function order(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Order::class);
|
||||
}
|
||||
|
||||
public function paymentStatus(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(PaymentStatus::class);
|
||||
}
|
||||
|
||||
protected function status(): Attribute
|
||||
{
|
||||
return Attribute::make(fn () => $this->paymentStatus?->name);
|
||||
}
|
||||
}
|
||||
22
backend/app/Models/PaymentStatus.php
Normal file
22
backend/app/Models/PaymentStatus.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @mixin IdeHelperPaymentStatus
|
||||
*/
|
||||
class PaymentStatus extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
];
|
||||
|
||||
public static function getIdByName(string $name): ?int
|
||||
{
|
||||
return static::where('name', $name)->value('id');
|
||||
}
|
||||
}
|
||||
14
backend/app/Services/Payment/CodPaymentGateway.php
Normal file
14
backend/app/Services/Payment/CodPaymentGateway.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Payment;
|
||||
|
||||
use App\Contracts\PaymentGateway;
|
||||
use App\Models\Order;
|
||||
|
||||
class CodPaymentGateway implements PaymentGateway
|
||||
{
|
||||
public function charge(Order $order)
|
||||
{
|
||||
// TODO: Implement charge() method
|
||||
}
|
||||
}
|
||||
16
backend/app/Services/Payment/PaymentGatewayFactory.php
Normal file
16
backend/app/Services/Payment/PaymentGatewayFactory.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Payment;
|
||||
|
||||
use App\Enums\PaymentModes;
|
||||
|
||||
class PaymentGatewayFactory
|
||||
{
|
||||
public function make(PaymentModes $mode)
|
||||
{
|
||||
return match ($mode) {
|
||||
PaymentModes::CashOnDelivery => new CodPaymentGateway,
|
||||
PaymentModes::StripeCheckout => new StripePaymentGateway,
|
||||
};
|
||||
}
|
||||
}
|
||||
83
backend/app/Services/Payment/StripePaymentGateway.php
Normal file
83
backend/app/Services/Payment/StripePaymentGateway.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Payment;
|
||||
|
||||
use App\Contracts\PaymentGateway;
|
||||
use App\Data\PaymentResponseDTO;
|
||||
use App\Data\StripeLineItemDTO;
|
||||
use App\Data\StripeSessionDataDTO;
|
||||
use App\Enums\PaymentModes;
|
||||
use App\Enums\StripeCurrency;
|
||||
use App\Enums\StripePaymentMode;
|
||||
use App\Models\Order;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Stripe\Checkout\Session;
|
||||
use Stripe\Stripe;
|
||||
|
||||
class StripePaymentGateway implements PaymentGateway
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
Stripe::setApiKey(config('services.stripe.secret'));
|
||||
}
|
||||
|
||||
public function charge(Order $order): PaymentResponseDTO
|
||||
{
|
||||
$cart = $order->cart()->withProducts()->first();
|
||||
|
||||
$totalAmount = 0;
|
||||
$lineItems = [];
|
||||
|
||||
foreach ($cart->products as $product) {
|
||||
$priceInCents = (int) ($product->pivot->price * 100);
|
||||
|
||||
$totalAmount += ($priceInCents * $product->pivot->quantity);
|
||||
|
||||
$lineItems[] = new StripeLineItemDTO(
|
||||
currency: StripeCurrency::INR,
|
||||
price: $priceInCents,
|
||||
productName: $product->title,
|
||||
productDescription: $product->description,
|
||||
quantity: $product->pivot->quantity,
|
||||
);
|
||||
}
|
||||
|
||||
$currency = StripeCurrency::INR->value;
|
||||
|
||||
try {
|
||||
$data = new StripeSessionDataDTO(
|
||||
lineItems: $lineItems,
|
||||
mode: StripePaymentMode::Payment,
|
||||
successUrl: config('app.frontend_url').'/order/payment/success?session_id={CHECKOUT_SESSION_ID}"',
|
||||
cancelUrl: config('app.frontend_url').'/order/payment/cancel',
|
||||
);
|
||||
$session = Session::create($data->toArray());
|
||||
|
||||
// add stripe session to order
|
||||
$order->stripeSession()->create(
|
||||
[
|
||||
'session_id' => $session->id,
|
||||
]
|
||||
);
|
||||
|
||||
return PaymentResponseDTO::success(
|
||||
transactionId: $session->id,
|
||||
amount: $totalAmount,
|
||||
currency: $currency,
|
||||
method: PaymentModes::StripeCheckout,
|
||||
redirectUrl: $session->url,
|
||||
);
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Stripe Checkout session cannot be created.', ['error' => $e->getMessage()]);
|
||||
|
||||
return PaymentResponseDTO::failure(
|
||||
amount: $totalAmount,
|
||||
currency: $currency,
|
||||
method: PaymentModes::StripeCheckout,
|
||||
errorMessage: 'Payment gateway unavailable. Please try again.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use App\Models\PaymentStatus;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('payments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('order_id');
|
||||
$table->string('transaction_id');
|
||||
$table->integer('amount');
|
||||
$table->string('currency');
|
||||
$table->string('payment_method');
|
||||
$table->foreignIdFor(PaymentStatus::class);
|
||||
$table->timestamps();
|
||||
$table->unique(['transaction_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('payments');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('payment_statuses', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('payment_statuses');
|
||||
}
|
||||
};
|
||||
20
backend/database/seeders/PaymentStatusSeeder.php
Normal file
20
backend/database/seeders/PaymentStatusSeeder.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Enums\PaymentStatusEnum;
|
||||
use App\Models\PaymentStatus;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class PaymentStatusSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$data = array_map(function (PaymentStatusEnum $status) {
|
||||
return [
|
||||
'name' => $status->value,
|
||||
];
|
||||
}, PaymentStatusEnum::cases());
|
||||
PaymentStatus::insertOrIgnore($data);
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@
|
||||
use App\Http\Controllers\CartController;
|
||||
use App\Http\Controllers\FavouriteProductController;
|
||||
use App\Http\Controllers\OrderController;
|
||||
use App\Http\Controllers\PaymentController;
|
||||
use App\Http\Controllers\ProductCategoryController;
|
||||
use App\Http\Controllers\ProductController;
|
||||
use App\Http\Controllers\ProductImagesController;
|
||||
@ -29,6 +30,7 @@
|
||||
|
||||
Route::apiResource('user.addresses', UserAddressController::class)->shallow();
|
||||
Route::apiResource('users.orders', OrderController::class)->shallow();
|
||||
Route::post('orders/{order}/payments', [PaymentController::class, 'store']);
|
||||
});
|
||||
Route::get('/categories', [ProductCategoryController::class, 'index']);
|
||||
Route::apiResource('products', ProductController::class);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user