Skip to content

Commit

Permalink
[MODINVOICE-554]. Invoices app: Incorrect formula for calculating adj…
Browse files Browse the repository at this point in the history
…ustments, that are included and pro-rated by amount (#509)

* [MODINVOICE-554]. Invoices app: Incorrect formula for calculating adjustments, that are included and pro-rated by amount

* [MODINVOICE-554]. Refactor methods, add tests
  • Loading branch information
BKadirkhodjaev authored Oct 11, 2024
1 parent b5bccb1 commit 4ababdf
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 10 deletions.
53 changes: 43 additions & 10 deletions src/main/java/org/folio/services/adjusment/AdjustmentsService.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import io.vertx.core.json.JsonObject;

public class AdjustmentsService {

private final Logger logger = LogManager.getLogger(this.getClass());
public static final Predicate<Adjustment> NOT_PRORATED_ADJUSTMENTS_PREDICATE = adj -> adj.getProrate() == NOT_PRORATED;
public static final Predicate<Adjustment> PRORATED_ADJUSTMENTS_PREDICATE = NOT_PRORATED_ADJUSTMENTS_PREDICATE.negate();
Expand Down Expand Up @@ -65,7 +66,7 @@ public List<InvoiceLine> applyProratedAdjustments(List<InvoiceLine> lines, Invoi
updatedLines.addAll(applyProratedAdjustmentByLines(adjustment, lines, currencyUnit));
break;
case BY_AMOUNT:
updatedLines.addAll(applyProratedAdjustmentByAmount(adjustment, lines, currencyUnit));
updatedLines.addAll(applyAdjustmentsAndUpdateLines(adjustment, lines, currencyUnit));
break;
case BY_QUANTITY:
updatedLines.addAll(applyProratedAdjustmentByQuantity(adjustment, lines, currencyUnit));
Expand All @@ -80,6 +81,15 @@ public List<InvoiceLine> applyProratedAdjustments(List<InvoiceLine> lines, Invoi
.collect(toList());
}

private List<InvoiceLine> applyAdjustmentsAndUpdateLines(Adjustment adjustment, List<InvoiceLine> lines,
CurrencyUnit currencyUnit) {
if (adjustment.getRelationToTotal() == Adjustment.RelationToTotal.INCLUDED_IN) {
return applyProratedAmountTypeIncludedInAdjustments(adjustment, lines, currencyUnit);
} else {
return applyProratedAdjustmentByAmount(adjustment, lines, currencyUnit);
}
}

public void processProratedAdjustments(List<InvoiceLine> lines, Invoice invoice) {
List<Adjustment> proratedAdjustments = getProratedAdjustments(invoice);

Expand All @@ -88,7 +98,6 @@ public void processProratedAdjustments(List<InvoiceLine> lines, Invoice invoice)

// Apply prorated adjustments to each invoice line
applyProratedAdjustments(lines, invoice);

}

/**
Expand All @@ -99,10 +108,10 @@ public void processProratedAdjustments(List<InvoiceLine> lines, Invoice invoice)
void filterDeletedAdjustments(List<Adjustment> proratedAdjustments, List<InvoiceLine> invoiceLines) {
List<String> adjIds = proratedAdjustments.stream()
.map(Adjustment::getId)
.collect(toList());
.toList();

invoiceLines.forEach(line -> line.getAdjustments()
.removeIf(adj -> Objects.nonNull(adj.getAdjustmentId()) && !adjIds.contains(adj.getAdjustmentId())));
.removeIf(adj -> Objects.nonNull(adj.getAdjustmentId()) && !adjIds.contains(adj.getAdjustmentId())));
}

/**
Expand Down Expand Up @@ -179,7 +188,7 @@ private List<InvoiceLine> applyAmountTypeProratedAdjustments(Adjustment adjustme
int remainderSignum = remainder.signum();
MonetaryAmount smallestUnit = getSmallestUnit(expectedAdjustmentTotal, remainderSignum);

for (ListIterator<InvoiceLine> iterator = getIterator(lines, remainderSignum); isIteratorHasNext(iterator, remainderSignum);) {
for (ListIterator<InvoiceLine> iterator = getIterator(lines, remainderSignum); isIteratorHasNext(iterator, remainderSignum); ) {

final InvoiceLine line = iteratorNext(iterator, remainderSignum);
MonetaryAmount amount = lineIdAdjustmentValueMap.get(line.getId());
Expand All @@ -190,8 +199,7 @@ private List<InvoiceLine> applyAmountTypeProratedAdjustments(Adjustment adjustme
}

Adjustment proratedAdjustment = prepareAdjustmentForLine(adjustment);
proratedAdjustment.setValue(amount.getNumber()
.doubleValue());
proratedAdjustment.setValue(amount.getNumber().doubleValue());
if (addAdjustmentToLine(line, proratedAdjustment)) {
updatedLines.add(line);
}
Expand All @@ -200,13 +208,40 @@ private List<InvoiceLine> applyAmountTypeProratedAdjustments(Adjustment adjustme
return updatedLines;
}

private List<InvoiceLine> applyProratedAmountTypeIncludedInAdjustments(Adjustment adjustment, List<InvoiceLine> lines,
CurrencyUnit currencyUnit) {
List<InvoiceLine> updatedLines = new ArrayList<>();
for (InvoiceLine line : lines) {
if (invoiceLineWasAdjustedById(adjustment, line)) {
continue;
}
MonetaryAmount lineSubtotal = Money.of(line.getSubTotal(), currencyUnit);
MonetaryAmount amountAdjustmentValue = lineSubtotal.multiply(adjustment.getValue())
.divide(Money.of(100, currencyUnit).add(Money.of(adjustment.getValue(), currencyUnit)).getNumber().doubleValue())
.with(Monetary.getDefaultRounding());
Adjustment preparedAdjustment = prepareAdjustmentForLine(adjustment.withType(Adjustment.Type.AMOUNT))
.withValue(amountAdjustmentValue.getNumber().doubleValue());
line.withSubTotal(lineSubtotal.subtract(amountAdjustmentValue).getNumber().doubleValue());
if (addAdjustmentToLine(line, preparedAdjustment)) {
updatedLines.add(line);
}
}
return updatedLines;
}

private static boolean invoiceLineWasAdjustedById(Adjustment adjustment, InvoiceLine line) {
return Objects.nonNull(adjustment.getId()) && line.getAdjustments().stream()
.map(Adjustment::getAdjustmentId)
.filter(Objects::nonNull)
.anyMatch(lineAdjustmentId -> lineAdjustmentId.equals(adjustment.getId()));
}

/**
* Each invoiceLine gets a portion of the amount proportionate to the invoiceLine's contribution to the invoice subTotal.
* Prorated percentage adjustments of this type aren't split but rather each invoiceLine gets an adjustment of that percentage
*/
private List<InvoiceLine> applyProratedAdjustmentByAmount(Adjustment adjustment, List<InvoiceLine> lines,
CurrencyUnit currencyUnit) {

if (adjustment.getType() == Adjustment.Type.PERCENTAGE) {
adjustment = convertToAmountAdjustment(adjustment, lines, currencyUnit);
}
Expand All @@ -232,7 +267,6 @@ private BiFunction<MonetaryAmount, InvoiceLine, MonetaryAmount> prorateByAmountF
*/
private List<InvoiceLine> applyProratedAdjustmentByQuantity(Adjustment adjustment, List<InvoiceLine> lines,
CurrencyUnit currencyUnit) {

if (adjustment.getType() == Adjustment.Type.PERCENTAGE) {
return applyPercentageAdjustmentsByQuantity(adjustment, lines, currencyUnit);
}
Expand Down Expand Up @@ -285,5 +319,4 @@ private InvoiceLine iteratorNext(ListIterator<InvoiceLine> iterator, int remaind
private BiFunction<MonetaryAmount, InvoiceLine, MonetaryAmount> prorateByLines(List<InvoiceLine> lines) {
return (amount, line) -> amount.divide(lines.size()).with(Monetary.getDefaultRounding());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
import static org.folio.rest.impl.MockServer.getInvoiceLineCreations;
import static org.folio.rest.impl.MockServer.getInvoiceLineUpdates;
import static org.folio.rest.impl.MockServer.getInvoiceUpdates;
import static org.folio.rest.jaxrs.model.Adjustment.Prorate.BY_AMOUNT;
import static org.folio.rest.jaxrs.model.Adjustment.RelationToTotal.INCLUDED_IN;
import static org.folio.rest.jaxrs.model.Adjustment.Type.PERCENTAGE;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
Expand All @@ -19,6 +22,7 @@

import io.vertx.junit5.VertxExtension;
import java.util.Collections;
import java.util.UUID;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Expand All @@ -27,6 +31,7 @@
import org.folio.rest.jaxrs.model.InvoiceLine;
import org.folio.rest.jaxrs.model.InvoiceLineCollection;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
Expand Down Expand Up @@ -182,6 +187,82 @@ public void testDeleteLineForInvoiceWithOneAdj(Adjustment.Prorate prorate, Adjus
assertThat(lineAdjustment.getValue(), is(expectedAdjValue));
}

@Test
public void testCreateInvoiceWithOnePercentageTypeByAmountProrateIncludedByTotalAdjustment() {
logger.info("=== Creating invoice with one adjustment by amount prorate included by total ===");

// Prepare data "from storage"
Invoice invoice = getMockAsJson(OPEN_INVOICE_SAMPLE_PATH).mapTo(Invoice.class).withId(randomUUID().toString());
Adjustment invoiceAdjustment = new Adjustment()
.withId(UUID.randomUUID().toString())
.withDescription("VAT")
.withProrate(BY_AMOUNT)
.withType(PERCENTAGE)
.withRelationToTotal(INCLUDED_IN)
.withValue(7d);
invoice.withAdjustments(Collections.singletonList(invoiceAdjustment));
addMockEntry(INVOICES, invoice);

// Prepare request body
InvoiceLine invoiceLineBody = getMockInvoiceLine(invoice.getId()).withAdjustmentsTotal(0d).withSubTotal(30d).withQuantity(1);

// Send create request
InvoiceLine invoiceLine = verifySuccessPost(INVOICE_LINES_PATH, invoiceLineBody).as(InvoiceLine.class);

// Verification
assertThat(getInvoiceLineUpdates(), Matchers.hasSize(0));
assertThat(getInvoiceUpdates(), Matchers.hasSize(1));
compareRecordWithSentToStorage(invoiceLine);

assertThat(invoiceLine.getAdjustments(), hasSize(1));
assertThat(invoiceLine.getAdjustmentsTotal(), is(0d));
assertThat(invoiceLine.getSubTotal(), is(28.04d));

Adjustment lineAdjustment = invoiceLine.getAdjustments().get(0);
verifyInvoiceLineAdjustmentCommon(invoiceAdjustment, lineAdjustment);
assertThat(lineAdjustment.getValue(), is(1.96d));
}

@Test
public void testDeleteInvoiceWithOnePercentageTypeByAmountProrateIncludedByTotalAdjustment() {
logger.info("=== Deleting invoice with one adjustment by amount prorate included by total ===");

// Prepare data "from storage"
Invoice invoice = getMockAsJson(OPEN_INVOICE_SAMPLE_PATH).mapTo(Invoice.class).withId(randomUUID().toString());
Adjustment invoiceAdjustment = new Adjustment()
.withId(UUID.randomUUID().toString())
.withDescription("VAT")
.withProrate(BY_AMOUNT)
.withType(PERCENTAGE)
.withRelationToTotal(INCLUDED_IN)
.withValue(7d);
invoice.withAdjustments(Collections.singletonList(invoiceAdjustment));
addMockEntry(INVOICES, invoice);

InvoiceLine line1 = getMockInvoiceLine(invoice.getId()).withAdjustmentsTotal(0d).withSubTotal(30d).withQuantity(1);
addMockEntry(INVOICE_LINES, line1);
InvoiceLine line2 = getMockInvoiceLine(invoice.getId()).withAdjustmentsTotal(0d).withSubTotal(30d).withQuantity(1);
addMockEntry(INVOICE_LINES, line2);

// Send delete request
verifyDeleteResponse(String.format(INVOICE_LINE_ID_PATH, line2.getId()), "", 204);

// Verification
assertThat(getInvoiceLineUpdates(), Matchers.hasSize(1));
assertThat(getInvoiceUpdates(), Matchers.hasSize(1));

InvoiceLine lineToStorage = getLineToStorageById(line1.getId());
assertThat(lineToStorage.getAdjustments(), hasSize(1));

assertThat(lineToStorage.getAdjustments(), hasSize(1));
assertThat(lineToStorage.getAdjustmentsTotal(), is(0d));
assertThat(lineToStorage.getSubTotal(), is(28.04));

Adjustment lineAdjustment = lineToStorage.getAdjustments().get(0);
verifyInvoiceLineAdjustmentCommon(invoiceAdjustment, lineAdjustment);
assertThat(lineAdjustment.getValue(), is(1.96d));
}

private InvoiceLine getLineToStorageById(String invoiceLineId) {
return getInvoiceLineUpdates().stream()
.filter(line -> invoiceLineId.equals(line.getString("id")))
Expand Down
107 changes: 107 additions & 0 deletions src/test/java/org/folio/rest/impl/InvoicesProratedAdjustmentsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import static org.folio.rest.jaxrs.model.Adjustment.Prorate.BY_LINE;
import static org.folio.rest.jaxrs.model.Adjustment.Prorate.BY_QUANTITY;
import static org.folio.rest.jaxrs.model.Adjustment.Prorate.NOT_PRORATED;
import static org.folio.rest.jaxrs.model.Adjustment.RelationToTotal.INCLUDED_IN;
import static org.folio.rest.jaxrs.model.Adjustment.Type.AMOUNT;
import static org.folio.rest.jaxrs.model.Adjustment.Type.PERCENTAGE;
import static org.hamcrest.MatcherAssert.assertThat;
Expand All @@ -30,6 +31,7 @@
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.stream.Stream;

import org.apache.logging.log4j.LogManager;
Expand Down Expand Up @@ -1015,6 +1017,111 @@ public void testUpdateInvoiceWithThreeLinesAddingPercentageAdjustmentByLines() {
});
}

@Test
public void testUpdateInvoiceWithOneLinePercentageTypeByAmountProrateIncludedByTotalAdjustment() {
logger.info("=== Updating invoice with one line adding 7% adjustment by amount prorate included by total ===");

// Prepare data "from storage"
Invoice invoice = getMockAsJson(OPEN_INVOICE_SAMPLE_PATH).mapTo(Invoice.class).withId(randomUUID().toString());
invoice.getAdjustments().clear();
addMockEntry(INVOICES, invoice);

InvoiceLine invoiceLine = getMockInvoiceLine(invoice.getId()).withAdjustmentsTotal(0d).withSubTotal(30d).withInvoiceLineNumber("n-1");
addMockEntry(INVOICE_LINES, invoiceLine);

// Prepare request body
Invoice invoiceBody = copyObject(invoice);
Adjustment adjustment = new Adjustment()
.withId(UUID.randomUUID().toString())
.withDescription("VAT")
.withProrate(BY_AMOUNT)
.withType(PERCENTAGE)
.withRelationToTotal(INCLUDED_IN)
.withValue(7d);
invoiceBody.getAdjustments().add(adjustment);

// Send update request
verifyPut(String.format(INVOICE_ID_PATH, invoice.getId()), invoiceBody, "", 204);

// Verification
assertThat(getInvoiceUpdates(), hasSize(1));
assertThat(getInvoiceLineUpdates(), hasSize(1));

Invoice invoiceToStorage = getInvoiceUpdates().get(0).mapTo(Invoice.class);
assertThat(invoiceToStorage.getAdjustments(), hasSize(1));
assertThat(invoiceToStorage.getAdjustmentsTotal(), is(0.0));
Adjustment invoiceAdjustment = invoiceToStorage.getAdjustments().get(0);
assertThat(invoiceAdjustment.getId(), not(is(emptyOrNullString())));

Stream.of(invoiceLine.getId())
.forEach(id -> {
InvoiceLine lineToStorage = getLineToStorageById(id);
assertThat(lineToStorage.getAdjustments(), hasSize(1));
assertThat(lineToStorage.getAdjustmentsTotal(), is(0d));
assertThat(lineToStorage.getSubTotal(), is(28.04d));

Adjustment lineAdjustment = lineToStorage.getAdjustments().get(0);
verifyInvoiceLineAdjustmentCommon(invoiceAdjustment, lineAdjustment);
assertThat(lineAdjustment.getValue(), is(1.96d));
});
}

@Test
public void testUpdateInvoiceWithThreeLinesPercentageTypeByAmountProrateIncludedByTotalAdjustment() {
logger.info("=== Updating invoice with three lines adding 7% adjustment by amount prorate included by total ===");

// Prepare data "from storage"
Invoice invoice = getMockAsJson(OPEN_INVOICE_SAMPLE_PATH).mapTo(Invoice.class).withId(randomUUID().toString());
invoice.getAdjustments().clear();
addMockEntry(INVOICES, invoice);

InvoiceLine invoiceLine1 = getMockInvoiceLine(invoice.getId()).withAdjustmentsTotal(0d).withSubTotal(30d).withInvoiceLineNumber("n-1");
addMockEntry(INVOICE_LINES, invoiceLine1);

InvoiceLine invoiceLine2 = getMockInvoiceLine(invoice.getId()).withAdjustmentsTotal(0d).withSubTotal(30d).withInvoiceLineNumber("n-2");
addMockEntry(INVOICE_LINES, invoiceLine2);

InvoiceLine invoiceLine3 = getMockInvoiceLine(invoice.getId()).withAdjustmentsTotal(0d).withSubTotal(30d).withInvoiceLineNumber("n-3");
addMockEntry(INVOICE_LINES, invoiceLine3);

// Prepare request body
Invoice invoiceBody = copyObject(invoice);
Adjustment adjustment = new Adjustment()
.withId(UUID.randomUUID().toString())
.withDescription("VAT")
.withProrate(BY_AMOUNT)
.withType(PERCENTAGE)
.withRelationToTotal(INCLUDED_IN)
.withValue(7d);
invoiceBody.getAdjustments().add(adjustment);

// Send update request
verifyPut(String.format(INVOICE_ID_PATH, invoice.getId()), invoiceBody, "", 204);

// Verification
assertThat(getInvoiceUpdates(), hasSize(1));
assertThat(getInvoiceLineUpdates(), hasSize(3));

Invoice invoiceToStorage = getInvoiceUpdates().get(0).mapTo(Invoice.class);
assertThat(invoiceToStorage.getAdjustments(), hasSize(1));
assertThat(invoiceToStorage.getAdjustmentsTotal(), is(0.0));
Adjustment invoiceAdjustment = invoiceToStorage.getAdjustments().get(0);
assertThat(invoiceAdjustment.getId(), not(is(emptyOrNullString())));

Stream.of(invoiceLine1.getId(), invoiceLine2.getId(), invoiceLine3.getId())
.forEach(id -> {
InvoiceLine lineToStorage = getLineToStorageById(id);
assertThat(lineToStorage.getAdjustments(), hasSize(1));
assertThat(lineToStorage.getAdjustmentsTotal(), is(0d));
assertThat(lineToStorage.getSubTotal(), is(28.04d));

Adjustment lineAdjustment = lineToStorage.getAdjustments().get(0);
verifyInvoiceLineAdjustmentCommon(invoiceAdjustment, lineAdjustment);
assertThat(lineAdjustment.getValue(), is(1.96d));
});
}


private InvoiceLine getLineToStorageById(String invoiceLineId) {
return getInvoiceLineUpdates().stream()
.filter(line -> invoiceLineId.equals(line.getString("id")))
Expand Down

0 comments on commit 4ababdf

Please sign in to comment.