001/* Copyright (C) 2014 konik.io
002 *
003 * This file is part of the Konik library.
004 *
005 * The Konik library is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * The Konik library is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with the Konik library. If not, see <http://www.gnu.org/licenses/>.
017 */
018package io.konik.validation;
019
020import com.google.common.base.Function;
021import com.neovisionaries.i18n.CurrencyCode;
022import io.konik.util.Amounts;
023import io.konik.util.Items;
024import io.konik.util.MonetarySummations;
025import io.konik.zugferd.Invoice;
026import io.konik.zugferd.entity.*;
027import io.konik.zugferd.entity.trade.MonetarySummation;
028import io.konik.zugferd.entity.trade.Settlement;
029import io.konik.zugferd.entity.trade.TradeTax;
030import io.konik.zugferd.entity.trade.item.*;
031import io.konik.zugferd.unece.codes.TaxCategory;
032import io.konik.zugferd.unece.codes.TaxCode;
033import io.konik.zugferd.unqualified.Amount;
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036
037import javax.annotation.Nullable;
038import java.math.BigDecimal;
039import java.math.RoundingMode;
040import java.util.Arrays;
041import java.util.LinkedList;
042import java.util.List;
043import java.util.Map;
044import java.util.concurrent.ConcurrentHashMap;
045import java.util.concurrent.ConcurrentMap;
046
047/**
048 * Calculate the missing amounts of the invoice.
049 */
050public final class AmountCalculator {
051
052        protected static Logger log = LoggerFactory.getLogger(AmountCalculator.class);
053
054        /**
055         * Calculates {@link MonetarySummation} for given {@link Invoice} basing on line {@link Item}s
056         * and global {@link io.konik.zugferd.entity.AllowanceCharge} and {@link LogisticsServiceCharge}
057         *
058         * @param invoice
059         * @return
060         */
061        public static RecalculationResult recalculate(final Invoice invoice) {
062                assertNotNull(invoice);
063
064                CurrencyCode currency = getCurrency(invoice);
065                List<Item> items = Items.purchasableItemsOnly(invoice.getTrade().getItems());
066                Settlement settlement = invoice.getTrade().getSettlement();
067
068                TaxAggregator taxAggregator = new TaxAggregator();
069
070                // If there are no items that can be used to recalculate monetary summation, return the current one
071                if (items.isEmpty()) {
072                        return new RecalculationResult(MonetarySummations.newMonetarySummation(settlement.getMonetarySummation()), taxAggregator);
073                }
074
075                MonetarySummation monetarySummation = MonetarySummations.newMonetarySummation(currency);
076                monetarySummation.setAllowanceTotal(new InvoiceAllowanceTotalCalculator().apply(settlement));
077                monetarySummation.setChargeTotal(new InvoiceChargeTotalCalculator().apply(settlement));
078
079                log.debug("Starting recalculating line total from {} items...", items.size());
080                int itemsCounter = 0;
081
082                for (Item item : items) {
083                        log.debug("==> {}:", ++itemsCounter);
084                        log.debug("Recalculating item: [{}]", item.getProduct().getName());
085
086                        Amount lineTotal = new ItemLineTotalCalculator().apply(item);
087                        ItemTax itemTax = new ItemTaxExtractor().apply(item);
088
089                        log.debug("Recalculated item line total = {}", lineTotal);
090                        log.debug("Recalculated item tax = {}%", itemTax.getPercentage());
091
092                        taxAggregator.add(itemTax, lineTotal != null ? lineTotal.getValue() : BigDecimal.ZERO);
093
094                        monetarySummation.setLineTotal(Amounts.add(
095                                        monetarySummation.getLineTotal(),
096                                        lineTotal
097                        ));
098
099                        log.debug("Current monetarySummation.lineTotal = {} (the sum of all line totals)", monetarySummation.getLineTotal());
100                }
101
102                log.debug("==> DONE!");
103                log.debug("Finished recalculating monetarySummation.lineTotal...");
104
105                appendTaxFromInvoiceAllowanceCharge(settlement, taxAggregator);
106
107                appendTaxFromInvoiceServiceCharge(settlement, taxAggregator);
108
109                monetarySummation.setTaxBasisTotal(new Amount(taxAggregator.calculateTaxBasis(), currency));
110                monetarySummation.setTaxTotal(new Amount(taxAggregator.calculateTaxTotal(), currency));
111
112                monetarySummation.setGrandTotal(Amounts.add(
113                                monetarySummation.getTaxBasisTotal(),
114                                monetarySummation.getTaxTotal()
115                ));
116
117                log.debug("Recalculated grand total = {} (tax basis total + tax total)", monetarySummation.getGrandTotal());
118
119                if (settlement.getMonetarySummation() != null && settlement.getMonetarySummation().getTotalPrepaid() != null) {
120                        monetarySummation.setTotalPrepaid(
121                                        settlement.getMonetarySummation().getTotalPrepaid()
122                        );
123                }
124
125                monetarySummation.setDuePayable(
126                                Amounts.add(monetarySummation.getGrandTotal(), Amounts.negate(monetarySummation.getTotalPrepaid()))
127                );
128
129                MonetarySummation result = MonetarySummations.precise(monetarySummation, 2, RoundingMode.HALF_UP);
130
131                log.debug("Recalculating invoice monetary summation DONE!");
132                log.debug(" ==> result: {}", result);
133                log.debug("");
134
135                return new RecalculationResult(result, taxAggregator);
136        }
137
138        /**
139         * Calculates {@link SpecifiedMonetarySummation} for given {@link Item}
140         *
141         * @param item
142         * @return
143         */
144        public static SpecifiedMonetarySummation calculateSpecifiedMonetarySummation(final Item item) {
145                log.debug("Recalculating specified monetary summation for [{}]", item.getProduct().getName());
146
147                CurrencyCode currencyCode = getCurrency(item);
148
149                SpecifiedMonetarySummation monetarySummation = MonetarySummations.newSpecifiedMonetarySummation(currencyCode);
150                monetarySummation.setLineTotal(Amounts.setPrecision(new ItemLineTotalCalculator().apply(item), 2, RoundingMode.HALF_UP));
151                monetarySummation.setTotalAllowanceCharge(Amounts.setPrecision(new ItemTotalAllowanceChargeCalculator(currencyCode).apply(item), 2, RoundingMode.HALF_UP));
152
153                log.debug("==> lineTotal = {}", monetarySummation.getLineTotal());
154                log.debug("==> totalAllowanceCharge = {}", monetarySummation.getTotalAllowanceCharge());
155
156                return monetarySummation;
157        }
158
159        private static void appendTaxFromInvoiceServiceCharge(Settlement settlement, TaxAggregator taxAggregator) {
160                log.debug("Adding tax amounts from invoice service charge...");
161                if (settlement.getServiceCharge() != null) {
162                        for (LogisticsServiceCharge charge : settlement.getServiceCharge()) {
163                                if (charge.getTradeTax() != null && charge.getAmount() != null) {
164                                        for (AppliedTax tax : charge.getTradeTax()) {
165                                                log.debug("==> added {} to {}%", charge.getAmount(), tax.getPercentage());
166
167                                                taxAggregator.add(tax, charge.getAmount().getValue());
168                                        }
169                                }
170                        }
171                }
172        }
173
174        private static void appendTaxFromInvoiceAllowanceCharge(Settlement settlement, TaxAggregator taxAggregator) {
175                log.debug("Adding tax amounts from invoice allowance charge...");
176                if (settlement.getAllowanceCharge() != null) {
177                        for (SpecifiedAllowanceCharge charge : settlement.getAllowanceCharge()) {
178                                if (charge.getCategory() != null && charge.getActual() != null) {
179                                        BigDecimal amount = charge.getActual().getValue();
180                                        if (charge.isDiscount()) {
181                                                amount = amount.negate();
182                                        }
183
184                                        log.debug("==> added {} to {}%", amount, charge.getCategory().getPercentage());
185
186                                        taxAggregator.add(charge.getCategory(), amount);
187                                }
188                        }
189                }
190        }
191
192        public static CurrencyCode getCurrency(final Invoice invoice) {
193                assertNotNull(invoice);
194                return invoice.getTrade().getSettlement().getCurrency();
195        }
196
197        /**
198         * Extracts {@link CurrencyCode} from {@link Item} object.
199         * @param item
200         * @return
201         */
202        public static CurrencyCode getCurrency(final Item item) {
203                assertNotNull(item);
204
205                SpecifiedAgreement agreement = item.getAgreement();
206                if (agreement != null && agreement.getGrossPrice() != null && agreement.getGrossPrice().getChargeAmount() != null) {
207                        return agreement.getGrossPrice().getChargeAmount().getCurrency();
208                }
209
210                if (agreement != null && agreement.getNetPrice() != null && agreement.getNetPrice().getChargeAmount() != null) {
211                        return agreement.getNetPrice().getChargeAmount().getCurrency();
212                }
213
214                SpecifiedSettlement settlement = item.getSettlement();
215                if (settlement != null && settlement.getMonetarySummation() != null && settlement.getMonetarySummation().getLineTotal() != null) {
216                        return settlement.getMonetarySummation().getLineTotal().getCurrency();
217                }
218
219                return null;
220        }
221
222
223        private static void assertNotNull(final Invoice invoice) {
224                if (invoice == null || invoice.getTrade() == null) {
225                        throw new IllegalArgumentException("Invoice and Trade objects cannot be null");
226                }
227        }
228
229        private static void assertNotNull(final Item item) {
230                if (item == null) {
231                        throw new IllegalArgumentException("Item cannot be null");
232                }
233        }
234
235
236        /**
237         * Helper class for calculating {@link Item}'s line total.
238         */
239        static final class ItemLineTotalCalculator implements Function<Item, Amount> {
240
241                @Nullable
242                @Override
243                public Amount apply(@Nullable Item item) {
244                        Amount originLineTotal = null;
245
246                        if (item != null && item.getSettlement() != null && item.getSettlement().getMonetarySummation() != null) {
247                                originLineTotal = Amounts.copy(item.getSettlement().getMonetarySummation().getLineTotal());
248                        }
249
250                        if (item == null || item.getDelivery() == null || item.getAgreement() == null) {
251                                return originLineTotal;
252                        }
253
254                        if (item.getAgreement().getNetPrice() == null) {
255                                return originLineTotal;
256                        }
257
258                        BigDecimal quantity = item.getDelivery().getBilled() != null ? item.getDelivery().getBilled().getValue() : BigDecimal.ZERO;
259                        Amount amount = item.getAgreement().getNetPrice().getChargeAmount();
260
261                        log.debug("Line total formula: {} (net price) x {} (quantity)", amount, quantity);
262
263                        return Amounts.multiply(amount, quantity);
264                }
265        }
266
267        /**
268         * Helper class for calculating {@link Item}'s tax total.
269         */
270        static final class ItemTaxTotalCalculator implements Function<Item, Amount> {
271
272                private final Amount lineTotal;
273
274                public ItemTaxTotalCalculator(final Amount lineTotal) {
275                        this.lineTotal = lineTotal;
276                }
277
278                @Override
279                public Amount apply(@Nullable Item item) {
280                        CurrencyCode currency = lineTotal.getCurrency();
281                        Amount taxTotal = Amounts.zero(currency);
282
283                        if (item != null && item.getSettlement() != null && item.getSettlement().getTradeTax() != null) {
284                                for (ItemTax tax : item.getSettlement().getTradeTax()) {
285                                        BigDecimal taxRate = tax.getPercentage().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_HALF_UP).setScale(2, RoundingMode.HALF_UP);
286                                        BigDecimal taxValue = lineTotal.getValue().multiply(taxRate).setScale(2, RoundingMode.HALF_UP);
287
288                                        taxTotal = Amounts.add(taxTotal, new Amount(taxValue, currency));
289                                }
290                        }
291                        return taxTotal;
292                }
293        }
294
295        /**
296         * Calculates total {@link io.konik.zugferd.entity.AllowanceCharge} for given {@link Item}.
297         */
298        public static final class ItemTotalAllowanceChargeCalculator implements Function<Item, Amount> {
299
300                private final CurrencyCode currencyCode;
301
302                public ItemTotalAllowanceChargeCalculator(CurrencyCode currencyCode) {
303                        this.currencyCode = currencyCode;
304                }
305
306                @Override
307                public Amount apply(@Nullable Item item) {
308                        Amount totalAllowanceCharge = Amounts.zero(currencyCode);
309                        BigDecimal quantity = BigDecimal.ONE;
310
311                        if (item != null && item.getDelivery() != null && item.getDelivery().getBilled() != null) {
312                                quantity = item.getDelivery().getBilled().getValue();
313                        }
314
315                        if (item != null && item.getAgreement() != null && item.getAgreement().getGrossPrice() != null) {
316                                GrossPrice grossPrice = item.getAgreement().getGrossPrice();
317
318                                if (grossPrice.getAllowanceCharges() != null && !grossPrice.getAllowanceCharges().isEmpty()) {
319                                        for (AllowanceCharge charge : grossPrice.getAllowanceCharges()) {
320                                                BigDecimal chargeValue = charge.getActual().getValue();
321                                                if (charge.isDiscount()) {
322                                                        chargeValue = chargeValue.negate();
323                                                }
324                                                Amount amount = new Amount(chargeValue.multiply(quantity), currencyCode);
325                                                totalAllowanceCharge = Amounts.add(totalAllowanceCharge, amount);
326                                        }
327
328                                        totalAllowanceCharge = Amounts.setPrecision(totalAllowanceCharge, 2, RoundingMode.HALF_UP);
329                                }
330                        }
331
332                        return totalAllowanceCharge;
333                }
334        }
335
336        public static final class InvoiceAllowanceTotalCalculator implements Function<Settlement, Amount> {
337                @Nullable
338                @Override
339                public Amount apply(@Nullable Settlement settlement) {
340                        if (settlement == null || settlement.getAllowanceCharge() == null) {
341                                return null;
342                        }
343
344                        BigDecimal chargeValue = BigDecimal.ZERO;
345
346                        for (SpecifiedAllowanceCharge charge : settlement.getAllowanceCharge()) {
347                                if (charge.isDiscount()) {
348                                        chargeValue = chargeValue.add(charge.getActual().getValue());
349                                }
350                        }
351                        Amount amount =  new Amount(chargeValue, settlement.getCurrency());
352
353                        log.debug("Invoice allowance total = {}", amount);
354
355                        return amount;
356                }
357        }
358
359        public static final class InvoiceChargeTotalCalculator implements Function<Settlement, Amount> {
360                @Nullable
361                @Override
362                public Amount apply(@Nullable Settlement settlement) {
363                        if (settlement == null || settlement.getAllowanceCharge() == null) {
364                                return null;
365                        }
366
367                        BigDecimal chargeValue = BigDecimal.ZERO;
368
369                        for (SpecifiedAllowanceCharge charge : settlement.getAllowanceCharge()) {
370                                if (charge.isSurcharge()) {
371                                        chargeValue = chargeValue.add(charge.getActual().getValue());
372                                }
373                        }
374
375                        for (LogisticsServiceCharge charge : settlement.getServiceCharge()) {
376                                chargeValue = chargeValue.add(charge.getAmount().getValue());
377                        }
378
379                        Amount amount =  new Amount(chargeValue, settlement.getCurrency());
380
381                        log.debug("Invoice charge total = {}", amount);
382
383                        return amount;
384                }
385        }
386
387        public static final class ItemTaxExtractor implements Function<Item, ItemTax> {
388                @Nullable
389                @Override
390                public ItemTax apply(@Nullable Item item) {
391                        if (item == null || item.getSettlement() == null) {
392                                return null;
393                        }
394
395                        if (item.getSettlement().getTradeTax().isEmpty()) {
396                                return null;
397                        }
398
399                        return item.getSettlement().getTradeTax().get(0);
400                }
401        }
402
403        /**
404         * Helper class for aggregating tax information and calculating
405         * tax basis and tax total values.
406         */
407        public static final class TaxAggregator {
408
409                private static final int PRECISION = 2;
410                private static final RoundingMode ROUNDING_MODE = RoundingMode.HALF_UP;
411
412                private final ConcurrentMap<Key, BigDecimal> map = new ConcurrentHashMap<Key, BigDecimal>();
413
414                public void add(Tax tax, BigDecimal amount) {
415                        Key key = Key.create(tax);
416                        map.putIfAbsent(key, BigDecimal.ZERO);
417                        map.put(key, map.get(key).add(amount));
418                }
419
420                public BigDecimal getTaxBasisForTaxPercentage(final BigDecimal percentage) {
421                        BigDecimal value = BigDecimal.ZERO;
422                        for (Key key : map.keySet()) {
423                                if (percentage.equals(key.getPercentage())) {
424                                        value = value.add(map.get(key));
425                                }
426                        }
427                        return value;
428                }
429
430                public BigDecimal calculateTaxBasis() {
431                        log.debug("Recalculating tax basis for tax percentages: {}", Arrays.toString(map.keySet().toArray()));
432                        BigDecimal taxBasis = BigDecimal.ZERO;
433                        for (BigDecimal amount : map.values()) {
434                                taxBasis = taxBasis.add(amount);
435                        }
436
437                        log.debug("Recalculated tax basis = {}", taxBasis);
438
439                        return taxBasis;
440                }
441
442                public BigDecimal calculateTaxTotal() {
443                        log.debug("Calculating tax total...");
444                        BigDecimal taxTotal = BigDecimal.ZERO;
445                        for (Map.Entry<Key, BigDecimal> entry : map.entrySet()) {
446                                BigDecimal percentage = entry.getKey().getPercentage();
447                                BigDecimal value = entry.getValue();
448                                BigDecimal taxAmount = calculateTaxAmount(percentage, value);
449
450                                log.debug("===> {} x {}% = {}", value, percentage, taxAmount);
451
452                                taxTotal = taxTotal.add(taxAmount);
453                        }
454
455                        log.debug("Recalculated tax total = {}", taxTotal);
456
457                        return taxTotal;
458                }
459
460                public List<TradeTax> generateTradeTaxList(final CurrencyCode currencyCode, final List<TradeTax> previousList) {
461                        List<TradeTax> taxes = new LinkedList<TradeTax>();
462
463                        for (Key key : map.keySet()) {
464                                TradeTax tradeTax = new TradeTax();
465                                tradeTax.setType(key.getCode());
466                                tradeTax.setCategory(key.getCategory());
467                                tradeTax.setPercentage(key.getPercentage());
468
469                                BigDecimal basis = map.get(key);
470                                BigDecimal calculated = calculateTaxAmount(key.getPercentage(), basis);
471
472                                tradeTax.setBasis(new Amount(basis, currencyCode));
473                                tradeTax.setCalculated(new Amount(calculated, currencyCode));
474
475                                TradeTax existing = null;
476                                if (previousList != null) {
477                                        for (TradeTax current : previousList) {
478                                                if (tradeTax.getType().equals(current.getType()) &&
479                                                                tradeTax.getCategory().equals(current.getCategory()) &&
480                                                                tradeTax.getPercentage().equals(current.getPercentage())) {
481                                                        existing = current;
482                                                        break;
483                                                }
484                                        }
485                                }
486
487                                if (existing != null) {
488                                        tradeTax.setExemptionReason(existing.getExemptionReason());
489
490                                        if (existing.getAllowanceCharge() != null) {
491                                                tradeTax.setAllowanceCharge(new Amount(existing.getAllowanceCharge().getValue(), existing.getAllowanceCharge().getCurrency()));
492                                        }
493
494                                        if (existing.getLineTotal() != null) {
495                                                tradeTax.setLineTotal(new Amount(existing.getLineTotal().getValue(), existing.getLineTotal().getCurrency()));
496                                        }
497                                }
498
499                                taxes.add(tradeTax);
500                        }
501
502                        return taxes;
503                }
504
505                public static BigDecimal calculateTaxAmount(final BigDecimal percentage, final BigDecimal value) {
506                        return value.multiply(percentage.divide(BigDecimal.valueOf(100))).setScale(PRECISION, ROUNDING_MODE);
507                }
508
509                @Override
510                public String toString() {
511                        return "TaxAggregator{" +
512                                        "map=" + map +
513                                        '}';
514                }
515
516                /**
517                 * Helper key for {@link TaxAggregator}
518                 */
519                static final class Key {
520                        private final BigDecimal percentage;
521                        private final TaxCode code;
522                        private final TaxCategory category;
523
524                        private Key(final Tax tax) {
525                                this.percentage = tax.getPercentage();
526                                this.category = tax.getCategory();
527                                this.code = tax.getType();
528                        }
529
530                        public static Key create(final Tax tax) {
531                                return new Key(tax);
532                        }
533
534                        public BigDecimal getPercentage() {
535                                return percentage;
536                        }
537
538                        public TaxCode getCode() {
539                                return code;
540                        }
541
542                        public TaxCategory getCategory() {
543                                return category;
544                        }
545
546                        @Override
547                        public boolean equals(Object o) {
548                                if (this == o) return true;
549                                if (!(o instanceof Key)) return false;
550
551                                Key key = (Key) o;
552
553                                if (!percentage.equals(key.percentage)) return false;
554                                if (code != key.code) return false;
555                                return category == key.category;
556
557                        }
558
559                        @Override
560                        public int hashCode() {
561                                int result = percentage.hashCode();
562                                result = 31 * result + (code != null ? code.hashCode() : 0);
563                                result = 31 * result + (category != null ? category.hashCode() : 0);
564                                return result;
565                        }
566
567                        @Override
568                        public String toString() {
569                                return "Key{" +
570                                                "percentage=" + percentage +
571                                                ", code=" + code +
572                                                ", category=" + category +
573                                                '}';
574                        }
575                }
576        }
577
578        public static final class RecalculationResult {
579                private final MonetarySummation monetarySummation;
580                private final TaxAggregator taxAggregator;
581
582                public RecalculationResult(MonetarySummation monetarySummation, TaxAggregator taxAggregator) {
583                        this.monetarySummation = monetarySummation;
584                        this.taxAggregator = taxAggregator;
585                }
586
587                public MonetarySummation getMonetarySummation() {
588                        return monetarySummation;
589                }
590
591                public TaxAggregator getTaxAggregator() {
592                        return taxAggregator;
593                }
594        }
595}