package org.folio.rest.impl; import static org.junit.Assert.assertEquals; import java.math.RoundingMode; import javax.money.CurrencyUnit; import javax.money.Monetary; import javax.money.MonetaryAmount; import javax.money.MonetaryRounding; import javax.money.RoundingQueryBuilder; import org.javamoney.moneta.Money; public class ProratedAdjustmentsPOC { public static class Adjustment { public final double value; public final boolean isPercentage; public MonetaryAmount amount; public Adjustment(double value, boolean isPercentage) { this.value = value; this.isPercentage = isPercentage; } public Adjustment(double value) { this.value = value; this.isPercentage = false; } public String toString() { return value + (isPercentage ? "%" : ""); } } public static class InvoiceLine { public final MonetaryAmount subTotal; public final int quantity; public Adjustment adjustment; public InvoiceLine(MonetaryAmount subTotal, int quantity) { this.subTotal = subTotal; this.quantity = quantity; } public String toString() { return "sub: " + subTotal + "\tqty: " + quantity + " \tadj: " + adjustment.amount; } } public static class Invoice { public final MonetaryAmount subTotal; public final Adjustment adjustment; public final InvoiceLine[] lines; public final CurrencyUnit curr; public final int quantity; public Invoice(MonetaryAmount subTotal, InvoiceLine[] lines, Adjustment adjustment, int quantity) { this.adjustment = adjustment; this.lines = lines; this.curr = subTotal.getCurrency(); this.subTotal = subTotal; this.quantity = quantity; } public void print() { System.out.println("subTotal:\t" + subTotal); System.out.println("quantity:\t" + quantity); System.out.println("adjustment:\t" + adjustment); System.out.println("currency:\t" + curr.getCurrencyCode()); for (int i = 0; i < lines.length; i++) { System.out.println("line #" + i + ":\t" + lines[i]); } } } public static void main(String[] args) { CurrencyUnit[] currencies = { Monetary.getCurrency("USD"),// two decimal places Monetary.getCurrency("JPY"), // zero decimal places Monetary.getCurrency("JOD") // 3 decimal places }; for (CurrencyUnit curr : currencies) { InvoiceLine[] invoiceLines = { new InvoiceLine(Money.of(10, curr), 1), new InvoiceLine(Money.of(20, curr), 10), new InvoiceLine(Money.of(30, curr), 2), new InvoiceLine(Money.of(5, curr), 20), new InvoiceLine(Money.of(10, curr), 3), new InvoiceLine(Money.of(15, curr), 30), new InvoiceLine(Money.of(20, curr), 4), new InvoiceLine(Money.of(25, curr), 40), new InvoiceLine(Money.of(30, curr), 5), new InvoiceLine(Money.of(35, curr), 50) }; Adjustment[] invoiceAdjs = { new Adjustment(9.9999, true), new Adjustment(9.5, true), new Adjustment(9.95, true), new Adjustment(9.595, true), new Adjustment(10, false), new Adjustment(9.98, false), new Adjustment(3.33, false), new Adjustment(0.01, false) }; for (int numLines = 2; numLines < invoiceLines.length; numLines++) { MonetaryAmount subTotal = Money.of(0, curr); int quantity = 0; InvoiceLine[] lines = new InvoiceLine[numLines]; for (int i = 0; i < numLines; i++) { lines[i] = invoiceLines[i]; subTotal = subTotal.add(invoiceLines[i].subTotal); quantity += invoiceLines[i].quantity; } for (Adjustment adjustment : invoiceAdjs) { Invoice invoice = new Invoice(subTotal, lines, adjustment, quantity); prorateByLines(invoice); prorateByAmount(invoice); prorateByQuantity(invoice); } } } } public static void prorateByAmount(Invoice invoice) { System.out.println("prorating by:\tamount"); MonetaryAmount sum = Money.of(0, invoice.curr); MonetaryAmount expected = (invoice.adjustment.isPercentage ? invoice.subTotal.multiply(invoice.adjustment.value / 100d) : Money.of(invoice.adjustment.value, invoice.curr)); System.out.println("no rounding:\t" + expected); expected = expected.with(Monetary.getDefaultRounding()); System.out.println("w/ rounding:\t" + expected); MonetaryRounding rndDown = Monetary.getRounding(RoundingQueryBuilder.of() .set(RoundingMode.DOWN) .setCurrency(invoice.curr) .build()); for (InvoiceLine line : invoice.lines) { double percent; if (invoice.adjustment.isPercentage) { // apply the percentage to each line percent = invoice.adjustment.value / 100d; line.adjustment = new Adjustment(line.subTotal.multiply(percent) .getNumber() .doubleValue()); } else { // prorate based on amount of each line... percent = line.subTotal.divide(invoice.subTotal.getNumber() .doubleValue()) .getNumber() .doubleValue(); line.adjustment = new Adjustment(invoice.adjustment.value * percent); } line.adjustment.amount = Money.of(line.adjustment.value, invoice.curr) .with(rndDown); sum = sum.add(line.adjustment.amount); } int decimalPlaces = invoice.curr.getDefaultFractionDigits(); MonetaryAmount smallestUnit = Money.of(1 / Math.pow(10, decimalPlaces), invoice.curr); MonetaryAmount remainder = expected.subtract(sum); if (remainder.isPositive()) { invoice.print(); System.out.println("sum: \t" + sum); System.out.println("expected:\t" + expected); // just add to the last line System.out.println("extra pennies:\t" + remainder); for (int i = 0; i < remainder.divide(smallestUnit.getNumber() .doubleValue()) .getNumber() .doubleValue(); i++) { InvoiceLine lastLine = invoice.lines[invoice.lines.length - i - 1]; lastLine.adjustment.amount = lastLine.adjustment.amount.add(smallestUnit); sum = sum.add(smallestUnit); } } invoice.print(); System.out.println("sum: \t" + sum); System.out.println("expected:\t" + expected + "\n"); assertEquals(expected, sum); } public static void prorateByQuantity(Invoice invoice) { if (invoice.adjustment.isPercentage) { // percentage adjustments don't make sense in the context of "By quantity" return; } System.out.println("prorating by:\tquantity"); MonetaryAmount sum = Money.of(0, invoice.curr); MonetaryAmount expected = (invoice.adjustment.isPercentage ? invoice.subTotal.multiply(invoice.adjustment.value / 100d) : Money.of(invoice.adjustment.value, invoice.curr)); System.out.println("no rounding:\t" + expected); expected = expected.with(Monetary.getDefaultRounding()); System.out.println("w/ rounding:\t" + expected); MonetaryRounding rndDown = Monetary.getRounding(RoundingQueryBuilder.of() .set(RoundingMode.DOWN) .setCurrency(invoice.curr) .build()); for (InvoiceLine line : invoice.lines) { // prorate based on quantity of each line... double percent = (double)line.quantity / invoice.quantity; line.adjustment = new Adjustment(invoice.adjustment.value * percent); line.adjustment.amount = Money.of(line.adjustment.value, invoice.curr).with(rndDown); sum = sum.add(line.adjustment.amount); } int decimalPlaces = invoice.curr.getDefaultFractionDigits(); MonetaryAmount smallestUnit = Money.of(1 / Math.pow(10, decimalPlaces), invoice.curr); MonetaryAmount remainder = expected.subtract(sum); if (remainder.isPositive()) { invoice.print(); System.out.println("sum: \t" + sum); System.out.println("expected:\t" + expected); // just add to the last line System.out.println("extra pennies:\t" + remainder); for (int i = 0; i < remainder.divide(smallestUnit.getNumber() .doubleValue()) .getNumber() .doubleValue(); i++) { InvoiceLine lastLine = invoice.lines[invoice.lines.length - i - 1]; lastLine.adjustment.amount = lastLine.adjustment.amount.add(smallestUnit); sum = sum.add(smallestUnit); } } invoice.print(); System.out.println("sum: \t" + sum); System.out.println("expected:\t" + expected + "\n"); assertEquals(expected, sum); } public static void prorateByLines(Invoice invoice) { System.out.println("prorating by:\tlines"); MonetaryAmount sum = Money.of(0, invoice.curr); MonetaryAmount expected = (invoice.adjustment.isPercentage) ? invoice.subTotal.multiply(invoice.adjustment.value) : Money.of(invoice.adjustment.value, invoice.curr); System.out.println("no rounding:\t" + expected); expected = expected.with(Monetary.getDefaultRounding()); System.out.println("w/ rounding:\t" + expected); MonetaryAmount adjustment = expected.divide(invoice.lines.length); MonetaryAmount remainder = expected.remainder(invoice.lines.length); int toRoundUp = (int) remainder.scaleByPowerOfTen(invoice.curr.getDefaultFractionDigits()) .getNumber() .doubleValue() % invoice.lines.length; MonetaryRounding rndDown = Monetary.getRounding(RoundingQueryBuilder.of() .set(RoundingMode.DOWN) .setCurrency(invoice.curr) .build()); for (int i = 0; i < invoice.lines.length - toRoundUp; i++) { invoice.lines[i].adjustment = new Adjustment(invoice.adjustment.value / invoice.lines.length, invoice.adjustment.isPercentage); invoice.lines[i].adjustment.amount = adjustment.with(rndDown); sum = sum.add(invoice.lines[i].adjustment.amount); } MonetaryRounding rndUp = Monetary.getRounding(RoundingQueryBuilder.of() .set(RoundingMode.UP) .setCurrency(invoice.curr) .build()); for (int i = invoice.lines.length - toRoundUp; i < invoice.lines.length; i++) { invoice.lines[i].adjustment = new Adjustment(invoice.adjustment.value / invoice.lines.length, invoice.adjustment.isPercentage); invoice.lines[i].adjustment.amount = adjustment.with(rndUp); sum = sum.add(invoice.lines[i].adjustment.amount); } invoice.print(); System.out.println("sum: \t" + sum); System.out.println("expected:\t" + expected + "\n"); assertEquals(expected, sum); } /* public static void prorateByAmountv2(Invoice invoice) { MonetaryRounding rndDown = Monetary.getRounding(RoundingQueryBuilder.of() .set(RoundingMode.DOWN) .setCurrency(invoice.curr) .build()); System.out.println("prorating by:\tamount"); MonetaryAmount sum = Money.of(0, invoice.curr); MonetaryAmount expected = (invoice.adjustment.isPercentage ? invoice.subTotal.multiply(invoice.adjustment.value / 100d) : Money.of(invoice.adjustment.value, invoice.curr)); System.out.println("no rounding:\t" + expected); expected = expected.with(Monetary.getDefaultRounding()); System.out.println("w/ rounding:\t" + expected); for (InvoiceLine line : invoice.lines) { double percent; if (invoice.adjustment.isPercentage) { // apply the percentage to each line percent = invoice.adjustment.value / 100d; line.adjustment = new Adjustment(line.subTotal.multiply(percent) .getNumber() .doubleValue()); } else { // prorate based on amount of each line... percent = line.subTotal.divide(invoice.subTotal.getNumber() .doubleValue()) .getNumber() .doubleValue(); line.adjustment = new Adjustment(invoice.adjustment.value * percent); } line.adjustment.amount = Money.of(line.adjustment.value, invoice.curr) .with(rndDown); sum = sum.add(line.adjustment.amount); } if (expected.isGreaterThan(sum)) { invoice.print(); int decimalPlaces = invoice.curr.getDefaultFractionDigits(); MonetaryAmount smallestUnit = Money.of(1 / Math.pow(10, decimalPlaces), invoice.curr); MonetaryAmount remainder = expected.subtract(sum); if (remainder.isGreaterThan(smallestUnit)) { // prorate the remainder System.out.println("sum: \t" + sum); System.out.println("expected:\t" + expected); System.out.println("prorating remainder: " + remainder); for (InvoiceLine line : invoice.lines) { double percent = line.subTotal.divide(invoice.subTotal.getNumber() .doubleValue()) .getNumber() .doubleValue(); MonetaryAmount toAdd = remainder.multiply(percent) .with(rndDown); line.adjustment.amount = line.adjustment.amount.add(toAdd); sum = sum.add(toAdd); } remainder = expected.subtract(sum); if (remainder.isPositive()) { invoice.print(); // just add to the last line System.out.println("sum: \t" + sum); System.out.println("expected:\t" + expected); System.out.println("extra pennies:\t" + remainder); for (int i = 0; i < remainder.divide(smallestUnit.getNumber() .doubleValue()) .getNumber() .doubleValue(); i++) { InvoiceLine lastLine = invoice.lines[invoice.lines.length - i - 1]; lastLine.adjustment.amount = lastLine.adjustment.amount.add(smallestUnit); sum = sum.add(smallestUnit); } } } else { // just add to the last line System.out.println("sum: \t" + sum); System.out.println("expected:\t" + expected); System.out.println("extra penny"); InvoiceLine lastLine = invoice.lines[invoice.lines.length - 1]; lastLine.adjustment.amount = lastLine.adjustment.amount.add(smallestUnit); sum = sum.add(smallestUnit); } } invoice.print(); System.out.println("sum: \t" + sum); System.out.println("expected:\t" + expected + "\n"); assertEquals(expected, sum); } public static void prorateByLinesv2(Invoice invoice) { MonetaryRounding rndDown = Monetary.getRounding(RoundingQueryBuilder.of() .set(RoundingMode.DOWN) .setCurrency(invoice.curr) .build()); MonetaryRounding rndUp = Monetary.getRounding(RoundingQueryBuilder.of() .set(RoundingMode.UP) .setCurrency(invoice.curr) .build()); MonetaryAmount invoiceAdj; if (invoice.adjustment.isPercentage) { invoiceAdj = invoice.subTotal.multiply(invoice.adjustment.value); } else { invoiceAdj = Money.of(invoice.adjustment.value, invoice.curr); } // For now use the default rounding strategy (half-even aka banker's rounding). // At some point we may need to allow different strategies to be specified // on a per-adjustment basis invoiceAdj = invoiceAdj.with(Monetary.getDefaultRounding()); System.out.println("invoiceAdj:\t" + invoiceAdj); System.out.println("prorating by:\tlines"); MonetaryAmount adjustment = invoiceAdj.divide(invoice.lines.length); MonetaryAmount sum = Money.of(0, invoice.curr); MonetaryAmount remainder = invoiceAdj.remainder(invoice.lines.length); int toRoundUp = (int) remainder.scaleByPowerOfTen(invoice.curr.getDefaultFractionDigits()) .getNumber() .doubleValue() % invoice.lines.length; System.out.println("no rounding:\t" + adjustment); for (int i = 0; i < invoice.lines.length - toRoundUp; i++) { MonetaryAmount adj = adjustment.with(rndDown); System.out.println("line #" + i + " adj:\t" + adj); sum = sum.add(adj); } for (int i = invoice.lines.length - toRoundUp; i < invoice.lines.length; i++) { MonetaryAmount adj = adjustment.with(rndUp); System.out.println("line #" + i + " adj:\t" + adj); sum = sum.add(adj); } assertEquals(invoiceAdj, sum); System.out.println("sum: \t" + sum + "\n"); } */ }