Passed
Push — master ( a707ad...480443 )
by Jan
05:15
created

getFSRKomBankAccount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
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 $limit_max_number_of_transactions, int $limit_max_amount, EntityManagerInterface $em = null, ?int $fsr_kom_bank_account_id = null, ?BankAccount $fsr_kom_bank_account = null)
39
    {
40
        //We can either pass an Bank Account directly or get one from the database.
41
        if ($fsr_kom_bank_account !== null) {
42
            $this->fsr_kom_bank_account = $fsr_kom_bank_account;
43
        } elseif ($em !== null && $fsr_kom_bank_account_id !== null) {
44
            $this->fsr_kom_bank_account = $em->find(BankAccount::class, $fsr_kom_bank_account_id);
45
        } else { //If we dont have any possibility to retrieve the FSRKom bank account, throw an exception
46
            throw new \RuntimeException('You either has to pass an bank account entity directly via $fsr_kom_bank_account or pass an entity manager and id!');
47
        }
48
49
        $this->limit_max_number_of_transactions = $limit_max_number_of_transactions;
50
        $this->limit_max_amount = $limit_max_amount;
51
    }
52
53
    /**
54
     * Groups the given payment orders by their bank accounts and split the groups according to the set Limits (see limit_ service params).
55
     * An SplObjectStorage is returned, where the bank accounts are the keys and an array containing arrays for the multiple files are contained.
56
     * [BankAccount1 =>
57
     *      [
58
     *          [PaymentOrder1, PaymentOrder2],
59
     *          [PaymentOrder3],
60
     *      ]
61
     * ]
62
     * @param  PaymentOrder[]  $payment_orders
63
     * @throws SEPAExportAutoModeNotPossible If an element has no assigned default bank account, then automatic mode is not possible
64
     * @return \SplObjectStorage
65
     */
66
    public function groupAndSplitPaymentOrdersForAutoExport(array $payment_orders): \SplObjectStorage
67
    {
68
        $grouped = new \SplObjectStorage();
69
70
        //Iterate over every payment order and sort them according to their bank accounts
71
        foreach ($payment_orders as $payment_order) {
72
            //Throw exception if the associated department has no default bank account and grouping is not possible
73
            if ($payment_order->getDepartment()->getBankAccount() === null) {
74
                throw new SEPAExportAutoModeNotPossible($payment_order->getDepartment());
75
            }
76
77
            //Normally we just use the default bank account of the department
78
            $bank_account = $payment_order->getDepartment()->getBankAccount();
79
80
            //Except if we have an fsr kom transaction, then we have to use the fsr-kom bank account
81
            if ($payment_order->isFsrKomResolution()) {
82
                $bank_account = $this->fsr_kom_bank_account;
83
            }
84
85
            //If no array object is existing yet, create the array
86
            if(!isset($grouped[$bank_account])) {
87
                $grouped[$bank_account] = [];
88
            }
89
90
            //Assign it to the grouped object (we have to do it this way otherwise, SplObjectStorage complains about indirect access)
91
            $tmp = $grouped[$bank_account];
92
            $tmp[] = $payment_order;
93
            $grouped[$bank_account] = $tmp;
94
        }
95
96
        //Split the elements for each element
97
        $split = new \SplObjectStorage();
98
        foreach ($grouped as $bank_account) {
99
            $group = $grouped[$bank_account];
100
            $split[$bank_account] = $this->splitPaymentOrders($group);
101
        }
102
103
        return $split;
104
    }
105
106
    /**
107
     * Split the given payment orders into arrays that fit the configured limits.
108
     * An array of the form [ [PaymentOrder1, PaymentOrder2], [PaymentOrder3]] is returned.
109
     * @param  PaymentOrder[]  $input The array of payment orders that should be split
110
     * @param  int|null $limit_max_transactions The maximum number of transactions per split group. Set to null to use global defaults
111
     * @param  int|null $limit_max_amount The maximimum amount in a single split group (in cents). Set to null to use global defaults.
112
     * @throws SinglePaymentOrderExceedsLimit
113
     * @return PaymentOrder[][]
114
     */
115
    public function splitPaymentOrders(array $input, ?int $limit_max_transactions = null, ?int $limit_max_amount = null): array
116
    {
117
        if ($limit_max_amount === null) {
118
            $limit_max_amount = $this->limit_max_amount;
119
        }
120
        if ($limit_max_transactions === null) {
121
            $limit_max_transactions = $this->limit_max_number_of_transactions;
122
        }
123
124
        //First we split according to number of transactions
125
        /** @var PaymentOrder[][] $output */
126
        $tmp = array_chunk($input, $limit_max_transactions);
127
128
        //Limit the sum amount of each group
129
        $groups_exceeding_limit = $tmp;
130
        $output = [];
131
        while(!empty($groups_exceeding_limit)) {
132
            foreach ($groups_exceeding_limit as $key => $group) {
133
                /** @var PaymentOrder[] $group */
134
                //If the group does not exceed the limit, then remove it from the bad list and put it to output array
135
                if ($this->calculateSumAmountOfPaymentOrders($group) <= $limit_max_amount) {
136
                    unset($groups_exceeding_limit[$key]);
137
                    $output[] = $group;
138
                    continue;
139
                }
140
141
                //Sort it and try to split the maximum amount of elements from it:
142
                $group = $this->sortPaymentOrderArrayByAmount($group, false);
143
                //We try to extract the maximum amount of elements from the list.
144
                $split_index = 1;
145
                for ($n = 0, $nMax = count($group); $n < $nMax; $n++) {
146
                    $part = array_slice($group, 0, $n + 1);
147
                    if ($this->calculateSumAmountOfPaymentOrders($part) > $limit_max_amount) {
148
                        //If our group contains just a single element which exceed the limit, then throw an exception, as we can not split it further.
149
                        if(count($part) === 1) {
150
                            throw new SinglePaymentOrderExceedsLimit($part[0], $limit_max_amount);
151
                        }
152
153
                        $split_index = $n;
154
                        break;
155
                    }
156
                }
157
158
                //Split group into our two subgroups of which at least one is below the limit
159
                $a = array_slice($group, 0 , $split_index);
160
                $b = array_slice($group, $split_index);
161
162
                //Remove the old group from list and add the new split groups
163
                unset($groups_exceeding_limit[$key]);
164
                $groups_exceeding_limit[] = $a;
165
                $groups_exceeding_limit[] = $b;
166
            }
167
        }
168
169
170
        return $output;
171
    }
172
173
    /**
174
     * Calculate the sum amount of all given payment orders. Returns value in cents.
175
     * @param  PaymentOrder[]  $payment_orders
176
     * @return int
177
     */
178
    public function calculateSumAmountOfPaymentOrders(array $payment_orders): int
179
    {
180
        $sum = 0;
181
        foreach ($payment_orders as $payment_order) {
182
            $sum += $payment_order->getAmount();
183
        }
184
185
        return $sum;
186
    }
187
188
    /**
189
     * Sorts the given array according to the amount of the payments.
190
     * @param  PaymentOrder[]  $payment_orders
191
     * @param  bool $ascending If true the payments are sorted ascending, otherwise descending.
192
     * @return PaymentOrder[]
193
     */
194
    public function sortPaymentOrderArrayByAmount(array $payment_orders, bool $ascending = true): array
195
    {
196
        usort($payment_orders, function (PaymentOrder $a, PaymentOrder $b) {
197
           return $a->getAmount() <=> $b->getAmount();
198
        });
199
200
        if (!$ascending) {
201
            return array_reverse($payment_orders);
202
        }
203
204
        return $payment_orders;
205
    }
206
207
    /**
208
     * Returns the bank account configured for the FSR Kom
209
     * @return BankAccount
210
     */
211
    public function getFSRKomBankAccount(): BankAccount
212
    {
213
        return $this->fsr_kom_bank_account;
214
    }
215
}