Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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