Passed
Push — master ( 0dc3be...a707ad )
by Jan
05:21
created

splitPaymentOrders()   B

Complexity

Conditions 9
Paths 28

Size

Total Lines 56
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 28
c 1
b 0
f 0
nc 28
nop 3
dl 0
loc 56
rs 8.0555

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/*
3
 * Copyright (C)  2020-2021  Jan Böhmer
4
 *
5
 * This program is free software: you can redistribute it and/or modify
6
 * it under the terms of the GNU Affero General Public License as published
7
 * by the Free Software Foundation, either version 3 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 * GNU Affero General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU Affero General Public License
16
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17
 */
18
19
namespace App\Services\SEPAExport;
20
21
use App\Entity\BankAccount;
22
use App\Entity\Embeddable\PayeeInfo;
23
use App\Entity\PaymentOrder;
24
use App\Exception\SEPAExportAutoModeNotPossible;
25
use App\Exception\SinglePaymentOrderExceedsLimit;
26
use Doctrine\ORM\EntityManagerInterface;
27
28
class SEPAExportGroupAndSplitHelper
29
{
30
    /** @var BankAccount */
31
    private $fsr_kom_bank_account;
32
33
    /** @var int The maximum number of transactions which should be put in a single SEPA XML file. */
34
    private $limit_max_number_of_transactions;
35
    /** @var int The maximum sum of transactions which should be put in a single SEPA XML file. This value is in cents  */
36
    private $limit_max_amount;
37
38
    public function __construct(int $fsr_kom_bank_account_id, EntityManagerInterface $em, int $limit_max_number_of_transactions, int $limit_max_amount)
39
    {
40
        $this->fsr_kom_bank_account = $em->find(BankAccount::class, $fsr_kom_bank_account_id);
41
42
        $this->limit_max_number_of_transactions = $limit_max_number_of_transactions;
43
        $this->limit_max_amount = $limit_max_amount;
44
    }
45
46
    /**
47
     * Groups the given payment orders by their bank accounts and split the groups according to the set Limits (see limit_ service params).
48
     * An SplObjectStorage is returned, where the bank accounts are the keys and an array containing arrays for the multiple files are contained.
49
     * [BankAccount1 =>
50
     *      [
51
     *          [PaymentOrder1, PaymentOrder2],
52
     *          [PaymentOrder3],
53
     *      ]
54
     * ]
55
     * @param  PaymentOrder[]  $payment_orders
56
     * @throws SEPAExportAutoModeNotPossible If an element has no assigned default bank account, then automatic mode is not possible
57
     * @return \SplObjectStorage
58
     */
59
    public function groupAndSplitPaymentOrdersForAutoExport(array $payment_orders): \SplObjectStorage
60
    {
61
        $grouped = new \SplObjectStorage();
62
63
        //Iterate over every payment order and sort them according to their bank accounts
64
        foreach ($payment_orders as $payment_order) {
65
            //Throw exception if the associated department has no default bank account and grouping is not possible
66
            if ($payment_order->getDepartment()->getBankAccount() === null) {
67
                throw new SEPAExportAutoModeNotPossible($payment_order->getDepartment());
68
            }
69
70
            //Normally we just use the default bank account of the department
71
            $bank_account = $payment_order->getDepartment()->getBankAccount();
72
73
            //Except if we have an fsr kom transaction, then we have to use the fsr-kom bank account
74
            if ($payment_order->isFsrKomResolution()) {
75
                $bank_account = $this->fsr_kom_bank_account;
76
            }
77
78
            //If no array object is existing yet, create the array
79
            if(!is_array($grouped[$bank_account])) {
80
                $grouped[$bank_account] = [];
81
            }
82
83
            //Assign it to the grouped object
84
            $grouped[$bank_account][] = $payment_order;
85
        }
86
87
        //Split the elements for each element
88
        $split = new \SplObjectStorage();
89
        foreach ($grouped as $bank_account => $group) {
90
            $split[$bank_account] = $this->splitPaymentOrders($group);
0 ignored issues
show
Bug introduced by
$group of type object is incompatible with the type array expected by parameter $input of App\Services\SEPAExport\...r::splitPaymentOrders(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

90
            $split[$bank_account] = $this->splitPaymentOrders(/** @scrutinizer ignore-type */ $group);
Loading history...
91
        }
92
93
        return $split;
94
    }
95
96
    /**
97
     * Split the given payment orders into arrays that fit the configured limits.
98
     * An array of the form [ [PaymentOrder1, PaymentOrder2], [PaymentOrder3]] is returned.
99
     * @param  PaymentOrder[]  $input The array of payment orders that should be split
100
     * @param  int|null $limit_max_transactions The maximum number of transactions per split group. Set to null to use global defaults
101
     * @param  int|null $limit_max_amount The maximimum amount in a single split group (in cents). Set to null to use global defaults.
102
     * @throws SinglePaymentOrderExceedsLimit
103
     * @return PaymentOrder[][]
104
     */
105
    public function splitPaymentOrders(array $input, ?int $limit_max_transactions = null, ?int $limit_max_amount = null): array
106
    {
107
        if ($limit_max_amount === null) {
108
            $limit_max_amount = $this->limit_max_amount;
109
        }
110
        if ($limit_max_transactions === null) {
111
            $limit_max_transactions = $this->limit_max_number_of_transactions;
112
        }
113
114
        //First we split according to number of transactions
115
        /** @var PaymentOrder[][] $output */
116
        $tmp = array_chunk($input, $limit_max_transactions);
117
118
        //Limit the sum amount of each group
119
        $groups_exceeding_limit = $tmp;
120
        $output = [];
121
        while(!empty($groups_exceeding_limit)) {
122
            foreach ($groups_exceeding_limit as $key => $group) {
123
                /** @var PaymentOrder[] $group */
124
                //If the group does not exceed the limit, then remove it from the bad list and put it to output array
125
                if ($this->calculateSumAmountOfPaymentOrders($group) <= $limit_max_amount) {
126
                    unset($groups_exceeding_limit[$key]);
127
                    $output[] = $group;
128
                    continue;
129
                }
130
131
                //Sort it and try to split the maximum amount of elements from it:
132
                $group = $this->sortPaymentOrderArrayByAmount($group, false);
133
                //We try to extract the maximum amount of elements from the list.
134
                $split_index = 1;
135
                for ($n = 0, $nMax = count($group); $n < $nMax; $n++) {
136
                    $part = array_slice($group, 0, $n + 1);
137
                    if ($this->calculateSumAmountOfPaymentOrders($part) > $limit_max_amount) {
138
                        //If our group contains just a single element which exceed the limit, then throw an exception, as we can not split it further.
139
                        if(count($part) === 1) {
140
                            throw new SinglePaymentOrderExceedsLimit($part[0], $limit_max_amount);
141
                        }
142
143
                        $split_index = $n;
144
                        break;
145
                    }
146
                }
147
148
                //Split group into our two subgroups of which at least one is below the limit
149
                $a = array_slice($group, 0 , $split_index);
150
                $b = array_slice($group, $split_index);
151
152
                //Remove the old group from list and add the new split groups
153
                unset($groups_exceeding_limit[$key]);
154
                $groups_exceeding_limit[] = $a;
155
                $groups_exceeding_limit[] = $b;
156
            }
157
        }
158
159
160
        return $output;
161
    }
162
163
    /**
164
     * Calculate the sum amount of all given payment orders. Returns value in cents.
165
     * @param  PaymentOrder[]  $payment_orders
166
     * @return int
167
     */
168
    public function calculateSumAmountOfPaymentOrders(array $payment_orders): int
169
    {
170
        $sum = 0;
171
        foreach ($payment_orders as $payment_order) {
172
            $sum += $payment_order->getAmount();
173
        }
174
175
        return $sum;
176
    }
177
178
    /**
179
     * Sorts the given array according to the amount of the payments.
180
     * @param  PaymentOrder[]  $payment_orders
181
     * @param  bool $ascending If true the payments are sorted ascending, otherwise descending.
182
     * @return PaymentOrder[]
183
     */
184
    public function sortPaymentOrderArrayByAmount(array $payment_orders, bool $ascending = true): array
185
    {
186
        usort($payment_orders, function (PaymentOrder $a, PaymentOrder $b) {
187
           return $a->getAmount() <=> $b->getAmount();
188
        });
189
190
        if (!$ascending) {
191
            return array_reverse($payment_orders);
192
        }
193
194
        return $payment_orders;
195
    }
196
}