Skip to content

Issue #2804227 Add $order->getBalance() #508

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
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
51 changes: 51 additions & 0 deletions modules/order/src/Entity/Order.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Drupal\commerce_order\Entity;

use Drupal\commerce_order\Adjustment;
use Drupal\commerce_price\Price;
use Drupal\commerce_store\Entity\StoreInterface;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityChangedTrait;
Expand Down Expand Up @@ -384,6 +385,49 @@ public function getTotalPrice() {
}
}

/**
* {@inheritdoc}
*/
public function addPayment(Price $amount) {
$this->setTotalPaid($this->getTotalPaid()->add($amount));
return $this;
}

/**
* {@inheritdoc}
*/
public function subtractPayment(Price $amount) {
$this->setTotalPaid($this->getTotalPaid()->subtract($amount));
return $this;
}

/**
* {@inheritdoc}
*/
public function getTotalPaid() {
if (!$this->get('total_paid')->isEmpty()) {
return $this->get('total_paid')->first()->toPrice();
}
return new Price('0', $this->getStore()->getDefaultCurrencyCode());
}

/**
* {@inheritdoc}
*/
public function setTotalPaid(Price $amount) {
$this->set('total_paid', $amount);
}

/**
* {@inheritdoc}
*/
public function getBalance() {
if ($this->getTotalPrice() && $this->getTotalPaid()) {
return $this->getTotalPrice()->subtract($this->getTotalPaid());
}
return $this->getTotalPrice();
}

/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -642,6 +686,13 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
->setDisplayConfigurable('form', FALSE)
->setDisplayConfigurable('view', TRUE);

$fields['total_paid'] = BaseFieldDefinition::create('commerce_price')
->setLabel(t('Total paid'))
->setDescription(t('The total amount paid on the order.'))
->setReadOnly(TRUE)
->setDisplayConfigurable('form', FALSE)
->setDisplayConfigurable('view', TRUE);

$fields['state'] = BaseFieldDefinition::create('state')
->setLabel(t('State'))
->setDescription(t('The order state.'))
Expand Down
45 changes: 45 additions & 0 deletions modules/order/src/Entity/OrderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Drupal\commerce_order\Entity;

use Drupal\commerce_order\EntityAdjustableInterface;
use Drupal\commerce_price\Price;
use Drupal\commerce_store\Entity\StoreInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityChangedInterface;
Expand Down Expand Up @@ -272,6 +273,50 @@ public function recalculateTotalPrice();
*/
public function getTotalPrice();

/**
* Adds an amount to the order total paid.
*
* @param \Drupal\commerce_price\Price $amount
* The amount to add to the total paid.
*
* @return $this
*/
public function addPayment(Price $amount);

/**
* Subtracts an amount from the order total paid.
*
* @param \Drupal\commerce_price\Price $amount
* The amount to subtract from the total paid.
*
* @return $this
*/
public function subtractPayment(Price $amount);

/**
* Gets the total amount paid on the order.
*
* @return \Drupal\commerce_price\Price
* The order total paid amount.
*/
public function getTotalPaid();

/**
* Sets the total amount paid on the order.
*
* @param \Drupal\commerce_price\Price $amount
* The amount to set as the order total paid.
*/
public function setTotalPaid(Price $amount);

/**
* Gets the remaining amount unpaid on the order.
*
* @return \Drupal\commerce_price\Price|null
* The total order amount minus the total paid, or NULL.
*/
public function getBalance();

/**
* Gets the order state.
*
Expand Down
86 changes: 85 additions & 1 deletion modules/order/tests/src/Kernel/Entity/OrderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
use Drupal\commerce_order\Entity\OrderItemType;
use Drupal\commerce_price\Exception\CurrencyMismatchException;
use Drupal\commerce_price\Price;
use Drupal\commerce_payment\Entity\Payment;
use Drupal\commerce_payment\Entity\PaymentGateway;
use Drupal\profile\Entity\Profile;
use Drupal\Tests\commerce\Kernel\CommerceKernelTestBase;

Expand All @@ -27,6 +29,13 @@ class OrderTest extends CommerceKernelTestBase {
*/
protected $user;

/**
* The payment gateway plugin.
*
* @var \Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsRefundsInterface
*/
protected $payment_gateway_plugin;

/**
* Modules to enable.
*
Expand All @@ -36,6 +45,8 @@ class OrderTest extends CommerceKernelTestBase {
'entity_reference_revisions',
'profile',
'state_machine',
'commerce_payment',
'commerce_payment_example',
'commerce_product',
'commerce_order',
];
Expand All @@ -49,6 +60,7 @@ protected function setUp() {
$this->installEntitySchema('profile');
$this->installEntitySchema('commerce_order');
$this->installEntitySchema('commerce_order_item');
$this->installEntitySchema('commerce_payment');
$this->installConfig('commerce_order');

// An order item type that doesn't need a purchasable entity, for simplicity.
Expand All @@ -58,6 +70,14 @@ protected function setUp() {
'orderType' => 'default',
])->save();

$payment_gateway = PaymentGateway::create([
'id' => 'example',
'label' => 'Example',
'plugin' => 'example_onsite',
]);
$payment_gateway->save();
$this->payment_gateway_plugin = $payment_gateway->getPlugin();

$user = $this->createUser();
$this->user = $this->reloadEntity($user);
}
Expand Down Expand Up @@ -94,6 +114,11 @@ protected function setUp() {
* @covers ::getSubtotalPrice
* @covers ::recalculateTotalPrice
* @covers ::getTotalPrice
* @covers ::getBalance
* @covers ::addPayment
* @covers ::subtractPayment
* @covers ::setTotalPaid
* @covers ::getTotalPaid
* @covers ::getState
* @covers ::getRefreshState
* @covers ::setRefreshState
Expand Down Expand Up @@ -133,6 +158,7 @@ public function testOrder() {
$order = Order::create([
'type' => 'default',
'state' => 'completed',
'store_id' => $this->store->id(),
]);
$order->save();

Expand Down Expand Up @@ -178,6 +204,7 @@ public function testOrder() {
$this->assertNotEmpty($order->hasItem($another_order_item));

$this->assertEquals(new Price('8.00', 'USD'), $order->getTotalPrice());
$this->assertEquals(new Price('8.00', 'USD'), $order->getBalance());
$adjustments = [];
$adjustments[] = new Adjustment([
'type' => 'custom',
Expand Down Expand Up @@ -208,6 +235,7 @@ public function testOrder() {
$order->removeAdjustment($adjustments[0]);
$this->assertEquals(new Price('8.00', 'USD'), $order->getSubtotalPrice());
$this->assertEquals(new Price('18.00', 'USD'), $order->getTotalPrice());
$this->assertEquals(new Price('18.00', 'USD'), $order->getBalance());
$this->assertEquals([$adjustments[1], $adjustments[2]], $order->getAdjustments());
$order->setAdjustments($adjustments);
$this->assertEquals($adjustments, $order->getAdjustments());
Expand All @@ -221,9 +249,65 @@ public function testOrder() {
'amount' => new Price('5.00', 'USD'),
]));
$order->addItem($another_order_item);
$this->assertEquals(new Price('27.00', 'USD'), $order->getTotalPrice());
$collected_adjustments = $order->collectAdjustments();
$this->assertEquals(new Price('10.00', 'USD'), $collected_adjustments[2]->getAmount());
$this->assertEquals(new Price('27.00', 'USD'), $order->getTotalPrice());
$this->assertEquals(new Price('27.00', 'USD'), $order->getBalance());

// Test that payments update the order total paid and balance.
$order->save();
$payment = Payment::create([
'order_id' => $order->id(),
'amount' => new Price('25.00', 'USD'),
'payment_gateway' => 'example',
'state' => 'capture_completed',
]);
$payment->save();
$order = Order::load($order->id());
$this->assertEquals(new Price('2.00', 'USD'), $order->getBalance());
$this->payment_gateway_plugin->refundPayment($payment, new Price('5.00', 'USD'));
$order = Order::load($order->id());
$this->assertEquals(new Price('7.00', 'USD'), $order->getBalance());
$payment->delete();
$order = Order::load($order->id());
$this->assertEquals(new Price('27.00', 'USD'), $order->getBalance());
$payment2 = Payment::create([
'order_id' => $order->id(),
'amount' => new Price('27.00', 'USD'),
'payment_gateway' => 'example',
'state' => 'capture_completed',
]);
$payment2->save();
$order = Order::load($order->id());
$this->assertEquals(new Price('0.00', 'USD'), $order->getBalance());

// Test that payments can be partially refunded multiple times.
$this->payment_gateway_plugin->refundPayment($payment2, new Price('17.00', 'USD'));
$order = Order::load($order->id());
$this->assertEquals(new Price('17.00', 'USD'), $order->getBalance());
$this->payment_gateway_plugin->refundPayment($payment2, new Price('5.00', 'USD'));
$order = Order::load($order->id());
$this->assertEquals(new Price('22.00', 'USD'), $order->getBalance());

// Test that the total paid amount can be set explicitly on the order.
$order->setTotalPaid(new Price('0.00', 'USD'));
$order->save();
$this->assertEquals(new Price('27.00', 'USD'), $order->getBalance());

// Test that deleted payments update the order total paid and balance.
$order->save();
$payment = Payment::create([
'order_id' => $order->id(),
'amount' => new Price('25.00', 'USD'),
'payment_gateway' => 'example',
]);
$payment->save();
$order = Order::load($order->id());
$this->assertEquals(new Price('25.00', 'USD'), $order->getTotalPaid());
$this->assertEquals(new Price('2.00', 'USD'), $order->getBalance());
$payment->delete();
$order = Order::load($order->id());
$this->assertEquals(new Price('0.00', 'USD'), $order->getTotalPaid());

$this->assertEquals('completed', $order->getState()->value);

Expand Down
23 changes: 23 additions & 0 deletions modules/payment/src/Entity/Payment.php
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,29 @@ public function preSave(EntityStorageInterface $storage) {
$refunded_amount = new Price('0', $this->getAmount()->getCurrencyCode());
$this->setRefundedAmount($refunded_amount);
}

// Add or subtract payments from order.
if ($this->isNew()) {
$this->getOrder()->addPayment($this->getAmount())->save();
}
else {
$original = $this->values['original'];
$net_refund = $this->getRefundedAmount()->subtract($original->getRefundedAmount());
$this->getOrder()->subtractPayment($net_refund)->save();
}
}

/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageInterface $storage, array $entities) {
parent::preDelete($storage, $entities);

// Subtract each payment from order.
foreach ($entities as $payment) {
$net_payment = $payment->getAmount()->subtract($payment->getRefundedAmount());
$payment->getOrder()->subtractPayment($net_payment)->save();
}
}

/**
Expand Down