Skip to content
advanced Phase 89 · Payment Deep Dive

Payment Authorization

Payment authorization - auth/capture flow, pre-authorization, deferred capture

45m
0 problems
Topic Progress 0%

Authorization Flow

Authorization Lifecycle

  ┌─────────────┐
  │   Place     │
  │   Order     │
  └──────┬──────┘
         │
  ┌──────▼──────┐
  │  Authorize  │
  │  (Hold $)   │
  └──────┬──────┘
         │
  ┌──────▼──────┐
  │  Capture    │
  │  (Collect)  │
  └──────┬──────┘
         │
  ┌──────▼──────┐
  │  Complete   │
  └─────────────┘

Authorization Service

namespace Vendor\Payment\Service\Auth;

class AuthorizationService
{
    private GatewayFactoryInterface $gatewayFactory;
    private TransactionRepositoryInterface $transactionRepo;
    private LoggerInterface $logger;

    public function authorize(
        OrderInterface $order,
        PaymentInterface $payment
    ): AuthResult {
        $gateway = $this->gatewayFactory->create($order->getPayment()->getMethod());

        try {
            $result = $gateway->authorize(new AuthorizeRequest([
                'amount' => $order->getGrandTotal(),
                'currency' => $order->getCurrencyCode(),
                'order_id' => $order->getIncrementId(),
                'payment_method' => $payment,
            ]));

            if ($result->isSuccess()) {
                // Save transaction
                $transaction = new TransactionData([
                    'order_id' => $order->getId(),
                    'transaction_id' => $result->getTransactionId(),
                    'type' => TransactionTypeInterface::TYPE_AUTH,
                    'amount' => $order->getGrandTotal(),
                    'status' => TransactionStatusInterface::STATUS_APPROVED,
                ]);
                $this->transactionRepo->save($transaction);

                // Update order state
                $order->setState(Order::STATE_PROCESSING);
                $order->getPayment()->setAuthorizationTransaction(
                    $result->getTransactionId()
                );
            }

            return $result;
        } catch (\Exception $e) {
            $this->logger->error('Authorization failed', [
                'order_id' => $order->getIncrementId(),
                'error' => $e->getMessage(),
            ]);
            throw new PaymentException('Authorization failed', 0, $e);
        }
    }
}

Capture Flow

Capture Service

namespace Vendor\Payment\Service\Capture;

class CaptureService
{
    public function capture(
        OrderInterface $order,
        float $amount = null
    ): CaptureResult {
        $payment = $order->getPayment();
        $authTransactionId = $payment->getAuthorizationTransaction();
        $amount = $amount ?? $order->getGrandTotal();

        // Validate capture amount
        $this->validateCaptureAmount($order, $amount);

        $gateway = $this->gatewayFactory->create($payment->getMethod());

        $result = $gateway->capture(new CaptureRequest([
            'transaction_id' => $authTransactionId,
            'amount' => $amount,
            'order_id' => $order->getIncrementId(),
        ]));

        if ($result->isSuccess()) {
            // Create capture transaction
            $transaction = new TransactionData([
                'order_id' => $order->getId(),
                'transaction_id' => $result->getTransactionId(),
                'parent_transaction_id' => $authTransactionId,
                'type' => TransactionTypeInterface::TYPE_CAPTURE,
                'amount' => $amount,
                'status' => TransactionStatusInterface::STATUS_APPROVED,
            ]);
            $this->transactionRepo->save($transaction);

            // Update order if fully captured
            $totalCaptured = $this->getTotalCaptured($order) + $amount;
            if ($totalCaptured >= $order->getGrandTotal()) {
                $payment->setIsTransactionClosed(true);
            }
        }

        return $result;
    }

    private function validateCaptureAmount(
        OrderInterface $order,
        float $amount
    ): void {
        $remaining = $this->getRemainingCaptureAmount($order);

        if ($amount > $remaining) {
            throw new PaymentException(
                sprintf(
                    'Capture amount %s exceeds remaining %s',
                    $amount,
                    $remaining
                )
            );
        }
    }
}

Partial Capture

Partial Capture Implementation

namespace Vendor\Payment\Service\Capture;

class PartialCaptureService
{
    public function captureItems(
        OrderInterface $order,
        array $items
    ): CaptureResult {
        $captureAmount = 0;

        foreach ($items as $item) {
            $captureAmount += $item->getPrice() * $item->getQty();
        }

        // Add shipping if needed
        if ($this->shouldCaptureShipping($order, $items)) {
            $captureAmount += $order->getShippingAmount();
        }

        // Add tax if needed
        if ($this->shouldCaptureTax($order, $items)) {
            $captureAmount += $this->calculateTaxForItems($order, $items);
        }

        return $this->captureService->capture($order, $captureAmount);
    }
}

Multi-Capture Tracking

namespace Vendor\Payment\Model\Transaction;

class CaptureTracker
{
    public function getCapturedAmount(OrderInterface $order): float
    {
        $transactions = $this->transactionRepo->getByOrder(
            $order->getId(),
            TransactionTypeInterface::TYPE_CAPTURE
        );

        $total = 0;
        foreach ($transactions as $transaction) {
            if ($transaction->getStatus() === TransactionStatusInterface::STATUS_APPROVED) {
                $total += $transaction->getAmount();
            }
        }

        return $total;
    }

    public function getRemainingAmount(OrderInterface $order): float
    {
        return $order->getGrandTotal() - $this->getCapturedAmount($order);
    }
}

Authorization Management

Authorization Expiration

namespace Vendor\Payment\Service\Auth;

class AuthorizationExpiration
{
    private int $defaultExpirationDays = 7;

    public function checkExpired(): array
    {
        $expiredAuths = $this->transactionRepo->getExpiredAuthorizations(
            $this->defaultExpirationDays
        );

        foreach ($expiredAuths as $auth) {
            $this->voidAuthorization($auth);
        }

        return $expiredAuths;
    }

    private function voidAuthorization(TransactionInterface $auth): void
    {
        $order = $this->orderRepo->get($auth->getOrderId());
        $gateway = $this->gatewayFactory->create(
            $order->getPayment()->getMethod()
        );

        $gateway->void(new VoidRequest([
            'transaction_id' => $auth->getTransactionId(),
        ]));

        $auth->setStatus(TransactionStatusInterface::STATUS_VOIDED);
        $this->transactionRepo->save($auth);
    }
}

Void Flow

namespace Vendor\Payment\Service\Void;

class VoidService
{
    public function void(OrderInterface $order): VoidResult
    {
        $payment = $order->getPayment();
        $authTransactionId = $payment->getAuthorizationTransaction();

        // Check if any captures exist
        $capturedAmount = $this->captureTracker->getCapturedAmount($order);
        if ($capturedAmount > 0) {
            throw new PaymentException(
                'Cannot void order with existing captures. Use refund instead.'
            );
        }

        $gateway = $this->gatewayFactory->create($payment->getMethod());

        $result = $gateway->void(new VoidRequest([
            'transaction_id' => $authTransactionId,
        ]));

        if ($result->isSuccess()) {
            $payment->setIsTransactionClosed(true);
            $order->setState(Order::STATE_CANCELED);
        }

        return $result;
    }
}

Quiz

1. What is the difference between authorize and capture?

Question 1 options

2. What is partial capture?

Question 2 options

3. Why void an authorization?

Question 3 options

Flashcards

Question

What does authorize() do?

Answer

Verifies and holds funds without collecting

Question

What does capture() do?

Answer

Collects previously authorized funds

Question

What is partial capture?

Answer

Capturing only a portion of the authorized amount

Question

When to void vs refund?

Answer

Void cancels authorization; refund returns captured funds

Revision Notes

Key Takeaways

  • 1. Authorize holds funds, capture collects them later
  • 2. Partial capture allows collecting portions of authorized amount
  • 3. Track all capture transactions against authorization
  • 4. Void releases held funds before any capture
  • 5. Authorizations expire after gateway-defined period

Interview Tips

  • Explain the full authorize → capture lifecycle
  • Discuss when partial capture is needed
  • Describe handling authorization expiration
  • Talk about void vs refund differences

Cheat Sheet

Payment Auth:
  authorize() → hold funds (7-30 days)
  capture()   → collect held funds
  partial     → capture portion
  void        → release hold

Transaction Types:
  AUTH → authorization
  CAPTURE → capture
  VOID → void
  REFUND → refund

Checks:
  Remaining = auth_amount - captured
  Expired = auth older than 7 days