Skip to content

Commit 7008f90

Browse files
authored
Merge pull request #57 from unzerdev/UMCS-317/update-automatic-cancel
Umcs 317/update automatic cancel
2 parents f9f87b8 + e4d1c27 commit 7008f90

20 files changed

+297
-39
lines changed

CHANGELOG.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
55
## [1.1.4.1](https://github.com/unzerdev/php-sdk/compare/1.1.4.0..1.1.4.1)
66
### Added
7-
* Added Apple Pay example.
8-
9-
### Updated
10-
* Updated jQuery and frameworks used in examples.
7+
* Added Apple Pay example.
118

129
### Changed
13-
* Several minor improvements.
10+
* Adjust `cancelAmount` logic to work properly with Invoice Secured payments.
11+
* Updated jQuery and frameworks used in examples.
12+
* Fixed failing card tests.
13+
* Several minor improvements.
1414

1515
## [1.1.4.0](https://github.com/unzerdev/php-sdk/compare/1.1.3.0..1.1.4.0)
1616
### Added

src/Constants/TransactionStatus.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
/**
3+
* This file contains definitions of the status of the payment transactions.
4+
*
5+
* Copyright (C) 2021 - today Unzer E-Com GmbH
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*
19+
* @link https://docs.unzer.com/
20+
*
21+
* @author David Owusu <[email protected]>
22+
*
23+
* @package UnzerSDK\Constants
24+
*/
25+
namespace UnzerSDK\Constants;
26+
27+
class TransactionStatus
28+
{
29+
public const STATUS_PENDING = 'pending';
30+
public const STATUS_SUCCESS = 'success';
31+
public const STATUS_ERROR = 'error';
32+
}

src/Resources/Payment.php

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,8 @@ private function updateAuthorizationTransaction($transaction): void
814814
$authorization = (new Authorization())->setPayment($this)->setId($transactionId);
815815
$this->setAuthorization($authorization);
816816
}
817-
$authorization->setAmount($transaction->amount);
817+
818+
$authorization->handleResponse($transaction);
818819
}
819820

820821
/**
@@ -834,7 +835,8 @@ private function updateChargeTransaction($transaction): void
834835
$charge = (new Charge())->setPayment($this)->setId($transactionId);
835836
$this->addCharge($charge);
836837
}
837-
$charge->setAmount($transaction->amount);
838+
839+
$charge->handleResponse($transaction);
838840
}
839841

840842
/**
@@ -859,7 +861,8 @@ private function updateReversalTransaction($transaction): void
859861
$cancellation = (new Cancellation())->setPayment($this)->setId($transactionId);
860862
$authorization->addCancellation($cancellation);
861863
}
862-
$cancellation->setAmount($transaction->amount);
864+
865+
$cancellation->handleResponse($transaction);
863866
}
864867

865868
/**
@@ -886,7 +889,8 @@ private function updateRefundTransaction($transaction): void
886889
$cancellation = (new Cancellation())->setPayment($this)->setId($refundId);
887890
$charge->addCancellation($cancellation);
888891
}
889-
$cancellation->setAmount($transaction->amount);
892+
893+
$cancellation->handleResponse($transaction);
890894
}
891895

892896
/**
@@ -906,7 +910,8 @@ private function updateShipmentTransaction($transaction): void
906910
$shipment = (new Shipment())->setId($shipmentId);
907911
$this->addShipment($shipment);
908912
}
909-
$shipment->setAmount($transaction->amount);
913+
914+
$shipment->handleResponse($transaction);
910915
}
911916

912917
/**
@@ -926,7 +931,8 @@ private function updatePayoutTransaction($transaction): void
926931
$payout = (new Payout())->setId($payoutId);
927932
$this->setPayout($payout);
928933
}
929-
$payout->setAmount($transaction->amount);
934+
935+
$payout->handleResponse($transaction);
930936
}
931937

932938
//</editor-fold>

src/Resources/TransactionTypes/AbstractTransactionType.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ abstract class AbstractTransactionType extends AbstractUnzerResource
5353

5454
/** @var Payment $payment */
5555
private $payment;
56-
5756
//</editor-fold>
5857

5958
//<editor-fold desc="Getters/Setters">

src/Resources/TransactionTypes/Charge.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@ public function getCancelledAmount(): ?float
107107
$amount = 0.0;
108108
foreach ($this->getCancellations() as $cancellation) {
109109
/** @var Cancellation $cancellation */
110-
$amount += $cancellation->getAmount();
110+
if ($cancellation->isSuccess()) {
111+
$amount += $cancellation->getAmount();
112+
}
111113
}
112114

113115
return $amount;

src/Services/CancelService.php

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,16 @@
2424
*/
2525
namespace UnzerSDK\Services;
2626

27+
use RuntimeException;
2728
use UnzerSDK\Constants\ApiResponseCodes;
2829
use UnzerSDK\Constants\CancelReasonCodes;
2930
use UnzerSDK\Exceptions\UnzerApiException;
30-
use UnzerSDK\Unzer;
3131
use UnzerSDK\Interfaces\CancelServiceInterface;
3232
use UnzerSDK\Resources\Payment;
3333
use UnzerSDK\Resources\TransactionTypes\Authorization;
3434
use UnzerSDK\Resources\TransactionTypes\Cancellation;
3535
use UnzerSDK\Resources\TransactionTypes\Charge;
36-
use RuntimeException;
36+
use UnzerSDK\Unzer;
3737
use function in_array;
3838
use function is_string;
3939

@@ -262,13 +262,29 @@ public function cancelPaymentCharges(
262262
$cancellations = [];
263263
$cancelWholePayment = $remainingToCancel === null;
264264

265-
/** @var Charge $charge */
266-
foreach ($payment->getCharges() as $charge) {
265+
/** @var array $charge */
266+
$charges = $payment->getCharges();
267+
$receiptAmount = $this->calculateReceiptAmount($charges);
268+
foreach ($charges as $index => $charge) {
267269
$cancelAmount = null;
268270
if (!$cancelWholePayment && $remainingToCancel <= $charge->getTotalAmount()) {
269271
$cancelAmount = $remainingToCancel;
270272
}
271273

274+
/** @var Charge $charge */
275+
// Calculate the maximum cancel amount for initial transaction.
276+
if ($index === 0 && $charge->isPending()) {
277+
$maxReversalAmount = $charge->getAmount() - $receiptAmount - $charge->getCancelledAmount();
278+
/* If canceled and charged amounts are equal or higher than the initial charge, skip it,
279+
because there won't be anything left to cancel. */
280+
if ($maxReversalAmount <= 0) {
281+
continue;
282+
}
283+
if ($maxReversalAmount < $cancelAmount) {
284+
$cancelAmount = $maxReversalAmount;
285+
}
286+
}
287+
272288
try {
273289
$cancellation = $charge->cancel($cancelAmount, $reasonCode, $referenceText, $amountNet, $amountVat);
274290
} catch (UnzerApiException $e) {
@@ -333,5 +349,17 @@ private function updateCancelAmount($remainingToCancel, float $amount): ?float
333349
return $remainingToCancel;
334350
}
335351

336-
//</editor-fold>
352+
//</editor-fold>/**
353+
354+
protected function calculateReceiptAmount(array $charges): float
355+
{
356+
$receiptAmount = 0;
357+
// Sum up Amounts of all successful charges from the list.
358+
foreach ($charges as $charge) {
359+
if ($charge->isSuccess()) {
360+
$receiptAmount += $charge->getAmount();
361+
}
362+
}
363+
return $receiptAmount;
364+
}
337365
}

src/Traits/HasStates.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
*/
2525
namespace UnzerSDK\Traits;
2626

27+
use RuntimeException;
28+
use UnzerSDK\Constants\TransactionStatus;
29+
2730
trait HasStates
2831
{
2932
/** @var bool $isError */
@@ -95,4 +98,55 @@ protected function setIsPending(bool $isPending): self
9598
}
9699

97100
//</editor-fold>
101+
102+
/**
103+
* Map the 'status' that is used for transactions in the transaction list of a payment resource.
104+
* The actual transaction resource only has the isSuccess, isPending and isError property.
105+
*
106+
* @param string $status
107+
*
108+
* @throws RuntimeException
109+
*/
110+
protected function setStatus(string $status): self
111+
{
112+
$this->validateTransactionStatus($status);
113+
114+
$this->setIsSuccess(false);
115+
$this->setIsPending(false);
116+
$this->setIsError(false);
117+
118+
switch ($status) {
119+
case (TransactionStatus::STATUS_ERROR):
120+
$this->setIsError(true);
121+
break;
122+
case (TransactionStatus::STATUS_PENDING):
123+
$this->setIsPending(true);
124+
break;
125+
case (TransactionStatus::STATUS_SUCCESS):
126+
$this->setIsSuccess(true);
127+
break;
128+
}
129+
130+
return $this;
131+
}
132+
133+
/**
134+
* Check if transaction status is valid. If status is invalid a RuntimeException is thrown
135+
*
136+
* @param string $status
137+
*
138+
* @throws RuntimeException
139+
*/
140+
public function validateTransactionStatus(string $status): void
141+
{
142+
$validStatusArray = [
143+
TransactionStatus::STATUS_ERROR,
144+
TransactionStatus::STATUS_PENDING,
145+
TransactionStatus::STATUS_SUCCESS,
146+
];
147+
148+
if (!in_array($status, $validStatusArray, true)) {
149+
throw new RuntimeException('Transaction status can not be set. Status is invalid for transaction.');
150+
}
151+
}
98152
}

test/BasePaymentTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ public function createBasket(): Basket
206206
*
207207
* @return Card
208208
*/
209-
protected function createCardObject(string $cardNumber = '5453010000059543'): Card
209+
protected function createCardObject(string $cardNumber = '4711100000000000'): Card
210210
{
211211
$expiryDate = $this->getNextYearsTimestamp()->format('m/Y');
212212
$card = new Card($cardNumber, $expiryDate);

test/integration/ApplepayAdapterTest.php

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,10 @@ protected function setUp(): void
5353
$this->merchantValidationUrl = 'https://apple-pay-gateway-cert.apple.com/paymentservices/startSession';
5454

5555
$appleMerchantIdPath = EnvironmentService::getAppleMerchantIdPath();
56-
$this->applepayCertPath = $appleMerchantIdPath . 'merchant_id.pem';
57-
$this->applepayKeyPath = $appleMerchantIdPath . 'merchant_id.key';
58-
$this->applepayCombinedCertPath = $appleMerchantIdPath . 'apple-pay-cert.pem';
56+
57+
$this->applepayCertPath = $this->createFilePath($appleMerchantIdPath, 'merchant_id.pem');
58+
$this->applepayKeyPath = $this->createFilePath($appleMerchantIdPath, 'merchant_id.key');
59+
$this->applepayCombinedCertPath = $this->createFilePath($appleMerchantIdPath, 'apple-pay-cert.pem');
5960
$this->appleCaCertificatePath = EnvironmentService::getAppleCaCertificatePath();
6061
}
6162

@@ -224,4 +225,16 @@ public function domainShouldBeValidatedCorrectlyDP(): array
224225
'invalid: (empty)' => ['', false],
225226
];
226227
}
228+
229+
/**
230+
* @param string $appleMerchantIdPath
231+
* @param string $merchantIdFile
232+
*
233+
* @return string
234+
*/
235+
protected function createFilePath(string $appleMerchantIdPath, string $merchantIdFile): string
236+
{
237+
$separator = DIRECTORY_SEPARATOR;
238+
return rtrim($appleMerchantIdPath, $separator) . $separator . $merchantIdFile;
239+
}
227240
}

test/integration/PaymentCancelTest.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
use UnzerSDK\Constants\CancelReasonCodes;
3030
use UnzerSDK\Resources\PaymentTypes\Invoice;
31+
use UnzerSDK\Resources\PaymentTypes\InvoiceSecured;
3132
use UnzerSDK\test\BaseIntegrationTest;
3233

3334
class PaymentCancelTest extends BaseIntegrationTest
@@ -377,6 +378,79 @@ public function partCancelOnInitialInvoiceChargeShouldBePossible(): void
377378
$this->assertAmounts($payment, 50.0, 0.0, 50.0, 0.0);
378379
}
379380

381+
/**
382+
* Verify part cancel on initial ivs charge (reversal)
383+
*
384+
* @test
385+
*/
386+
public function partCancelOnInitialInvoiceSecuredChargeShouldCancelMaxUnpaidAmount(): void
387+
{
388+
/** @var InvoiceSecured $invoiceSecured */
389+
$invoiceSecured = $this->unzer->createPaymentType(new InvoiceSecured());
390+
391+
$customer = $this->getMaximumCustomer();
392+
$customer->setShippingAddress($customer->getBillingAddress());
393+
394+
$basket = $this->createBasket();
395+
$invoiceId = 'i' . self::generateRandomId();
396+
$charge = $invoiceSecured->charge(100.0, 'EUR', self::RETURN_URL, $customer, $basket->getOrderId(), null, $basket, null, $invoiceId);
397+
$charge->getPayment()->ship();
398+
$paymentId = $charge->getPaymentId();
399+
400+
$this->assertTrue($charge->isPending()); // Set your break point here.
401+
$payment = $this->unzer->fetchPayment($charge->getPaymentId());
402+
if (count($payment->getCharges()) !== 2) {
403+
$testDescription = 'This test needs assistance:
404+
To perform this test properly, first set a breakpoint after charge, before the payment gets fetched.
405+
Then perform a receipt manually over 60€ on the "reservation".
406+
After that this test can be continued';
407+
$this->markTestSkipped($testDescription);
408+
}
409+
$this->assertTrue($payment->isCompleted());
410+
$this->assertAmounts($payment, 0, 100, 100.0, 0);
411+
412+
$this->assertCount(2, $payment->cancelAmount(50.0));
413+
$this->assertTrue($payment->isCompleted());
414+
$this->assertAmounts($payment, 0, 50.0, 100.0, 50.0);
415+
}
416+
417+
/**
418+
* Verify skip cancel on initial ivs charge
419+
*
420+
* @test
421+
*/
422+
public function fullCancelOnPaidInvoiceSecuredPaymentShouldBePossible(): void
423+
{
424+
/** @var InvoiceSecured $invoiceSecured */
425+
$invoiceSecured = $this->unzer->createPaymentType(new InvoiceSecured());
426+
427+
$customer = $this->getMaximumCustomer();
428+
$customer->setShippingAddress($customer->getBillingAddress());
429+
430+
$basket = $this->createBasket();
431+
$invoiceId = 'i' . self::generateRandomId();
432+
$charge = $invoiceSecured->charge(100.0, 'EUR', self::RETURN_URL, $customer, $basket->getOrderId(), null, $basket, null, $invoiceId);
433+
$charge->getPayment()->ship();
434+
$paymentId = $charge->getPaymentId();
435+
436+
$this->assertTrue($charge->isPending()); // Set your break point here.
437+
$payment = $this->unzer->fetchPayment($charge->getPaymentId());
438+
if (count($payment->getCharges()) !== 2) {
439+
$testDescription = 'This test needs assistance:
440+
To perform this test properly, first set a breakpoint after charge, before the payment gets fetched.
441+
Then perform a receipt manually over 100€ on the "reservation".
442+
After that this test can be continued';
443+
$this->markTestSkipped($testDescription);
444+
}
445+
$this->assertTrue($payment->isCompleted());
446+
$this->assertAmounts($payment, 0, 100.0, 100.0, 0);
447+
448+
$cancellations = $payment->cancelAmount();
449+
$this->assertCount(1, $cancellations);
450+
$this->assertTrue($payment->isCompleted());
451+
$this->assertAmounts($payment, 0, 0, 100.0, 100.0);
452+
}
453+
380454
/**
381455
* Verify cancelling more than was charged.
382456
* PHPLIB-228 - Case 15

0 commit comments

Comments
 (0)