From 143c26f7e92682cb6389bc36c3d5ac23ab80927b Mon Sep 17 00:00:00 2001 From: Andy Kim Date: Thu, 11 Jan 2024 14:49:00 +0700 Subject: [PATCH] Support Fee object for magento (#175) * Support Fee object for magento * update version --- CHANGELOG.md | 3 + Controller/Checkout/Invoice.php | 6 + Controller/Checkout/InvoiceMultishipping.php | 9 + Helper/Data.php | 101 ++++++- Test/Unit/Helper/DataTest.php | 298 ++++++++++++++++++- composer.json | 2 +- etc/module.xml | 2 +- 7 files changed, 415 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81c8d74..be8eed2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # CHANGELOG +## 12.0.1 (2024-01-11) +- Support Xendit invoice fees + ## 12.0.0 (2023-11-21) - Update xendit url diff --git a/Controller/Checkout/Invoice.php b/Controller/Checkout/Invoice.php index 716f2b6..c54a3e9 100644 --- a/Controller/Checkout/Invoice.php +++ b/Controller/Checkout/Invoice.php @@ -120,6 +120,12 @@ private function getApiRequestData(Order $order) 'items' => $items ]; + // Extract order fees and send it to Xendit invoice + $orderFees = $this->getDataHelper()->extractOrderFees($order); + if (!empty($orderFees)) { + $payload['fees'] = $orderFees; + } + if (!empty($customerObject)) { $payload['customer'] = $customerObject; } diff --git a/Controller/Checkout/InvoiceMultishipping.php b/Controller/Checkout/InvoiceMultishipping.php index 30f13a0..4360ff5 100644 --- a/Controller/Checkout/InvoiceMultishipping.php +++ b/Controller/Checkout/InvoiceMultishipping.php @@ -28,6 +28,7 @@ public function execute() $currency = ''; $billingEmail = ''; $customerObject = []; + $feesObject = []; $orderIncrementIds = []; $preferredMethod = ''; @@ -95,6 +96,9 @@ public function execute() if (empty($customerObject)) { $customerObject = $this->getDataHelper()->extractXenditInvoiceCustomerFromOrder($order); } + + // Extract order fees and send it to Xendit invoice + $feesObject[] = $this->getDataHelper()->extractOrderFees($order); } if ($orderProcessed) { @@ -115,6 +119,11 @@ public function execute() 'failure_redirect_url' => $this->getDataHelper()->getFailureUrl($orderIncrementIds), 'items' => $items ]; + + if (!empty($feesObject)) { + $requestData['fees'] = $this->getDataHelper()->mergeFeesObject($feesObject); + } + if (!empty($customerObject)) { $requestData['customer'] = $customerObject; } diff --git a/Helper/Data.php b/Helper/Data.php index c559184..06d1e03 100644 --- a/Helper/Data.php +++ b/Helper/Data.php @@ -30,7 +30,7 @@ */ class Data extends AbstractHelper { - const XENDIT_M2INVOICE_VERSION = '12.0.0'; + const XENDIT_M2INVOICE_VERSION = '12.0.1'; /** * @var StoreManagerInterface @@ -663,4 +663,103 @@ public function extractProductCategoryName(Product $product): string } return !empty($categoryNames) ? implode(', ', $categoryNames) : 'n/a'; } + + /** + * Extract order fees + * @param Order $order + * @return array + */ + public function extractOrderFees(Order $order): array + { + $fees = [ + [ + 'type' => __('Discount'), + 'value' => (float) $order->getDiscountAmount() + ], + [ + 'type' => __('Shipping fee'), + 'value' => (float) $order->getShippingAmount() + ], + [ + 'type' => __('Tax fee'), + 'value' => $order->getTaxAmount() + ] + ]; + + // Make sure it will cover the other fees + $otherFees = $this->getOtherFees($order); + if ($otherFees > 0) { + $fees[] = [ + 'type' => __('Other Fees'), + 'value' => $this->getOtherFees($order) + ]; + } + + return array_values( + array_filter($fees, function ($value) { + return $value['value'] != 0; + }, ARRAY_FILTER_USE_BOTH) + ); + } + + /** + * Get other fees amount + * In case order has the other fees not Magento standard + * + * @param Order $order + * @return float + */ + public function getOtherFees(Order $order): float + { + return $order->getTotalDue() - (float)array_sum( + [ + $order->getSubtotal(), // items total + $order->getTaxAmount(), + $order->getShippingAmount(), + $order->getDiscountAmount() + ] + ); + } + + /** + * Merge Fees object + * + * @param array $feesObject + * @return array + */ + public function mergeFeesObject(array $feesObject = []): array + { + if (empty($feesObject)) { + return []; + } + + $mergedFeesObject = []; + foreach ($feesObject as $feeObject) { + foreach ($feeObject as $fee) { + /** @var \Magento\Framework\Phrase $type */ + $type = $fee['type']; + $typeLabel = $type->getText(); + $value = $fee['value']; + + if (isset($mergedFeesObject[$typeLabel])) { + $mergedFeesObject[$typeLabel] = (float)$mergedFeesObject[$typeLabel] + $value; + } else { + $mergedFeesObject[$typeLabel] = $value; + } + } + } + + if (empty($mergedFeesObject)) { + return []; + } + + $response = []; + foreach ($mergedFeesObject as $typeLabel => $value) { + $response[] = [ + 'type' => $typeLabel, + 'value' => $value + ]; + } + return $response; + } } diff --git a/Test/Unit/Helper/DataTest.php b/Test/Unit/Helper/DataTest.php index 54b0e72..17770ff 100644 --- a/Test/Unit/Helper/DataTest.php +++ b/Test/Unit/Helper/DataTest.php @@ -3,9 +3,11 @@ namespace Xendit\M2Invoice\Test\Unit\Helper; -use Xendit\M2Invoice\Helper\Data; +use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Sales\Model\Order; use PHPUnit\Framework\TestCase; +use Xendit\M2Invoice\Helper\Data; class DataTest extends TestCase { @@ -14,13 +16,33 @@ class DataTest extends TestCase */ protected $_dataHelper; + /** + * @var ObjectManager $objectManager + */ + protected $objectManager; + + /** + * @var \Magento\Directory\Model\PriceCurrency|(\Magento\Directory\Model\PriceCurrency&object&\PHPUnit\Framework\MockObject\MockObject)|(\Magento\Directory\Model\PriceCurrency&\PHPUnit\Framework\MockObject\MockObject)|(object&\PHPUnit\Framework\MockObject\MockObject)|\PHPUnit\Framework\MockObject\MockObject + */ + protected $priceCurrencyMock; + /** * @return void */ protected function setUp(): void { - $objectManager = new ObjectManager($this); - $this->_dataHelper = $objectManager->getObject(Data::class); + $this->objectManager = \Magento\Framework\App\ObjectManager::getInstance(); + $this->_dataHelper = $this->objectManager->create(Data::class); + + $this->priceCurrencyMock = $this->getMockForAbstractClass( + PriceCurrencyInterface::class, + [], + '', + false, + false, + true, + ['round'] + ); } /** @@ -32,4 +54,274 @@ public function testTruncateDecimal() $this->assertEquals(100000, $this->_dataHelper->truncateDecimal(100000.59)); $this->assertEquals(100000, $this->_dataHelper->truncateDecimal(100000.99)); } + + /** + * Create mock order + * + * @param int $entityId + * @param $totalDue + * @param $subTotal + * @param $totalPaid + * @param $shippingAmount + * @param $taxAmount + * @param $discountAmount + * @return Order + */ + protected function createMockOrder(int $entityId, $totalDue, $subTotal, $totalPaid, $shippingAmount, $taxAmount, $discountAmount): Order + { + $helper = new ObjectManager($this); + + /** @var Order $order */ + $order = $helper->getObject(Order::class, ['priceCurrency' => $this->priceCurrencyMock]); + + $order->setEntityId($entityId); + $order->setSubtotal($subTotal); + $order->setGrandTotal($totalDue); + $order->setTotalPaid($totalPaid); + $order->setShippingAmount($shippingAmount); + $order->setTaxAmount($taxAmount); + $order->setDiscountAmount($discountAmount); + + return $order; + } + + /** + * Test extract order fees with 3 items: Shipping, Tax, Discount + * + * @return void + */ + public function testExtractOrderFeesWithThreeItems() + { + $totalDue = 50000; + $order = $this->createMockOrder(999, $totalDue, 25000, 0, 20000, 10000, -5000); + + $this->priceCurrencyMock->expects($this->any())->method('round')->with($totalDue)->willReturnArgument(0); + $this->assertEquals($totalDue, $order->getTotalDue()); + + $orderFees = $this->_dataHelper->extractOrderFees($order); + $this->assertEquals( + [ + [ + 'type' => 'Discount', + 'value' => -5000 + ], + [ + 'type' => 'Shipping fee', + 'value' => 20000 + ], + [ + 'type' => 'Tax fee', + 'value' => 10000 + ] + ], + $orderFees + ); + } + + /** + * Test extract order fees with 2 items: Shipping, Tax + * + * @return void + */ + public function testExtractOrderFeesWithTwoItems() + { + $totalDue = 50000; + $order = $this->createMockOrder(999, $totalDue, 25000, 0, 20000, 10000, 0); + + $this->priceCurrencyMock->expects($this->any())->method('round')->with($totalDue)->willReturnArgument(0); + $this->assertEquals($totalDue, $order->getTotalDue()); + + $orderFees = $this->_dataHelper->extractOrderFees($order); + $this->assertEquals( + [ + [ + 'type' => 'Shipping fee', + 'value' => 20000 + ], + [ + 'type' => 'Tax fee', + 'value' => 10000 + ] + ], + $orderFees + ); + } + + /** + * Test extract empty order fees + * + * @return void + */ + public function testExtractOrderFeesWithEmptyItems() + { + $totalDue = 50000; + $order = $this->createMockOrder(999, $totalDue, 50000, 0, 0, 0, 0); + + $this->priceCurrencyMock->expects($this->any())->method('round')->with($totalDue)->willReturnArgument(0); + $this->assertEquals($totalDue, $order->getTotalDue()); + + $orderFees = $this->_dataHelper->extractOrderFees($order); + $this->assertEquals( + [], + $orderFees + ); + } + + /** + * Test extract order other fees + * In case the Total > Subtotal and Shipping = Tax = Discount = 0 + * + * @return void + */ + public function testExtractOrderFeesWithOtherFees() + { + $totalDue = 50000; + $order = $this->createMockOrder(999, $totalDue, 20000, 0, 0, 0, 0); + + $this->priceCurrencyMock->expects($this->any())->method('round')->with($totalDue)->willReturnArgument(0); + $this->assertEquals($totalDue, $order->getTotalDue()); + + $orderFees = $this->_dataHelper->extractOrderFees($order); + $this->assertEquals( + [ + [ + 'type' => 'Other Fees', + 'value' => 30000 + ] + ], + $orderFees + ); + } + + /** + * Should return fee object which has: Shipping, Tax, Discount + * + * @return void + */ + public function testMergeFeesObjectShouldReturnFeeObjectForFirstOrder() + { + $totalDue = 50000; + $firstOrder = $this->createMockOrder(999, $totalDue, 25000, 0, 20000, 10000, -5000); + $secondOrder = $this->createMockOrder(1000, $totalDue, 50000, 0, 0, 0, 0); + + $this->priceCurrencyMock->expects($this->any())->method('round')->with($totalDue)->willReturnArgument(0); + $this->assertEquals($totalDue, $firstOrder->getTotalDue()); + $this->assertEquals($totalDue, $secondOrder->getTotalDue()); + + $orderFees[] = $this->_dataHelper->extractOrderFees($firstOrder); + $orderFees[] = $this->_dataHelper->extractOrderFees($secondOrder); + + $finalOrderFees = $this->_dataHelper->mergeFeesObject($orderFees); + $this->assertEquals( + [ + [ + 'type' => 'Discount', + 'value' => -5000 + ], + [ + 'type' => 'Shipping fee', + 'value' => 20000 + ], + [ + 'type' => 'Tax fee', + 'value' => 10000 + ] + ], + $finalOrderFees + ); + } + + /** + * Should return fee object which has: Shipping, Tax, Discount + * + * @return void + */ + public function testMergeFeesObjectShouldReturnFeeObject() + { + $totalDue = 50000; + $firstOrder = $this->createMockOrder(999, $totalDue, 25000, 0, 20000, 10000, -5000); + $secondOrder = $this->createMockOrder(1000, $totalDue, 38000, 0, 10000, 10000, -2000); + + $this->priceCurrencyMock->expects($this->any())->method('round')->with($totalDue)->willReturnArgument(0); + $this->assertEquals($totalDue, $firstOrder->getTotalDue()); + $this->assertEquals($totalDue, $secondOrder->getTotalDue()); + + $orderFees[] = $this->_dataHelper->extractOrderFees($firstOrder); + $orderFees[] = $this->_dataHelper->extractOrderFees($secondOrder); + + $finalOrderFees = $this->_dataHelper->mergeFeesObject($orderFees); + $this->assertEquals( + [ + [ + 'type' => 'Discount', + 'value' => -7000 + ], + [ + 'type' => 'Shipping fee', + 'value' => 30000 + ], + [ + 'type' => 'Tax fee', + 'value' => 20000 + ] + ], + $finalOrderFees + ); + } + + /** + * Should return empty array because has no fees for first order & second order + * + * @return void + */ + public function testMergeFeesObjectShouldReturnEmptyArray() + { + $totalDue = $subTotal = 50000; // Total = SubTotal => Order has no fees + $firstOrder = $this->createMockOrder(999, $totalDue, $subTotal, 0, 0, 0, 0); + $secondOrder = $this->createMockOrder(1000, $totalDue, $subTotal, 0, 0, 0, 0); + + $this->priceCurrencyMock->expects($this->any())->method('round')->with($totalDue)->willReturnArgument(0); + $this->assertEquals($totalDue, $firstOrder->getTotalDue()); + $this->assertEquals($totalDue, $secondOrder->getTotalDue()); + + $orderFees[] = $this->_dataHelper->extractOrderFees($firstOrder); + $orderFees[] = $this->_dataHelper->extractOrderFees($secondOrder); + + $finalOrderFees = $this->_dataHelper->mergeFeesObject($orderFees); + $this->assertEquals( + [], + $finalOrderFees + ); + } + + /** + * Test extract order other fees for first_order + second_order + * In case the Total > Subtotal and Shipping = Tax = Discount = 0 + * + * @return void + */ + public function testMergeFeesObjectShouldReturnOtherFees() + { + $totalDue = 50000; + $firstOrder = $this->createMockOrder(999, $totalDue, 20000, 0, 0, 0, 0); + $secondOrder = $this->createMockOrder(1000, $totalDue, 20000, 0, 0, 0, 0); + + $this->priceCurrencyMock->expects($this->any())->method('round')->with($totalDue)->willReturnArgument(0); + $this->assertEquals($totalDue, $firstOrder->getTotalDue()); + $this->assertEquals($totalDue, $secondOrder->getTotalDue()); + + $orderFees[] = $this->_dataHelper->extractOrderFees($firstOrder); + $orderFees[] = $this->_dataHelper->extractOrderFees($secondOrder); + + $finalOrderFees = $this->_dataHelper->mergeFeesObject($orderFees); + $this->assertEquals( + [ + [ + 'type' => 'Other Fees', + 'value' => 60000 // Total fees of first_order + second_order + ] + ], + $finalOrderFees + ); + } } diff --git a/composer.json b/composer.json index 4aed799..c10ac04 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "xendit/m2invoice", "description": "Xendit Payment Gateway Module", "type": "magento2-module", - "version": "12.0.0", + "version": "12.0.1", "license": [ "GPL-3.0" ], diff --git a/etc/module.xml b/etc/module.xml index 3e22500..0d8e682 100644 --- a/etc/module.xml +++ b/etc/module.xml @@ -1,4 +1,4 @@ - +