Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ STRIPE_PUBLIC_KEY=
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=

RAZORPAY_KEY_ID=
RAZORPAY_KEY_SECRET=
RAZORPAY_WEBHOOK_SECRET=

CORS_ALLOWED_ORIGINS=*

LOG_CHANNEL=stderr
Expand Down
1 change: 1 addition & 0 deletions backend/app/DomainObjects/Enums/PaymentProviders.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ enum PaymentProviders: string

case STRIPE = 'STRIPE';
case OFFLINE = 'OFFLINE';
case RAZORPAY = 'RAZORPAY';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<?php

namespace HiEvents\DomainObjects\Generated;

/**
* THIS FILE IS AUTOGENERATED - DO NOT EDIT IT DIRECTLY.
* @package HiEvents\DomainObjects\Generated
*/
abstract class RazorpayOrderDomainObjectAbstract extends \HiEvents\DomainObjects\AbstractDomainObject
{
final public const SINGULAR_NAME = 'razorpay_order';
final public const PLURAL_NAME = 'razorpay_orders';
final public const ID = 'id';
final public const ORDER_ID = 'order_id';
final public const RAZORPAY_ORDER_ID = 'razorpay_order_id';
final public const RAZORPAY_PAYMENT_ID = 'razorpay_payment_id';
final public const RAZORPAY_SIGNATURE = 'razorpay_signature';
final public const AMOUNT_MINOR = 'amount_minor';
final public const CURRENCY = 'currency';
final public const STATUS = 'status';
final public const CREATED_AT = 'created_at';
final public const UPDATED_AT = 'updated_at';
final public const DELETED_AT = 'deleted_at';

protected int $id;
protected int $order_id;
protected string $razorpay_order_id;
protected ?string $razorpay_payment_id = null;
protected ?string $razorpay_signature = null;
protected int $amount_minor;
protected string $currency;
protected string $status = 'created';
protected ?string $created_at = null;
protected ?string $updated_at = null;
protected ?string $deleted_at = null;

public function toArray(): array
{
return [
'id' => $this->id ?? null,
'order_id' => $this->order_id ?? null,
'razorpay_order_id' => $this->razorpay_order_id ?? null,
'razorpay_payment_id' => $this->razorpay_payment_id ?? null,
'razorpay_signature' => $this->razorpay_signature ?? null,
'amount_minor' => $this->amount_minor ?? null,
'currency' => $this->currency ?? null,
'status' => $this->status ?? null,
'created_at' => $this->created_at ?? null,
'updated_at' => $this->updated_at ?? null,
'deleted_at' => $this->deleted_at ?? null,
];
}

public function setId(int $id): self
{
$this->id = $id;
return $this;
}

public function getId(): int
{
return $this->id;
}

public function setOrderId(int $order_id): self
{
$this->order_id = $order_id;
return $this;
}

public function getOrderId(): int
{
return $this->order_id;
}

public function setRazorpayOrderId(string $razorpay_order_id): self
{
$this->razorpay_order_id = $razorpay_order_id;
return $this;
}

public function getRazorpayOrderId(): string
{
return $this->razorpay_order_id;
}

public function setRazorpayPaymentId(?string $razorpay_payment_id): self
{
$this->razorpay_payment_id = $razorpay_payment_id;
return $this;
}

public function getRazorpayPaymentId(): ?string
{
return $this->razorpay_payment_id;
}

public function setRazorpaySignature(?string $razorpay_signature): self
{
$this->razorpay_signature = $razorpay_signature;
return $this;
}

public function getRazorpaySignature(): ?string
{
return $this->razorpay_signature;
}

public function setAmountMinor(int $amount_minor): self
{
$this->amount_minor = $amount_minor;
return $this;
}

public function getAmountMinor(): int
{
return $this->amount_minor;
}

public function setCurrency(string $currency): self
{
$this->currency = $currency;
return $this;
}

public function getCurrency(): string
{
return $this->currency;
}

public function setStatus(string $status): self
{
$this->status = $status;
return $this;
}

public function getStatus(): string
{
return $this->status;
}

public function setCreatedAt(?string $created_at): self
{
$this->created_at = $created_at;
return $this;
}

public function getCreatedAt(): ?string
{
return $this->created_at;
}

public function setUpdatedAt(?string $updated_at): self
{
$this->updated_at = $updated_at;
return $this;
}

public function getUpdatedAt(): ?string
{
return $this->updated_at;
}

public function setDeletedAt(?string $deleted_at): self
{
$this->deleted_at = $deleted_at;
return $this;
}

public function getDeletedAt(): ?string
{
return $this->deleted_at;
}
}
12 changes: 12 additions & 0 deletions backend/app/DomainObjects/RazorpayOrderDomainObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,16 @@

class RazorpayOrderDomainObject extends Generated\RazorpayOrderDomainObjectAbstract
{
private ?OrderDomainObject $order = null;

public function getOrder(): ?OrderDomainObject
{
return $this->order;
}

public function setOrder(?OrderDomainObject $order): self
{
$this->order = $order;
return $this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace HiEvents\Exceptions\Razorpay;

use HiEvents\Exceptions\BaseException;

class RazorpayClientConfigurationException extends BaseException
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace HiEvents\Http\Actions\Common\Webhooks;

use HiEvents\Http\Actions\BaseAction;
use HiEvents\Services\Application\Handlers\Order\Payment\Razorpay\IncomingWebhookHandler;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class RazorpayIncomingWebhookAction extends BaseAction
{
public function __construct(
private readonly IncomingWebhookHandler $incomingWebhookHandler,
) {
}

public function __invoke(Request $request): Response
{
$signature = $request->header('x-razorpay-signature');

if (!$signature) {
return response()->noContent(Response::HTTP_BAD_REQUEST);
}

// We dispatch the handler asynchronously to ensure the webhook returns a 200/204
// to Razorpay immediately, avoiding timeout retries.
dispatch(function () use ($request, $signature) {
$this->incomingWebhookHandler->handle(
rawBody: $request->getContent(),
signature: $signature,
payload: $request->all(),
);
});

return response()->noContent();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace HiEvents\Http\Actions\Orders\Payment\Razorpay;

use HiEvents\Http\Actions\BaseAction;
use HiEvents\Services\Application\Handlers\Order\Payment\Razorpay\CreateRazorpayOrderHandler;
use Illuminate\Http\JsonResponse;

class CreateRazorpayOrderActionPublic extends BaseAction
{
public function __construct(
private readonly CreateRazorpayOrderHandler $createRazorpayOrderHandler,
) {
}

public function __invoke(int $eventId, string $orderShortId): JsonResponse
{
$response = $this->createRazorpayOrderHandler->handle($orderShortId);

return $this->jsonResponse($response->toArray());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace HiEvents\Http\Actions\Orders\Payment\Razorpay;

use HiEvents\Http\Actions\BaseAction;
use HiEvents\Services\Application\Handlers\Order\Payment\Razorpay\HandlePaymentCallbackHandler;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class RazorpayPaymentCallbackActionPublic extends BaseAction
{
public function __construct(
private readonly HandlePaymentCallbackHandler $handlePaymentCallbackHandler,
) {
}

public function __invoke(Request $request, int $eventId, string $orderShortId): JsonResponse
{
$request->validate([
'razorpay_order_id' => 'required|string',
'razorpay_payment_id' => 'required|string',
'razorpay_signature' => 'required|string',
]);

$this->handlePaymentCallbackHandler->handle(
razorpayOrderId: $request->input('razorpay_order_id'),
razorpayPaymentId: $request->input('razorpay_payment_id'),
razorpaySignature: $request->input('razorpay_signature'),
);

return $this->jsonResponse(['status' => 'success']);
}
}
33 changes: 22 additions & 11 deletions backend/app/Http/Actions/Orders/Payment/RefundOrderAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,21 @@
use HiEvents\Http\Request\Order\RefundOrderRequest;
use HiEvents\Resources\Order\OrderResource;
use HiEvents\Services\Application\Handlers\Order\DTO\RefundOrderDTO;
use HiEvents\Services\Application\Handlers\Order\Payment\Stripe\RefundOrderHandler;
use HiEvents\Services\Application\Handlers\Order\Payment\Razorpay\RefundOrderHandler as RazorpayRefundOrderHandler;
use HiEvents\Services\Application\Handlers\Order\Payment\Stripe\RefundOrderHandler as StripeRefundOrderHandler;
use HiEvents\Repository\Interfaces\OrderRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException;
use Stripe\Exception\ApiErrorException;
use Throwable;

class RefundOrderAction extends BaseAction
{
public function __construct(private readonly RefundOrderHandler $refundOrderHandler)
{
public function __construct(
private readonly StripeRefundOrderHandler $stripeRefundOrderHandler,
private readonly RazorpayRefundOrderHandler $razorpayRefundOrderHandler,
private readonly OrderRepositoryInterface $orderRepository
) {
}

/**
Expand All @@ -29,16 +34,22 @@ public function __invoke(RefundOrderRequest $request, int $eventId, int $orderId
$this->isActionAuthorized($eventId, EventDomainObject::class);

try {
$order = $this->refundOrderHandler->handle(
refundOrderDTO: RefundOrderDTO::fromArray(array_merge($request->validated(), [
'event_id' => $eventId,
'order_id' => $orderId,
]))
);
} catch (ApiErrorException|RefundNotPossibleException $exception) {
$order = $this->orderRepository->findById($orderId);

$refundOrderDTO = RefundOrderDTO::fromArray(array_merge($request->validated(), [
'event_id' => $eventId,
'order_id' => $orderId,
]));

if ($order->getPaymentProvider() === \HiEvents\DomainObjects\Enums\PaymentProviders::RAZORPAY->name) {
$order = $this->razorpayRefundOrderHandler->handle($refundOrderDTO);
} else {
$order = $this->stripeRefundOrderHandler->handle($refundOrderDTO);
}
} catch (ApiErrorException|RefundNotPossibleException|\HiEvents\Exceptions\Razorpay\RazorpayClientConfigurationException $exception) {
throw ValidationException::withMessages([
'amount' => $exception instanceof ApiErrorException
? 'Stripe error: ' . $exception->getMessage()
? 'Payment gateway error: ' . $exception->getMessage()
: $exception->getMessage(),
]);
}
Expand Down
21 changes: 21 additions & 0 deletions backend/app/Models/RazorpayOrder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace HiEvents\Models;

use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;

class RazorpayOrder extends BaseModel
{
use SoftDeletes;

protected function getTimestampsEnabled(): bool
{
return true;
}

public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
}
Loading
Loading