Skip to content

Commit

Permalink
MODINVOICE-548 - Invoices do not display fund codes (#496)
Browse files Browse the repository at this point in the history
  • Loading branch information
RuslanLavrov authored Jun 6, 2024
1 parent 3bfd369 commit a8271f4
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 11 deletions.
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## 5.9.0 - Unreleased
* [MODINVOICE-543](https://folio-org.atlassian.net/browse/MODINVOICE-543) - EDIFACT invoice lines load out of order
* [MODINVOICE-548](https://folio-org.atlassian.net/browse/MODINVOICE-548) - Provide fund code population when fund distributions mapping is specified

## 5.8.0 - Released (Quesnelia R1 2024)
The focus of this release was to fix bugs and make improvement in transaction call and error codes
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@
<dependency>
<groupId>org.folio</groupId>
<artifactId>data-import-processing-core</artifactId>
<version>4.2.0</version>
<version>4.3.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.folio</groupId>
Expand All @@ -193,7 +193,7 @@
<dependency>
<groupId>org.folio</groupId>
<artifactId>mod-di-converter-storage-client</artifactId>
<version>2.1.8</version>
<version>2.3.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import static io.vertx.core.Future.succeededFuture;
import static java.lang.String.format;
import static one.util.streamex.StreamEx.ofSubLists;
import static org.apache.commons.collections4.CollectionUtils.isEmpty;
import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
import static org.apache.commons.lang.StringUtils.isNotEmpty;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.folio.ActionProfile.Action.CREATE;
import static org.folio.ActionProfile.FolioRecord.INVOICE;
import static org.folio.DataImportEventTypes.DI_INVOICE_CREATED;
Expand All @@ -13,7 +16,7 @@
import static org.folio.invoices.utils.ResourcePathResolver.resourcesPath;
import static org.folio.rest.jaxrs.model.EntityType.EDIFACT_INVOICE;
import static org.folio.rest.jaxrs.model.InvoiceLine.InvoiceLineStatus.OPEN;
import static org.folio.rest.jaxrs.model.ProfileSnapshotWrapper.ContentType.ACTION_PROFILE;
import static org.folio.rest.jaxrs.model.ProfileType.ACTION_PROFILE;

import io.vertx.core.Future;
import io.vertx.core.Vertx;
Expand Down Expand Up @@ -44,6 +47,7 @@
import org.folio.ParsedRecord;
import org.folio.Record;
import org.folio.dataimport.utils.DataImportUtils;
import org.folio.dbschema.ObjectMapperTool;
import org.folio.domain.relationship.RecordToEntity;
import org.folio.invoices.rest.exceptions.HttpException;
import org.folio.invoices.utils.HelperUtils;
Expand All @@ -64,6 +68,7 @@
import org.folio.rest.jaxrs.model.Invoice;
import org.folio.rest.jaxrs.model.InvoiceLine;
import org.folio.rest.jaxrs.model.InvoiceLineCollection;
import org.folio.rest.jaxrs.model.ProfileSnapshotWrapper;
import org.folio.services.invoice.IdStorageService;

public class CreateInvoiceEventHandler implements EventHandler {
Expand All @@ -83,6 +88,8 @@ public class CreateInvoiceEventHandler implements EventHandler {
private static final String INVOICE_LINES_RULE_NAME = "invoiceLines";
private static final String REFERENCE_NUMBERS_RULE_NAME = "referenceNumbers";
private static final String REF_NUMBER_RULE_NAME = "refNumber";
private static final String FUND_DISTRIBUTIONS_RULE_NAME = "fundDistributions";
private static final String FUND_ID_RULE_NAME = "fundId";
private static final String POL_NUMBER_KEY = "POL_NUMBER_%s";
private static final String POL_EXPENSE_CLASS_KEY = "POL_EXPENSE_CLASS_%s";
private static final String POL_FUND_DISTRIBUTIONS_KEY = "POL_FUND_DISTRIBUTIONS_%s";
Expand All @@ -93,6 +100,8 @@ public class CreateInvoiceEventHandler implements EventHandler {
private final RestClient restClient;
private final IdStorageService idStorageService;
public static final String UNIQUE_KEY_CONSTRAINT_ERROR = "duplicate key value violates unique constraint";
private static final char LEFT_BRACKET = '(';
private static final char RIGHT_BRACKET = ')';

public CreateInvoiceEventHandler(RestClient restClient, IdStorageService idStorageService) {
this.restClient = restClient;
Expand Down Expand Up @@ -372,9 +381,45 @@ private List<InvoiceLine> prepareInvoiceLinesToSave(String invoiceId, DataImport
.collect(Collectors.toList());

linkInvoiceLinesToPoLines(invoiceLines, associatedPoLines);
ensureFundCode(invoiceLines, dataImportEventPayload);
return invoiceLines;
}

private void ensureFundCode(List<InvoiceLine> invoiceLines, DataImportEventPayload dataImportEventPayload) {
if (invoiceLines.stream().allMatch(line -> isEmpty(line.getFundDistributions()))) {
return;
}

Map<String, String> idToFundName = extractFundsData(dataImportEventPayload);
if (!idToFundName.isEmpty()) {
invoiceLines.stream()
.filter(invoiceLine -> isNotEmpty(invoiceLine.getFundDistributions()))
.forEach(invoiceLine -> invoiceLine.getFundDistributions().stream()
.filter(fundDistribution -> isNotEmpty(fundDistribution.getFundId()) && isEmpty(fundDistribution.getCode()))
.forEach(fundDistribution -> populateFundCode(fundDistribution, idToFundName)));
}
}

private Map<String, String> extractFundsData(DataImportEventPayload dataImportEventPayload) {
ProfileSnapshotWrapper mappingProfileWrapper = dataImportEventPayload.getCurrentNode();
MappingProfile mappingProfile = ObjectMapperTool.getMapper().convertValue(mappingProfileWrapper.getContent(), MappingProfile.class);

return mappingProfile.getMappingDetails().getMappingFields().stream()
.filter(mappingRule -> INVOICE_LINES_RULE_NAME.equals(mappingRule.getName()) && !mappingRule.getSubfields().isEmpty())
.flatMap(mappingRule -> mappingRule.getSubfields().get(0).getFields().stream())
.filter(mappingRule -> FUND_DISTRIBUTIONS_RULE_NAME.equals(mappingRule.getName()) && !mappingRule.getSubfields().isEmpty())
.flatMap(mappingRule -> mappingRule.getSubfields().get(0).getFields().stream())
.filter(mappingRule -> FUND_ID_RULE_NAME.equals(mappingRule.getName()))
.map(mappingRule -> ((Map<String, String>) mappingRule.getAcceptedValues()))
.findAny()
.orElse(Collections.emptyMap());
}

private void populateFundCode(org.folio.rest.jaxrs.model.FundDistribution fundDistribution, Map<String, String> idToFundName) {
String fundName = idToFundName.get(fundDistribution.getFundId());
fundDistribution.setCode(fundName.substring(fundName.lastIndexOf(LEFT_BRACKET) + 1, fundName.lastIndexOf(RIGHT_BRACKET)));
}

private void linkInvoiceLinesToPoLines(List<InvoiceLine> invoiceLines, Map<Integer, PoLine> associatedPoLines) {
for (int i = 0; i < invoiceLines.size(); i++) {
if (associatedPoLines.get(i + 1) != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package org.folio.dataimport.cache;

import static org.folio.rest.impl.MockServer.addMockEntry;
import static org.folio.rest.jaxrs.model.ProfileSnapshotWrapper.ContentType.ACTION_PROFILE;
import static org.folio.rest.jaxrs.model.ProfileSnapshotWrapper.ContentType.JOB_PROFILE;
import static org.folio.rest.jaxrs.model.ProfileType.ACTION_PROFILE;
import static org.folio.rest.jaxrs.model.ProfileType.JOB_PROFILE;

import java.util.List;
import java.util.Map;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@
import static org.folio.rest.impl.MockServer.addMockEntry;
import static org.folio.rest.jaxrs.model.EntityType.EDIFACT_INVOICE;
import static org.folio.rest.jaxrs.model.EntityType.INVOICE;
import static org.folio.rest.jaxrs.model.ProfileSnapshotWrapper.ContentType.ACTION_PROFILE;
import static org.folio.rest.jaxrs.model.ProfileSnapshotWrapper.ContentType.JOB_PROFILE;
import static org.folio.rest.jaxrs.model.ProfileSnapshotWrapper.ContentType.MAPPING_PROFILE;
import static org.folio.rest.jaxrs.model.FundDistribution.DistributionType.PERCENTAGE;
import static org.folio.rest.jaxrs.model.ProfileType.ACTION_PROFILE;
import static org.folio.rest.jaxrs.model.ProfileType.JOB_PROFILE;
import static org.folio.rest.jaxrs.model.ProfileType.MAPPING_PROFILE;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasProperty;
Expand Down Expand Up @@ -202,6 +203,7 @@ public class CreateInvoiceEventHandlerTest extends ApiTestBase {
.withName("poLineId")
.withValue("RFF+LI[2]; else {POL_NUMBER}"),
new MappingRule().withPath("invoice.invoiceLines[].fundDistributions[]")
.withName("fundDistributions")
.withRepeatableFieldAction(MappingRule.RepeatableFieldAction.EXTEND_EXISTING)
.withValue("{POL_FUND_DISTRIBUTIONS}"),
new MappingRule().withPath("invoice.invoiceLines[].subTotal")
Expand Down Expand Up @@ -761,6 +763,99 @@ public void shouldMatchPoLineByPoLineNumberAndLeaveEmptyInvoiceLineFundDistribut
assertNull(createdInvoiceLines.getInvoiceLines().get(2).getFundDistributions().get(0).getExpenseClassId());
}

@Test
void shouldCreateInvoiceLinesWithFundDistributionsWhenFundDistributionMappingIsSpecified() throws InterruptedException {
// given
String expectedFundId = "7fbd5d84-62d1-44c6-9c45-6cb173998bbd";
String expectedFundCode = "AFRICAHIST";
double expectedDistributionValue = 100;

MappingProfile mappingProfileWithFundDistribution = new MappingProfile()
.withId(UUID.randomUUID().toString())
.withIncomingRecordType(EDIFACT_INVOICE)
.withExistingRecordType(INVOICE)
.withMappingDetails(new MappingDetail()
.withMappingFields(List.of(
new MappingRule().withPath("invoice.vendorInvoiceNo").withValue("BGM+380+[1]").withEnabled("true"),
new MappingRule().withPath("invoice.currency").withValue("CUX+2[2]").withEnabled("true"),
new MappingRule().withPath("invoice.status").withValue("\"Open\"").withEnabled("true"),
new MappingRule().withPath("invoice.invoiceLines[]").withEnabled("true").withName("invoiceLines")
.withRepeatableFieldAction(MappingRule.RepeatableFieldAction.EXTEND_EXISTING)
.withSubfields(List.of(new RepeatableSubfieldMapping()
.withOrder(0)
.withPath("invoice.invoiceLines[]")
.withFields(List.of(
new MappingRule().withPath("invoice.invoiceLines[].subTotal")
.withValue("MOA+203[2]"),
new MappingRule().withPath("invoice.invoiceLines[].quantity")
.withValue("QTY+47[2]"),
new MappingRule().withPath("invoice.invoiceLines[].fundDistributions[]")
.withName("fundDistributions")
.withRepeatableFieldAction(MappingRule.RepeatableFieldAction.EXTEND_EXISTING)
.withSubfields(List.of(new RepeatableSubfieldMapping()
.withOrder(0)
.withPath("invoice.invoiceLines[].fundDistributions[]")
.withFields(List.of(
new MappingRule().withPath("invoice.invoiceLines[].fundDistributions[].fundId")
.withName("fundId")
.withValue("\"African (History) (AFRICAHIST)\"")
.withAcceptedValues(new HashMap<>(Map.of(expectedFundId, "African (History) (AFRICAHIST)"))),
new MappingRule().withPath("invoice.invoiceLines[].fundDistributions[].value").withValue("\"100\""),
new MappingRule().withPath("invoice.invoiceLines[].fundDistributions[].distributionType")
.withValue("\"percentage\"")
)))))))))));

Record record = new Record().withParsedRecord(new ParsedRecord().withContent(edifactParsedContent)).withId(RECORD_ID);
ProfileSnapshotWrapper profileSnapshotWrapper = buildProfileSnapshotWrapper(jobProfile, actionProfile, mappingProfileWithFundDistribution);
addMockEntry(JOB_PROFILE_SNAPSHOTS_MOCK, profileSnapshotWrapper);
String testId = UUID.randomUUID().toString();

HashMap<String, String> payloadContext = new HashMap<>();
payloadContext.put(EDIFACT_INVOICE.value(), Json.encode(record));
payloadContext.put(JOB_PROFILE_SNAPSHOT_ID_KEY, profileSnapshotWrapper.getId());
payloadContext.put("testId", testId);

DataImportEventPayload dataImportEventPayload = new DataImportEventPayload()
.withEventType(DI_INCOMING_EDIFACT_RECORD_PARSED.value())
.withTenant(DI_POST_INVOICE_LINES_SUCCESS_TENANT)
.withOkapiUrl(OKAPI_URL)
.withToken(TOKEN)
.withContext(payloadContext);

String topic = KafkaTopicNameHelper.formatTopicName(KAFKA_ENV_VALUE, getDefaultNameSpace(), DI_POST_INVOICE_LINES_SUCCESS_TENANT, dataImportEventPayload.getEventType());
Event event = new Event().withEventPayload(Json.encode(dataImportEventPayload));
KeyValue<String, String> kafkaRecord = new KeyValue<>("test-key", Json.encode(event));
kafkaRecord.addHeader(RECORD_ID_HEADER, record.getId(), UTF_8);
SendKeyValues<String, String> request = SendKeyValues.to(topic, Collections.singletonList(kafkaRecord))
.useDefaults();

// when
kafkaCluster.send(request);

// then
String topicToObserve = KafkaTopicNameHelper.formatTopicName(KAFKA_ENV_VALUE, getDefaultNameSpace(), DI_POST_INVOICE_LINES_SUCCESS_TENANT, DI_COMPLETED.value());

List<String> observedValues = observeValuesAndFilterByTestId(testId, topicToObserve, 1);

Event obtainedEvent = Json.decodeValue(observedValues.get(0), Event.class);
DataImportEventPayload eventPayload = Json.decodeValue(obtainedEvent.getEventPayload(), DataImportEventPayload.class);
assertEquals(DI_INVOICE_CREATED.value(), eventPayload.getEventsChain().get(eventPayload.getEventsChain().size() -1));

assertNotNull(eventPayload.getContext().get(INVOICE.value()));
assertNotNull(eventPayload.getContext().get(INVOICE_LINES_KEY));
InvoiceLineCollection createdInvoiceLines = Json.decodeValue(eventPayload.getContext().get(INVOICE_LINES_KEY), InvoiceLineCollection.class);
assertEquals(3, createdInvoiceLines.getTotalRecords());
assertEquals(3, createdInvoiceLines.getInvoiceLines().size());

createdInvoiceLines.getInvoiceLines().forEach(invLine -> {
assertEquals(1, invLine.getFundDistributions().size());
assertEquals(expectedFundId, invLine.getFundDistributions().get(0).getFundId());
assertEquals(expectedFundCode, invLine.getFundDistributions().get(0).getCode());
assertEquals(expectedDistributionValue, invLine.getFundDistributions().get(0).getValue());
assertEquals(PERCENTAGE, invLine.getFundDistributions().get(0).getDistributionType());
});
}

@Test
public void shouldPublishDiErrorEventWhenHasNoSourceRecord() throws InterruptedException {
// given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
import static org.folio.rest.impl.MockServer.addMockEntry;
import static org.folio.rest.jaxrs.model.EntityType.EDIFACT_INVOICE;
import static org.folio.rest.jaxrs.model.EntityType.INVOICE;
import static org.folio.rest.jaxrs.model.ProfileSnapshotWrapper.ContentType.ACTION_PROFILE;
import static org.folio.rest.jaxrs.model.ProfileSnapshotWrapper.ContentType.JOB_PROFILE;
import static org.folio.rest.jaxrs.model.ProfileSnapshotWrapper.ContentType.MAPPING_PROFILE;
import static org.folio.rest.jaxrs.model.ProfileType.ACTION_PROFILE;
import static org.folio.rest.jaxrs.model.ProfileType.JOB_PROFILE;
import static org.folio.rest.jaxrs.model.ProfileType.MAPPING_PROFILE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
Expand Down

0 comments on commit a8271f4

Please sign in to comment.