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\CartController;
|
||||||
use App\Http\Controllers\FavouriteProductController;
|
use App\Http\Controllers\FavouriteProductController;
|
||||||
use App\Http\Controllers\OrderController;
|
use App\Http\Controllers\OrderController;
|
||||||
|
use App\Http\Controllers\PaymentController;
|
||||||
use App\Http\Controllers\ProductCategoryController;
|
use App\Http\Controllers\ProductCategoryController;
|
||||||
use App\Http\Controllers\ProductController;
|
use App\Http\Controllers\ProductController;
|
||||||
use App\Http\Controllers\ProductImagesController;
|
use App\Http\Controllers\ProductImagesController;
|
||||||
@ -29,6 +30,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::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