On Payments page for an order (eg admin/commerce/orders/29/payments) doing a Receive for a smaller amount that the displayed total changes status to Completed and only shows a Refund button, so I guess partial payments needs supporting.
This would include showing outstanding balance to pay.

Comments

Jons created an issue. See original summary.

jons’s picture

Title: Add outstanding balance to Payments page » Support partial Payments
Issue summary: View changes
bradjones1’s picture

Status: Active » Postponed (maintainer needs more info)

Partial payments can be applied; this is probably also linked at least in spirit to #2912996: Support admin order payments for admin payments.

I'm going to mark this as needs more info, though, because at least in theory Commerce allows for partial payments, and there is now an API for retrieving the order balance. So, your use case may need to get fleshed out a bit more.

mvonfrie’s picture

I don't know what @jons' use case is, but I can tell you my use case regarding a travel booking system:

Customers have to pay X% deposit (e. g. 10%) on booking (only if the booking takes place more than Y days before begin of the trip) and the remaining payment latest Z days before arrival. How can I implement this kind of payment splitting?

  1. The first partial payment is done as part of the booking process with either credit card (via Commerce Stripe) or PayPal.
  2. The second payment at Z days before arrival should be performed either by the same payment method as the first one or handled as manual payment.

In addition, there is a discussion about this topic #2847378: Document how a gateway can implement multiple captures.

mvonfrie’s picture

Status: Postponed (maintainer needs more info) » Active
bradjones1’s picture

Title: Support partial Payments » Native support for deposits/later payment of balance

You can definitely support that with some custom code however there is no out-of-the-box setting for partial payment up front. We do something like this with Big Island Fish by altering the payment during creation/gateway transaction.

I'm re-titling to better reflect your feature request.

mglaman’s picture

Also, I wonder if part of this requires us to review / do house keeping on older code that existed before we supported methods for getting the balance of an order and the amount paid:

  /**
   * Gets the total paid price.
   *
   * @return \Drupal\commerce_price\Price|null
   *   The total paid price, or NULL.
   */
  public function getTotalPaid();

  /**
   * Sets the total paid price.
   *
   * @param \Drupal\commerce_price\Price $total_paid
   *   The total paid price.
   */
  public function setTotalPaid(Price $total_paid);

  /**
   * Gets the order balance.
   *
   * Calculated by subtracting the total paid price from the total price.
   * Can be negative in case the order was overpaid.
   *
   * @return \Drupal\commerce_price\Price|null
   *   The order balance, or NULL.
   */
  public function getBalance();
nno’s picture

@mvonfrie
You can do this by setting "Transaction mode" in Checkout flow to "Authorize only", then capture Deposit in commerce_order.place.post_transition EventSubscriber.

Something like this

class BookingOrderCaptureDepositSubscriber implements EventSubscriberInterface {

  use StringTranslationTrait;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * Constructs a new BookingOrderCaptureDepositSubscriber object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   */

  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
    $this->entityTypeManager = $entity_type_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events = ['commerce_order.place.post_transition' => ['captureDeposit', -50]];
    return $events;
  }

  /**
   * Capture deposit amount on order place.
   *
   * @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event
   *   The event we subscribed to.
   */
  public function captureDeposit(WorkflowTransitionEvent $event) {
    /** @var \Drupal\commerce_order\Entity\OrderInterface $order */
    $order = $event->getEntity();

    if ($order->bundle() != 'booking') {
      return;
    }

    /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */
    $payment_gateway = $order->payment_gateway->entity;
    $payment_gateway_plugin = $payment_gateway->getPlugin();

    $payment_storage = $this->entityTypeManager->getStorage('commerce_payment');

    // Load first payment
    $payments = $payment_storage->loadMultipleByOrder($order);
    $first_payment = current($payments);

    // Only proceed if payment is not captured
    if ($first_payment->isCompleted()) {
      return;
    }
      
    // Set deposit amount
    $deposit_amount = new \Drupal\commerce_price\Price('50.00', 'USD');

    // Create deposit payment
    /** @var \Drupal\commerce_payment\Entity\PaymentInterface $payment */
    $payment = $payment_storage->create([
      'state' => 'new',
      'amount' => $deposit_amount,
      'payment_gateway' => $payment_gateway->id(),
      'order_id' => $order->id(),
    ]);

    $payment->payment_method = $order->payment_method->entity;

    // Capture deposit payment
    if ($payment_gateway_plugin instanceof OnsitePaymentGatewayInterface) {
      $payment_gateway_plugin->createPayment($payment, 'capture');
    }
    elseif ($payment_gateway_plugin instanceof ManualPaymentGatewayInterface) {
      $payment_gateway_plugin->createPayment($payment, TRUE);
    }
    else {
      return;
    }

    // Subtract captured amount from authorized amount and save first payment
    $first_payment_amount = $first_payment->getAmount();
    $first_payment->setAmount($first_payment_amount->subtract($deposit_amount));
    $first_payment->save();

  }
}

Similarly you can capture balance later in another state transition.