From 105adf18a907ad09bdfcdbac2d3e36eb9f732c22 Mon Sep 17 00:00:00 2001 From: Dave Earley Date: Fri, 31 Oct 2025 10:11:30 +0000 Subject: [PATCH] Feature: Store Stripe platform fees --- ...PaymentPlatformFeeDomainObjectAbstract.php | 188 +++++++++++++++ .../OrderPaymentPlatformFeeDomainObject.php | 7 + backend/app/Models/OrderItem.php | 5 - .../app/Models/OrderPaymentPlatformFee.php | 23 ++ .../Providers/RepositoryServiceProvider.php | 3 + .../OrderPaymentPlatformFeeRepository.php | 20 ++ ...rPaymentPlatformFeeRepositoryInterface.php | 14 ++ .../Payment/Stripe/IncomingWebhookHandler.php | 10 +- .../EventStatisticsIncrementService.php | 8 +- .../Order/OrderPaymentPlatformFeeService.php | 48 ++++ .../EventHandlers/ChargeSucceededHandler.php | 71 ++++++ .../PaymentIntentSucceededHandler.php | 22 +- ...ipePaymentPlatformFeeExtractionService.php | 164 +++++++++++++ ...eate_order_payment_platform_fees_table.php | 40 ++++ ...aymentPlatformFeeExtractionServiceTest.php | 222 ++++++++++++++++++ 15 files changed, 824 insertions(+), 21 deletions(-) create mode 100644 backend/app/DomainObjects/Generated/OrderPaymentPlatformFeeDomainObjectAbstract.php create mode 100644 backend/app/DomainObjects/OrderPaymentPlatformFeeDomainObject.php create mode 100644 backend/app/Models/OrderPaymentPlatformFee.php create mode 100644 backend/app/Repository/Eloquent/OrderPaymentPlatformFeeRepository.php create mode 100644 backend/app/Repository/Interfaces/OrderPaymentPlatformFeeRepositoryInterface.php create mode 100644 backend/app/Services/Domain/Order/OrderPaymentPlatformFeeService.php create mode 100644 backend/app/Services/Domain/Payment/Stripe/EventHandlers/ChargeSucceededHandler.php create mode 100644 backend/app/Services/Domain/Payment/Stripe/StripePaymentPlatformFeeExtractionService.php create mode 100644 backend/database/migrations/2025_10_30_081843_create_order_payment_platform_fees_table.php create mode 100644 backend/tests/Unit/Services/Domain/Payment/Stripe/StripePaymentPlatformFeeExtractionServiceTest.php diff --git a/backend/app/DomainObjects/Generated/OrderPaymentPlatformFeeDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/OrderPaymentPlatformFeeDomainObjectAbstract.php new file mode 100644 index 0000000000..1b0627db1b --- /dev/null +++ b/backend/app/DomainObjects/Generated/OrderPaymentPlatformFeeDomainObjectAbstract.php @@ -0,0 +1,188 @@ + $this->id ?? null, + 'order_id' => $this->order_id ?? null, + 'payment_platform' => $this->payment_platform ?? null, + 'fee_rollup' => $this->fee_rollup ?? null, + 'payment_platform_fee_amount' => $this->payment_platform_fee_amount ?? null, + 'application_fee_amount' => $this->application_fee_amount ?? null, + 'currency' => $this->currency ?? null, + 'transaction_id' => $this->transaction_id ?? null, + 'paid_at' => $this->paid_at ?? null, + 'deleted_at' => $this->deleted_at ?? null, + 'created_at' => $this->created_at ?? null, + 'updated_at' => $this->updated_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 setPaymentPlatform(string $payment_platform): self + { + $this->payment_platform = $payment_platform; + return $this; + } + + public function getPaymentPlatform(): string + { + return $this->payment_platform; + } + + public function setFeeRollup(array|string|null $fee_rollup): self + { + $this->fee_rollup = $fee_rollup; + return $this; + } + + public function getFeeRollup(): array|string|null + { + return $this->fee_rollup; + } + + public function setPaymentPlatformFeeAmount(float $payment_platform_fee_amount): self + { + $this->payment_platform_fee_amount = $payment_platform_fee_amount; + return $this; + } + + public function getPaymentPlatformFeeAmount(): float + { + return $this->payment_platform_fee_amount; + } + + public function setApplicationFeeAmount(float $application_fee_amount): self + { + $this->application_fee_amount = $application_fee_amount; + return $this; + } + + public function getApplicationFeeAmount(): float + { + return $this->application_fee_amount; + } + + public function setCurrency(string $currency): self + { + $this->currency = $currency; + return $this; + } + + public function getCurrency(): string + { + return $this->currency; + } + + public function setTransactionId(?string $transaction_id): self + { + $this->transaction_id = $transaction_id; + return $this; + } + + public function getTransactionId(): ?string + { + return $this->transaction_id; + } + + public function setPaidAt(?string $paid_at): self + { + $this->paid_at = $paid_at; + return $this; + } + + public function getPaidAt(): ?string + { + return $this->paid_at; + } + + public function setDeletedAt(?string $deleted_at): self + { + $this->deleted_at = $deleted_at; + return $this; + } + + public function getDeletedAt(): ?string + { + return $this->deleted_at; + } + + 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; + } +} diff --git a/backend/app/DomainObjects/OrderPaymentPlatformFeeDomainObject.php b/backend/app/DomainObjects/OrderPaymentPlatformFeeDomainObject.php new file mode 100644 index 0000000000..9c2c591bfd --- /dev/null +++ b/backend/app/DomainObjects/OrderPaymentPlatformFeeDomainObject.php @@ -0,0 +1,7 @@ +hasOne(ProductPrice::class); diff --git a/backend/app/Models/OrderPaymentPlatformFee.php b/backend/app/Models/OrderPaymentPlatformFee.php new file mode 100644 index 0000000000..66468a593f --- /dev/null +++ b/backend/app/Models/OrderPaymentPlatformFee.php @@ -0,0 +1,23 @@ + 'array', + ]; + } + + public function order(): BelongsTo + { + return $this->belongsTo(Order::class); + } +} diff --git a/backend/app/Providers/RepositoryServiceProvider.php b/backend/app/Providers/RepositoryServiceProvider.php index 387c4dfe4c..200079b8af 100644 --- a/backend/app/Providers/RepositoryServiceProvider.php +++ b/backend/app/Providers/RepositoryServiceProvider.php @@ -23,6 +23,7 @@ use HiEvents\Repository\Eloquent\MessageRepository; use HiEvents\Repository\Eloquent\OrderApplicationFeeRepository; use HiEvents\Repository\Eloquent\OrderItemRepository; +use HiEvents\Repository\Eloquent\OrderPaymentPlatformFeeRepository; use HiEvents\Repository\Eloquent\OrderRefundRepository; use HiEvents\Repository\Eloquent\OrderRepository; use HiEvents\Repository\Eloquent\OrganizerRepository; @@ -62,6 +63,7 @@ use HiEvents\Repository\Interfaces\MessageRepositoryInterface; use HiEvents\Repository\Interfaces\OrderApplicationFeeRepositoryInterface; use HiEvents\Repository\Interfaces\OrderItemRepositoryInterface; +use HiEvents\Repository\Interfaces\OrderPaymentPlatformFeeRepositoryInterface; use HiEvents\Repository\Interfaces\OrderRefundRepositoryInterface; use HiEvents\Repository\Interfaces\OrderRepositoryInterface; use HiEvents\Repository\Interfaces\OrganizerRepositoryInterface; @@ -123,6 +125,7 @@ class RepositoryServiceProvider extends ServiceProvider WebhookRepositoryInterface::class => WebhookRepository::class, WebhookLogRepositoryInterface::class => WebhookLogRepository::class, OrderApplicationFeeRepositoryInterface::class => OrderApplicationFeeRepository::class, + OrderPaymentPlatformFeeRepositoryInterface::class => OrderPaymentPlatformFeeRepository::class, AccountConfigurationRepositoryInterface::class => AccountConfigurationRepository::class, QuestionAndAnswerViewRepositoryInterface::class => QuestionAndAnswerViewRepository::class, OutgoingMessageRepositoryInterface::class => OutgoingMessageRepository::class, diff --git a/backend/app/Repository/Eloquent/OrderPaymentPlatformFeeRepository.php b/backend/app/Repository/Eloquent/OrderPaymentPlatformFeeRepository.php new file mode 100644 index 0000000000..7cfa12cfa2 --- /dev/null +++ b/backend/app/Repository/Eloquent/OrderPaymentPlatformFeeRepository.php @@ -0,0 +1,20 @@ + + */ +interface OrderPaymentPlatformFeeRepositoryInterface extends RepositoryInterface +{ + +} diff --git a/backend/app/Services/Application/Handlers/Order/Payment/Stripe/IncomingWebhookHandler.php b/backend/app/Services/Application/Handlers/Order/Payment/Stripe/IncomingWebhookHandler.php index 4eef8a6e05..b5deb944b8 100644 --- a/backend/app/Services/Application/Handlers/Order/Payment/Stripe/IncomingWebhookHandler.php +++ b/backend/app/Services/Application/Handlers/Order/Payment/Stripe/IncomingWebhookHandler.php @@ -6,6 +6,7 @@ use HiEvents\Services\Application\Handlers\Order\Payment\Stripe\DTO\StripeWebhookDTO; use HiEvents\Services\Domain\Payment\Stripe\EventHandlers\AccountUpdateHandler; use HiEvents\Services\Domain\Payment\Stripe\EventHandlers\ChargeRefundUpdatedHandler; +use HiEvents\Services\Domain\Payment\Stripe\EventHandlers\ChargeSucceededHandler; use HiEvents\Services\Domain\Payment\Stripe\EventHandlers\PaymentIntentFailedHandler; use HiEvents\Services\Domain\Payment\Stripe\EventHandlers\PaymentIntentSucceededHandler; use Illuminate\Cache\Repository; @@ -25,10 +26,13 @@ class IncomingWebhookHandler Event::PAYMENT_INTENT_PAYMENT_FAILED, Event::ACCOUNT_UPDATED, Event::REFUND_UPDATED, + Event::CHARGE_SUCCEEDED, + Event::CHARGE_UPDATED, ]; public function __construct( private readonly ChargeRefundUpdatedHandler $refundEventHandlerService, + private readonly ChargeSucceededHandler $chargeSucceededHandler, private readonly PaymentIntentSucceededHandler $paymentIntentSucceededHandler, private readonly PaymentIntentFailedHandler $paymentIntentFailedHandler, private readonly AccountUpdateHandler $accountUpdateHandler, @@ -70,7 +74,7 @@ public function handle(StripeWebhookDTO $webhookDTO): void return; } - $this->logger->debug('Stripe event received', $event->data->object->toArray()); + $this->logger->debug('Stripe event received: ' . $event->type, $event->data->object->toArray()); switch ($event->type) { case Event::PAYMENT_INTENT_SUCCEEDED: @@ -79,6 +83,10 @@ public function handle(StripeWebhookDTO $webhookDTO): void case Event::PAYMENT_INTENT_PAYMENT_FAILED: $this->paymentIntentFailedHandler->handleEvent($event->data->object); break; + case Event::CHARGE_SUCCEEDED: + case Event::CHARGE_UPDATED: + $this->chargeSucceededHandler->handleEvent($event->data->object); + break; case Event::REFUND_UPDATED: $this->refundEventHandlerService->handleEvent($event->data->object); break; diff --git a/backend/app/Services/Domain/EventStatistics/EventStatisticsIncrementService.php b/backend/app/Services/Domain/EventStatistics/EventStatisticsIncrementService.php index 47921a2799..d6b7a1b826 100644 --- a/backend/app/Services/Domain/EventStatistics/EventStatisticsIncrementService.php +++ b/backend/app/Services/Domain/EventStatistics/EventStatisticsIncrementService.php @@ -48,10 +48,10 @@ public function incrementForOrder(OrderDomainObject $order): void ->findById($order->getId()); $this->retrier->retry( - callableAction: function (int $attempt) use ($order): void { - $this->databaseManager->transaction(function () use ($order, $attempt): void { - $this->incrementAggregateStatistics($order, $attempt); - $this->incrementDailyStatistics($order, $attempt); + callableAction: function () use ($order): void { + $this->databaseManager->transaction(function () use ($order): void { + $this->incrementAggregateStatistics($order); + $this->incrementDailyStatistics($order); $this->incrementPromoCodeUsage($order); $this->incrementProductStatistics($order); }); diff --git a/backend/app/Services/Domain/Order/OrderPaymentPlatformFeeService.php b/backend/app/Services/Domain/Order/OrderPaymentPlatformFeeService.php new file mode 100644 index 0000000000..05af58ed2c --- /dev/null +++ b/backend/app/Services/Domain/Order/OrderPaymentPlatformFeeService.php @@ -0,0 +1,48 @@ +orderPaymentPlatformFeeRepository->create([ + OrderPaymentPlatformFeeDomainObjectAbstract::ORDER_ID => $orderId, + OrderPaymentPlatformFeeDomainObjectAbstract::PAYMENT_PLATFORM => $paymentPlatform, + OrderPaymentPlatformFeeDomainObjectAbstract::FEE_ROLLUP => $feeRollup, + OrderPaymentPlatformFeeDomainObjectAbstract::PAYMENT_PLATFORM_FEE_AMOUNT => $paymentPlatformFeeAmount, + OrderPaymentPlatformFeeDomainObjectAbstract::APPLICATION_FEE_AMOUNT => $applicationFeeAmount, + OrderPaymentPlatformFeeDomainObjectAbstract::CURRENCY => strtoupper($currency), + OrderPaymentPlatformFeeDomainObjectAbstract::TRANSACTION_ID => $transactionId, + OrderPaymentPlatformFeeDomainObjectAbstract::PAID_AT => now()->toDateTimeString(), + ]); + } +} diff --git a/backend/app/Services/Domain/Payment/Stripe/EventHandlers/ChargeSucceededHandler.php b/backend/app/Services/Domain/Payment/Stripe/EventHandlers/ChargeSucceededHandler.php new file mode 100644 index 0000000000..fdaee890ec --- /dev/null +++ b/backend/app/Services/Domain/Payment/Stripe/EventHandlers/ChargeSucceededHandler.php @@ -0,0 +1,71 @@ +logger->info(__('Processing charge event'), [ + 'charge_id' => $charge->id, + 'payment_intent_id' => $charge->payment_intent, + 'status' => $charge->status, + ]); + + if ($charge->status !== 'succeeded') { + $this->logger->info(__('Charge not in succeeded status, skipping'), [ + 'charge_id' => $charge->id, + 'status' => $charge->status, + ]); + return; + } + + /**@var StripePaymentDomainObject $stripePayment */ + $stripePayment = $this->stripePaymentsRepository + ->loadRelation(new Relationship(OrderDomainObject::class, name: 'order')) + ->findFirstWhere([ + StripePaymentDomainObjectAbstract::PAYMENT_INTENT_ID => $charge->payment_intent, + ]); + + if (!$stripePayment) { + $this->logger->warning(__('Stripe payment not found for charge'), [ + 'charge_id' => $charge->id, + 'payment_intent_id' => $charge->payment_intent, + ]); + return; + } + + $order = $stripePayment->getOrder(); + if (!$order) { + $this->logger->warning(__('Order not found for charge'), [ + 'charge_id' => $charge->id, + 'payment_intent_id' => $charge->payment_intent, + 'stripe_payment_id' => $stripePayment->getId(), + ]); + return; + } + + $this->platformFeeExtractionService->extractAndStorePlatformFee( + order: $order, + charge: $charge, + stripePayment: $stripePayment + ); + } +} diff --git a/backend/app/Services/Domain/Payment/Stripe/EventHandlers/PaymentIntentSucceededHandler.php b/backend/app/Services/Domain/Payment/Stripe/EventHandlers/PaymentIntentSucceededHandler.php index 5cb290978f..790bb84a8d 100644 --- a/backend/app/Services/Domain/Payment/Stripe/EventHandlers/PaymentIntentSucceededHandler.php +++ b/backend/app/Services/Domain/Payment/Stripe/EventHandlers/PaymentIntentSucceededHandler.php @@ -39,17 +39,17 @@ class PaymentIntentSucceededHandler { public function __construct( - private readonly OrderRepositoryInterface $orderRepository, - private readonly StripePaymentsRepository $stripePaymentsRepository, - private readonly AffiliateRepositoryInterface $affiliateRepository, - private readonly ProductQuantityUpdateService $quantityUpdateService, - private readonly StripeRefundExpiredOrderService $refundExpiredOrderService, - private readonly AttendeeRepositoryInterface $attendeeRepository, - private readonly DatabaseManager $databaseManager, - private readonly LoggerInterface $logger, - private readonly Repository $cache, - private readonly DomainEventDispatcherService $domainEventDispatcherService, - private readonly OrderApplicationFeeService $orderApplicationFeeService, + private readonly OrderRepositoryInterface $orderRepository, + private readonly StripePaymentsRepository $stripePaymentsRepository, + private readonly AffiliateRepositoryInterface $affiliateRepository, + private readonly ProductQuantityUpdateService $quantityUpdateService, + private readonly StripeRefundExpiredOrderService $refundExpiredOrderService, + private readonly AttendeeRepositoryInterface $attendeeRepository, + private readonly DatabaseManager $databaseManager, + private readonly LoggerInterface $logger, + private readonly Repository $cache, + private readonly DomainEventDispatcherService $domainEventDispatcherService, + private readonly OrderApplicationFeeService $orderApplicationFeeService, ) { } diff --git a/backend/app/Services/Domain/Payment/Stripe/StripePaymentPlatformFeeExtractionService.php b/backend/app/Services/Domain/Payment/Stripe/StripePaymentPlatformFeeExtractionService.php new file mode 100644 index 0000000000..6446a60474 --- /dev/null +++ b/backend/app/Services/Domain/Payment/Stripe/StripePaymentPlatformFeeExtractionService.php @@ -0,0 +1,164 @@ +logger->info(__('Extracting platform fee for order'), [ + 'order_id' => $order->getId(), + 'charge_id' => $charge->id, + ]); + + if (!$charge->balance_transaction || is_string($charge->balance_transaction)) { + $this->logger->info(__('Retrieving balance transaction from Stripe'), [ + 'charge_id' => $charge->id, + 'order_id' => $order->getId(), + 'connected_account_id' => $stripePayment->getConnectedAccountId(), + 'balance_transaction_type' => gettype($charge->balance_transaction), + ]); + + $stripeClient = $this->stripeClientFactory->createForPlatform( + $stripePayment->getStripePlatformEnum() + ); + + $params = ['expand' => ['balance_transaction']]; + $opts = []; + + if ($stripePayment->getConnectedAccountId()) { + $opts['stripe_account'] = $stripePayment->getConnectedAccountId(); + } + + $charge = $stripeClient->charges->retrieve($charge->id, $params, $opts); + } + + if (!$charge->balance_transaction || is_string($charge->balance_transaction)) { + $this->logger->warning(__('No balance transaction found for charge'), [ + 'charge_id' => $charge->id, + 'order_id' => $order->getId(), + ]); + return; + } + + $balanceTransaction = $charge->balance_transaction; + + $existingRecord = $this->orderPaymentPlatformFeeRepository->findFirstWhere([ + OrderPaymentPlatformFeeDomainObjectAbstract::ORDER_ID => $order->getId(), + OrderPaymentPlatformFeeDomainObjectAbstract::TRANSACTION_ID => $balanceTransaction->id, + ]); + + if ($existingRecord) { + $this->logger->info(__('Platform fee already stored for this transaction'), [ + 'order_id' => $order->getId(), + 'transaction_id' => $balanceTransaction->id, + 'charge_id' => $charge->id, + ]); + return; + } + $feeDetails = $this->extractFeeDetails($balanceTransaction); + + $totalFee = $balanceTransaction->fee ?? 0; + $applicationFee = $this->extractApplicationFee($feeDetails); + $paymentPlatformFee = $this->extractStripeFee($feeDetails); + + $this->orderPaymentPlatformFeeService->createOrderPaymentPlatformFee( + orderId: $order->getId(), + paymentPlatform: PaymentProviders::STRIPE->value, + feeRollup: [ + 'total_fee' => $totalFee, + 'payment_platform_fee' => $paymentPlatformFee, + 'application_fee' => $applicationFee, + 'fee_details' => $feeDetails, + 'net' => $balanceTransaction->net ?? 0, + 'exchange_rate' => $balanceTransaction->exchange_rate ?? null, + ], + paymentPlatformFeeAmountMinorUnit: $paymentPlatformFee, + applicationFeeAmountMinorUnit: $applicationFee, + currency: $balanceTransaction->currency ?? $order->getCurrency(), + transactionId: $balanceTransaction->id ?? null, + ); + + $this->logger->info(__('Platform fee stored successfully'), [ + 'order_id' => $order->getId(), + 'total_fee' => $totalFee, + 'payment_platform_fee' => $paymentPlatformFee, + 'application_fee' => $applicationFee, + 'currency' => strtoupper($balanceTransaction->currency ?? $order->getCurrency()), + ]); + } catch (Throwable $exception) { + $this->logger->error(__('Failed to store platform fee'), [ + 'exception' => $exception->getMessage(), + 'order_id' => $order->getId(), + 'charge_id' => $charge->id, + ]); + + throw $exception; + } + } + + private function extractFeeDetails($balanceTransaction): array + { + $feeDetails = []; + + if (isset($balanceTransaction->fee_details)) { + foreach ($balanceTransaction->fee_details as $feeDetail) { + $feeDetails[] = [ + 'type' => $feeDetail->type ?? null, + 'amount' => $feeDetail->amount ?? 0, + 'currency' => $feeDetail->currency ?? $balanceTransaction->currency, + 'description' => $feeDetail->description ?? null, + ]; + } + } + + return $feeDetails; + } + + private function extractStripeFee(array $feeDetails): int + { + foreach ($feeDetails as $detail) { + if ($detail['type'] === 'stripe_fee') { + return $detail['amount']; + } + } + + return 0; + } + + private function extractApplicationFee(array $feeDetails): int + { + foreach ($feeDetails as $detail) { + if ($detail['type'] === 'application_fee') { + return $detail['amount']; + } + } + + return 0; + } +} diff --git a/backend/database/migrations/2025_10_30_081843_create_order_payment_platform_fees_table.php b/backend/database/migrations/2025_10_30_081843_create_order_payment_platform_fees_table.php new file mode 100644 index 0000000000..a565b6e605 --- /dev/null +++ b/backend/database/migrations/2025_10_30_081843_create_order_payment_platform_fees_table.php @@ -0,0 +1,40 @@ +id(); + + $table->foreignId('order_id') + ->constrained('orders') + ->onDelete('cascade'); + $table->string('payment_platform', 50); + $table->jsonb('fee_rollup')->nullable(); + $table->decimal('payment_platform_fee_amount', 10, 2); + $table->decimal('application_fee_amount', 10, 2)->default(0); + $table->string('currency', 10)->default('USD'); + $table->string('transaction_id')->nullable(); + $table->timestamp('paid_at')->nullable(); + + $table->softDeletes(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('order_payment_platform_fees'); + } +}; diff --git a/backend/tests/Unit/Services/Domain/Payment/Stripe/StripePaymentPlatformFeeExtractionServiceTest.php b/backend/tests/Unit/Services/Domain/Payment/Stripe/StripePaymentPlatformFeeExtractionServiceTest.php new file mode 100644 index 0000000000..bc0985594f --- /dev/null +++ b/backend/tests/Unit/Services/Domain/Payment/Stripe/StripePaymentPlatformFeeExtractionServiceTest.php @@ -0,0 +1,222 @@ +stripeClientFactory = m::mock(StripeClientFactory::class); + $this->orderPaymentPlatformFeeService = m::mock(OrderPaymentPlatformFeeService::class); + $this->orderPaymentPlatformFeeRepository = m::mock(OrderPaymentPlatformFeeRepositoryInterface::class); + $this->logger = m::mock(LoggerInterface::class); + + $this->service = new StripePaymentPlatformFeeExtractionService( + $this->stripeClientFactory, + $this->orderPaymentPlatformFeeService, + $this->orderPaymentPlatformFeeRepository, + $this->logger + ); + } + + public function testExtractAndStorePlatformFeeNoBalanceTransaction(): void + { + $order = m::mock(OrderDomainObject::class); + $order->shouldReceive('getId')->andReturn(123); + + $stripePayment = m::mock(StripePaymentDomainObject::class); + $stripePayment->shouldReceive('getStripePlatformEnum')->andReturn(null); + $stripePayment->shouldReceive('getConnectedAccountId')->andReturn(null); + + $charge = Charge::constructFrom([ + 'id' => 'ch_123', + 'balance_transaction' => null, + ]); + + $this->orderPaymentPlatformFeeRepository->shouldReceive('findFirstWhere') + ->never(); + + $this->logger->shouldReceive('info') + ->with(__('Extracting platform fee for order'), [ + 'order_id' => 123, + 'charge_id' => 'ch_123', + ]) + ->once(); + + $this->logger->shouldReceive('info') + ->with(__('Retrieving balance transaction from Stripe'), [ + 'charge_id' => 'ch_123', + 'order_id' => 123, + 'connected_account_id' => null, + 'balance_transaction_type' => 'NULL', + ]) + ->once(); + + $stripeClient = m::mock(\Stripe\StripeClient::class); + $chargesService = m::mock(); + $stripeClient->charges = $chargesService; + + $this->stripeClientFactory->shouldReceive('createForPlatform') + ->with(null) + ->andReturn($stripeClient); + + $chargesService->shouldReceive('retrieve') + ->with('ch_123', ['expand' => ['balance_transaction']], []) + ->andReturn(Charge::constructFrom([ + 'id' => 'ch_123', + 'balance_transaction' => null, + ])); + + $this->logger->shouldReceive('warning') + ->with(__('No balance transaction found for charge'), [ + 'charge_id' => 'ch_123', + 'order_id' => 123, + ]) + ->once(); + + $this->orderPaymentPlatformFeeService->shouldNotReceive('createOrderPaymentPlatformFee'); + + $this->service->extractAndStorePlatformFee($order, $charge, $stripePayment); + + $this->assertTrue(true); + } + + public function testExtractAndStorePlatformFeeWithConnectedAccount(): void + { + $order = m::mock(OrderDomainObject::class); + $order->shouldReceive('getId')->andReturn(123); + + $stripePayment = m::mock(StripePaymentDomainObject::class); + $stripePayment->shouldReceive('getStripePlatformEnum')->andReturn(null); + $stripePayment->shouldReceive('getConnectedAccountId')->andReturn('acct_123'); + + $charge = Charge::constructFrom([ + 'id' => 'ch_123', + 'balance_transaction' => null, + ]); + + $this->orderPaymentPlatformFeeRepository->shouldReceive('findFirstWhere') + ->never(); + + $this->logger->shouldReceive('info') + ->with(__('Extracting platform fee for order'), [ + 'order_id' => 123, + 'charge_id' => 'ch_123', + ]) + ->once(); + + $this->logger->shouldReceive('info') + ->with(__('Retrieving balance transaction from Stripe'), [ + 'charge_id' => 'ch_123', + 'order_id' => 123, + 'connected_account_id' => 'acct_123', + 'balance_transaction_type' => 'NULL', + ]) + ->once(); + + $stripeClient = m::mock(\Stripe\StripeClient::class); + $chargesService = m::mock(); + $stripeClient->charges = $chargesService; + + $this->stripeClientFactory->shouldReceive('createForPlatform') + ->with(null) + ->andReturn($stripeClient); + + $chargesService->shouldReceive('retrieve') + ->with( + 'ch_123', + ['expand' => ['balance_transaction']], + ['stripe_account' => 'acct_123'] + ) + ->andReturn(Charge::constructFrom([ + 'id' => 'ch_123', + 'balance_transaction' => null, + ])); + + $this->logger->shouldReceive('warning') + ->with(__('No balance transaction found for charge'), [ + 'charge_id' => 'ch_123', + 'order_id' => 123, + ]) + ->once(); + + $this->orderPaymentPlatformFeeService->shouldNotReceive('createOrderPaymentPlatformFee'); + + $this->service->extractAndStorePlatformFee($order, $charge, $stripePayment); + + $this->assertTrue(true); + } + + public function testExtractAndStorePlatformFeeHandlesException(): void + { + $order = m::mock(OrderDomainObject::class); + $order->shouldReceive('getId')->andReturn(123); + + $stripePayment = m::mock(StripePaymentDomainObject::class); + $stripePayment->shouldReceive('getStripePlatformEnum')->andReturn(null); + $stripePayment->shouldReceive('getConnectedAccountId')->andReturn(null); + + $charge = Charge::constructFrom([ + 'id' => 'ch_123', + 'balance_transaction' => null, + ]); + + $this->orderPaymentPlatformFeeRepository->shouldReceive('findFirstWhere') + ->never(); + + $this->logger->shouldReceive('info') + ->with(__('Extracting platform fee for order'), [ + 'order_id' => 123, + 'charge_id' => 'ch_123', + ]) + ->once(); + + $this->logger->shouldReceive('info') + ->with(__('Retrieving balance transaction from Stripe'), [ + 'charge_id' => 'ch_123', + 'order_id' => 123, + 'connected_account_id' => null, + 'balance_transaction_type' => 'NULL', + ]) + ->once(); + + $this->stripeClientFactory->shouldReceive('createForPlatform') + ->with(null) + ->andThrow(new \Exception('Stripe API error')); + + $this->logger->shouldReceive('error') + ->with(__('Failed to store platform fee'), m::type('array')) + ->once(); + + $this->orderPaymentPlatformFeeService->shouldNotReceive('createOrderPaymentPlatformFee'); + + $this->expectException(\Exception::class); + + $this->service->extractAndStorePlatformFee($order, $charge, $stripePayment); + } + + protected function tearDown(): void + { + m::close(); + parent::tearDown(); + } +}