diff --git a/backend/app/DomainObjects/Generated/StripeCustomerDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/StripeCustomerDomainObjectAbstract.php new file mode 100644 index 00000000..66b6f685 --- /dev/null +++ b/backend/app/DomainObjects/Generated/StripeCustomerDomainObjectAbstract.php @@ -0,0 +1,118 @@ + $this->id ?? null, + 'name' => $this->name ?? null, + 'email' => $this->email ?? null, + 'stripe_customer_id' => $this->stripe_customer_id ?? 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 setName(string $name): self + { + $this->name = $name; + return $this; + } + + public function getName(): string + { + return $this->name; + } + + public function setEmail(string $email): self + { + $this->email = $email; + return $this; + } + + public function getEmail(): string + { + return $this->email; + } + + public function setStripeCustomerId(string $stripe_customer_id): self + { + $this->stripe_customer_id = $stripe_customer_id; + return $this; + } + + public function getStripeCustomerId(): string + { + return $this->stripe_customer_id; + } + + 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; + } +} diff --git a/backend/app/DomainObjects/StripeCustomerDomainObject.php b/backend/app/DomainObjects/StripeCustomerDomainObject.php new file mode 100644 index 00000000..dd4aa33f --- /dev/null +++ b/backend/app/DomainObjects/StripeCustomerDomainObject.php @@ -0,0 +1,7 @@ + OrganizerRepository::class, AccountUserRepositoryInterface::class => AccountUserRepository::class, CapacityAssignmentRepositoryInterface::class => CapacityAssignmentRepository::class, - ReservationRepositoryInterface::class => ReservationRepository::class, + StripeCustomerRepositoryInterface::class => StripeCustomerRepository::class, ]; public function register(): void diff --git a/backend/app/Repository/Eloquent/StripeCustomerRepository.php b/backend/app/Repository/Eloquent/StripeCustomerRepository.php new file mode 100644 index 00000000..799203c4 --- /dev/null +++ b/backend/app/Repository/Eloquent/StripeCustomerRepository.php @@ -0,0 +1,20 @@ + + */ +interface StripeCustomerRepositoryInterface extends RepositoryInterface +{ + +} diff --git a/backend/app/Services/Domain/Payment/Stripe/DTOs/CreatePaymentIntentRequestDTO.php b/backend/app/Services/Domain/Payment/Stripe/DTOs/CreatePaymentIntentRequestDTO.php index a63c1b0d..fa27bf2e 100644 --- a/backend/app/Services/Domain/Payment/Stripe/DTOs/CreatePaymentIntentRequestDTO.php +++ b/backend/app/Services/Domain/Payment/Stripe/DTOs/CreatePaymentIntentRequestDTO.php @@ -4,6 +4,7 @@ use HiEvents\DataTransferObjects\BaseDTO; use HiEvents\DomainObjects\AccountDomainObject; +use HiEvents\DomainObjects\OrderDomainObject; class CreatePaymentIntentRequestDTO extends BaseDTO { @@ -11,6 +12,7 @@ public function __construct( public readonly int $amount, public readonly string $currencyCode, public AccountDomainObject $account, + public OrderDomainObject $order, ) { } diff --git a/backend/app/Services/Domain/Payment/Stripe/StripePaymentIntentCreationService.php b/backend/app/Services/Domain/Payment/Stripe/StripePaymentIntentCreationService.php index 5a9f1858..be408ef9 100644 --- a/backend/app/Services/Domain/Payment/Stripe/StripePaymentIntentCreationService.php +++ b/backend/app/Services/Domain/Payment/Stripe/StripePaymentIntentCreationService.php @@ -2,20 +2,26 @@ namespace HiEvents\Services\Domain\Payment\Stripe; +use HiEvents\DomainObjects\StripeCustomerDomainObject; use HiEvents\Exceptions\Stripe\CreatePaymentIntentFailedException; +use HiEvents\Repository\Interfaces\StripeCustomerRepositoryInterface; use HiEvents\Services\Domain\Payment\Stripe\DTOs\CreatePaymentIntentRequestDTO; use HiEvents\Services\Domain\Payment\Stripe\DTOs\CreatePaymentIntentResponseDTO; use Illuminate\Config\Repository; +use Illuminate\Database\DatabaseManager; use Psr\Log\LoggerInterface; use Stripe\Exception\ApiErrorException; use Stripe\StripeClient; +use Throwable; -readonly class StripePaymentIntentCreationService +class StripePaymentIntentCreationService { public function __construct( - private StripeClient $stripeClient, - private LoggerInterface $logger, - private Repository $config, + readonly private StripeClient $stripeClient, + readonly private LoggerInterface $logger, + readonly private Repository $config, + readonly private StripeCustomerRepositoryInterface $stripeCustomerRepository, + readonly private DatabaseManager $databaseManager, ) { } @@ -47,16 +53,26 @@ public function retrievePaymentIntentClientSecret( /** * @throws CreatePaymentIntentFailedException + * @throws ApiErrorException|Throwable */ public function createPaymentIntent(CreatePaymentIntentRequestDTO $paymentIntentDTO): CreatePaymentIntentResponseDTO { try { + $this->databaseManager->beginTransaction(); + $applicationFee = $this->getApplicationFee($paymentIntentDTO); $paymentIntent = $this->stripeClient->paymentIntents->create([ 'amount' => $paymentIntentDTO->amount, 'currency' => $paymentIntentDTO->currencyCode, + 'customer' => $this->upsertStripeCustomer($paymentIntentDTO)->getStripeCustomerId(), 'setup_future_usage' => 'on_session', + 'metadata' => [ + 'order_id' => $paymentIntentDTO->order->getId(), + 'event_id' => $paymentIntentDTO->order->getEventId(), + 'order_short_id' => $paymentIntentDTO->order->getShortId(), + 'account_id' => $paymentIntentDTO->account->getId(), + ], 'automatic_payment_methods' => [ 'enabled' => true, ], @@ -68,6 +84,8 @@ public function createPaymentIntent(CreatePaymentIntentRequestDTO $paymentIntent 'paymentIntentDTO' => $paymentIntentDTO->toArray(['account']), ]); + $this->databaseManager->commit(); + return new CreatePaymentIntentResponseDTO( paymentIntentId: $paymentIntent->id, clientSecret: $paymentIntent->client_secret, @@ -82,6 +100,10 @@ public function createPaymentIntent(CreatePaymentIntentRequestDTO $paymentIntent throw new CreatePaymentIntentFailedException( __('There was an error communicating with the payment provider. Please try again later.') ); + } catch (Throwable $exception) { + $this->databaseManager->rollBack(); + + throw $exception; } } @@ -119,4 +141,46 @@ private function getStripeAccountData(CreatePaymentIntentRequestDTO $paymentInte 'stripe_account' => $paymentIntentDTO->account->getStripeAccountId() ]; } + + /** + * @throws ApiErrorException|CreatePaymentIntentFailedException + */ + private function upsertStripeCustomer(CreatePaymentIntentRequestDTO $paymentIntentDTO): StripeCustomerDomainObject + { + $customer = $this->stripeCustomerRepository->findFirstWhere([ + 'email' => $paymentIntentDTO->order->getEmail(), + ]); + + if ($customer === null) { + $stripeCustomer = $this->stripeClient->customers->create( + params: [ + 'email' => $paymentIntentDTO->order->getEmail(), + 'name' => $paymentIntentDTO->order->getFullName(), + ], + opts: $this->getStripeAccountData($paymentIntentDTO) + ); + + return $this->stripeCustomerRepository->create([ + 'name' => $stripeCustomer->name, + 'email' => $stripeCustomer->email, + 'stripe_customer_id' => $stripeCustomer->id, + ]); + } + + if ($customer->getName() === $paymentIntentDTO->order->getFullName()) { + return $customer; + } + + $stripeCustomer = $this->stripeClient->customers->update( + id: $customer->getStripeCustomerId(), + params: ['name' => $paymentIntentDTO->order->getFullName()], + opts: $this->getStripeAccountData($paymentIntentDTO), + ); + + $this->stripeCustomerRepository->updateFromArray($customer->getId(), [ + 'name' => $stripeCustomer->name, + ]); + + return $customer; + } } diff --git a/backend/app/Services/Handlers/Order/Payment/Stripe/CreatePaymentIntentHandler.php b/backend/app/Services/Handlers/Order/Payment/Stripe/CreatePaymentIntentHandler.php index 08f6daaf..30423a3e 100644 --- a/backend/app/Services/Handlers/Order/Payment/Stripe/CreatePaymentIntentHandler.php +++ b/backend/app/Services/Handlers/Order/Payment/Stripe/CreatePaymentIntentHandler.php @@ -69,6 +69,7 @@ public function handle(string $orderShortId): CreatePaymentIntentResponseDTO 'amount' => Money::of($order->getTotalGross(), $order->getCurrency())->getMinorAmount()->toInt(), 'currencyCode' => $order->getCurrency(), 'account' => $account, + 'order' => $order, ])); $this->stripePaymentsRepository->create([ diff --git a/backend/database/migrations/2024_08_07_005807_create_stripe_customers_table.php b/backend/database/migrations/2024_08_07_005807_create_stripe_customers_table.php new file mode 100644 index 00000000..6b62ad4d --- /dev/null +++ b/backend/database/migrations/2024_08_07_005807_create_stripe_customers_table.php @@ -0,0 +1,26 @@ +id(); + + $table->string('name'); + $table->string('email'); + $table->string('stripe_customer_id'); + + $table->timestamps(); + $table->softDeletes(); + }); + } + + public function down(): void + { + Schema::dropIfExists('stripe_customers'); + } +};