Passed
Push — master ( 6556ee...887ac8 )
by Jan
12:04
created

PricedetailHelper::calculateAvgPrice()   B

Complexity

Conditions 7
Paths 18

Size

Total Lines 37
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 7
eloc 18
c 2
b 0
f 0
nc 18
nop 3
dl 0
loc 37
rs 8.8333
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 Doctrine\ORM\PersistentCollection;
49
use Locale;
50
51
class PricedetailHelper
52
{
53
    protected $base_currency;
54
    protected $locale;
55
56
    public function __construct(string $base_currency)
57
    {
58
        $this->base_currency = $base_currency;
59
        $this->locale = Locale::getDefault();
60
    }
61
62
    /**
63
     * Determines the highest amount, for which you get additional discount.
64
     * This function determines the highest min_discount_quantity for the given part.
65
     */
66
    public function getMaxDiscountAmount(Part $part): ?float
67
    {
68
        $orderdetails = $part->getOrderdetails(true);
69
70
        $max = 0;
71
72
        foreach ($orderdetails as $orderdetail) {
73
            $pricedetails = $orderdetail->getPricedetails();
74
            //The orderdetail must have pricedetails, otherwise this will not work!
75
            if (0 === \count($pricedetails)) {
76
                continue;
77
            }
78
79
            if ($pricedetails instanceof PersistentCollection) {
80
                /* Pricedetails in orderdetails are ordered by min discount quantity,
81
                    so our first object is our min order amount for the current orderdetail */
82
                $max_amount = $pricedetails->last()->getMinDiscountQuantity();
83
            } else {
84
                // We have to sort the pricedetails manually
85
                $array = $pricedetails->map(
86
                    function (Pricedetail $pricedetail) {
87
                        return $pricedetail->getMinDiscountQuantity();
88
                    }
89
                )->toArray();
90
                sort($array);
91
                $max_amount = end($array);
92
            }
93
94
            if ($max_amount > $max) {
95
                $max = $max_amount;
96
            }
97
        }
98
99
        if ($max > 0.0) {
100
            return $max;
101
        }
102
103
        return null;
104
    }
105
106
    /**
107
     * Determines the minimum amount of the part that can be ordered.
108
     *
109
     * @param Part $part the part for which the minimum order amount should be determined
110
     *
111
     * @return float
112
     */
113
    public function getMinOrderAmount(Part $part): ?float
114
    {
115
        $orderdetails = $part->getOrderdetails(true);
116
117
        $min = INF;
118
119
        foreach ($orderdetails as $orderdetail) {
120
            $pricedetails = $orderdetail->getPricedetails();
121
            //The orderdetail must have pricedetails, otherwise this will not work!
122
            if (0 === \count($pricedetails)) {
123
                continue;
124
            }
125
126
            /* Pricedetails in orderdetails are ordered by min discount quantity,
127
                so our first object is our min order amount for the current orderdetail */
128
            $min_amount = $pricedetails[0]->getMinDiscountQuantity();
129
130
            if ($min_amount < $min) {
131
                $min = $min_amount;
132
            }
133
        }
134
135
        if ($min < INF) {
136
            return $min;
137
        }
138
139
        return null;
140
    }
141
142
    /**
143
     * Calculates the average price of a part, when ordering the amount $amount.
144
     *
145
     * @param Part          $part     the part for which the average price should be calculated
146
     * @param float         $amount   The order amount for which the average price should be calculated.
147
     *                                If set to null, the mininmum order amount for the part is used.
148
     * @param Currency|null $currency The currency in which the average price should be calculated
149
     *
150
     * @return string|null The Average price as bcmath string. Returns null, if it was not possible to calculate the
151
     *                     price for the given
152
     */
153
    public function calculateAvgPrice(Part $part, ?float $amount = null, ?Currency $currency = null): ?string
154
    {
155
        if (null === $amount) {
156
            $amount = $this->getMinOrderAmount($part);
157
        }
158
159
        if (null === $amount) {
160
            return null;
161
        }
162
163
        $orderdetails = $part->getOrderdetails(true);
164
165
        $avg = '0';
166
        $count = 0;
167
168
        //Find the price for the amount, for the given
169
        foreach ($orderdetails as $orderdetail) {
170
            $pricedetail = $orderdetail->findPriceForQty($amount);
171
172
            //When we dont have informations about this amount, ignore it
173
            if (null === $pricedetail) {
174
                continue;
175
            }
176
177
            $converted = $this->convertMoneyToCurrency($pricedetail->getPricePerUnit(), $pricedetail->getCurrency(), $currency);
178
            //Ignore price informations that can not be converted to base currency.
179
            if ($converted !== null) {
180
                $avg = bcadd($avg, $converted, Pricedetail::PRICE_PRECISION);
181
                ++$count;
182
            }
183
        }
184
185
        if (0 === $count) {
186
            return null;
187
        }
188
189
        return bcdiv($avg, (string) $count, Pricedetail::PRICE_PRECISION);
190
    }
191
192
    /**
193
     * Converts the given value in origin currency to the choosen target currency.
194
     *
195
     * @param Currency|null $originCurrency The currency the $value is given in.
196
     *                                      Set to null, to use global base currency.
197
     * @param Currency|null $targetCurrency The target currency, to which $value should be converted.
198
     *                                      Set to null, to use global base currency.
199
     *
200
     * @return string|null The value in $targetCurrency given as bcmath string.
201
     *                     Returns null, if it was not possible to convert between both values (e.g. when the exchange rates are missing)
202
     */
203
    public function convertMoneyToCurrency($value, ?Currency $originCurrency = null, ?Currency $targetCurrency = null): ?string
204
    {
205
        //Skip conversion, if both currencies are same
206
        if ($originCurrency === $targetCurrency) {
207
            return $value;
208
        }
209
210
        $value = (string) $value;
211
212
        //Convert value to base currency
213
        $val_base = $value;
214
        if (null !== $originCurrency) {
215
            //Without an exchange rate we can not calculate the exchange rate
216
            if (0.0 === (float) $originCurrency->getExchangeRate()) {
0 ignored issues
show
introduced by
The condition 0.0 === (double)$originC...ency->getExchangeRate() is always false.
Loading history...
217
                return null;
218
            }
219
220
            $val_base = bcmul($value, $originCurrency->getExchangeRate(), Pricedetail::PRICE_PRECISION);
221
        }
222
223
        //Convert value in base currency to target currency
224
        $val_target = $val_base;
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 = bcmul($val_base, $targetCurrency->getInverseExchangeRate(), Pricedetail::PRICE_PRECISION);
232
        }
233
234
        return $val_target;
235
    }
236
}
237