Passed
Push — master ( 08267b...db956f )
by Jan
04:49
created

PricedetailHelper::getMaxDiscountAmount()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 38
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 20
nc 12
nop 1
dl 0
loc 38
rs 8.9777
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4
 *
5
 * Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU Affero General Public License as published
9
 * by the Free Software Foundation, either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU Affero General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Affero General Public License
18
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19
 */
20
21
declare(strict_types=1);
22
23
/**
24
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
25
 *
26
 * Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
27
 *
28
 * This program is free software; you can redistribute it and/or
29
 * modify it under the terms of the GNU General Public License
30
 * as published by the Free Software Foundation; either version 2
31
 * of the License, or (at your option) any later version.
32
 *
33
 * This program is distributed in the hope that it will be useful,
34
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
35
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
36
 * GNU General Public License for more details.
37
 *
38
 * You should have received a copy of the GNU General Public License
39
 * along with this program; if not, write to the Free Software
40
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
41
 */
42
43
namespace App\Services;
44
45
use App\Entity\Parts\Part;
46
use App\Entity\PriceInformations\Currency;
47
use App\Entity\PriceInformations\Pricedetail;
48
use Brick\Math\BigDecimal;
49
use Brick\Math\RoundingMode;
50
use Doctrine\ORM\PersistentCollection;
51
use Locale;
52
53
class PricedetailHelper
54
{
55
    protected $base_currency;
56
    protected $locale;
57
58
    public function __construct(string $base_currency)
59
    {
60
        $this->base_currency = $base_currency;
61
        $this->locale = Locale::getDefault();
62
    }
63
64
    /**
65
     * Determines the highest amount, for which you get additional discount.
66
     * This function determines the highest min_discount_quantity for the given part.
67
     */
68
    public function getMaxDiscountAmount(Part $part): ?float
69
    {
70
        $orderdetails = $part->getOrderdetails(true);
71
72
        $max = 0;
73
74
        foreach ($orderdetails as $orderdetail) {
75
            $pricedetails = $orderdetail->getPricedetails();
76
            //The orderdetail must have pricedetails, otherwise this will not work!
77
            if (0 === \count($pricedetails)) {
78
                continue;
79
            }
80
81
            if ($pricedetails instanceof PersistentCollection) {
82
                /* Pricedetails in orderdetails are ordered by min discount quantity,
83
                    so our first object is our min order amount for the current orderdetail */
84
                $max_amount = $pricedetails->last()->getMinDiscountQuantity();
85
            } else {
86
                // We have to sort the pricedetails manually
87
                $array = $pricedetails->map(
88
                    function (Pricedetail $pricedetail) {
89
                        return $pricedetail->getMinDiscountQuantity();
90
                    }
91
                )->toArray();
92
                sort($array);
93
                $max_amount = end($array);
94
            }
95
96
            if ($max_amount > $max) {
97
                $max = $max_amount;
98
            }
99
        }
100
101
        if ($max > 0.0) {
102
            return $max;
103
        }
104
105
        return null;
106
    }
107
108
    /**
109
     * Determines the minimum amount of the part that can be ordered.
110
     *
111
     * @param Part $part the part for which the minimum order amount should be determined
112
     *
113
     * @return float
114
     */
115
    public function getMinOrderAmount(Part $part): ?float
116
    {
117
        $orderdetails = $part->getOrderdetails(true);
118
119
        $min = INF;
120
121
        foreach ($orderdetails as $orderdetail) {
122
            $pricedetails = $orderdetail->getPricedetails();
123
            //The orderdetail must have pricedetails, otherwise this will not work!
124
            if (0 === \count($pricedetails)) {
125
                continue;
126
            }
127
128
            /* Pricedetails in orderdetails are ordered by min discount quantity,
129
                so our first object is our min order amount for the current orderdetail */
130
            $min_amount = $pricedetails[0]->getMinDiscountQuantity();
131
132
            if ($min_amount < $min) {
133
                $min = $min_amount;
134
            }
135
        }
136
137
        if ($min < INF) {
138
            return $min;
139
        }
140
141
        return null;
142
    }
143
144
    /**
145
     * Calculates the average price of a part, when ordering the amount $amount.
146
     *
147
     * @param Part          $part     the part for which the average price should be calculated
148
     * @param float         $amount   The order amount for which the average price should be calculated.
149
     *                                If set to null, the mininmum order amount for the part is used.
150
     * @param Currency|null $currency The currency in which the average price should be calculated
151
     *
152
     * @return BigDecimal|null The Average price as bcmath string. Returns null, if it was not possible to calculate the
153
     *                     price for the given
154
     */
155
    public function calculateAvgPrice(Part $part, ?float $amount = null, ?Currency $currency = null): ?BigDecimal
156
    {
157
        if (null === $amount) {
158
            $amount = $this->getMinOrderAmount($part);
159
        }
160
161
        if (null === $amount) {
162
            return null;
163
        }
164
165
        $orderdetails = $part->getOrderdetails(true);
166
167
        $avg = BigDecimal::zero();
168
        $count = 0;
169
170
        //Find the price for the amount, for the given
171
        foreach ($orderdetails as $orderdetail) {
172
            $pricedetail = $orderdetail->findPriceForQty($amount);
173
174
            //When we dont have informations about this amount, ignore it
175
            if (null === $pricedetail) {
176
                continue;
177
            }
178
179
            $converted = $this->convertMoneyToCurrency($pricedetail->getPricePerUnit(), $pricedetail->getCurrency(), $currency);
180
            //Ignore price informations that can not be converted to base currency.
181
            if (null !== $converted) {
182
                $avg = $avg->plus($converted);
183
                ++$count;
184
            }
185
        }
186
187
        if (0 === $count) {
188
            return null;
189
        }
190
191
        return $avg->dividedBy($count)->toScale(Pricedetail::PRICE_PRECISION);
192
    }
193
194
    /**
195
     * Converts the given value in origin currency to the choosen target currency.
196
     *
197
     * @param Currency|null $originCurrency The currency the $value is given in.
198
     *                                      Set to null, to use global base currency.
199
     * @param Currency|null $targetCurrency The target currency, to which $value should be converted.
200
     *                                      Set to null, to use global base currency.
201
     *
202
     * @return BigDecimal|null The value in $targetCurrency given as bcmath string.
203
     *                     Returns null, if it was not possible to convert between both values (e.g. when the exchange rates are missing)
204
     */
205
    public function convertMoneyToCurrency(BigDecimal $value, ?Currency $originCurrency = null, ?Currency $targetCurrency = null): ?BigDecimal
206
    {
207
        //Skip conversion, if both currencies are same
208
        if ($originCurrency === $targetCurrency) {
209
            return $value;
210
        }
211
212
        $val_base = $value;
213
        //Convert value to base currency
214
        if (null !== $originCurrency) {
215
            //Without an exchange rate we can not calculate the exchange rate
216
            if ($originCurrency->getExchangeRate() === null || $originCurrency->getExchangeRate()->isZero()) {
217
                return null;
218
            }
219
220
            $val_base = $value->multipliedBy($originCurrency->getExchangeRate());
221
        }
222
223
        $val_target = $val_base;
224
        //Convert value in base currency to target currency
225
        if (null !== $targetCurrency) {
226
            //Without an exchange rate we can not calculate the exchange rate
227
            if (null === $targetCurrency->getExchangeRate()) {
228
                return null;
229
            }
230
231
            $val_target = $val_base->multipliedBy($targetCurrency->getInverseExchangeRate());
232
        }
233
234
        return $val_target->toScale(Pricedetail::PRICE_PRECISION, RoundingMode::HALF_UP);
235
    }
236
}
237