From a01605b7357b2812f79df3a3fb98ec8556b1f019 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Wed, 11 Mar 2026 13:09:59 +0100 Subject: [PATCH 01/76] Update submodule --- .gitmodules | 3 ++- folio-export-common | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 214ef69b3..b9822923a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "folio-export-common"] path = folio-export-common - url = https://github.com/folio-org/folio-export-common.git + url = https://github.com/slub/folio-export-common.git + branch = UIORGS-477 diff --git a/folio-export-common b/folio-export-common index d98ac2205..f38bdd225 160000 --- a/folio-export-common +++ b/folio-export-common @@ -1 +1 @@ -Subproject commit d98ac22051c12c7bbfa5f099f67b8222e6feca0e +Subproject commit f38bdd2258921294f59430e5cb1dafcbada47814 From 4dff439f9efd61a0d66873c7cb3505380b91fa09 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Wed, 11 Mar 2026 16:02:04 +0100 Subject: [PATCH 02/76] Add implementation to send emails --- pom.xml | 29 +++++++++ .../jobs/EdifactExportJobConfig.java | 43 ++++++++----- .../jobs/MapToEdifactTasklet.java | 11 +++- .../acquisitions/jobs/SendToEmailTasklet.java | 63 +++++++++++++++++++ .../decider/SendToEmailTaskletDecider.java | 32 ++++++++++ .../org/folio/dew/client/EmailClient.java | 16 +++++ src/main/resources/swagger.api/email.yaml | 11 ++++ 7 files changed, 187 insertions(+), 18 deletions(-) create mode 100644 src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java create mode 100644 src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java create mode 100644 src/main/java/org/folio/dew/client/EmailClient.java create mode 100644 src/main/resources/swagger.api/email.yaml diff --git a/pom.xml b/pom.xml index dd574224e..9efff5f3a 100644 --- a/pom.xml +++ b/pom.xml @@ -47,6 +47,9 @@ ${project.basedir}/src/main/resources/swagger.api/refresh-presigned-url.yaml + + ${project.basedir}/src/main/resources/swagger.api/email.yaml + 10.0.0-SNAPSHOT 4.1.1 9.0.0 @@ -587,6 +590,32 @@ + + email-worker + + generate + + + ${email.yaml.file} + ${project.build.directory}/generated-sources + spring + ${project.groupId}.dew.domain.dto.email + false + false + false + true + false + true + true + ApiUtil.java + + true + java + true + true + + + diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/EdifactExportJobConfig.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/EdifactExportJobConfig.java index 2007a6c1b..50f1b4a79 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/EdifactExportJobConfig.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/EdifactExportJobConfig.java @@ -7,6 +7,7 @@ import org.folio.dew.batch.acquisitions.jobs.decider.ExportHistoryTaskletDecider; import org.folio.dew.batch.acquisitions.jobs.decider.SaveToFileStorageTaskletDecider; +import org.folio.dew.batch.acquisitions.jobs.decider.SendToEmailTaskletDecider; import org.folio.dew.domain.dto.ExportType; import org.springframework.batch.core.Job; import org.springframework.batch.core.Step; @@ -36,25 +37,24 @@ private Job constructEdifactExportJob(JobBuilder jobBuilder, Step mapToFileStep, Step saveToMinIOStep, Step saveToFTPStep, + Step sendToEmailStep, Step createExportHistoryRecordsStep, Map optionalStepDeciders) { var ftpStepDecider = optionalStepDeciders.get(saveToFTPStep.getName()); + var emailStepDecider = optionalStepDeciders.get(sendToEmailStep.getName()); var exportHistoryStepDecider = optionalStepDeciders.get(createExportHistoryRecordsStep.getName()); return jobBuilder.incrementer(new RunIdIncrementer()) .listener(ediExportJobCompletionListener) .start(mapToFileStep) .next(saveToMinIOStep) - // FTP and Export History are optional independent steps when the integration type is "Ordering" - // both FTP and/or Export History can be enabled, whereas when integration type is "Claiming" - // only the FTP can be used when the transmission method is "FTP". The syntax below tries to - // account for all branching choices within the limit of Spring Batch conditional flow .next(ftpStepDecider) .on(PROCESS.getStatus()).to(saveToFTPStep) - .from(saveToFTPStep) - .next(exportHistoryStepDecider) - .on(PROCESS.getStatus()).to(createExportHistoryRecordsStep) - .from(exportHistoryStepDecider).on(SKIP.getStatus()).end() - .from(ftpStepDecider).on(SKIP.getStatus()).to(exportHistoryStepDecider) + .from(saveToFTPStep).on("*").to(emailStepDecider) + .from(ftpStepDecider).on(SKIP.getStatus()).to(emailStepDecider) + .next(emailStepDecider) + .on(PROCESS.getStatus()).to(sendToEmailStep) + .from(sendToEmailStep).on("*").to(exportHistoryStepDecider) + .from(emailStepDecider).on(SKIP.getStatus()).to(exportHistoryStepDecider) .next(exportHistoryStepDecider) .on(PROCESS.getStatus()).to(createExportHistoryRecordsStep) .from(exportHistoryStepDecider).on(SKIP.getStatus()).end() @@ -64,18 +64,18 @@ private Job constructEdifactExportJob(JobBuilder jobBuilder, @Bean public Job edifactOrdersExportJob(EdiExportJobCompletionListener ediExportJobCompletionListener, JobRepository jobRepository, - Step mapToEdifactOrdersStep, Step saveToMinIOStep, Step saveToFTPStep, Step createExportHistoryRecordsStep, - Map deciders) { + Step mapToEdifactOrdersStep, Step saveToMinIOStep, Step saveToFTPStep, Step sendToEmailStep, + Step createExportHistoryRecordsStep, Map deciders) { return constructEdifactExportJob(new JobBuilder(ExportType.EDIFACT_ORDERS_EXPORT.getValue(), jobRepository), - ediExportJobCompletionListener, mapToEdifactOrdersStep, saveToMinIOStep, saveToFTPStep, createExportHistoryRecordsStep, deciders); + ediExportJobCompletionListener, mapToEdifactOrdersStep, saveToMinIOStep, saveToFTPStep, sendToEmailStep, createExportHistoryRecordsStep, deciders); } @Bean public Job edifactClaimsExportJob(EdiExportJobCompletionListener ediExportJobCompletionListener, JobRepository jobRepository, - Step mapToEdifactClaimsStep, Step saveToMinIOStep, Step saveToFTPStep, Step createExportHistoryRecordsStep, - Map deciders) { + Step mapToEdifactClaimsStep, Step saveToMinIOStep, Step saveToFTPStep, Step sendToEmailStep, + Step createExportHistoryRecordsStep, Map deciders) { return constructEdifactExportJob(new JobBuilder(ExportType.CLAIMS.getValue(), jobRepository), - ediExportJobCompletionListener, mapToEdifactClaimsStep, saveToMinIOStep, saveToFTPStep, createExportHistoryRecordsStep, deciders); + ediExportJobCompletionListener, mapToEdifactClaimsStep, saveToMinIOStep, saveToFTPStep, sendToEmailStep, createExportHistoryRecordsStep, deciders); } @Bean @@ -110,6 +110,14 @@ public Step saveToFTPStep(SaveToFileStorageTasklet saveToFileStorageTasklet, Job .build(); } + @Bean + public Step sendToEmailStep(SendToEmailTasklet sendToEmailTasklet, JobRepository jobRepository, + PlatformTransactionManager transactionManager) { + return new StepBuilder("sendToEmailStep", jobRepository) + .tasklet(sendToEmailTasklet, transactionManager) + .build(); + } + @Bean public Step createExportHistoryRecordsStep(ExportHistoryTasklet exportHistoryTasklet, JobRepository jobRepository, PlatformTransactionManager transactionManager) { @@ -119,11 +127,14 @@ public Step createExportHistoryRecordsStep(ExportHistoryTasklet exportHistoryTas } @Bean - public Map optionalStepDeciders(Step saveToFTPStep, Step createExportHistoryRecordsStep, + public Map optionalStepDeciders(Step saveToFTPStep, Step sendToEmailStep, + Step createExportHistoryRecordsStep, ObjectMapper objectMapper) { return Map.ofEntries( Map.entry(saveToFTPStep.getName(), new SaveToFileStorageTaskletDecider(objectMapper, saveToFTPStep.getName())), + Map.entry(sendToEmailStep.getName(), + new SendToEmailTaskletDecider(objectMapper, sendToEmailStep.getName())), Map.entry(createExportHistoryRecordsStep.getName(), new ExportHistoryTaskletDecider(objectMapper, createExportHistoryRecordsStep.getName())) ); diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactTasklet.java index fa6156951..015fc8cf1 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactTasklet.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactTasklet.java @@ -46,6 +46,8 @@ @Log4j2 public abstract class MapToEdifactTasklet implements Tasklet { + private final ExportResourceMapper edifactMapper; + private final ExportResourceMapper csvMapper; private final ObjectMapper ediObjectMapper; private final OrganizationsService organizationsService; protected final OrdersService ordersService; @@ -131,8 +133,13 @@ private T convertTo(Object value, Class c) { } } - protected abstract ExportResourceMapper getExportResourceMapper(VendorEdiOrdersExportConfig ediOrdersExportConfig); - + protected ExportResourceMapper getExportResourceMapper(VendorEdiOrdersExportConfig ediOrdersExportConfig) { + return switch (ediOrdersExportConfig.getFileFormat()) { + case EDI -> edifactMapper; + case CSV -> csvMapper; + default -> edifactMapper; + }; + } protected abstract List getExportConfigMissingFields(VendorEdiOrdersExportConfig ediOrdersExportConfig); protected abstract ExportHolder buildEdifactExportHolder(VendorEdiOrdersExportConfig ediExportConfig, Map jobParameters) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java new file mode 100644 index 000000000..cf17301ed --- /dev/null +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java @@ -0,0 +1,63 @@ +package org.folio.dew.batch.acquisitions.jobs; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; +import org.folio.dew.batch.ExecutionContextUtils; +import org.folio.dew.batch.acquisitions.services.FTPStorageService; +import org.folio.dew.client.EmailClient; +import org.folio.dew.domain.dto.VendorEdiOrdersExportConfig; +import org.folio.dew.domain.dto.email.Attachment; +import org.folio.dew.domain.dto.email.EmailEntity; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; + +import static org.folio.dew.domain.dto.JobParameterNames.*; + +@Log4j2 +@StepScope +@Component +@RequiredArgsConstructor +public class SendToEmailTasklet implements Tasklet { + + private final ObjectMapper ediObjectMapper; + private final EmailClient emailClient; + + @Override + @SneakyThrows + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { + var exportConfig = ediObjectMapper.readValue((String) chunkContext.getStepContext().getJobParameters().get(EDIFACT_ORDERS_EXPORT), VendorEdiOrdersExportConfig.class); + var jobId = (String) chunkContext.getStepContext().getJobParameters().get(JOB_ID); + + var emailEntity = new EmailEntity(); + emailEntity.setNotificationId(jobId); + emailEntity.setFrom(exportConfig.getEdiEmail().getEmailFrom()); + emailEntity.setTo(exportConfig.getEdiEmail().getEmailTo()); + emailEntity.setHeader("Testsubject"); + emailEntity.setBody("Kleiner Testbody"); + + var stepExecution = chunkContext.getStepContext().getStepExecution(); + var fileName = (String) ExecutionContextUtils.getExecutionVariable(stepExecution, ACQ_EXPORT_FILE_NAME); + var edifactOrderAsString = (String) ExecutionContextUtils.getExecutionVariable(stepExecution, ACQ_EXPORT_FILE); + + var attachment = new Attachment(); + attachment.setName(fileName); + attachment.setData(java.util.Base64.getEncoder().encodeToString(edifactOrderAsString.getBytes(StandardCharsets.UTF_8))); + attachment.setContentType("application/EDIFACT"); + + emailEntity.setAttachments(java.util.List.of(attachment)); + + emailClient.sendEmail(emailEntity); + + return RepeatStatus.FINISHED; + } + +} diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java new file mode 100644 index 000000000..b970204b8 --- /dev/null +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java @@ -0,0 +1,32 @@ +package org.folio.dew.batch.acquisitions.jobs.decider; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.log4j.Log4j2; +import org.folio.dew.domain.dto.VendorEdiOrdersExportConfig; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.StepExecution; + +import static org.folio.dew.domain.dto.VendorEdiOrdersExportConfig.IntegrationTypeEnum.ORDERING; +import static org.folio.dew.domain.dto.VendorEdiOrdersExportConfig.TransmissionMethodEnum.EMAIL; + + +@Log4j2 +public class SendToEmailTaskletDecider extends ExportStepDecider { + + public SendToEmailTaskletDecider(ObjectMapper objectMapper, String stepName) { + super(objectMapper, stepName); + } + + @Override + public ExportStepDecision decide(VendorEdiOrdersExportConfig exportConfig, JobExecution jobExecution, StepExecution stepExecution) { + // Always execute if the integration type is ORDERING, or execute for other integration type if the transmission method is Email + if (exportConfig.getIntegrationType() == ORDERING || exportConfig.getTransmissionMethod() == EMAIL) { + log.info("decide:: Processing step: {}", stepName); + return ExportStepDecision.PROCESS; + } + log.info("decide:: Integration type is not ORDERING or Transmission method is not FTP, skipping the step: {}", stepName); + return ExportStepDecision.SKIP; + } + +} diff --git a/src/main/java/org/folio/dew/client/EmailClient.java b/src/main/java/org/folio/dew/client/EmailClient.java new file mode 100644 index 000000000..9113c7ce7 --- /dev/null +++ b/src/main/java/org/folio/dew/client/EmailClient.java @@ -0,0 +1,16 @@ +package org.folio.dew.client; + +import org.folio.dew.config.feign.FeignClientConfiguration; +import org.folio.dew.domain.dto.email.EmailEntity; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +@FeignClient(name = "email", configuration = FeignClientConfiguration.class) +public interface EmailClient { + + @PostMapping(value = "/email", consumes = MediaType.APPLICATION_JSON_VALUE) + void sendEmail(@RequestBody EmailEntity email); + +} \ No newline at end of file diff --git a/src/main/resources/swagger.api/email.yaml b/src/main/resources/swagger.api/email.yaml new file mode 100644 index 000000000..e63e43545 --- /dev/null +++ b/src/main/resources/swagger.api/email.yaml @@ -0,0 +1,11 @@ +openapi: 3.0.0 +info: + title: Email + version: v1 +paths: {} +components: + schemas: + EmailEntity: + $ref: '../../../../folio-export-common/schemas/email/email_entity.json' + Attachment: + $ref: '../../../../folio-export-common/schemas/email/attachment.json' From b4d94e7bd1778d4e86e029c32f332c504b661fbf Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Wed, 11 Mar 2026 16:16:53 +0100 Subject: [PATCH 03/76] Update submodule --- .gitmodules | 2 +- folio-export-common | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index b9822923a..e45ec8d54 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "folio-export-common"] path = folio-export-common url = https://github.com/slub/folio-export-common.git - branch = UIORGS-477 + branch = send-email diff --git a/folio-export-common b/folio-export-common index f38bdd225..3f5d46830 160000 --- a/folio-export-common +++ b/folio-export-common @@ -1 +1 @@ -Subproject commit f38bdd2258921294f59430e5cb1dafcbada47814 +Subproject commit 3f5d46830d74137fd248fdb84bb6225965a8f5f8 From 055fa21dbcb1ea35554d7549e8641abe0b63f745 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Wed, 11 Mar 2026 20:04:17 +0100 Subject: [PATCH 04/76] Improvement to compile module --- .../dew/batch/acquisitions/jobs/MapToEdifactTasklet.java | 9 --------- .../jobs/decider/SendToEmailTaskletDecider.java | 8 ++++---- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactTasklet.java index 015fc8cf1..b8b7a42fa 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactTasklet.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactTasklet.java @@ -46,8 +46,6 @@ @Log4j2 public abstract class MapToEdifactTasklet implements Tasklet { - private final ExportResourceMapper edifactMapper; - private final ExportResourceMapper csvMapper; private final ObjectMapper ediObjectMapper; private final OrganizationsService organizationsService; protected final OrdersService ordersService; @@ -133,13 +131,6 @@ private T convertTo(Object value, Class c) { } } - protected ExportResourceMapper getExportResourceMapper(VendorEdiOrdersExportConfig ediOrdersExportConfig) { - return switch (ediOrdersExportConfig.getFileFormat()) { - case EDI -> edifactMapper; - case CSV -> csvMapper; - default -> edifactMapper; - }; - } protected abstract List getExportConfigMissingFields(VendorEdiOrdersExportConfig ediOrdersExportConfig); protected abstract ExportHolder buildEdifactExportHolder(VendorEdiOrdersExportConfig ediExportConfig, Map jobParameters) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java index b970204b8..bc1100c5e 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java @@ -7,7 +7,7 @@ import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.StepExecution; -import static org.folio.dew.domain.dto.VendorEdiOrdersExportConfig.IntegrationTypeEnum.ORDERING; +import static org.folio.dew.domain.dto.VendorEdiOrdersExportConfig.IntegrationTypeEnum.CLAIMING; import static org.folio.dew.domain.dto.VendorEdiOrdersExportConfig.TransmissionMethodEnum.EMAIL; @@ -20,12 +20,12 @@ public SendToEmailTaskletDecider(ObjectMapper objectMapper, String stepName) { @Override public ExportStepDecision decide(VendorEdiOrdersExportConfig exportConfig, JobExecution jobExecution, StepExecution stepExecution) { - // Always execute if the integration type is ORDERING, or execute for other integration type if the transmission method is Email - if (exportConfig.getIntegrationType() == ORDERING || exportConfig.getTransmissionMethod() == EMAIL) { + // Always execute if the integration type is CLAIMING, or execute for other integration type if the transmission method is Email + if (exportConfig.getIntegrationType() == CLAIMING || exportConfig.getTransmissionMethod() == EMAIL) { log.info("decide:: Processing step: {}", stepName); return ExportStepDecision.PROCESS; } - log.info("decide:: Integration type is not ORDERING or Transmission method is not FTP, skipping the step: {}", stepName); + log.info("decide:: Integration type is not CLAIMING or Transmission method is not Email, skipping the step: {}", stepName); return ExportStepDecision.SKIP; } From 1dbf08c388d990e8f806379da290b088b23519bc Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Wed, 11 Mar 2026 20:10:53 +0100 Subject: [PATCH 05/76] Improvement to compile module --- .../dew/batch/acquisitions/jobs/MapToEdifactClaimsTasklet.java | 1 + .../folio/dew/batch/acquisitions/jobs/MapToEdifactTasklet.java | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactClaimsTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactClaimsTasklet.java index 226918afa..26ab818f3 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactClaimsTasklet.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactClaimsTasklet.java @@ -49,6 +49,7 @@ protected ExportResourceMapper getExportResourceMapper(VendorEdiOrdersExportConf return switch (ediOrdersExportConfig.getFileFormat()) { case EDI -> edifactMapper; case CSV -> csvMapper; + default -> edifactMapper; }; } diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactTasklet.java index b8b7a42fa..fa6156951 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactTasklet.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactTasklet.java @@ -131,6 +131,8 @@ private T convertTo(Object value, Class c) { } } + protected abstract ExportResourceMapper getExportResourceMapper(VendorEdiOrdersExportConfig ediOrdersExportConfig); + protected abstract List getExportConfigMissingFields(VendorEdiOrdersExportConfig ediOrdersExportConfig); protected abstract ExportHolder buildEdifactExportHolder(VendorEdiOrdersExportConfig ediExportConfig, Map jobParameters) From eeff6fd0b592942827504fdeb9a83034f8a6aa42 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Wed, 11 Mar 2026 20:44:35 +0100 Subject: [PATCH 06/76] Improvement to compile module --- src/main/java/org/folio/dew/client/EmailClient.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/folio/dew/client/EmailClient.java b/src/main/java/org/folio/dew/client/EmailClient.java index 9113c7ce7..def356bb2 100644 --- a/src/main/java/org/folio/dew/client/EmailClient.java +++ b/src/main/java/org/folio/dew/client/EmailClient.java @@ -1,7 +1,8 @@ package org.folio.dew.client; -import org.folio.dew.config.feign.FeignClientConfiguration; + import org.folio.dew.domain.dto.email.EmailEntity; +import org.folio.spring.config.FeignClientConfiguration; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PostMapping; @@ -13,4 +14,4 @@ public interface EmailClient { @PostMapping(value = "/email", consumes = MediaType.APPLICATION_JSON_VALUE) void sendEmail(@RequestBody EmailEntity email); -} \ No newline at end of file +} From 8cdaf4398f89dffed20a14507261776515b9dfb8 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Thu, 12 Mar 2026 10:37:53 +0100 Subject: [PATCH 07/76] Improve email implementation --- .../folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java | 2 -- src/main/java/org/folio/dew/client/EmailClient.java | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java index cf17301ed..2d45a7e51 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java @@ -4,9 +4,7 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.log4j.Log4j2; -import org.apache.commons.lang3.StringUtils; import org.folio.dew.batch.ExecutionContextUtils; -import org.folio.dew.batch.acquisitions.services.FTPStorageService; import org.folio.dew.client.EmailClient; import org.folio.dew.domain.dto.VendorEdiOrdersExportConfig; import org.folio.dew.domain.dto.email.Attachment; diff --git a/src/main/java/org/folio/dew/client/EmailClient.java b/src/main/java/org/folio/dew/client/EmailClient.java index def356bb2..8e2a13e71 100644 --- a/src/main/java/org/folio/dew/client/EmailClient.java +++ b/src/main/java/org/folio/dew/client/EmailClient.java @@ -2,13 +2,12 @@ import org.folio.dew.domain.dto.email.EmailEntity; -import org.folio.spring.config.FeignClientConfiguration; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -@FeignClient(name = "email", configuration = FeignClientConfiguration.class) +@FeignClient(name = "email", url = "${folio.email-url:http://localhost:8000}") public interface EmailClient { @PostMapping(value = "/email", consumes = MediaType.APPLICATION_JSON_VALUE) From 3e32a52e31653bae6fb4c81318b6fbb303a9379a Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Thu, 12 Mar 2026 18:57:43 +0100 Subject: [PATCH 08/76] Override Kafka-propagated x-okapi-url with worker's own Eureka sidecar URL for Feign routing --- .../java/org/folio/dew/client/EmailClient.java | 2 +- .../config/feign/FeignEncoderConfiguration.java | 3 ++- .../dew/service/JobCommandsReceiverService.java | 16 +++++++++++++++- src/main/resources/application.yml | 1 + 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/folio/dew/client/EmailClient.java b/src/main/java/org/folio/dew/client/EmailClient.java index 8e2a13e71..b74072452 100644 --- a/src/main/java/org/folio/dew/client/EmailClient.java +++ b/src/main/java/org/folio/dew/client/EmailClient.java @@ -7,7 +7,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -@FeignClient(name = "email", url = "${folio.email-url:http://localhost:8000}") +@FeignClient(name = "email") public interface EmailClient { @PostMapping(value = "/email", consumes = MediaType.APPLICATION_JSON_VALUE) diff --git a/src/main/java/org/folio/dew/config/feign/FeignEncoderConfiguration.java b/src/main/java/org/folio/dew/config/feign/FeignEncoderConfiguration.java index cdc2e395b..17cc5eaf2 100644 --- a/src/main/java/org/folio/dew/config/feign/FeignEncoderConfiguration.java +++ b/src/main/java/org/folio/dew/config/feign/FeignEncoderConfiguration.java @@ -6,7 +6,8 @@ import feign.jackson.JacksonEncoder; import org.springframework.context.annotation.Bean; -public class FeignEncoderConfiguration { +public class +FeignEncoderConfiguration { @Bean public Encoder feignEncoder() { return new JacksonEncoder(customFeignObjectMapper()); diff --git a/src/main/java/org/folio/dew/service/JobCommandsReceiverService.java b/src/main/java/org/folio/dew/service/JobCommandsReceiverService.java index 85dad6a81..3334a9a98 100644 --- a/src/main/java/org/folio/dew/service/JobCommandsReceiverService.java +++ b/src/main/java/org/folio/dew/service/JobCommandsReceiverService.java @@ -9,6 +9,8 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -54,6 +56,8 @@ public class JobCommandsReceiverService { @Value("${spring.application.name}") private String springApplicationName; + @Value("${folio.okapi-url:}") + private String okapiUrl; private String workDir; @PostConstruct @@ -73,7 +77,7 @@ public void postConstruct() { topicPattern = "${application.kafka.topic-pattern}", groupId = "${application.kafka.group-id}") public void receiveStartJobCommand(@Payload JobCommand jobCommand, @Headers Map messageHeaders) { - var defaultFolioExecutionContext = DefaultFolioExecutionContext.fromMessageHeaders(folioModuleMetadata, messageHeaders); + var defaultFolioExecutionContext = buildFolioExecutionContext(messageHeaders); try (var context = new FolioExecutionContextSetter(defaultFolioExecutionContext)) { log.info("Received {}.", jobCommand); @@ -108,6 +112,16 @@ public void receiveStartJobCommand(@Payload JobCommand jobCommand, @Headers Map< } } + private DefaultFolioExecutionContext buildFolioExecutionContext(Map messageHeaders) { + var baseContext = DefaultFolioExecutionContext.fromMessageHeaders(folioModuleMetadata, messageHeaders); + if (StringUtils.isBlank(okapiUrl)) { + return baseContext; + } + Map> headers = new HashMap<>(baseContext.getOkapiHeaders()); + headers.put("x-okapi-url", List.of(okapiUrl)); + return new DefaultFolioExecutionContext(folioModuleMetadata, headers); + } + private String resolveJobKey(JobCommand jobCommand) { return jobCommand.getExportType().toString(); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d3f4cdf85..9a136462a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -116,6 +116,7 @@ application: platform: ${PLATFORM:okapi} folio: + okapi-url: ${OKAPI_URL:http://mod-data-export-worker-sc.eureka:8081} tenant: validation: enabled: true From fae19f8fecf97144e2752e1f406590de78cbcfa7 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Thu, 12 Mar 2026 19:42:06 +0100 Subject: [PATCH 09/76] add logging --- .../dew/batch/acquisitions/jobs/SendToEmailTasklet.java | 4 ++++ .../batch/acquisitions/services/OrganizationsService.java | 2 -- .../folio/dew/config/feign/FeignEncoderConfiguration.java | 3 +-- .../org/folio/dew/service/JobCommandsReceiverService.java | 6 +++++- src/main/resources/application.yml | 1 + 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java index 2d45a7e51..36ef8cefc 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java @@ -9,6 +9,7 @@ import org.folio.dew.domain.dto.VendorEdiOrdersExportConfig; import org.folio.dew.domain.dto.email.Attachment; import org.folio.dew.domain.dto.email.EmailEntity; +import org.folio.spring.FolioExecutionContext; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.configuration.annotation.StepScope; import org.springframework.batch.core.scope.context.ChunkContext; @@ -28,6 +29,7 @@ public class SendToEmailTasklet implements Tasklet { private final ObjectMapper ediObjectMapper; private final EmailClient emailClient; + private final FolioExecutionContext folioExecutionContext; @Override @SneakyThrows @@ -53,6 +55,8 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon emailEntity.setAttachments(java.util.List.of(attachment)); + log.info("SendToEmailTasklet:: okapiUrl='{}', tenant='{}'", + folioExecutionContext.getOkapiUrl(), folioExecutionContext.getTenantId()); emailClient.sendEmail(emailEntity); return RepeatStatus.FINISHED; diff --git a/src/main/java/org/folio/dew/batch/acquisitions/services/OrganizationsService.java b/src/main/java/org/folio/dew/batch/acquisitions/services/OrganizationsService.java index d1fabb900..ebdd4a2bf 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/services/OrganizationsService.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/services/OrganizationsService.java @@ -18,6 +18,4 @@ public JsonNode getOrganizationById(String id) { return organizationsClient.getOrganizationById(id); } - - } diff --git a/src/main/java/org/folio/dew/config/feign/FeignEncoderConfiguration.java b/src/main/java/org/folio/dew/config/feign/FeignEncoderConfiguration.java index 17cc5eaf2..cdc2e395b 100644 --- a/src/main/java/org/folio/dew/config/feign/FeignEncoderConfiguration.java +++ b/src/main/java/org/folio/dew/config/feign/FeignEncoderConfiguration.java @@ -6,8 +6,7 @@ import feign.jackson.JacksonEncoder; import org.springframework.context.annotation.Bean; -public class -FeignEncoderConfiguration { +public class FeignEncoderConfiguration { @Bean public Encoder feignEncoder() { return new JacksonEncoder(customFeignObjectMapper()); diff --git a/src/main/java/org/folio/dew/service/JobCommandsReceiverService.java b/src/main/java/org/folio/dew/service/JobCommandsReceiverService.java index 3334a9a98..6cb67e1d7 100644 --- a/src/main/java/org/folio/dew/service/JobCommandsReceiverService.java +++ b/src/main/java/org/folio/dew/service/JobCommandsReceiverService.java @@ -114,12 +114,16 @@ public void receiveStartJobCommand(@Payload JobCommand jobCommand, @Headers Map< private DefaultFolioExecutionContext buildFolioExecutionContext(Map messageHeaders) { var baseContext = DefaultFolioExecutionContext.fromMessageHeaders(folioModuleMetadata, messageHeaders); + log.info("buildFolioExecutionContext:: configured okapiUrl='{}', baseContext.okapiUrl='{}'", + okapiUrl, baseContext.getOkapiUrl()); if (StringUtils.isBlank(okapiUrl)) { return baseContext; } Map> headers = new HashMap<>(baseContext.getOkapiHeaders()); headers.put("x-okapi-url", List.of(okapiUrl)); - return new DefaultFolioExecutionContext(folioModuleMetadata, headers); + var ctx = new DefaultFolioExecutionContext(folioModuleMetadata, headers); + log.info("buildFolioExecutionContext:: overridden context okapiUrl='{}'", ctx.getOkapiUrl()); + return ctx; } private String resolveJobKey(JobCommand jobCommand) { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9a136462a..a967e00c4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -123,5 +123,6 @@ folio: logging: level: org.folio.dew.client.*: debug + org.folio.spring.client: debug org.springframework.core.log.*: ERROR org.apache.kafka.*: ERROR From c64c9b5c8c14c34d8ca9f1d120542b5ea6c7fa7e Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Thu, 12 Mar 2026 19:49:51 +0100 Subject: [PATCH 10/76] add configuration --- src/main/java/org/folio/dew/client/EmailClient.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/folio/dew/client/EmailClient.java b/src/main/java/org/folio/dew/client/EmailClient.java index b74072452..def356bb2 100644 --- a/src/main/java/org/folio/dew/client/EmailClient.java +++ b/src/main/java/org/folio/dew/client/EmailClient.java @@ -2,12 +2,13 @@ import org.folio.dew.domain.dto.email.EmailEntity; +import org.folio.spring.config.FeignClientConfiguration; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -@FeignClient(name = "email") +@FeignClient(name = "email", configuration = FeignClientConfiguration.class) public interface EmailClient { @PostMapping(value = "/email", consumes = MediaType.APPLICATION_JSON_VALUE) From d403ebae80b5b96101694223c917e4fe0fd7086e Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Thu, 12 Mar 2026 19:59:11 +0100 Subject: [PATCH 11/76] Update Client --- src/main/java/org/folio/dew/client/EmailClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/folio/dew/client/EmailClient.java b/src/main/java/org/folio/dew/client/EmailClient.java index def356bb2..d39f0b2d6 100644 --- a/src/main/java/org/folio/dew/client/EmailClient.java +++ b/src/main/java/org/folio/dew/client/EmailClient.java @@ -8,7 +8,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -@FeignClient(name = "email", configuration = FeignClientConfiguration.class) +@FeignClient(name = "email", url = "http://email", configuration = FeignClientConfiguration.class) public interface EmailClient { @PostMapping(value = "/email", consumes = MediaType.APPLICATION_JSON_VALUE) From cce9aefed68e68b712c97d6899353a9ff66274a9 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Thu, 12 Mar 2026 20:40:12 +0100 Subject: [PATCH 12/76] Update client config --- src/main/java/org/folio/dew/client/EmailClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/folio/dew/client/EmailClient.java b/src/main/java/org/folio/dew/client/EmailClient.java index d39f0b2d6..f187b1c0b 100644 --- a/src/main/java/org/folio/dew/client/EmailClient.java +++ b/src/main/java/org/folio/dew/client/EmailClient.java @@ -1,8 +1,8 @@ package org.folio.dew.client; +import org.folio.dew.config.feign.FeignClientConfiguration; import org.folio.dew.domain.dto.email.EmailEntity; -import org.folio.spring.config.FeignClientConfiguration; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PostMapping; From 98ec7c95bef195720e6d2144e1d617a4087bd58a Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Fri, 13 Mar 2026 11:58:32 +0100 Subject: [PATCH 13/76] Update emailclient --- src/main/java/org/folio/dew/client/EmailClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/folio/dew/client/EmailClient.java b/src/main/java/org/folio/dew/client/EmailClient.java index f187b1c0b..34d044395 100644 --- a/src/main/java/org/folio/dew/client/EmailClient.java +++ b/src/main/java/org/folio/dew/client/EmailClient.java @@ -8,10 +8,10 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -@FeignClient(name = "email", url = "http://email", configuration = FeignClientConfiguration.class) +@FeignClient(name = "email", configuration = FeignClientConfiguration.class) public interface EmailClient { - @PostMapping(value = "/email", consumes = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) void sendEmail(@RequestBody EmailEntity email); } From 3b860bd7d13a1241da9917aefcd500ee2c37bf07 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Fri, 13 Mar 2026 12:51:22 +0100 Subject: [PATCH 14/76] Add template request --- .../acquisitions/jobs/SendToEmailTasklet.java | 60 +++++++++++++++---- .../dew/client/TemplateEngineClient.java | 17 ++++++ .../TemplateProcessingRequest.java | 28 +++++++++ 3 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 src/main/java/org/folio/dew/client/TemplateEngineClient.java create mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingRequest.java diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java index 36ef8cefc..3968a3e0d 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java @@ -1,14 +1,17 @@ package org.folio.dew.batch.acquisitions.jobs; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.log4j.Log4j2; import org.folio.dew.batch.ExecutionContextUtils; import org.folio.dew.client.EmailClient; +import org.folio.dew.client.TemplateEngineClient; import org.folio.dew.domain.dto.VendorEdiOrdersExportConfig; import org.folio.dew.domain.dto.email.Attachment; import org.folio.dew.domain.dto.email.EmailEntity; +import org.folio.dew.domain.dto.templateengine.TemplateProcessingRequest; import org.folio.spring.FolioExecutionContext; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.configuration.annotation.StepScope; @@ -18,6 +21,10 @@ import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.util.Base64; +import java.util.List; +import java.util.Optional; import static org.folio.dew.domain.dto.JobParameterNames.*; @@ -29,31 +36,35 @@ public class SendToEmailTasklet implements Tasklet { private final ObjectMapper ediObjectMapper; private final EmailClient emailClient; + private final TemplateEngineClient templateEngineClient; private final FolioExecutionContext folioExecutionContext; @Override @SneakyThrows public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { - var exportConfig = ediObjectMapper.readValue((String) chunkContext.getStepContext().getJobParameters().get(EDIFACT_ORDERS_EXPORT), VendorEdiOrdersExportConfig.class); + var exportConfig = ediObjectMapper.readValue( + (String) chunkContext.getStepContext().getJobParameters().get(EDIFACT_ORDERS_EXPORT), + VendorEdiOrdersExportConfig.class); var jobId = (String) chunkContext.getStepContext().getJobParameters().get(JOB_ID); + var stepExecution = chunkContext.getStepContext().getStepExecution(); + var fileName = (String) ExecutionContextUtils.getExecutionVariable(stepExecution, ACQ_EXPORT_FILE_NAME); + var edifactOrderAsString = (String) ExecutionContextUtils.getExecutionVariable(stepExecution, ACQ_EXPORT_FILE); + var emailEntity = new EmailEntity(); emailEntity.setNotificationId(jobId); emailEntity.setFrom(exportConfig.getEdiEmail().getEmailFrom()); emailEntity.setTo(exportConfig.getEdiEmail().getEmailTo()); - emailEntity.setHeader("Testsubject"); - emailEntity.setBody("Kleiner Testbody"); - var stepExecution = chunkContext.getStepContext().getStepExecution(); - var fileName = (String) ExecutionContextUtils.getExecutionVariable(stepExecution, ACQ_EXPORT_FILE_NAME); - var edifactOrderAsString = (String) ExecutionContextUtils.getExecutionVariable(stepExecution, ACQ_EXPORT_FILE); + var templateResult = resolveTemplate(exportConfig, fileName); + emailEntity.setHeader(templateResult[0]); + emailEntity.setBody(templateResult[1]); var attachment = new Attachment(); attachment.setName(fileName); - attachment.setData(java.util.Base64.getEncoder().encodeToString(edifactOrderAsString.getBytes(StandardCharsets.UTF_8))); + attachment.setData(Base64.getEncoder().encodeToString(edifactOrderAsString.getBytes(StandardCharsets.UTF_8))); attachment.setContentType("application/EDIFACT"); - - emailEntity.setAttachments(java.util.List.of(attachment)); + emailEntity.setAttachments(List.of(attachment)); log.info("SendToEmailTasklet:: okapiUrl='{}', tenant='{}'", folioExecutionContext.getOkapiUrl(), folioExecutionContext.getTenantId()); @@ -62,4 +73,33 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon return RepeatStatus.FINISHED; } -} + private String[] resolveTemplate(VendorEdiOrdersExportConfig exportConfig, String fileName) { + var templateId = Optional.ofNullable(exportConfig.getEdiEmail()) + .map(e -> e.getEmailTemplate()) + .orElse(null); + + if (templateId == null) { + log.warn("SendToEmailTasklet:: no emailTemplate configured, using empty subject and body"); + return new String[]{"", ""}; + } + + int pieceCount = Optional.ofNullable(exportConfig.getClaimPieceIds()) + .map(List::size) + .orElse(0); + + var request = TemplateProcessingRequest.builder() + .templateId(templateId) + .context(TemplateProcessingRequest.ClaimsContext.builder() + .configName(exportConfig.getConfigName()) + .fileName(fileName) + .exportDate(LocalDate.now().toString()) + .pieceCount(pieceCount) + .build()) + .build(); + + JsonNode response = templateEngineClient.processTemplate(request); + JsonNode result = response.path("result"); + return new String[]{result.path("header").asText(""), result.path("body").asText("")}; + } + +} \ No newline at end of file diff --git a/src/main/java/org/folio/dew/client/TemplateEngineClient.java b/src/main/java/org/folio/dew/client/TemplateEngineClient.java new file mode 100644 index 000000000..cb5f26140 --- /dev/null +++ b/src/main/java/org/folio/dew/client/TemplateEngineClient.java @@ -0,0 +1,17 @@ +package org.folio.dew.client; + +import com.fasterxml.jackson.databind.JsonNode; +import org.folio.dew.config.feign.FeignClientConfiguration; +import org.folio.dew.domain.dto.templateengine.TemplateProcessingRequest; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +@FeignClient(name = "template-engine", configuration = FeignClientConfiguration.class) +public interface TemplateEngineClient { + + @PostMapping(value = "/template-request", consumes = MediaType.APPLICATION_JSON_VALUE) + JsonNode processTemplate(@RequestBody TemplateProcessingRequest request); + +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingRequest.java b/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingRequest.java new file mode 100644 index 000000000..350c70779 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingRequest.java @@ -0,0 +1,28 @@ +package org.folio.dew.domain.dto.templateengine; + +import lombok.Builder; +import lombok.Data; + +import java.util.UUID; + +@Data +@Builder +public class TemplateProcessingRequest { + + private UUID templateId; + @Builder.Default + private String lang = "en"; + @Builder.Default + private String outputFormat = "text/plain"; + private ClaimsContext context; + + @Data + @Builder + public static class ClaimsContext { + private String configName; + private String fileName; + private String exportDate; + private int pieceCount; + } + +} \ No newline at end of file From b11579bb64dbea22ac7dce45981516e616915afc Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Fri, 13 Mar 2026 13:16:33 +0100 Subject: [PATCH 15/76] Update template engline --- .../dew/batch/acquisitions/jobs/SendToEmailTasklet.java | 6 +++++- .../java/org/folio/dew/client/TemplateEngineClient.java | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java index 3968a3e0d..b15a256ba 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java @@ -97,9 +97,13 @@ private String[] resolveTemplate(VendorEdiOrdersExportConfig exportConfig, Strin .build()) .build(); + log.info("SendToEmailTasklet:: calling template-engine with templateId='{}', configName='{}', fileName='{}', pieceCount={}", + templateId, exportConfig.getConfigName(), fileName, pieceCount); JsonNode response = templateEngineClient.processTemplate(request); JsonNode result = response.path("result"); + log.info("SendToEmailTasklet:: template-engine response: header='{}', body='{}'", + result.path("header").asText(""), result.path("body").asText("")); return new String[]{result.path("header").asText(""), result.path("body").asText("")}; } -} \ No newline at end of file +} diff --git a/src/main/java/org/folio/dew/client/TemplateEngineClient.java b/src/main/java/org/folio/dew/client/TemplateEngineClient.java index cb5f26140..12828c4d2 100644 --- a/src/main/java/org/folio/dew/client/TemplateEngineClient.java +++ b/src/main/java/org/folio/dew/client/TemplateEngineClient.java @@ -8,10 +8,10 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -@FeignClient(name = "template-engine", configuration = FeignClientConfiguration.class) +@FeignClient(name = "template-request", configuration = FeignClientConfiguration.class) public interface TemplateEngineClient { - @PostMapping(value = "/template-request", consumes = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) JsonNode processTemplate(@RequestBody TemplateProcessingRequest request); } From 0348c224da12495b17dae22781870707fe15875f Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Fri, 13 Mar 2026 13:39:19 +0100 Subject: [PATCH 16/76] fix(template-engine): change default outputFormat to text/html mod-template-engine returns 400 when outputFormat does not match the format registered in the template. The claims template requires text/html. --- .../domain/dto/templateengine/TemplateProcessingRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingRequest.java b/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingRequest.java index 350c70779..124202d9b 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingRequest.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingRequest.java @@ -13,7 +13,7 @@ public class TemplateProcessingRequest { @Builder.Default private String lang = "en"; @Builder.Default - private String outputFormat = "text/plain"; + private String outputFormat = "text/html"; private ClaimsContext context; @Data From 39425f0f7bf91ae9de6520cb6d6860e947eeb30d Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Fri, 13 Mar 2026 13:52:04 +0100 Subject: [PATCH 17/76] propagate template outputFormat to email entity Read outputFormat from template-engine response meta and forward it to EmailEntity so the email is sent in the same format the template was rendered in (e.g. text/html). --- .../batch/acquisitions/jobs/SendToEmailTasklet.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java index b15a256ba..02432f00e 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java @@ -59,6 +59,7 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon var templateResult = resolveTemplate(exportConfig, fileName); emailEntity.setHeader(templateResult[0]); emailEntity.setBody(templateResult[1]); + emailEntity.setOutputFormat(templateResult[2]); var attachment = new Attachment(); attachment.setName(fileName); @@ -80,7 +81,7 @@ private String[] resolveTemplate(VendorEdiOrdersExportConfig exportConfig, Strin if (templateId == null) { log.warn("SendToEmailTasklet:: no emailTemplate configured, using empty subject and body"); - return new String[]{"", ""}; + return new String[]{"", "", ""}; } int pieceCount = Optional.ofNullable(exportConfig.getClaimPieceIds()) @@ -101,9 +102,10 @@ private String[] resolveTemplate(VendorEdiOrdersExportConfig exportConfig, Strin templateId, exportConfig.getConfigName(), fileName, pieceCount); JsonNode response = templateEngineClient.processTemplate(request); JsonNode result = response.path("result"); - log.info("SendToEmailTasklet:: template-engine response: header='{}', body='{}'", - result.path("header").asText(""), result.path("body").asText("")); - return new String[]{result.path("header").asText(""), result.path("body").asText("")}; + String outputFormat = response.path("meta").path("outputFormat").asText(request.getOutputFormat()); + log.info("SendToEmailTasklet:: template-engine response: header='{}', body='{}', outputFormat='{}'", + result.path("header").asText(""), result.path("body").asText(""), outputFormat); + return new String[]{result.path("header").asText(""), result.path("body").asText(""), outputFormat}; } } From d4d70e70ce57942f953f12feddf468c7ab0a6fd8 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Fri, 27 Mar 2026 14:08:22 +0100 Subject: [PATCH 18/76] Add SendToEmailTasklet error handling and tests Wrap templateEngineClient and emailClient calls in EdifactException, derive attachment content type from fileFormat, extract buildAttachment and sendEmail helper methods, and add unit/integration tests for the tasklet and its decider. --- .../acquisitions/jobs/SendToEmailTasklet.java | 65 +++++---- .../decider/SendToEmailTaskletDecider.java | 2 +- .../jobs/SendToEmailTaskletTest.java | 129 ++++++++++++++++++ .../SendToEmailTaskletDeciderTest.java | 64 +++++++++ .../edifact/edifactEmailOrdersExport.json | 31 +++++ .../edifactEmailOrdersExportNoTemplate.json | 30 ++++ 6 files changed, 294 insertions(+), 27 deletions(-) create mode 100644 src/test/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTaskletTest.java create mode 100644 src/test/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDeciderTest.java create mode 100644 src/test/resources/edifact/edifactEmailOrdersExport.json create mode 100644 src/test/resources/edifact/edifactEmailOrdersExportNoTemplate.json diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java index 02432f00e..e644d437f 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java @@ -6,13 +6,14 @@ import lombok.SneakyThrows; import lombok.extern.log4j.Log4j2; import org.folio.dew.batch.ExecutionContextUtils; +import org.folio.dew.batch.acquisitions.exceptions.EdifactException; import org.folio.dew.client.EmailClient; import org.folio.dew.client.TemplateEngineClient; +import org.folio.dew.domain.dto.EdiEmail; import org.folio.dew.domain.dto.VendorEdiOrdersExportConfig; import org.folio.dew.domain.dto.email.Attachment; import org.folio.dew.domain.dto.email.EmailEntity; import org.folio.dew.domain.dto.templateengine.TemplateProcessingRequest; -import org.folio.spring.FolioExecutionContext; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.configuration.annotation.StepScope; import org.springframework.batch.core.scope.context.ChunkContext; @@ -25,6 +26,7 @@ import java.util.Base64; import java.util.List; import java.util.Optional; +import java.util.UUID; import static org.folio.dew.domain.dto.JobParameterNames.*; @@ -34,10 +36,12 @@ @RequiredArgsConstructor public class SendToEmailTasklet implements Tasklet { + private static final String EMAIL_ERROR_MESSAGE = "Failed to send the email"; + private static final String TEMPLATE_ENGINE_ERROR_MESSAGE = "Failed to retrieve the template"; + private final ObjectMapper ediObjectMapper; private final EmailClient emailClient; private final TemplateEngineClient templateEngineClient; - private final FolioExecutionContext folioExecutionContext; @Override @SneakyThrows @@ -61,29 +65,17 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon emailEntity.setBody(templateResult[1]); emailEntity.setOutputFormat(templateResult[2]); - var attachment = new Attachment(); - attachment.setName(fileName); - attachment.setData(Base64.getEncoder().encodeToString(edifactOrderAsString.getBytes(StandardCharsets.UTF_8))); - attachment.setContentType("application/EDIFACT"); - emailEntity.setAttachments(List.of(attachment)); - - log.info("SendToEmailTasklet:: okapiUrl='{}', tenant='{}'", - folioExecutionContext.getOkapiUrl(), folioExecutionContext.getTenantId()); - emailClient.sendEmail(emailEntity); + emailEntity.setAttachments(List.of(buildAttachment(fileName, edifactOrderAsString, exportConfig.getFileFormat()))); + sendEmail(emailEntity); return RepeatStatus.FINISHED; } private String[] resolveTemplate(VendorEdiOrdersExportConfig exportConfig, String fileName) { - var templateId = Optional.ofNullable(exportConfig.getEdiEmail()) - .map(e -> e.getEmailTemplate()) + UUID templateId = Optional.ofNullable(exportConfig.getEdiEmail()) + .map(EdiEmail::getEmailTemplate) .orElse(null); - if (templateId == null) { - log.warn("SendToEmailTasklet:: no emailTemplate configured, using empty subject and body"); - return new String[]{"", "", ""}; - } - int pieceCount = Optional.ofNullable(exportConfig.getClaimPieceIds()) .map(List::size) .orElse(0); @@ -98,14 +90,35 @@ private String[] resolveTemplate(VendorEdiOrdersExportConfig exportConfig, Strin .build()) .build(); - log.info("SendToEmailTasklet:: calling template-engine with templateId='{}', configName='{}', fileName='{}', pieceCount={}", - templateId, exportConfig.getConfigName(), fileName, pieceCount); - JsonNode response = templateEngineClient.processTemplate(request); - JsonNode result = response.path("result"); - String outputFormat = response.path("meta").path("outputFormat").asText(request.getOutputFormat()); - log.info("SendToEmailTasklet:: template-engine response: header='{}', body='{}', outputFormat='{}'", - result.path("header").asText(""), result.path("body").asText(""), outputFormat); - return new String[]{result.path("header").asText(""), result.path("body").asText(""), outputFormat}; + try { + JsonNode response = templateEngineClient.processTemplate(request); + JsonNode result = response.path("result"); + String outputFormat = response.path("meta").path("outputFormat").asText(request.getOutputFormat()); + return new String[]{result.path("header").asText(), result.path("body").asText(), outputFormat}; + } catch (Exception e) { + log.error(TEMPLATE_ENGINE_ERROR_MESSAGE, e); + throw new EdifactException(TEMPLATE_ENGINE_ERROR_MESSAGE); + } + } + + private Attachment buildAttachment(String fileName, String fileContent, VendorEdiOrdersExportConfig.FileFormatEnum fileFormat) { + var contentType = fileFormat == VendorEdiOrdersExportConfig.FileFormatEnum.CSV + ? "text/csv" + : "application/EDIFACT"; + var attachment = new Attachment(); + attachment.setName(fileName); + attachment.setData(Base64.getEncoder().encodeToString(fileContent.getBytes(StandardCharsets.UTF_8))); + attachment.setContentType(contentType); + return attachment; + } + + private void sendEmail(EmailEntity emailEntity) { + try { + emailClient.sendEmail(emailEntity); + } catch (Exception e) { + log.error(EMAIL_ERROR_MESSAGE, e); + throw new EdifactException(EMAIL_ERROR_MESSAGE); + } } } diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java index bc1100c5e..ed642316a 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java @@ -25,7 +25,7 @@ public ExportStepDecision decide(VendorEdiOrdersExportConfig exportConfig, JobEx log.info("decide:: Processing step: {}", stepName); return ExportStepDecision.PROCESS; } - log.info("decide:: Integration type is not CLAIMING or Transmission method is not Email, skipping the step: {}", stepName); + log.info("decide:: Transmission method is not Email, skipping the step: {}", stepName); return ExportStepDecision.SKIP; } diff --git a/src/test/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTaskletTest.java b/src/test/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTaskletTest.java new file mode 100644 index 000000000..f597f5719 --- /dev/null +++ b/src/test/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTaskletTest.java @@ -0,0 +1,129 @@ +package org.folio.dew.batch.acquisitions.jobs; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.folio.dew.domain.dto.JobParameterNames.ACQ_EXPORT_FILE; +import static org.folio.dew.domain.dto.JobParameterNames.ACQ_EXPORT_FILE_NAME; +import static org.folio.dew.domain.dto.JobParameterNames.EDIFACT_ORDERS_EXPORT; +import static org.folio.dew.domain.dto.JobParameterNames.JOB_ID; +import static org.folio.dew.utils.TestUtils.getMockData; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; + +import java.io.IOException; +import java.util.UUID; + +import org.apache.commons.lang3.RandomStringUtils; +import org.folio.dew.BaseBatchTest; +import org.folio.dew.batch.acquisitions.services.OrganizationsService; +import org.folio.dew.client.EmailClient; +import org.folio.dew.client.TemplateEngineClient; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.test.JobLauncherTestUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import lombok.SneakyThrows; + +class SendToEmailTaskletTest extends BaseBatchTest { + + @Autowired + @Qualifier("edifactOrdersExportJob") + private Job edifactExportJob; + @MockitoBean + private EmailClient emailClient; + @MockitoBean + private TemplateEngineClient templateEngineClient; + @MockitoBean + private OrganizationsService organizationsService; + + @BeforeAll + static void beforeAll() { + setUpTenant(NON_CONSORTIUM_TENANT); + } + + @Override + @SneakyThrows + @BeforeEach + protected void setUp() { + super.setUp(); + + doReturn(objectMapper.readTree("{\"code\": \"GOBI\"}")).when(organizationsService).getOrganizationById(any()); + doNothing().when(emailClient).sendEmail(any()); + doReturn(objectMapper.readTree( + "{\"result\":{\"header\":\"Test Subject\",\"body\":\"Test Body\"},\"meta\":{\"outputFormat\":\"text/html\"}}" + )).when(templateEngineClient).processTemplate(any()); + } + + @Test + @DirtiesContext + void testSendEmailSuccessful() throws Exception { + JobLauncherTestUtils testLauncher = createTestLauncher(edifactExportJob); + + var jobParameters = getJobParameters("edifact/edifactEmailOrdersExport.json"); + ExecutionContext executionContext = getExecutionContext(); + JobExecution jobExecution = testLauncher.launchStep("sendToEmailStep", jobParameters, executionContext); + + assertThat(jobExecution.getExitStatus()).isEqualTo(ExitStatus.COMPLETED); + } + + @Test + @DirtiesContext + void testSendEmail_shouldFail_whenTemplateEngineThrows() throws Exception { + doThrow(new RuntimeException("template-engine unavailable")).when(templateEngineClient).processTemplate(any()); + JobLauncherTestUtils testLauncher = createTestLauncher(edifactExportJob); + + var jobParameters = getJobParameters("edifact/edifactEmailOrdersExport.json"); + ExecutionContext executionContext = getExecutionContext(); + JobExecution jobExecution = testLauncher.launchStep("sendToEmailStep", jobParameters, executionContext); + + assertThat(jobExecution.getExitStatus().getExitCode()).isEqualTo(ExitStatus.FAILED.getExitCode()); + } + + @Test + @DirtiesContext + void testSendEmail_shouldFail_whenEmailClientThrows() throws Exception { + doThrow(new RuntimeException("email service unavailable")).when(emailClient).sendEmail(any()); + JobLauncherTestUtils testLauncher = createTestLauncher(edifactExportJob); + + var jobParameters = getJobParameters("edifact/edifactEmailOrdersExport.json"); + ExecutionContext executionContext = getExecutionContext(); + JobExecution jobExecution = testLauncher.launchStep("sendToEmailStep", jobParameters, executionContext); + + assertThat(jobExecution.getExitStatus().getExitCode()).isEqualTo(ExitStatus.FAILED.getExitCode()); + } + + private JobParameters getJobParameters(String edifactOrdersExport) throws IOException { + JobParametersBuilder paramsBuilder = new JobParametersBuilder(); + paramsBuilder.addString(EDIFACT_ORDERS_EXPORT, getMockData(edifactOrdersExport)); + paramsBuilder.addString(JOB_ID, UUID.randomUUID().toString()); + return paramsBuilder.toJobParameters(); + } + + private ExecutionContext getExecutionContext() { + ExecutionContext executionContext = new ExecutionContext(); + executionContext.put(ACQ_EXPORT_FILE_NAME, "testEdiFile.edi"); + executionContext.put(ACQ_EXPORT_FILE, RandomStringUtils.secure().next(100)); + return executionContext; + } + + protected JobLauncherTestUtils createTestLauncher(Job job) { + JobLauncherTestUtils testLauncher = new JobLauncherTestUtils(); + testLauncher.setJob(job); + testLauncher.setJobLauncher(jobLauncher); + testLauncher.setJobRepository(jobRepository); + return testLauncher; + } + +} diff --git a/src/test/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDeciderTest.java b/src/test/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDeciderTest.java new file mode 100644 index 000000000..836c8f825 --- /dev/null +++ b/src/test/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDeciderTest.java @@ -0,0 +1,64 @@ +package org.folio.dew.batch.acquisitions.jobs.decider; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +import org.folio.dew.CopilotGenerated; +import org.folio.dew.domain.dto.VendorEdiOrdersExportConfig; +import org.folio.dew.domain.dto.VendorEdiOrdersExportConfig.IntegrationTypeEnum; +import org.folio.dew.domain.dto.VendorEdiOrdersExportConfig.TransmissionMethodEnum; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.StepExecution; + +import com.fasterxml.jackson.databind.ObjectMapper; + +@CopilotGenerated +class SendToEmailTaskletDeciderTest { + + private SendToEmailTaskletDecider decider; + + @BeforeEach + void setUp() { + decider = new SendToEmailTaskletDecider(new ObjectMapper(), "sendToEmailStep"); + } + + @Test + void decide_shouldProcess_whenIntegrationTypeIsClaiming() { + VendorEdiOrdersExportConfig config = new VendorEdiOrdersExportConfig(); + config.setIntegrationType(IntegrationTypeEnum.CLAIMING); + JobExecution jobExecution = mock(JobExecution.class); + StepExecution stepExecution = mock(StepExecution.class); + + ExportStepDecision decision = decider.decide(config, jobExecution, stepExecution); + + assertEquals(ExportStepDecision.PROCESS, decision); + } + + @Test + void decide_shouldProcess_whenTransmissionMethodIsEmail() { + VendorEdiOrdersExportConfig config = new VendorEdiOrdersExportConfig(); + config.setIntegrationType(IntegrationTypeEnum.ORDERING); + config.setTransmissionMethod(TransmissionMethodEnum.EMAIL); + JobExecution jobExecution = mock(JobExecution.class); + StepExecution stepExecution = mock(StepExecution.class); + + ExportStepDecision decision = decider.decide(config, jobExecution, stepExecution); + + assertEquals(ExportStepDecision.PROCESS, decision); + } + + @Test + void decide_shouldSkip_whenIntegrationTypeIsNotClaimingAndTransmissionMethodIsNotEmail() { + VendorEdiOrdersExportConfig config = new VendorEdiOrdersExportConfig(); + config.setIntegrationType(IntegrationTypeEnum.ORDERING); + config.setTransmissionMethod(TransmissionMethodEnum.FTP); + JobExecution jobExecution = mock(JobExecution.class); + StepExecution stepExecution = mock(StepExecution.class); + + ExportStepDecision decision = decider.decide(config, jobExecution, stepExecution); + + assertEquals(ExportStepDecision.SKIP, decision); + } +} \ No newline at end of file diff --git a/src/test/resources/edifact/edifactEmailOrdersExport.json b/src/test/resources/edifact/edifactEmailOrdersExport.json new file mode 100644 index 000000000..49ddda923 --- /dev/null +++ b/src/test/resources/edifact/edifactEmailOrdersExport.json @@ -0,0 +1,31 @@ +{ + "vendorId": "e54f0eb5-36c6-43c8-a219-dabbafb0176c", + "configName": "EDIFACT Email", + "configDescription": "EDI configuration for testing email export", + "integrationType": "Ordering", + "transmissionMethod": "Email", + "fileFormat": "EDI", + "ediConfig": { + "accountNoList": [ + "BRXXXXX-01" + ], + "defaultAcquisitionMethods": [ + "306489dd-0053-49ee-a068-c316444a8f55" + ], + "ediNamingConvention": "*.edi", + "libEdiCode": "7659876", + "libEdiType": "31B/US-SAN", + "vendorEdiCode": "1694510", + "vendorEdiType": "31B/US-SAN", + "notes": "no EDI invoices for our tech services invoices", + "sendAccountNumber": true, + "supportOrder": true, + "supportInvoice": true + }, + "ediEmail": { + "emailFrom": "sender@example.com", + "emailTo": "vendor@example.com", + "emailTemplate": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + }, + "isDefaultConfig": false +} \ No newline at end of file diff --git a/src/test/resources/edifact/edifactEmailOrdersExportNoTemplate.json b/src/test/resources/edifact/edifactEmailOrdersExportNoTemplate.json new file mode 100644 index 000000000..15ba1deb9 --- /dev/null +++ b/src/test/resources/edifact/edifactEmailOrdersExportNoTemplate.json @@ -0,0 +1,30 @@ +{ + "vendorId": "e54f0eb5-36c6-43c8-a219-dabbafb0176c", + "configName": "EDIFACT Email No Template", + "configDescription": "EDI configuration for testing email export without template", + "integrationType": "Ordering", + "transmissionMethod": "Email", + "fileFormat": "EDI", + "ediConfig": { + "accountNoList": [ + "BRXXXXX-01" + ], + "defaultAcquisitionMethods": [ + "306489dd-0053-49ee-a068-c316444a8f55" + ], + "ediNamingConvention": "*.edi", + "libEdiCode": "7659876", + "libEdiType": "31B/US-SAN", + "vendorEdiCode": "1694510", + "vendorEdiType": "31B/US-SAN", + "notes": "no EDI invoices for our tech services invoices", + "sendAccountNumber": true, + "supportOrder": true, + "supportInvoice": true + }, + "ediEmail": { + "emailFrom": "sender@example.com", + "emailTo": "vendor@example.com" + }, + "isDefaultConfig": false +} \ No newline at end of file From f8b55a9b3fb9ca9a977cd62366b5482b2c686faa Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Fri, 27 Mar 2026 14:45:54 +0100 Subject: [PATCH 19/76] Support email sending for ORDERING integration type Remove template context from TemplateProcessingRequest for now since required template variables are not yet known; mod-template-engine treats a missing context as an empty object. Add ORDERING + EMAIL test coverage. --- .../acquisitions/jobs/SendToEmailTasklet.java | 11 ------- .../TemplateProcessingRequest.java | 11 ------- .../jobs/SendToEmailTaskletTest.java | 12 +++++++ .../edifactEmailOrdersExportOrdering.json | 31 +++++++++++++++++++ 4 files changed, 43 insertions(+), 22 deletions(-) create mode 100644 src/test/resources/edifact/edifactEmailOrdersExportOrdering.json diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java index e644d437f..1273831fe 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java @@ -22,7 +22,6 @@ import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; -import java.time.LocalDate; import java.util.Base64; import java.util.List; import java.util.Optional; @@ -76,18 +75,8 @@ private String[] resolveTemplate(VendorEdiOrdersExportConfig exportConfig, Strin .map(EdiEmail::getEmailTemplate) .orElse(null); - int pieceCount = Optional.ofNullable(exportConfig.getClaimPieceIds()) - .map(List::size) - .orElse(0); - var request = TemplateProcessingRequest.builder() .templateId(templateId) - .context(TemplateProcessingRequest.ClaimsContext.builder() - .configName(exportConfig.getConfigName()) - .fileName(fileName) - .exportDate(LocalDate.now().toString()) - .pieceCount(pieceCount) - .build()) .build(); try { diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingRequest.java b/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingRequest.java index 124202d9b..990a3db0f 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingRequest.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingRequest.java @@ -14,15 +14,4 @@ public class TemplateProcessingRequest { private String lang = "en"; @Builder.Default private String outputFormat = "text/html"; - private ClaimsContext context; - - @Data - @Builder - public static class ClaimsContext { - private String configName; - private String fileName; - private String exportDate; - private int pieceCount; - } - } \ No newline at end of file diff --git a/src/test/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTaskletTest.java b/src/test/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTaskletTest.java index f597f5719..d20a80695 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTaskletTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTaskletTest.java @@ -78,6 +78,18 @@ void testSendEmailSuccessful() throws Exception { assertThat(jobExecution.getExitStatus()).isEqualTo(ExitStatus.COMPLETED); } + @Test + @DirtiesContext + void testSendEmailSuccessful_forOrderingIntegrationType() throws Exception { + JobLauncherTestUtils testLauncher = createTestLauncher(edifactExportJob); + + var jobParameters = getJobParameters("edifact/edifactEmailOrdersExportOrdering.json"); + ExecutionContext executionContext = getExecutionContext(); + JobExecution jobExecution = testLauncher.launchStep("sendToEmailStep", jobParameters, executionContext); + + assertThat(jobExecution.getExitStatus()).isEqualTo(ExitStatus.COMPLETED); + } + @Test @DirtiesContext void testSendEmail_shouldFail_whenTemplateEngineThrows() throws Exception { diff --git a/src/test/resources/edifact/edifactEmailOrdersExportOrdering.json b/src/test/resources/edifact/edifactEmailOrdersExportOrdering.json new file mode 100644 index 000000000..a83d62026 --- /dev/null +++ b/src/test/resources/edifact/edifactEmailOrdersExportOrdering.json @@ -0,0 +1,31 @@ +{ + "vendorId": "e54f0eb5-36c6-43c8-a219-dabbafb0176c", + "configName": "EDIFACT Ordering Email", + "configDescription": "EDI configuration for testing ordering email export", + "integrationType": "Ordering", + "transmissionMethod": "Email", + "fileFormat": "EDI", + "ediConfig": { + "accountNoList": [ + "BRXXXXX-01" + ], + "defaultAcquisitionMethods": [ + "306489dd-0053-49ee-a068-c316444a8f55" + ], + "ediNamingConvention": "*.edi", + "libEdiCode": "7659876", + "libEdiType": "31B/US-SAN", + "vendorEdiCode": "1694510", + "vendorEdiType": "31B/US-SAN", + "notes": "no EDI invoices for our tech services invoices", + "sendAccountNumber": true, + "supportOrder": true, + "supportInvoice": true + }, + "ediEmail": { + "emailFrom": "sender@example.com", + "emailTo": "vendor@example.com", + "emailTemplate": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + }, + "isDefaultConfig": false +} From a099ee8dc4ed2fa931c0b471f649f6fccb20d12b Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Fri, 27 Mar 2026 15:51:27 +0100 Subject: [PATCH 20/76] Fix Spring Batch and Feign client migration for email send step Migrate EmailClient and TemplateEngineClient from @FeignClient to Spring 6 @HttpExchange. Update Spring Batch import paths to new package structure. Fix SendToEmailTaskletTest to use JobOperatorTestUtils and correct mock types. --- .../acquisitions/jobs/SendToEmailTasklet.java | 4 +- .../decider/SendToEmailTaskletDecider.java | 4 +- .../org/folio/dew/client/EmailClient.java | 13 +++--- .../dew/client/TemplateEngineClient.java | 12 +++--- .../jobs/SendToEmailTaskletTest.java | 40 +++++++++---------- .../SendToEmailTaskletDeciderTest.java | 6 +-- 6 files changed, 35 insertions(+), 44 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java index 1273831fe..b5556f1e4 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java @@ -14,11 +14,11 @@ import org.folio.dew.domain.dto.email.Attachment; import org.folio.dew.domain.dto.email.EmailEntity; import org.folio.dew.domain.dto.templateengine.TemplateProcessingRequest; -import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.step.StepContribution; import org.springframework.batch.core.configuration.annotation.StepScope; import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.tasklet.Tasklet; -import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.batch.infrastructure.repeat.RepeatStatus; import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java index ed642316a..6ec306a4f 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java @@ -4,8 +4,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.log4j.Log4j2; import org.folio.dew.domain.dto.VendorEdiOrdersExportConfig; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.job.JobExecution; +import org.springframework.batch.core.step.StepExecution; import static org.folio.dew.domain.dto.VendorEdiOrdersExportConfig.IntegrationTypeEnum.CLAIMING; import static org.folio.dew.domain.dto.VendorEdiOrdersExportConfig.TransmissionMethodEnum.EMAIL; diff --git a/src/main/java/org/folio/dew/client/EmailClient.java b/src/main/java/org/folio/dew/client/EmailClient.java index 34d044395..34a9d6eec 100644 --- a/src/main/java/org/folio/dew/client/EmailClient.java +++ b/src/main/java/org/folio/dew/client/EmailClient.java @@ -1,17 +1,14 @@ package org.folio.dew.client; - -import org.folio.dew.config.feign.FeignClientConfiguration; import org.folio.dew.domain.dto.email.EmailEntity; -import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.service.annotation.HttpExchange; +import org.springframework.web.service.annotation.PostExchange; -@FeignClient(name = "email", configuration = FeignClientConfiguration.class) +@HttpExchange(url = "email", contentType = MediaType.APPLICATION_JSON_VALUE) public interface EmailClient { - @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) - void sendEmail(@RequestBody EmailEntity email); + @PostExchange + void sendEmail(EmailEntity email); } diff --git a/src/main/java/org/folio/dew/client/TemplateEngineClient.java b/src/main/java/org/folio/dew/client/TemplateEngineClient.java index 12828c4d2..3d05660d0 100644 --- a/src/main/java/org/folio/dew/client/TemplateEngineClient.java +++ b/src/main/java/org/folio/dew/client/TemplateEngineClient.java @@ -1,17 +1,15 @@ package org.folio.dew.client; import com.fasterxml.jackson.databind.JsonNode; -import org.folio.dew.config.feign.FeignClientConfiguration; import org.folio.dew.domain.dto.templateengine.TemplateProcessingRequest; -import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.service.annotation.HttpExchange; +import org.springframework.web.service.annotation.PostExchange; -@FeignClient(name = "template-request", configuration = FeignClientConfiguration.class) +@HttpExchange(url = "template-request", contentType = MediaType.APPLICATION_JSON_VALUE) public interface TemplateEngineClient { - @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) - JsonNode processTemplate(@RequestBody TemplateProcessingRequest request); + @PostExchange + JsonNode processTemplate(TemplateProcessingRequest request); } diff --git a/src/test/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTaskletTest.java b/src/test/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTaskletTest.java index d20a80695..417319c43 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTaskletTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTaskletTest.java @@ -17,18 +17,19 @@ import org.apache.commons.lang3.RandomStringUtils; import org.folio.dew.BaseBatchTest; import org.folio.dew.batch.acquisitions.services.OrganizationsService; +import org.folio.dew.domain.dto.acquisitions.edifact.Organization; import org.folio.dew.client.EmailClient; import org.folio.dew.client.TemplateEngineClient; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.batch.core.ExitStatus; -import org.springframework.batch.core.Job; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.JobParameters; -import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.test.JobLauncherTestUtils; +import org.springframework.batch.core.job.Job; +import org.springframework.batch.core.job.JobExecution; +import org.springframework.batch.core.job.parameters.JobParameters; +import org.springframework.batch.core.job.parameters.JobParametersBuilder; +import org.springframework.batch.infrastructure.item.ExecutionContext; +import org.springframework.batch.test.JobOperatorTestUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.test.annotation.DirtiesContext; @@ -59,7 +60,9 @@ static void beforeAll() { protected void setUp() { super.setUp(); - doReturn(objectMapper.readTree("{\"code\": \"GOBI\"}")).when(organizationsService).getOrganizationById(any()); + Organization organization = new Organization(); + organization.setCode("GOBI"); + doReturn(organization).when(organizationsService).getOrganizationById(any()); doNothing().when(emailClient).sendEmail(any()); doReturn(objectMapper.readTree( "{\"result\":{\"header\":\"Test Subject\",\"body\":\"Test Body\"},\"meta\":{\"outputFormat\":\"text/html\"}}" @@ -69,11 +72,11 @@ protected void setUp() { @Test @DirtiesContext void testSendEmailSuccessful() throws Exception { - JobLauncherTestUtils testLauncher = createTestLauncher(edifactExportJob); + JobOperatorTestUtils testLauncher = createTestLauncher(edifactExportJob); var jobParameters = getJobParameters("edifact/edifactEmailOrdersExport.json"); ExecutionContext executionContext = getExecutionContext(); - JobExecution jobExecution = testLauncher.launchStep("sendToEmailStep", jobParameters, executionContext); + JobExecution jobExecution = testLauncher.startStep("sendToEmailStep", jobParameters, executionContext); assertThat(jobExecution.getExitStatus()).isEqualTo(ExitStatus.COMPLETED); } @@ -81,11 +84,11 @@ void testSendEmailSuccessful() throws Exception { @Test @DirtiesContext void testSendEmailSuccessful_forOrderingIntegrationType() throws Exception { - JobLauncherTestUtils testLauncher = createTestLauncher(edifactExportJob); + JobOperatorTestUtils testLauncher = createTestLauncher(edifactExportJob); var jobParameters = getJobParameters("edifact/edifactEmailOrdersExportOrdering.json"); ExecutionContext executionContext = getExecutionContext(); - JobExecution jobExecution = testLauncher.launchStep("sendToEmailStep", jobParameters, executionContext); + JobExecution jobExecution = testLauncher.startStep("sendToEmailStep", jobParameters, executionContext); assertThat(jobExecution.getExitStatus()).isEqualTo(ExitStatus.COMPLETED); } @@ -94,11 +97,11 @@ void testSendEmailSuccessful_forOrderingIntegrationType() throws Exception { @DirtiesContext void testSendEmail_shouldFail_whenTemplateEngineThrows() throws Exception { doThrow(new RuntimeException("template-engine unavailable")).when(templateEngineClient).processTemplate(any()); - JobLauncherTestUtils testLauncher = createTestLauncher(edifactExportJob); + JobOperatorTestUtils testLauncher = createTestLauncher(edifactExportJob); var jobParameters = getJobParameters("edifact/edifactEmailOrdersExport.json"); ExecutionContext executionContext = getExecutionContext(); - JobExecution jobExecution = testLauncher.launchStep("sendToEmailStep", jobParameters, executionContext); + JobExecution jobExecution = testLauncher.startStep("sendToEmailStep", jobParameters, executionContext); assertThat(jobExecution.getExitStatus().getExitCode()).isEqualTo(ExitStatus.FAILED.getExitCode()); } @@ -107,11 +110,11 @@ void testSendEmail_shouldFail_whenTemplateEngineThrows() throws Exception { @DirtiesContext void testSendEmail_shouldFail_whenEmailClientThrows() throws Exception { doThrow(new RuntimeException("email service unavailable")).when(emailClient).sendEmail(any()); - JobLauncherTestUtils testLauncher = createTestLauncher(edifactExportJob); + JobOperatorTestUtils testLauncher = createTestLauncher(edifactExportJob); var jobParameters = getJobParameters("edifact/edifactEmailOrdersExport.json"); ExecutionContext executionContext = getExecutionContext(); - JobExecution jobExecution = testLauncher.launchStep("sendToEmailStep", jobParameters, executionContext); + JobExecution jobExecution = testLauncher.startStep("sendToEmailStep", jobParameters, executionContext); assertThat(jobExecution.getExitStatus().getExitCode()).isEqualTo(ExitStatus.FAILED.getExitCode()); } @@ -130,12 +133,5 @@ private ExecutionContext getExecutionContext() { return executionContext; } - protected JobLauncherTestUtils createTestLauncher(Job job) { - JobLauncherTestUtils testLauncher = new JobLauncherTestUtils(); - testLauncher.setJob(job); - testLauncher.setJobLauncher(jobLauncher); - testLauncher.setJobRepository(jobRepository); - return testLauncher; - } } diff --git a/src/test/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDeciderTest.java b/src/test/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDeciderTest.java index 836c8f825..cdb0a45af 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDeciderTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDeciderTest.java @@ -9,8 +9,8 @@ import org.folio.dew.domain.dto.VendorEdiOrdersExportConfig.TransmissionMethodEnum; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.job.JobExecution; +import org.springframework.batch.core.step.StepExecution; import com.fasterxml.jackson.databind.ObjectMapper; @@ -61,4 +61,4 @@ void decide_shouldSkip_whenIntegrationTypeIsNotClaimingAndTransmissionMethodIsNo assertEquals(ExportStepDecision.SKIP, decision); } -} \ No newline at end of file +} From 9774da85d23a36d7739c337936bffc4b86bf25d3 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Fri, 27 Mar 2026 17:11:02 +0100 Subject: [PATCH 21/76] Register EmailClient and TemplateEngineClient beans in HttpClientConfiguration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both clients use @HttpExchange and require explicit @Bean registration via HttpServiceProxyFactory — they were missing, causing a NoSuchBeanDefinitionException when sendToEmailStep attempted to inject EmailClient. --- src/main/java/org/folio/dew/client/EmailClient.java | 2 +- .../folio/dew/config/HttpClientConfiguration.java | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/folio/dew/client/EmailClient.java b/src/main/java/org/folio/dew/client/EmailClient.java index 34a9d6eec..9fd9ad9ea 100644 --- a/src/main/java/org/folio/dew/client/EmailClient.java +++ b/src/main/java/org/folio/dew/client/EmailClient.java @@ -11,4 +11,4 @@ public interface EmailClient { @PostExchange void sendEmail(EmailEntity email); -} +} \ No newline at end of file diff --git a/src/main/java/org/folio/dew/config/HttpClientConfiguration.java b/src/main/java/org/folio/dew/config/HttpClientConfiguration.java index a2e0dfea4..bbcd034c6 100644 --- a/src/main/java/org/folio/dew/config/HttpClientConfiguration.java +++ b/src/main/java/org/folio/dew/config/HttpClientConfiguration.java @@ -7,6 +7,7 @@ import org.folio.dew.client.AgreementClient; import org.folio.dew.client.AuditClient; import org.folio.dew.client.DataExportSpringClient; +import org.folio.dew.client.EmailClient; import org.folio.dew.client.EntitiesLinksStatsClient; import org.folio.dew.client.ExpenseClassClient; import org.folio.dew.client.HoldingClient; @@ -21,6 +22,7 @@ import org.folio.dew.client.OrganizationsClient; import org.folio.dew.client.ServicePointClient; import org.folio.dew.client.TenantAddressesClient; +import org.folio.dew.client.TemplateEngineClient; import org.folio.dew.client.TransferClient; import org.folio.dew.client.UserClient; import org.folio.dew.client.UserTenantsClient; @@ -152,6 +154,16 @@ public ServicePointClient servicePointClient(HttpServiceProxyFactory factory) { return factory.createClient(ServicePointClient.class); } + @Bean + public EmailClient emailClient(HttpServiceProxyFactory factory) { + return factory.createClient(EmailClient.class); + } + + @Bean + public TemplateEngineClient templateEngineClient(HttpServiceProxyFactory factory) { + return factory.createClient(TemplateEngineClient.class); + } + @Primary @Bean public RestClient restClient(RestClient.Builder builder) { From 1f66e3d8ebcdf56890c807bcbdef876df0102867 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Fri, 27 Mar 2026 17:25:05 +0100 Subject: [PATCH 22/76] Add @RequestBody to EmailClient and TemplateEngineClient parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spring HttpServiceProxyFactory requires @RequestBody on POST parameters to resolve them as the request body — unlike Feign which inferred this from unannotated arguments. --- src/main/java/org/folio/dew/client/EmailClient.java | 3 ++- src/main/java/org/folio/dew/client/TemplateEngineClient.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/folio/dew/client/EmailClient.java b/src/main/java/org/folio/dew/client/EmailClient.java index 9fd9ad9ea..e54fe3ba8 100644 --- a/src/main/java/org/folio/dew/client/EmailClient.java +++ b/src/main/java/org/folio/dew/client/EmailClient.java @@ -2,6 +2,7 @@ import org.folio.dew.domain.dto.email.EmailEntity; import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.service.annotation.HttpExchange; import org.springframework.web.service.annotation.PostExchange; @@ -9,6 +10,6 @@ public interface EmailClient { @PostExchange - void sendEmail(EmailEntity email); + void sendEmail(@RequestBody EmailEntity email); } \ No newline at end of file diff --git a/src/main/java/org/folio/dew/client/TemplateEngineClient.java b/src/main/java/org/folio/dew/client/TemplateEngineClient.java index 3d05660d0..d57b78652 100644 --- a/src/main/java/org/folio/dew/client/TemplateEngineClient.java +++ b/src/main/java/org/folio/dew/client/TemplateEngineClient.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.JsonNode; import org.folio.dew.domain.dto.templateengine.TemplateProcessingRequest; import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.service.annotation.HttpExchange; import org.springframework.web.service.annotation.PostExchange; @@ -10,6 +11,6 @@ public interface TemplateEngineClient { @PostExchange - JsonNode processTemplate(TemplateProcessingRequest request); + JsonNode processTemplate(@RequestBody TemplateProcessingRequest request); } From a931b58d3d1e4ce92274601cf3dae393ab3816f9 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Fri, 27 Mar 2026 17:36:39 +0100 Subject: [PATCH 23/76] Fix Jackson version conflict in TemplateEngineClient response deserialization RestClient uses Jackson 3 (tools.jackson) which cannot deserialize into the abstract com.fasterxml.jackson.databind.JsonNode (Jackson 2). Return String instead and parse with ediObjectMapper in the tasklet. --- .../folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java | 2 +- src/main/java/org/folio/dew/client/TemplateEngineClient.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java index b5556f1e4..74108591e 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java @@ -80,7 +80,7 @@ private String[] resolveTemplate(VendorEdiOrdersExportConfig exportConfig, Strin .build(); try { - JsonNode response = templateEngineClient.processTemplate(request); + JsonNode response = ediObjectMapper.readTree(templateEngineClient.processTemplate(request)); JsonNode result = response.path("result"); String outputFormat = response.path("meta").path("outputFormat").asText(request.getOutputFormat()); return new String[]{result.path("header").asText(), result.path("body").asText(), outputFormat}; diff --git a/src/main/java/org/folio/dew/client/TemplateEngineClient.java b/src/main/java/org/folio/dew/client/TemplateEngineClient.java index d57b78652..ddc1b44c5 100644 --- a/src/main/java/org/folio/dew/client/TemplateEngineClient.java +++ b/src/main/java/org/folio/dew/client/TemplateEngineClient.java @@ -1,6 +1,5 @@ package org.folio.dew.client; -import com.fasterxml.jackson.databind.JsonNode; import org.folio.dew.domain.dto.templateengine.TemplateProcessingRequest; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestBody; @@ -11,6 +10,6 @@ public interface TemplateEngineClient { @PostExchange - JsonNode processTemplate(@RequestBody TemplateProcessingRequest request); + String processTemplate(@RequestBody TemplateProcessingRequest request); } From b2391e57e30261648fc4d6be35669cc68fbe5ea8 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Fri, 27 Mar 2026 17:46:17 +0100 Subject: [PATCH 24/76] Replace String/JsonNode workaround with concrete TemplateProcessingResponse DTO Jackson 3 (RestClient) cannot deserialize JSON into String or the abstract JsonNode (Jackson 2). A concrete DTO is the correct and consistent approach, matching how all other clients in this module handle responses. --- .../acquisitions/jobs/SendToEmailTasklet.java | 13 +++++++----- .../dew/client/TemplateEngineClient.java | 3 ++- .../TemplateProcessingResponse.java | 21 +++++++++++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingResponse.java diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java index 74108591e..68addd675 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java @@ -1,8 +1,8 @@ package org.folio.dew.batch.acquisitions.jobs; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; + import lombok.SneakyThrows; import lombok.extern.log4j.Log4j2; import org.folio.dew.batch.ExecutionContextUtils; @@ -14,6 +14,7 @@ import org.folio.dew.domain.dto.email.Attachment; import org.folio.dew.domain.dto.email.EmailEntity; import org.folio.dew.domain.dto.templateengine.TemplateProcessingRequest; +import org.folio.dew.domain.dto.templateengine.TemplateProcessingResponse; import org.springframework.batch.core.step.StepContribution; import org.springframework.batch.core.configuration.annotation.StepScope; import org.springframework.batch.core.scope.context.ChunkContext; @@ -80,10 +81,12 @@ private String[] resolveTemplate(VendorEdiOrdersExportConfig exportConfig, Strin .build(); try { - JsonNode response = ediObjectMapper.readTree(templateEngineClient.processTemplate(request)); - JsonNode result = response.path("result"); - String outputFormat = response.path("meta").path("outputFormat").asText(request.getOutputFormat()); - return new String[]{result.path("header").asText(), result.path("body").asText(), outputFormat}; + TemplateProcessingResponse response = templateEngineClient.processTemplate(request); + String outputFormat = response.getMeta() != null && response.getMeta().getOutputFormat() != null + ? response.getMeta().getOutputFormat() : request.getOutputFormat(); + String header = response.getResult() != null ? response.getResult().getHeader() : ""; + String body = response.getResult() != null ? response.getResult().getBody() : ""; + return new String[]{header, body, outputFormat}; } catch (Exception e) { log.error(TEMPLATE_ENGINE_ERROR_MESSAGE, e); throw new EdifactException(TEMPLATE_ENGINE_ERROR_MESSAGE); diff --git a/src/main/java/org/folio/dew/client/TemplateEngineClient.java b/src/main/java/org/folio/dew/client/TemplateEngineClient.java index ddc1b44c5..9adc9ebf4 100644 --- a/src/main/java/org/folio/dew/client/TemplateEngineClient.java +++ b/src/main/java/org/folio/dew/client/TemplateEngineClient.java @@ -1,6 +1,7 @@ package org.folio.dew.client; import org.folio.dew.domain.dto.templateengine.TemplateProcessingRequest; +import org.folio.dew.domain.dto.templateengine.TemplateProcessingResponse; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.service.annotation.HttpExchange; @@ -10,6 +11,6 @@ public interface TemplateEngineClient { @PostExchange - String processTemplate(@RequestBody TemplateProcessingRequest request); + TemplateProcessingResponse processTemplate(@RequestBody TemplateProcessingRequest request); } diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingResponse.java b/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingResponse.java new file mode 100644 index 000000000..a62f4aa05 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingResponse.java @@ -0,0 +1,21 @@ +package org.folio.dew.domain.dto.templateengine; + +import lombok.Data; + +@Data +public class TemplateProcessingResponse { + + private Result result; + private Meta meta; + + @Data + public static class Result { + private String header; + private String body; + } + + @Data + public static class Meta { + private String outputFormat; + } +} \ No newline at end of file From 690e88d1b973ab85e8e4330e6f1c7a27faafcd7d Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Fri, 27 Mar 2026 17:55:55 +0100 Subject: [PATCH 25/76] Update SendToEmailTaskletTest mock to use TemplateProcessingResponse DTO --- .../acquisitions/jobs/SendToEmailTaskletTest.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTaskletTest.java b/src/test/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTaskletTest.java index 417319c43..c60999a04 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTaskletTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTaskletTest.java @@ -20,6 +20,7 @@ import org.folio.dew.domain.dto.acquisitions.edifact.Organization; import org.folio.dew.client.EmailClient; import org.folio.dew.client.TemplateEngineClient; +import org.folio.dew.domain.dto.templateengine.TemplateProcessingResponse; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -64,9 +65,16 @@ protected void setUp() { organization.setCode("GOBI"); doReturn(organization).when(organizationsService).getOrganizationById(any()); doNothing().when(emailClient).sendEmail(any()); - doReturn(objectMapper.readTree( - "{\"result\":{\"header\":\"Test Subject\",\"body\":\"Test Body\"},\"meta\":{\"outputFormat\":\"text/html\"}}" - )).when(templateEngineClient).processTemplate(any()); + + var templateResult = new TemplateProcessingResponse(); + var result = new TemplateProcessingResponse.Result(); + result.setHeader("Test Subject"); + result.setBody("Test Body"); + var meta = new TemplateProcessingResponse.Meta(); + meta.setOutputFormat("text/html"); + templateResult.setResult(result); + templateResult.setMeta(meta); + doReturn(templateResult).when(templateEngineClient).processTemplate(any()); } @Test From db458f64fb1f57e43127273a2835f4209032d758 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Mon, 30 Mar 2026 13:16:24 +0200 Subject: [PATCH 26/76] Fix missing newline at end of file in several source files --- .../dew/batch/acquisitions/services/OrganizationsService.java | 2 ++ src/main/java/org/folio/dew/client/EmailClient.java | 2 +- .../domain/dto/templateengine/TemplateProcessingRequest.java | 2 +- .../domain/dto/templateengine/TemplateProcessingResponse.java | 2 +- src/test/resources/edifact/edifactEmailOrdersExport.json | 2 +- .../resources/edifact/edifactEmailOrdersExportNoTemplate.json | 2 +- 6 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/services/OrganizationsService.java b/src/main/java/org/folio/dew/batch/acquisitions/services/OrganizationsService.java index cd0b2aa35..68ffabf93 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/services/OrganizationsService.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/services/OrganizationsService.java @@ -17,4 +17,6 @@ public Organization getOrganizationById(String id) { return organizationsClient.getOrganizationById(id); } + + } diff --git a/src/main/java/org/folio/dew/client/EmailClient.java b/src/main/java/org/folio/dew/client/EmailClient.java index e54fe3ba8..3cd2cdd23 100644 --- a/src/main/java/org/folio/dew/client/EmailClient.java +++ b/src/main/java/org/folio/dew/client/EmailClient.java @@ -12,4 +12,4 @@ public interface EmailClient { @PostExchange void sendEmail(@RequestBody EmailEntity email); -} \ No newline at end of file +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingRequest.java b/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingRequest.java index 990a3db0f..102e2b376 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingRequest.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingRequest.java @@ -14,4 +14,4 @@ public class TemplateProcessingRequest { private String lang = "en"; @Builder.Default private String outputFormat = "text/html"; -} \ No newline at end of file +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingResponse.java b/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingResponse.java index a62f4aa05..34b318820 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingResponse.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingResponse.java @@ -18,4 +18,4 @@ public static class Result { public static class Meta { private String outputFormat; } -} \ No newline at end of file +} diff --git a/src/test/resources/edifact/edifactEmailOrdersExport.json b/src/test/resources/edifact/edifactEmailOrdersExport.json index 49ddda923..f6a6ce33e 100644 --- a/src/test/resources/edifact/edifactEmailOrdersExport.json +++ b/src/test/resources/edifact/edifactEmailOrdersExport.json @@ -28,4 +28,4 @@ "emailTemplate": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }, "isDefaultConfig": false -} \ No newline at end of file +} diff --git a/src/test/resources/edifact/edifactEmailOrdersExportNoTemplate.json b/src/test/resources/edifact/edifactEmailOrdersExportNoTemplate.json index 15ba1deb9..dad35a939 100644 --- a/src/test/resources/edifact/edifactEmailOrdersExportNoTemplate.json +++ b/src/test/resources/edifact/edifactEmailOrdersExportNoTemplate.json @@ -27,4 +27,4 @@ "emailTo": "vendor@example.com" }, "isDefaultConfig": false -} \ No newline at end of file +} From e2e3dd8eb977d9ce131c01c280675395e2ce6a1c Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Mon, 30 Mar 2026 13:32:28 +0200 Subject: [PATCH 27/76] Fix SendToEmailTaskletDecider to trigger on transmissionMethod only Removed the erroneous CLAIMING integration type check; email delivery is driven exclusively by transmissionMethod == EMAIL regardless of integration type. Also added a comment in EdifactExportJobConfig summarising the decider conditions for all three optional steps. --- .../dew/batch/acquisitions/jobs/EdifactExportJobConfig.java | 4 ++++ .../acquisitions/jobs/decider/SendToEmailTaskletDecider.java | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/EdifactExportJobConfig.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/EdifactExportJobConfig.java index 61e431db4..e653e264a 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/EdifactExportJobConfig.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/EdifactExportJobConfig.java @@ -46,6 +46,10 @@ private Job constructEdifactExportJob(JobBuilder jobBuilder, .listener(ediExportJobCompletionListener) .start(mapToFileStep) .next(saveToMinIOStep) + // FTP, Email, and Export History are optional steps, each guarded by a decider. + // FTP and Email are mutually exclusive via transmissionMethod; Export History + // is exclusive to Ordering. All three steps are chained sequentially and each + // decider independently returns PROCESS or SKIP. .next(ftpStepDecider) .on(PROCESS.getStatus()).to(saveToFTPStep) .from(saveToFTPStep).on("*").to(emailStepDecider) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java index 6ec306a4f..bc111b2e8 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java @@ -20,8 +20,8 @@ public SendToEmailTaskletDecider(ObjectMapper objectMapper, String stepName) { @Override public ExportStepDecision decide(VendorEdiOrdersExportConfig exportConfig, JobExecution jobExecution, StepExecution stepExecution) { - // Always execute if the integration type is CLAIMING, or execute for other integration type if the transmission method is Email - if (exportConfig.getIntegrationType() == CLAIMING || exportConfig.getTransmissionMethod() == EMAIL) { + // Always execute if the transmission method is Email + if (exportConfig.getTransmissionMethod() == EMAIL) { log.info("decide:: Processing step: {}", stepName); return ExportStepDecision.PROCESS; } From 050fb92521a47f2ee098611707ae49ea570f00c4 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Mon, 30 Mar 2026 16:08:22 +0200 Subject: [PATCH 28/76] Update folio-export-common submodule branch to MODEXPW-625 --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index e45ec8d54..6c5d7350b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "folio-export-common"] path = folio-export-common url = https://github.com/slub/folio-export-common.git - branch = send-email + branch = MODEXPW-625 From de25a6c107279d41db5ed357bddc5ceeefd97893 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Mon, 30 Mar 2026 16:26:48 +0200 Subject: [PATCH 29/76] Remove stale Feign client debug logging from application.yml --- src/main/resources/application.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e7499fc08..7af954949 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -115,6 +115,5 @@ folio: logging: level: org.folio.dew.client.*: debug - org.folio.spring.client: debug org.springframework.core.log.*: ERROR org.apache.kafka.*: ERROR From 6d760136b40ddaec9c53040411f814fd23d69f33 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Mon, 30 Mar 2026 16:40:34 +0200 Subject: [PATCH 30/76] Fix SendToEmailTaskletDeciderTest to correctly cover transmissionMethod The previous tests checked integrationType alone and had an incorrect PROCESS assertion for FILE_DOWNLOAD. Tests now verify that PROCESS is returned only when transmissionMethod is EMAIL, and SKIP for all other transmission methods. --- .../SendToEmailTaskletDeciderTest.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDeciderTest.java b/src/test/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDeciderTest.java index cdb0a45af..0031b9918 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDeciderTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDeciderTest.java @@ -25,15 +25,16 @@ void setUp() { } @Test - void decide_shouldProcess_whenIntegrationTypeIsClaiming() { + void decide_shouldSkip_whenTransmissionMethodIsFileDownload() { VendorEdiOrdersExportConfig config = new VendorEdiOrdersExportConfig(); config.setIntegrationType(IntegrationTypeEnum.CLAIMING); + config.setTransmissionMethod(TransmissionMethodEnum.FILE_DOWNLOAD); JobExecution jobExecution = mock(JobExecution.class); StepExecution stepExecution = mock(StepExecution.class); ExportStepDecision decision = decider.decide(config, jobExecution, stepExecution); - assertEquals(ExportStepDecision.PROCESS, decision); + assertEquals(ExportStepDecision.SKIP, decision); } @Test @@ -50,7 +51,20 @@ void decide_shouldProcess_whenTransmissionMethodIsEmail() { } @Test - void decide_shouldSkip_whenIntegrationTypeIsNotClaimingAndTransmissionMethodIsNotEmail() { + void decide_shouldProcess_whenIntegrationTypeIsClaimingAndTransmissionMethodIsEmail() { + VendorEdiOrdersExportConfig config = new VendorEdiOrdersExportConfig(); + config.setIntegrationType(IntegrationTypeEnum.CLAIMING); + config.setTransmissionMethod(TransmissionMethodEnum.EMAIL); + JobExecution jobExecution = mock(JobExecution.class); + StepExecution stepExecution = mock(StepExecution.class); + + ExportStepDecision decision = decider.decide(config, jobExecution, stepExecution); + + assertEquals(ExportStepDecision.PROCESS, decision); + } + + @Test + void decide_shouldSkip_whenTransmissionMethodIsFtp() { VendorEdiOrdersExportConfig config = new VendorEdiOrdersExportConfig(); config.setIntegrationType(IntegrationTypeEnum.ORDERING); config.setTransmissionMethod(TransmissionMethodEnum.FTP); From c205964508a56b7ac3095839bc26a0d0fc3e43f6 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Mon, 30 Mar 2026 17:33:19 +0200 Subject: [PATCH 31/76] Add email config validation to MapToEdifactTasklet Introduces validateEmailFields analogous to validateFtpFields, checking emailFrom, emailTo, and emailTemplate when transmissionMethod is Email. Fixes NPE caused by unconditional FTP field validation for Email transmission jobs. --- .../batch/acquisitions/jobs/MapToEdifactTasklet.java | 9 ++++++++- .../batch/acquisitions/utils/ExportConfigFields.java | 5 ++++- .../dew/batch/acquisitions/utils/ExportUtils.java | 10 ++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactTasklet.java index 94dcf729b..58998441b 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactTasklet.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactTasklet.java @@ -7,11 +7,13 @@ import static org.folio.dew.batch.acquisitions.utils.ExportConfigFields.INTEGRATION_TYPE; import static org.folio.dew.batch.acquisitions.utils.ExportConfigFields.TRANSMISSION_METHOD; import static org.folio.dew.batch.acquisitions.utils.ExportUtils.generateFileName; +import static org.folio.dew.batch.acquisitions.utils.ExportUtils.validateEmailFields; import static org.folio.dew.batch.acquisitions.utils.ExportUtils.validateField; import static org.folio.dew.batch.acquisitions.utils.ExportUtils.validateFtpFields; import static org.folio.dew.domain.dto.JobParameterNames.ACQ_EXPORT_FILE; import static org.folio.dew.domain.dto.JobParameterNames.ACQ_EXPORT_FILE_NAME; import static org.folio.dew.domain.dto.JobParameterNames.EDIFACT_ORDERS_EXPORT; +import static org.folio.dew.domain.dto.VendorEdiOrdersExportConfig.TransmissionMethodEnum.EMAIL; import java.util.List; import java.util.Map; @@ -76,7 +78,12 @@ private void validateEdiExportConfig(VendorEdiOrdersExportConfig ediExportConfig validateField(INTEGRATION_TYPE.getName(), ediExportConfig.getIntegrationType(), Objects::nonNull, missingFields); validateField(TRANSMISSION_METHOD.getName(), ediExportConfig.getTransmissionMethod(), Objects::nonNull, missingFields); validateField(FILE_FORMAT.getName(), ediExportConfig.getFileFormat(), Objects::nonNull, missingFields); - validateFtpFields(ediExportConfig, missingFields); + + if (ediExportConfig.getTransmissionMethod() == EMAIL) { + validateEmailFields(ediExportConfig, missingFields); + } else { + validateFtpFields(ediExportConfig, missingFields); + } if (!missingFields.isEmpty()) { throw new EdifactException("Export configuration is incomplete, missing required fields: %s".formatted(missingFields)); diff --git a/src/main/java/org/folio/dew/batch/acquisitions/utils/ExportConfigFields.java b/src/main/java/org/folio/dew/batch/acquisitions/utils/ExportConfigFields.java index 4722eab18..09ef5ae79 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/utils/ExportConfigFields.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/utils/ExportConfigFields.java @@ -16,7 +16,10 @@ public enum ExportConfigFields { LIB_EDI_CODE("libEdiCode"), VENDOR_EDI_TYPE("vendorEdiType"), VENDOR_EDI_CODE("vendorEdiCode"), - CLAIM_PIECE_IDS("claimPieceIds"); + CLAIM_PIECE_IDS("claimPieceIds"), + EMAIL_FROM("emailFrom"), + EMAIL_TO("emailTo"), + EMAIL_TEMPLATE("emailTemplate"); private final String name; diff --git a/src/main/java/org/folio/dew/batch/acquisitions/utils/ExportUtils.java b/src/main/java/org/folio/dew/batch/acquisitions/utils/ExportUtils.java index 8ebcc5f98..3ea6dd291 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/utils/ExportUtils.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/utils/ExportUtils.java @@ -1,5 +1,8 @@ package org.folio.dew.batch.acquisitions.utils; +import static org.folio.dew.batch.acquisitions.utils.ExportConfigFields.EMAIL_FROM; +import static org.folio.dew.batch.acquisitions.utils.ExportConfigFields.EMAIL_TEMPLATE; +import static org.folio.dew.batch.acquisitions.utils.ExportConfigFields.EMAIL_TO; import static org.folio.dew.batch.acquisitions.utils.ExportConfigFields.FTP_PORT; import static org.folio.dew.batch.acquisitions.utils.ExportConfigFields.SERVER_ADDRESS; import static org.folio.dew.domain.dto.ReferenceNumberItem.RefNumberTypeEnum.ORDER_REFERENCE_NUMBER; @@ -72,6 +75,13 @@ public static void validateFtpFields(VendorEdiOrdersExportConfig ediExportConfig } } + public static void validateEmailFields(VendorEdiOrdersExportConfig ediExportConfig, List missingFields) { + var emailConfig = ediExportConfig.getEdiEmail(); + validateField(EMAIL_FROM.getName(), emailConfig.getEmailFrom(), StringUtils::isNotEmpty, missingFields); + validateField(EMAIL_TO.getName(), emailConfig.getEmailTo(), StringUtils::isNotEmpty, missingFields); + validateField(EMAIL_TEMPLATE.getName(), emailConfig.getEmailTemplate(), Objects::nonNull, missingFields); + } + public static String generateFileName(String vendorName, String configName, VendorEdiOrdersExportConfig.IntegrationTypeEnum integrationType, FileFormatEnum fileFormat) { var filePrefix = getFilePrefix(integrationType, fileFormat); var timestamp = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(new Date()); From 8835ae028557662e2502c5cb20f47106d9297bf1 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Tue, 31 Mar 2026 14:18:47 +0200 Subject: [PATCH 32/76] Reset folio.okapi-url default to okapi:9130 --- src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7af954949..0c1a92739 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -106,7 +106,7 @@ application: platform: ${PLATFORM:okapi} folio: - okapi-url: ${OKAPI_URL:http://mod-data-export-worker-sc.eureka:8081} + okapi-url: ${OKAPI_URL:http://okapi:9130} tenant: validation: enabled: true From 5a8e0777791359aab97dbfc7b149c5dd9f0bffd3 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Tue, 31 Mar 2026 16:01:56 +0200 Subject: [PATCH 33/76] Add email and template-engine as optional interfaces in ModuleDescriptor --- descriptors/ModuleDescriptor-template.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index cbe3ff735..57e2f5f01 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -164,6 +164,14 @@ "id": "data-export-spring", "version": "2.0" }, + { + "id": "email", + "version": "1.1" + }, + { + "id": "template-engine", + "version": "2.2" + }, { "id": "users-keycloak", "version": "1.0" From 4770c8e57badd2dec0eea821734685693fd4299c Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Wed, 8 Apr 2026 11:19:33 +0200 Subject: [PATCH 34/76] Refactor EdifactExportJobConfig to use lazy constructor injection for shared steps Move shared step and decider dependencies from method parameters to @Lazy constructor-injected fields, reducing constructEdifactExportJob from 8 to 3 parameters. Also update folio-export-common submodule and remove unused fileName parameter from SendToEmailTasklet.resolveTemplate. --- folio-export-common | 2 +- .../jobs/EdifactExportJobConfig.java | 42 ++++++++++++------- .../acquisitions/jobs/SendToEmailTasklet.java | 4 +- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/folio-export-common b/folio-export-common index 3f5d46830..c55ae172e 160000 --- a/folio-export-common +++ b/folio-export-common @@ -1 +1 @@ -Subproject commit 3f5d46830d74137fd248fdb84bb6225965a8f5f8 +Subproject commit c55ae172eb072be011144003bf0911a9d82e3a39 diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/EdifactExportJobConfig.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/EdifactExportJobConfig.java index e653e264a..dd98c0b3f 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/EdifactExportJobConfig.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/EdifactExportJobConfig.java @@ -2,7 +2,6 @@ import java.util.Map; -import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.folio.dew.batch.acquisitions.jobs.decider.ExportHistoryTaskletDecider; @@ -17,6 +16,7 @@ import org.springframework.batch.core.step.builder.StepBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.transaction.PlatformTransactionManager; import com.fasterxml.jackson.databind.ObjectMapper; @@ -26,19 +26,31 @@ @Configuration @Log4j2 -@RequiredArgsConstructor public class EdifactExportJobConfig { public static final String POL_MEM_KEY = "poLineIds"; + private final Step saveToMinIOStep; + private final Step saveToFTPStep; + private final Step sendToEmailStep; + private final Step createExportHistoryRecordsStep; + private final Map optionalStepDeciders; + + public EdifactExportJobConfig(@Lazy Step saveToMinIOStep, + @Lazy Step saveToFTPStep, + @Lazy Step sendToEmailStep, + @Lazy Step createExportHistoryRecordsStep, + @Lazy Map optionalStepDeciders) { + this.saveToMinIOStep = saveToMinIOStep; + this.saveToFTPStep = saveToFTPStep; + this.sendToEmailStep = sendToEmailStep; + this.createExportHistoryRecordsStep = createExportHistoryRecordsStep; + this.optionalStepDeciders = optionalStepDeciders; + } + private Job constructEdifactExportJob(JobBuilder jobBuilder, EdiExportJobCompletionListener ediExportJobCompletionListener, - Step mapToFileStep, - Step saveToMinIOStep, - Step saveToFTPStep, - Step sendToEmailStep, - Step createExportHistoryRecordsStep, - Map optionalStepDeciders) { + Step mapToFileStep) { var ftpStepDecider = optionalStepDeciders.get(saveToFTPStep.getName()); var emailStepDecider = optionalStepDeciders.get(sendToEmailStep.getName()); var exportHistoryStepDecider = optionalStepDeciders.get(createExportHistoryRecordsStep.getName()); @@ -66,19 +78,17 @@ private Job constructEdifactExportJob(JobBuilder jobBuilder, } @Bean - public Job edifactOrdersExportJob(EdiExportJobCompletionListener ediExportJobCompletionListener, JobRepository jobRepository, - Step mapToEdifactOrdersStep, Step saveToMinIOStep, Step saveToFTPStep, Step sendToEmailStep, - Step createExportHistoryRecordsStep, Map deciders) { + public Job edifactOrdersExportJob(EdiExportJobCompletionListener ediExportJobCompletionListener, + JobRepository jobRepository, Step mapToEdifactOrdersStep) { return constructEdifactExportJob(new JobBuilder(ExportType.EDIFACT_ORDERS_EXPORT.getValue(), jobRepository), - ediExportJobCompletionListener, mapToEdifactOrdersStep, saveToMinIOStep, saveToFTPStep, sendToEmailStep, createExportHistoryRecordsStep, deciders); + ediExportJobCompletionListener, mapToEdifactOrdersStep); } @Bean - public Job edifactClaimsExportJob(EdiExportJobCompletionListener ediExportJobCompletionListener, JobRepository jobRepository, - Step mapToEdifactClaimsStep, Step saveToMinIOStep, Step saveToFTPStep, Step sendToEmailStep, - Step createExportHistoryRecordsStep, Map deciders) { + public Job edifactClaimsExportJob(EdiExportJobCompletionListener ediExportJobCompletionListener, + JobRepository jobRepository, Step mapToEdifactClaimsStep) { return constructEdifactExportJob(new JobBuilder(ExportType.CLAIMS.getValue(), jobRepository), - ediExportJobCompletionListener, mapToEdifactClaimsStep, saveToMinIOStep, saveToFTPStep, sendToEmailStep, createExportHistoryRecordsStep, deciders); + ediExportJobCompletionListener, mapToEdifactClaimsStep); } @Bean diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java index 68addd675..08982fe25 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java @@ -60,7 +60,7 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon emailEntity.setFrom(exportConfig.getEdiEmail().getEmailFrom()); emailEntity.setTo(exportConfig.getEdiEmail().getEmailTo()); - var templateResult = resolveTemplate(exportConfig, fileName); + var templateResult = resolveTemplate(exportConfig); emailEntity.setHeader(templateResult[0]); emailEntity.setBody(templateResult[1]); emailEntity.setOutputFormat(templateResult[2]); @@ -71,7 +71,7 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon return RepeatStatus.FINISHED; } - private String[] resolveTemplate(VendorEdiOrdersExportConfig exportConfig, String fileName) { + private String[] resolveTemplate(VendorEdiOrdersExportConfig exportConfig) { UUID templateId = Optional.ofNullable(exportConfig.getEdiEmail()) .map(EdiEmail::getEmailTemplate) .orElse(null); From 5d0a8565c3c7efce4704dfd199c481ec46a663bf Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Wed, 8 Apr 2026 11:59:36 +0200 Subject: [PATCH 35/76] Fix SaveToFileStorageTaskletDecider to skip FTP step when transmissionMethod is not FTP The decider previously always executed the FTP step for ORDERING integration type regardless of transmissionMethod, causing an NPE when ediFtp config is absent (e.g. Email-only exports). --- .../SaveToFileStorageTaskletDecider.java | 6 ++--- .../decider/SendToEmailTaskletDecider.java | 1 - .../SaveToFileStorageTaskletDeciderTest.java | 22 +++++++++++++++---- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SaveToFileStorageTaskletDecider.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SaveToFileStorageTaskletDecider.java index 39e383146..437854c23 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SaveToFileStorageTaskletDecider.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SaveToFileStorageTaskletDecider.java @@ -1,6 +1,5 @@ package org.folio.dew.batch.acquisitions.jobs.decider; -import static org.folio.dew.domain.dto.VendorEdiOrdersExportConfig.IntegrationTypeEnum.ORDERING; import static org.folio.dew.domain.dto.VendorEdiOrdersExportConfig.TransmissionMethodEnum.FTP; import org.folio.dew.domain.dto.VendorEdiOrdersExportConfig; @@ -20,12 +19,11 @@ public SaveToFileStorageTaskletDecider(ObjectMapper objectMapper, String stepNam @Override public ExportStepDecision decide(VendorEdiOrdersExportConfig exportConfig, JobExecution jobExecution, StepExecution stepExecution) { - // Always execute if the integration type is ORDERING, or execute for other integration type if the transmission method is FTP - if (exportConfig.getIntegrationType() == ORDERING || exportConfig.getTransmissionMethod() == FTP) { + if (exportConfig.getTransmissionMethod() == FTP) { log.info("decide:: Processing step: {}", stepName); return ExportStepDecision.PROCESS; } - log.info("decide:: Integration type is not ORDERING or Transmission method is not FTP, skipping the step: {}", stepName); + log.info("decide:: Transmission method is not FTP, skipping the step: {}", stepName); return ExportStepDecision.SKIP; } diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java index bc111b2e8..c900d85ac 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/decider/SendToEmailTaskletDecider.java @@ -20,7 +20,6 @@ public SendToEmailTaskletDecider(ObjectMapper objectMapper, String stepName) { @Override public ExportStepDecision decide(VendorEdiOrdersExportConfig exportConfig, JobExecution jobExecution, StepExecution stepExecution) { - // Always execute if the transmission method is Email if (exportConfig.getTransmissionMethod() == EMAIL) { log.info("decide:: Processing step: {}", stepName); return ExportStepDecision.PROCESS; diff --git a/src/test/java/org/folio/dew/batch/acquisitions/jobs/decider/SaveToFileStorageTaskletDeciderTest.java b/src/test/java/org/folio/dew/batch/acquisitions/jobs/decider/SaveToFileStorageTaskletDeciderTest.java index 126621582..f3cc9fd9b 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/jobs/decider/SaveToFileStorageTaskletDeciderTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/jobs/decider/SaveToFileStorageTaskletDeciderTest.java @@ -25,9 +25,10 @@ void setUp() { } @Test - void decide_shouldProcess_whenIntegrationTypeIsOrdering() { + void decide_shouldProcess_whenTransmissionMethodIsFtpAndIntegrationTypeIsOrdering() { VendorEdiOrdersExportConfig config = new VendorEdiOrdersExportConfig(); config.setIntegrationType(IntegrationTypeEnum.ORDERING); + config.setTransmissionMethod(TransmissionMethodEnum.FTP); JobExecution jobExecution = mock(JobExecution.class); StepExecution stepExecution = mock(StepExecution.class); @@ -37,7 +38,7 @@ void decide_shouldProcess_whenIntegrationTypeIsOrdering() { } @Test - void decide_shouldProcess_whenTransmissionMethodIsFtp() { + void decide_shouldProcess_whenTransmissionMethodIsFtpAndIntegrationTypeIsClaiming() { VendorEdiOrdersExportConfig config = new VendorEdiOrdersExportConfig(); config.setIntegrationType(IntegrationTypeEnum.CLAIMING); config.setTransmissionMethod(TransmissionMethodEnum.FTP); @@ -50,7 +51,20 @@ void decide_shouldProcess_whenTransmissionMethodIsFtp() { } @Test - void decide_shouldSkip_whenIntegrationTypeIsNotOrderingAndTransmissionMethodIsNotFtp() { + void decide_shouldSkip_whenTransmissionMethodIsEmail() { + VendorEdiOrdersExportConfig config = new VendorEdiOrdersExportConfig(); + config.setIntegrationType(IntegrationTypeEnum.ORDERING); + config.setTransmissionMethod(TransmissionMethodEnum.EMAIL); + JobExecution jobExecution = mock(JobExecution.class); + StepExecution stepExecution = mock(StepExecution.class); + + ExportStepDecision decision = decider.decide(config, jobExecution, stepExecution); + + assertEquals(ExportStepDecision.SKIP, decision); + } + + @Test + void decide_shouldSkip_whenTransmissionMethodIsNotFtp() { VendorEdiOrdersExportConfig config = new VendorEdiOrdersExportConfig(); config.setIntegrationType(IntegrationTypeEnum.CLAIMING); config.setTransmissionMethod(TransmissionMethodEnum.FILE_DOWNLOAD); @@ -61,4 +75,4 @@ void decide_shouldSkip_whenIntegrationTypeIsNotOrderingAndTransmissionMethodIsNo assertEquals(ExportStepDecision.SKIP, decision); } -} +} \ No newline at end of file From d258084e73a6e85be5678c24714726ae0e82e564 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Wed, 8 Apr 2026 12:13:21 +0200 Subject: [PATCH 36/76] Add @Qualifier to EdifactExportJobConfig constructor to resolve Step bean ambiguity Spring 6.1+ no longer retains parameter names by default, so @Lazy Step injection matched all 14 Step beans instead of the intended one. --- .../batch/acquisitions/jobs/EdifactExportJobConfig.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/EdifactExportJobConfig.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/EdifactExportJobConfig.java index dd98c0b3f..957a24648 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/EdifactExportJobConfig.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/EdifactExportJobConfig.java @@ -14,6 +14,7 @@ import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.step.Step; import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; @@ -36,10 +37,10 @@ public class EdifactExportJobConfig { private final Step createExportHistoryRecordsStep; private final Map optionalStepDeciders; - public EdifactExportJobConfig(@Lazy Step saveToMinIOStep, - @Lazy Step saveToFTPStep, - @Lazy Step sendToEmailStep, - @Lazy Step createExportHistoryRecordsStep, + public EdifactExportJobConfig(@Lazy @Qualifier("saveToMinIOStep") Step saveToMinIOStep, + @Lazy @Qualifier("saveToFTPStep") Step saveToFTPStep, + @Lazy @Qualifier("sendToEmailStep") Step sendToEmailStep, + @Lazy @Qualifier("createExportHistoryRecordsStep") Step createExportHistoryRecordsStep, @Lazy Map optionalStepDeciders) { this.saveToMinIOStep = saveToMinIOStep; this.saveToFTPStep = saveToFTPStep; From 449f7e7dff36901d6247de93c997c3c456df3040 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Wed, 8 Apr 2026 12:33:57 +0200 Subject: [PATCH 37/76] Revert EdifactExportJobConfig to @Bean method parameter injection Constructor injection with @Lazy/@Qualifier fails for same-class @Bean definitions due to circular dependencies and Spring 6.1 parameter name retention changes. Revert to the original pattern of passing shared steps as @Bean method parameters. --- .../jobs/EdifactExportJobConfig.java | 43 +++++++------------ 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/EdifactExportJobConfig.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/EdifactExportJobConfig.java index 957a24648..e653e264a 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/EdifactExportJobConfig.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/EdifactExportJobConfig.java @@ -2,6 +2,7 @@ import java.util.Map; +import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.folio.dew.batch.acquisitions.jobs.decider.ExportHistoryTaskletDecider; @@ -14,10 +15,8 @@ import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.step.Step; import org.springframework.batch.core.step.builder.StepBuilder; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Lazy; import org.springframework.transaction.PlatformTransactionManager; import com.fasterxml.jackson.databind.ObjectMapper; @@ -27,31 +26,19 @@ @Configuration @Log4j2 +@RequiredArgsConstructor public class EdifactExportJobConfig { public static final String POL_MEM_KEY = "poLineIds"; - private final Step saveToMinIOStep; - private final Step saveToFTPStep; - private final Step sendToEmailStep; - private final Step createExportHistoryRecordsStep; - private final Map optionalStepDeciders; - - public EdifactExportJobConfig(@Lazy @Qualifier("saveToMinIOStep") Step saveToMinIOStep, - @Lazy @Qualifier("saveToFTPStep") Step saveToFTPStep, - @Lazy @Qualifier("sendToEmailStep") Step sendToEmailStep, - @Lazy @Qualifier("createExportHistoryRecordsStep") Step createExportHistoryRecordsStep, - @Lazy Map optionalStepDeciders) { - this.saveToMinIOStep = saveToMinIOStep; - this.saveToFTPStep = saveToFTPStep; - this.sendToEmailStep = sendToEmailStep; - this.createExportHistoryRecordsStep = createExportHistoryRecordsStep; - this.optionalStepDeciders = optionalStepDeciders; - } - private Job constructEdifactExportJob(JobBuilder jobBuilder, EdiExportJobCompletionListener ediExportJobCompletionListener, - Step mapToFileStep) { + Step mapToFileStep, + Step saveToMinIOStep, + Step saveToFTPStep, + Step sendToEmailStep, + Step createExportHistoryRecordsStep, + Map optionalStepDeciders) { var ftpStepDecider = optionalStepDeciders.get(saveToFTPStep.getName()); var emailStepDecider = optionalStepDeciders.get(sendToEmailStep.getName()); var exportHistoryStepDecider = optionalStepDeciders.get(createExportHistoryRecordsStep.getName()); @@ -79,17 +66,19 @@ private Job constructEdifactExportJob(JobBuilder jobBuilder, } @Bean - public Job edifactOrdersExportJob(EdiExportJobCompletionListener ediExportJobCompletionListener, - JobRepository jobRepository, Step mapToEdifactOrdersStep) { + public Job edifactOrdersExportJob(EdiExportJobCompletionListener ediExportJobCompletionListener, JobRepository jobRepository, + Step mapToEdifactOrdersStep, Step saveToMinIOStep, Step saveToFTPStep, Step sendToEmailStep, + Step createExportHistoryRecordsStep, Map deciders) { return constructEdifactExportJob(new JobBuilder(ExportType.EDIFACT_ORDERS_EXPORT.getValue(), jobRepository), - ediExportJobCompletionListener, mapToEdifactOrdersStep); + ediExportJobCompletionListener, mapToEdifactOrdersStep, saveToMinIOStep, saveToFTPStep, sendToEmailStep, createExportHistoryRecordsStep, deciders); } @Bean - public Job edifactClaimsExportJob(EdiExportJobCompletionListener ediExportJobCompletionListener, - JobRepository jobRepository, Step mapToEdifactClaimsStep) { + public Job edifactClaimsExportJob(EdiExportJobCompletionListener ediExportJobCompletionListener, JobRepository jobRepository, + Step mapToEdifactClaimsStep, Step saveToMinIOStep, Step saveToFTPStep, Step sendToEmailStep, + Step createExportHistoryRecordsStep, Map deciders) { return constructEdifactExportJob(new JobBuilder(ExportType.CLAIMS.getValue(), jobRepository), - ediExportJobCompletionListener, mapToEdifactClaimsStep); + ediExportJobCompletionListener, mapToEdifactClaimsStep, saveToMinIOStep, saveToFTPStep, sendToEmailStep, createExportHistoryRecordsStep, deciders); } @Bean From 76eb73de45bb328b78315ade03c241685500bbc8 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Fri, 24 Apr 2026 14:22:12 +0200 Subject: [PATCH 38/76] Update submodule --- folio-export-common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/folio-export-common b/folio-export-common index c21b232c2..2df1f861e 160000 --- a/folio-export-common +++ b/folio-export-common @@ -1 +1 @@ -Subproject commit c21b232c23b309436d2cb23d6d11f6dea6db4735 +Subproject commit 2df1f861eafcf7ce582a3e4f99abc44540456ddd From 0d37e63538703483c51120038bb3cb91d32b7e0a Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Wed, 29 Apr 2026 17:14:03 +0200 Subject: [PATCH 39/76] Add order/line template context for EDIFACT email export Introduce OrderEmailContext with a nested orders[]{order, orderLines[]{orderLine}} structure passed to mod-template-engine, enabling Mustache templates to reference order and PO line fields (poNumber, title, pricing, fund codes, vendor ref, etc.). MapToEdifactTasklet serializes the assembled orders into the job execution context so SendToEmailTasklet can build and attach the context to the template request. --- .../jobs/MapToEdifactTasklet.java | 3 +- .../acquisitions/jobs/SendToEmailTasklet.java | 11 +- .../mapper/OrderEmailContextMapper.java | 131 ++++++++++++++++++ .../dew/domain/dto/JobParameterNames.java | 1 + .../dto/templateengine/OrderContext.java | 18 +++ .../dto/templateengine/OrderEmailContext.java | 13 ++ .../dto/templateengine/OrderLineContext.java | 27 ++++ .../dto/templateengine/OrderLineWrapper.java | 4 + .../dto/templateengine/OrderWrapper.java | 6 + .../TemplateProcessingRequest.java | 1 + .../jobs/SendToEmailTaskletTest.java | 2 + .../mapper/OrderEmailContextMapperTest.java | 110 +++++++++++++++ 12 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java create mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java create mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/OrderEmailContext.java create mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java create mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineWrapper.java create mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/OrderWrapper.java create mode 100644 src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactTasklet.java index 58998441b..4153d533f 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactTasklet.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/MapToEdifactTasklet.java @@ -12,6 +12,7 @@ import static org.folio.dew.batch.acquisitions.utils.ExportUtils.validateFtpFields; import static org.folio.dew.domain.dto.JobParameterNames.ACQ_EXPORT_FILE; import static org.folio.dew.domain.dto.JobParameterNames.ACQ_EXPORT_FILE_NAME; +import static org.folio.dew.domain.dto.JobParameterNames.ACQ_EXPORT_ORDERS; import static org.folio.dew.domain.dto.JobParameterNames.EDIFACT_ORDERS_EXPORT; import static org.folio.dew.domain.dto.VendorEdiOrdersExportConfig.TransmissionMethodEnum.EMAIL; @@ -66,10 +67,10 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon String jobName = jobParameters.get(JobParameterNames.JOB_NAME).toString(); var edifactStringResult = getExportResourceMapper(ediExportConfig).convertForExport(holder.orders(), holder.pieces(), ediExportConfig, jobName); - // save edifact file content and name in memory var stepExecution = chunkContext.getStepContext().getStepExecution(); ExecutionContextUtils.addToJobExecutionContext(stepExecution, ACQ_EXPORT_FILE, edifactStringResult, ""); ExecutionContextUtils.addToJobExecutionContext(stepExecution, ACQ_EXPORT_FILE_NAME, getFileName(ediExportConfig), ""); + ExecutionContextUtils.addToJobExecutionContext(stepExecution, ACQ_EXPORT_ORDERS, ediObjectMapper.writeValueAsString(holder.orders()), ""); return RepeatStatus.FINISHED; } diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java index 08982fe25..ecc0d0fe6 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java @@ -1,5 +1,6 @@ package org.folio.dew.batch.acquisitions.jobs; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; @@ -7,8 +8,10 @@ import lombok.extern.log4j.Log4j2; import org.folio.dew.batch.ExecutionContextUtils; import org.folio.dew.batch.acquisitions.exceptions.EdifactException; +import org.folio.dew.batch.acquisitions.mapper.OrderEmailContextMapper; import org.folio.dew.client.EmailClient; import org.folio.dew.client.TemplateEngineClient; +import org.folio.dew.domain.dto.CompositePurchaseOrder; import org.folio.dew.domain.dto.EdiEmail; import org.folio.dew.domain.dto.VendorEdiOrdersExportConfig; import org.folio.dew.domain.dto.email.Attachment; @@ -42,6 +45,7 @@ public class SendToEmailTasklet implements Tasklet { private final ObjectMapper ediObjectMapper; private final EmailClient emailClient; private final TemplateEngineClient templateEngineClient; + private final OrderEmailContextMapper orderEmailContextMapper; @Override @SneakyThrows @@ -54,13 +58,15 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon var stepExecution = chunkContext.getStepContext().getStepExecution(); var fileName = (String) ExecutionContextUtils.getExecutionVariable(stepExecution, ACQ_EXPORT_FILE_NAME); var edifactOrderAsString = (String) ExecutionContextUtils.getExecutionVariable(stepExecution, ACQ_EXPORT_FILE); + var ordersJson = (String) ExecutionContextUtils.getExecutionVariable(stepExecution, ACQ_EXPORT_ORDERS); + var orders = ediObjectMapper.readValue(ordersJson, new TypeReference>() {}); var emailEntity = new EmailEntity(); emailEntity.setNotificationId(jobId); emailEntity.setFrom(exportConfig.getEdiEmail().getEmailFrom()); emailEntity.setTo(exportConfig.getEdiEmail().getEmailTo()); - var templateResult = resolveTemplate(exportConfig); + var templateResult = resolveTemplate(exportConfig, orders); emailEntity.setHeader(templateResult[0]); emailEntity.setBody(templateResult[1]); emailEntity.setOutputFormat(templateResult[2]); @@ -71,13 +77,14 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon return RepeatStatus.FINISHED; } - private String[] resolveTemplate(VendorEdiOrdersExportConfig exportConfig) { + private String[] resolveTemplate(VendorEdiOrdersExportConfig exportConfig, List orders) { UUID templateId = Optional.ofNullable(exportConfig.getEdiEmail()) .map(EdiEmail::getEmailTemplate) .orElse(null); var request = TemplateProcessingRequest.builder() .templateId(templateId) + .context(orderEmailContextMapper.buildContext(orders)) .build(); try { diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java new file mode 100644 index 000000000..92c2afa27 --- /dev/null +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -0,0 +1,131 @@ +package org.folio.dew.batch.acquisitions.mapper; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.folio.dew.batch.acquisitions.utils.ExportUtils; +import org.folio.dew.domain.dto.CompositePurchaseOrder; +import org.folio.dew.domain.dto.Cost; +import org.folio.dew.domain.dto.Details; +import org.folio.dew.domain.dto.Eresource; +import org.folio.dew.domain.dto.FundDistribution; +import org.folio.dew.domain.dto.Physical; +import org.folio.dew.domain.dto.PoLine; +import org.folio.dew.domain.dto.ProductIdentifier; +import org.folio.dew.domain.dto.ReferenceNumberItem; +import org.folio.dew.domain.dto.VendorDetail; +import org.folio.dew.domain.dto.templateengine.OrderContext; +import org.folio.dew.domain.dto.templateengine.OrderEmailContext; +import org.folio.dew.domain.dto.templateengine.OrderLineContext; +import org.folio.dew.domain.dto.templateengine.OrderLineWrapper; +import org.folio.dew.domain.dto.templateengine.OrderWrapper; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Component +public class OrderEmailContextMapper { + + public OrderEmailContext buildContext(List orders) { + var orderWrappers = orders.stream() + .map(order -> new OrderWrapper( + mapOrder(order), + order.getPoLines().stream() + .map(line -> new OrderLineWrapper(mapOrderLine(line))) + .toList())) + .toList(); + return OrderEmailContext.builder().orders(orderWrappers).build(); + } + + private OrderContext mapOrder(CompositePurchaseOrder order) { + return OrderContext.builder() + .poNumber(order.getPoNumber()) + .orderDate(ExportUtils.getFormattedDate(order.getDateOrdered())) + .orderType(Optional.ofNullable(order.getOrderType()).map(CompositePurchaseOrder.OrderTypeEnum::getValue).orElse(null)) + .createdBy(Optional.ofNullable(order.getMetadata()).map(m -> m.getCreatedByUsername()).orElse(null)) + .totalEstimatedPrice(formatDecimal(order.getTotalEstimatedPrice())) + .shipTo(Optional.ofNullable(order.getShipTo()).map(Object::toString).orElse(null)) + .billTo(Optional.ofNullable(order.getBillTo()).map(Object::toString).orElse(null)) + .note(joinNotes(order.getNotes())) + .build(); + } + + private OrderLineContext mapOrderLine(PoLine line) { + var cost = line.getCost(); + var physQty = Optional.ofNullable(cost).map(Cost::getQuantityPhysical).orElse(0); + var elecQty = Optional.ofNullable(cost).map(Cost::getQuantityElectronic).orElse(0); + return OrderLineContext.builder() + .poLineNumber(line.getPoLineNumber()) + .title(line.getTitleOrPackage()) + .publisher(line.getPublisher()) + .publicationDate(line.getPublicationDate()) + .edition(line.getEdition()) + .productIdentifier(mapProductIdentifiers(line.getDetails())) + .materialType(resolveMaterialType(line.getPhysical(), line.getEresource())) + .listUnitPrice(formatDecimal(Optional.ofNullable(cost).map(Cost::getListUnitPrice).orElse(null))) + .listUnitPriceElectronic(formatDecimal(Optional.ofNullable(cost).map(Cost::getListUnitPriceElectronic).orElse(null))) + .quantityPhysical(physQty) + .quantityElectronic(elecQty) + .quantity(physQty + elecQty) + .estimatedPrice(formatDecimal(Optional.ofNullable(cost).map(Cost::getPoLineEstimatedPrice).orElse(null))) + .currency(Optional.ofNullable(cost).map(Cost::getCurrency).orElse(null)) + .fundCodes(mapFundCodes(line.getFundDistribution())) + .vendorRefNumber(mapVendorRefNumber(line.getVendorDetail())) + .instructions(Optional.ofNullable(line.getVendorDetail()).map(VendorDetail::getInstructions).orElse(null)) + .build(); + } + + private String formatDecimal(BigDecimal value) { + return value != null ? value.toPlainString() : null; + } + + private String joinNotes(List notes) { + return CollectionUtils.isEmpty(notes) ? null : String.join("; ", notes); + } + + private String mapProductIdentifiers(Details details) { + if (details == null || CollectionUtils.isEmpty(details.getProductIds())) { + return null; + } + return details.getProductIds().stream() + .map(this::formatProductId) + .collect(Collectors.joining("; ")); + } + + private String formatProductId(ProductIdentifier pid) { + return StringUtils.isNotBlank(pid.getProductIdType()) + ? "%s (%s)".formatted(pid.getProductId(), pid.getProductIdType()) + : pid.getProductId(); + } + + private String resolveMaterialType(Physical physical, Eresource eresource) { + if (physical != null && StringUtils.isNotBlank(physical.getMaterialType())) { + return physical.getMaterialType(); + } + if (eresource != null && StringUtils.isNotBlank(eresource.getMaterialType())) { + return eresource.getMaterialType(); + } + return null; + } + + private String mapFundCodes(List fundDistributions) { + if (CollectionUtils.isEmpty(fundDistributions)) { + return null; + } + return fundDistributions.stream() + .map(FundDistribution::getCode) + .filter(StringUtils::isNotBlank) + .collect(Collectors.joining(", ")); + } + + private String mapVendorRefNumber(VendorDetail vendorDetail) { + return Optional.ofNullable(vendorDetail) + .map(VendorDetail::getReferenceNumbers) + .filter(refs -> !refs.isEmpty()) + .map(refs -> refs.get(0)) + .map(ReferenceNumberItem::getRefNumber) + .orElse(null); + } +} diff --git a/src/main/java/org/folio/dew/domain/dto/JobParameterNames.java b/src/main/java/org/folio/dew/domain/dto/JobParameterNames.java index d2378b2b2..46e5cdf12 100644 --- a/src/main/java/org/folio/dew/domain/dto/JobParameterNames.java +++ b/src/main/java/org/folio/dew/domain/dto/JobParameterNames.java @@ -18,6 +18,7 @@ public class JobParameterNames { public static final String UPLOADED_FILE_PATH = "uploadedFilePath"; public static final String ACQ_EXPORT_FILE = "acqExportFile"; public static final String ACQ_EXPORT_FILE_NAME = "acqExportFileName"; + public static final String ACQ_EXPORT_ORDERS = "acqExportOrders"; public static final String E_HOLDINGS_FILE_NAME = "eHoldingsFileName"; public static final String CIRCULATION_LOG_FILE_NAME = "circulationLogFileName"; public static final String AUTHORITY_CONTROL_FILE_NAME = "authorityControlFileName"; diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java new file mode 100644 index 000000000..0a4215a9e --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java @@ -0,0 +1,18 @@ +package org.folio.dew.domain.dto.templateengine; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class OrderContext { + + private String poNumber; + private String orderDate; + private String orderType; + private String createdBy; + private String totalEstimatedPrice; + private String shipTo; + private String billTo; + private String note; +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderEmailContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderEmailContext.java new file mode 100644 index 000000000..1afce0482 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderEmailContext.java @@ -0,0 +1,13 @@ +package org.folio.dew.domain.dto.templateengine; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class OrderEmailContext { + + private List orders; +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java new file mode 100644 index 000000000..c95b330ba --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java @@ -0,0 +1,27 @@ +package org.folio.dew.domain.dto.templateengine; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class OrderLineContext { + + private String poLineNumber; + private String title; + private String publisher; + private String publicationDate; + private String edition; + private String productIdentifier; + private String materialType; + private String listUnitPrice; + private String listUnitPriceElectronic; + private Integer quantityPhysical; + private Integer quantityElectronic; + private Integer quantity; + private String estimatedPrice; + private String currency; + private String fundCodes; + private String vendorRefNumber; + private String instructions; +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineWrapper.java b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineWrapper.java new file mode 100644 index 000000000..f4e3bfede --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineWrapper.java @@ -0,0 +1,4 @@ +package org.folio.dew.domain.dto.templateengine; + +public record OrderLineWrapper(OrderLineContext orderLine) { +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderWrapper.java b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderWrapper.java new file mode 100644 index 000000000..9c4d3ae30 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderWrapper.java @@ -0,0 +1,6 @@ +package org.folio.dew.domain.dto.templateengine; + +import java.util.List; + +public record OrderWrapper(OrderContext order, List orderLines) { +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingRequest.java b/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingRequest.java index 102e2b376..d1d5a3c2d 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingRequest.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/TemplateProcessingRequest.java @@ -14,4 +14,5 @@ public class TemplateProcessingRequest { private String lang = "en"; @Builder.Default private String outputFormat = "text/html"; + private Object context; } diff --git a/src/test/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTaskletTest.java b/src/test/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTaskletTest.java index c60999a04..fae1d980f 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTaskletTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTaskletTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.folio.dew.domain.dto.JobParameterNames.ACQ_EXPORT_FILE; import static org.folio.dew.domain.dto.JobParameterNames.ACQ_EXPORT_FILE_NAME; +import static org.folio.dew.domain.dto.JobParameterNames.ACQ_EXPORT_ORDERS; import static org.folio.dew.domain.dto.JobParameterNames.EDIFACT_ORDERS_EXPORT; import static org.folio.dew.domain.dto.JobParameterNames.JOB_ID; import static org.folio.dew.utils.TestUtils.getMockData; @@ -138,6 +139,7 @@ private ExecutionContext getExecutionContext() { ExecutionContext executionContext = new ExecutionContext(); executionContext.put(ACQ_EXPORT_FILE_NAME, "testEdiFile.edi"); executionContext.put(ACQ_EXPORT_FILE, RandomStringUtils.secure().next(100)); + executionContext.put(ACQ_EXPORT_ORDERS, "[]"); return executionContext; } diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java new file mode 100644 index 000000000..5daa8f4b1 --- /dev/null +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -0,0 +1,110 @@ +package org.folio.dew.batch.acquisitions.mapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.folio.dew.utils.TestUtils.getMockData; + +import java.io.IOException; +import java.util.List; + +import org.folio.dew.domain.dto.CompositePurchaseOrder; +import org.folio.dew.domain.dto.templateengine.OrderEmailContext; +import org.folio.dew.domain.dto.templateengine.OrderLineContext; +import org.folio.dew.domain.dto.templateengine.OrderWrapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; + +class OrderEmailContextMapperTest { + + private OrderEmailContextMapper mapper; + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + mapper = new OrderEmailContextMapper(); + objectMapper = new ObjectMapper(); + } + + @Test + void buildContext_mapsOrderFields() throws IOException { + var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); + + OrderEmailContext ctx = mapper.buildContext(List.of(order)); + + assertThat(ctx.getOrders()).hasSize(1); + var wrapper = ctx.getOrders().get(0); + assertThat(wrapper.order().getPoNumber()).isEqualTo("10000"); + assertThat(wrapper.order().getOrderType()).isEqualTo("One-Time"); + } + + @Test + void buildContext_mapsOrderLineFields() throws IOException { + var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); + + OrderEmailContext ctx = mapper.buildContext(List.of(order)); + + assertThat(ctx.getOrders()).hasSize(1); + var lines = ctx.getOrders().get(0).orderLines(); + assertThat(lines).hasSize(1); + + OrderLineContext line = lines.get(0).orderLine(); + assertThat(line.getPoLineNumber()).isEqualTo("10000-1"); + assertThat(line.getTitle()).isEqualTo("Futures, biometrics and neuroscience research Luiz Moutinho, Mladen Sokele, editors"); + assertThat(line.getPublisher()).isEqualTo("Palgrave Macmillan"); + assertThat(line.getListUnitPrice()).isEqualTo("2.0"); + assertThat(line.getCurrency()).isEqualTo("USD"); + assertThat(line.getQuantityPhysical()).isEqualTo(1); + assertThat(line.getQuantityElectronic()).isEqualTo(0); + assertThat(line.getQuantity()).isEqualTo(1); + assertThat(line.getEstimatedPrice()).isEqualTo("1.8"); + assertThat(line.getFundCodes()).isEqualTo("USHIST"); + assertThat(line.getVendorRefNumber()).isEqualTo("ORD1000"); + } + + @Test + void buildContext_multipleOrders_producesOneWrapperPerOrder() throws IOException { + var order1 = loadOrder("edifact/acquisitions/composite_purchase_order.json"); + var order2 = loadOrder("edifact/acquisitions/minimalistic_composite_purchase_order.json"); + + OrderEmailContext ctx = mapper.buildContext(List.of(order1, order2)); + + assertThat(ctx.getOrders()).hasSize(2); + } + + @Test + void buildContext_emptyOrders_returnsEmptyList() { + OrderEmailContext ctx = mapper.buildContext(List.of()); + + assertThat(ctx.getOrders()).isEmpty(); + } + + @Test + void buildContext_nullCost_doesNotThrow() throws IOException { + var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); + order.getPoLines().get(0).setCost(null); + + OrderEmailContext ctx = mapper.buildContext(List.of(order)); + + OrderLineContext line = ctx.getOrders().get(0).orderLines().get(0).orderLine(); + assertThat(line.getListUnitPrice()).isNull(); + assertThat(line.getCurrency()).isNull(); + assertThat(line.getQuantity()).isEqualTo(0); + } + + @Test + void buildContext_nullVendorDetail_doesNotThrow() throws IOException { + var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); + order.getPoLines().get(0).setVendorDetail(null); + + OrderEmailContext ctx = mapper.buildContext(List.of(order)); + + OrderLineContext line = ctx.getOrders().get(0).orderLines().get(0).orderLine(); + assertThat(line.getVendorRefNumber()).isNull(); + assertThat(line.getInstructions()).isNull(); + } + + private CompositePurchaseOrder loadOrder(String path) throws IOException { + return objectMapper.readValue(getMockData(path), CompositePurchaseOrder.class); + } +} From 2782921c3e7b9d66813795ce9a6307b2ce632bd7 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Mon, 11 May 2026 17:31:51 +0200 Subject: [PATCH 40/76] Add contributors and productIdentifierType to order email context --- .../mapper/OrderEmailContextMapper.java | 35 ++++++++++++++++--- .../dto/templateengine/OrderLineContext.java | 2 ++ .../mapper/OrderEmailContextMapperTest.java | 16 ++++++++- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index 92c2afa27..12a1dad7d 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -1,9 +1,12 @@ package org.folio.dew.batch.acquisitions.mapper; +import lombok.RequiredArgsConstructor; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.folio.dew.batch.acquisitions.services.IdentifierTypeService; import org.folio.dew.batch.acquisitions.utils.ExportUtils; import org.folio.dew.domain.dto.CompositePurchaseOrder; +import org.folio.dew.domain.dto.Contributor; import org.folio.dew.domain.dto.Cost; import org.folio.dew.domain.dto.Details; import org.folio.dew.domain.dto.Eresource; @@ -26,8 +29,11 @@ import java.util.stream.Collectors; @Component +@RequiredArgsConstructor public class OrderEmailContextMapper { + private final IdentifierTypeService identifierTypeService; + public OrderEmailContext buildContext(List orders) { var orderWrappers = orders.stream() .map(order -> new OrderWrapper( @@ -59,10 +65,12 @@ private OrderLineContext mapOrderLine(PoLine line) { return OrderLineContext.builder() .poLineNumber(line.getPoLineNumber()) .title(line.getTitleOrPackage()) + .contributors(mapContributors(line.getContributors())) .publisher(line.getPublisher()) .publicationDate(line.getPublicationDate()) .edition(line.getEdition()) .productIdentifier(mapProductIdentifiers(line.getDetails())) + .productIdentifierType(mapProductIdentifierTypes(line.getDetails())) .materialType(resolveMaterialType(line.getPhysical(), line.getEresource())) .listUnitPrice(formatDecimal(Optional.ofNullable(cost).map(Cost::getListUnitPrice).orElse(null))) .listUnitPriceElectronic(formatDecimal(Optional.ofNullable(cost).map(Cost::getListUnitPriceElectronic).orElse(null))) @@ -85,19 +93,36 @@ private String joinNotes(List notes) { return CollectionUtils.isEmpty(notes) ? null : String.join("; ", notes); } + private String mapContributors(List contributors) { + if (CollectionUtils.isEmpty(contributors)) { + return null; + } + return contributors.stream() + .map(Contributor::getContributor) + .filter(StringUtils::isNotBlank) + .collect(Collectors.joining("; ")); + } + private String mapProductIdentifiers(Details details) { if (details == null || CollectionUtils.isEmpty(details.getProductIds())) { return null; } return details.getProductIds().stream() - .map(this::formatProductId) + .map(ProductIdentifier::getProductId) + .filter(StringUtils::isNotBlank) .collect(Collectors.joining("; ")); } - private String formatProductId(ProductIdentifier pid) { - return StringUtils.isNotBlank(pid.getProductIdType()) - ? "%s (%s)".formatted(pid.getProductId(), pid.getProductIdType()) - : pid.getProductId(); + private String mapProductIdentifierTypes(Details details) { + if (details == null || CollectionUtils.isEmpty(details.getProductIds())) { + return null; + } + return details.getProductIds().stream() + .map(ProductIdentifier::getProductIdType) + .filter(StringUtils::isNotBlank) + .map(identifierTypeService::getIdentifierTypeName) + .filter(StringUtils::isNotBlank) + .collect(Collectors.joining("; ")); } private String resolveMaterialType(Physical physical, Eresource eresource) { diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java index c95b330ba..f99ca039e 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java @@ -9,10 +9,12 @@ public class OrderLineContext { private String poLineNumber; private String title; + private String contributors; private String publisher; private String publicationDate; private String edition; private String productIdentifier; + private String productIdentifierType; private String materialType; private String listUnitPrice; private String listUnitPriceElectronic; diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index 5daa8f4b1..c7bd6e57b 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -2,28 +2,39 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.folio.dew.utils.TestUtils.getMockData; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; import java.io.IOException; import java.util.List; +import org.folio.dew.batch.acquisitions.services.IdentifierTypeService; import org.folio.dew.domain.dto.CompositePurchaseOrder; import org.folio.dew.domain.dto.templateengine.OrderEmailContext; import org.folio.dew.domain.dto.templateengine.OrderLineContext; import org.folio.dew.domain.dto.templateengine.OrderWrapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import com.fasterxml.jackson.databind.ObjectMapper; +@ExtendWith(MockitoExtension.class) class OrderEmailContextMapperTest { + @Mock + private IdentifierTypeService identifierTypeService; + private OrderEmailContextMapper mapper; private ObjectMapper objectMapper; @BeforeEach void setUp() { - mapper = new OrderEmailContextMapper(); + mapper = new OrderEmailContextMapper(identifierTypeService); objectMapper = new ObjectMapper(); + lenient().when(identifierTypeService.getIdentifierTypeName(anyString())).thenReturn("ISBN"); } @Test @@ -51,6 +62,9 @@ void buildContext_mapsOrderLineFields() throws IOException { OrderLineContext line = lines.get(0).orderLine(); assertThat(line.getPoLineNumber()).isEqualTo("10000-1"); assertThat(line.getTitle()).isEqualTo("Futures, biometrics and neuroscience research Luiz Moutinho, Mladen Sokele, editors"); + assertThat(line.getContributors()).isEqualTo("Moutinho, Luiz"); + assertThat(line.getProductIdentifier()).isEqualTo("9783319643991"); + assertThat(line.getProductIdentifierType()).isEqualTo("ISBN"); assertThat(line.getPublisher()).isEqualTo("Palgrave Macmillan"); assertThat(line.getListUnitPrice()).isEqualTo("2.0"); assertThat(line.getCurrency()).isEqualTo("USD"); From 1763611a64bddb2c234842eb5afb88bbfe42d46a Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Tue, 12 May 2026 15:45:20 +0200 Subject: [PATCH 41/76] Resolve shipTo/billTo UUIDs to addresses in OrderEmailContext Replace raw UUID stringification with ConfigurationService.getAddressConfig for shipTo and billTo. Rename OrderContext.note to notes. Return empty string instead of null for all absent fields. Add full field coverage in mapper tests. --- .../mapper/OrderEmailContextMapper.java | 46 +++++++++--------- .../dto/templateengine/OrderContext.java | 2 +- .../mapper/OrderEmailContextMapperTest.java | 47 ++++++++++++++++--- .../composite_purchase_order.json | 13 +++-- 4 files changed, 76 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index 12a1dad7d..4e95f55e3 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.folio.dew.batch.acquisitions.services.ConfigurationService; import org.folio.dew.batch.acquisitions.services.IdentifierTypeService; import org.folio.dew.batch.acquisitions.utils.ExportUtils; import org.folio.dew.domain.dto.CompositePurchaseOrder; @@ -33,6 +34,7 @@ public class OrderEmailContextMapper { private final IdentifierTypeService identifierTypeService; + private final ConfigurationService configurationService; public OrderEmailContext buildContext(List orders) { var orderWrappers = orders.stream() @@ -47,14 +49,14 @@ public OrderEmailContext buildContext(List orders) { private OrderContext mapOrder(CompositePurchaseOrder order) { return OrderContext.builder() - .poNumber(order.getPoNumber()) - .orderDate(ExportUtils.getFormattedDate(order.getDateOrdered())) - .orderType(Optional.ofNullable(order.getOrderType()).map(CompositePurchaseOrder.OrderTypeEnum::getValue).orElse(null)) - .createdBy(Optional.ofNullable(order.getMetadata()).map(m -> m.getCreatedByUsername()).orElse(null)) + .poNumber(StringUtils.defaultString(order.getPoNumber())) + .orderDate(StringUtils.defaultString(ExportUtils.getFormattedDate(order.getDateOrdered()))) + .orderType(Optional.ofNullable(order.getOrderType()).map(CompositePurchaseOrder.OrderTypeEnum::getValue).orElse("")) + .createdBy(Optional.ofNullable(order.getMetadata()).map(m -> m.getCreatedByUsername()).orElse("")) .totalEstimatedPrice(formatDecimal(order.getTotalEstimatedPrice())) - .shipTo(Optional.ofNullable(order.getShipTo()).map(Object::toString).orElse(null)) - .billTo(Optional.ofNullable(order.getBillTo()).map(Object::toString).orElse(null)) - .note(joinNotes(order.getNotes())) + .shipTo(configurationService.getAddressConfig(order.getShipTo())) + .billTo(configurationService.getAddressConfig(order.getBillTo())) + .notes(joinNotes(order.getNotes())) .build(); } @@ -63,12 +65,12 @@ private OrderLineContext mapOrderLine(PoLine line) { var physQty = Optional.ofNullable(cost).map(Cost::getQuantityPhysical).orElse(0); var elecQty = Optional.ofNullable(cost).map(Cost::getQuantityElectronic).orElse(0); return OrderLineContext.builder() - .poLineNumber(line.getPoLineNumber()) - .title(line.getTitleOrPackage()) + .poLineNumber(StringUtils.defaultString(line.getPoLineNumber())) + .title(StringUtils.defaultString(line.getTitleOrPackage())) .contributors(mapContributors(line.getContributors())) - .publisher(line.getPublisher()) - .publicationDate(line.getPublicationDate()) - .edition(line.getEdition()) + .publisher(StringUtils.defaultString(line.getPublisher())) + .publicationDate(StringUtils.defaultString(line.getPublicationDate())) + .edition(StringUtils.defaultString(line.getEdition())) .productIdentifier(mapProductIdentifiers(line.getDetails())) .productIdentifierType(mapProductIdentifierTypes(line.getDetails())) .materialType(resolveMaterialType(line.getPhysical(), line.getEresource())) @@ -78,24 +80,24 @@ private OrderLineContext mapOrderLine(PoLine line) { .quantityElectronic(elecQty) .quantity(physQty + elecQty) .estimatedPrice(formatDecimal(Optional.ofNullable(cost).map(Cost::getPoLineEstimatedPrice).orElse(null))) - .currency(Optional.ofNullable(cost).map(Cost::getCurrency).orElse(null)) + .currency(Optional.ofNullable(cost).map(Cost::getCurrency).orElse("")) .fundCodes(mapFundCodes(line.getFundDistribution())) .vendorRefNumber(mapVendorRefNumber(line.getVendorDetail())) - .instructions(Optional.ofNullable(line.getVendorDetail()).map(VendorDetail::getInstructions).orElse(null)) + .instructions(Optional.ofNullable(line.getVendorDetail()).map(VendorDetail::getInstructions).orElse("")) .build(); } private String formatDecimal(BigDecimal value) { - return value != null ? value.toPlainString() : null; + return value != null ? value.toPlainString() : ""; } private String joinNotes(List notes) { - return CollectionUtils.isEmpty(notes) ? null : String.join("; ", notes); + return CollectionUtils.isEmpty(notes) ? "" : String.join("; ", notes); } private String mapContributors(List contributors) { if (CollectionUtils.isEmpty(contributors)) { - return null; + return ""; } return contributors.stream() .map(Contributor::getContributor) @@ -105,7 +107,7 @@ private String mapContributors(List contributors) { private String mapProductIdentifiers(Details details) { if (details == null || CollectionUtils.isEmpty(details.getProductIds())) { - return null; + return ""; } return details.getProductIds().stream() .map(ProductIdentifier::getProductId) @@ -115,7 +117,7 @@ private String mapProductIdentifiers(Details details) { private String mapProductIdentifierTypes(Details details) { if (details == null || CollectionUtils.isEmpty(details.getProductIds())) { - return null; + return ""; } return details.getProductIds().stream() .map(ProductIdentifier::getProductIdType) @@ -132,12 +134,12 @@ private String resolveMaterialType(Physical physical, Eresource eresource) { if (eresource != null && StringUtils.isNotBlank(eresource.getMaterialType())) { return eresource.getMaterialType(); } - return null; + return ""; } private String mapFundCodes(List fundDistributions) { if (CollectionUtils.isEmpty(fundDistributions)) { - return null; + return ""; } return fundDistributions.stream() .map(FundDistribution::getCode) @@ -151,6 +153,6 @@ private String mapVendorRefNumber(VendorDetail vendorDetail) { .filter(refs -> !refs.isEmpty()) .map(refs -> refs.get(0)) .map(ReferenceNumberItem::getRefNumber) - .orElse(null); + .orElse(""); } } diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java index 0a4215a9e..e9282a9e8 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java @@ -14,5 +14,5 @@ public class OrderContext { private String totalEstimatedPrice; private String shipTo; private String billTo; - private String note; + private String notes; } diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index c7bd6e57b..2744c56b1 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -2,12 +2,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.folio.dew.utils.TestUtils.getMockData; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; import java.io.IOException; import java.util.List; +import java.util.UUID; +import org.folio.dew.batch.acquisitions.services.ConfigurationService; import org.folio.dew.batch.acquisitions.services.IdentifierTypeService; import org.folio.dew.domain.dto.CompositePurchaseOrder; import org.folio.dew.domain.dto.templateengine.OrderEmailContext; @@ -24,21 +28,31 @@ @ExtendWith(MockitoExtension.class) class OrderEmailContextMapperTest { + private static final UUID SHIP_TO_UUID = UUID.fromString("11111111-1111-1111-1111-111111111111"); + private static final UUID BILL_TO_UUID = UUID.fromString("22222222-2222-2222-2222-222222222222"); + private static final String SHIP_TO_ADDRESS = "Library Loading Dock, 123 Main St, Springfield IL"; + private static final String BILL_TO_ADDRESS = "Accounts Payable, PO Box 42, Springfield IL"; + @Mock private IdentifierTypeService identifierTypeService; + @Mock + private ConfigurationService configurationService; private OrderEmailContextMapper mapper; private ObjectMapper objectMapper; @BeforeEach void setUp() { - mapper = new OrderEmailContextMapper(identifierTypeService); + mapper = new OrderEmailContextMapper(identifierTypeService, configurationService); objectMapper = new ObjectMapper(); lenient().when(identifierTypeService.getIdentifierTypeName(anyString())).thenReturn("ISBN"); + lenient().when(configurationService.getAddressConfig(any())).thenReturn(""); } @Test void buildContext_mapsOrderFields() throws IOException { + when(configurationService.getAddressConfig(SHIP_TO_UUID)).thenReturn(SHIP_TO_ADDRESS); + when(configurationService.getAddressConfig(BILL_TO_UUID)).thenReturn(BILL_TO_ADDRESS); var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); OrderEmailContext ctx = mapper.buildContext(List.of(order)); @@ -46,7 +60,23 @@ void buildContext_mapsOrderFields() throws IOException { assertThat(ctx.getOrders()).hasSize(1); var wrapper = ctx.getOrders().get(0); assertThat(wrapper.order().getPoNumber()).isEqualTo("10000"); + assertThat(wrapper.order().getOrderDate()).isEqualTo("2021-01-15"); assertThat(wrapper.order().getOrderType()).isEqualTo("One-Time"); + assertThat(wrapper.order().getCreatedBy()).isEqualTo("jdoe"); + assertThat(wrapper.order().getTotalEstimatedPrice()).isEqualTo("1.8"); + assertThat(wrapper.order().getShipTo()).isEqualTo(SHIP_TO_ADDRESS); + assertThat(wrapper.order().getBillTo()).isEqualTo(BILL_TO_ADDRESS); + assertThat(wrapper.order().getNotes()).isEqualTo("First note; Second note"); + } + + @Test + void buildContext_shipToBillToNotResolved_returnsEmptyString() throws IOException { + var order = loadOrder("edifact/acquisitions/minimalistic_composite_purchase_order.json"); + + OrderEmailContext ctx = mapper.buildContext(List.of(order)); + + assertThat(ctx.getOrders().get(0).order().getShipTo()).isEmpty(); + assertThat(ctx.getOrders().get(0).order().getBillTo()).isEmpty(); } @Test @@ -63,10 +93,14 @@ void buildContext_mapsOrderLineFields() throws IOException { assertThat(line.getPoLineNumber()).isEqualTo("10000-1"); assertThat(line.getTitle()).isEqualTo("Futures, biometrics and neuroscience research Luiz Moutinho, Mladen Sokele, editors"); assertThat(line.getContributors()).isEqualTo("Moutinho, Luiz"); + assertThat(line.getPublisher()).isEqualTo("Palgrave Macmillan"); + assertThat(line.getPublicationDate()).isEqualTo("2021"); + assertThat(line.getEdition()).isEqualTo("2nd ed."); assertThat(line.getProductIdentifier()).isEqualTo("9783319643991"); assertThat(line.getProductIdentifierType()).isEqualTo("ISBN"); - assertThat(line.getPublisher()).isEqualTo("Palgrave Macmillan"); + assertThat(line.getMaterialType()).isEqualTo("1a54b431-2e4f-452d-9cae-9cee66c9a892"); assertThat(line.getListUnitPrice()).isEqualTo("2.0"); + assertThat(line.getListUnitPriceElectronic()).isEqualTo("3.0"); assertThat(line.getCurrency()).isEqualTo("USD"); assertThat(line.getQuantityPhysical()).isEqualTo(1); assertThat(line.getQuantityElectronic()).isEqualTo(0); @@ -74,6 +108,7 @@ void buildContext_mapsOrderLineFields() throws IOException { assertThat(line.getEstimatedPrice()).isEqualTo("1.8"); assertThat(line.getFundCodes()).isEqualTo("USHIST"); assertThat(line.getVendorRefNumber()).isEqualTo("ORD1000"); + assertThat(line.getInstructions()).isEqualTo("Handle with care"); } @Test @@ -101,8 +136,8 @@ void buildContext_nullCost_doesNotThrow() throws IOException { OrderEmailContext ctx = mapper.buildContext(List.of(order)); OrderLineContext line = ctx.getOrders().get(0).orderLines().get(0).orderLine(); - assertThat(line.getListUnitPrice()).isNull(); - assertThat(line.getCurrency()).isNull(); + assertThat(line.getListUnitPrice()).isEmpty(); + assertThat(line.getCurrency()).isEmpty(); assertThat(line.getQuantity()).isEqualTo(0); } @@ -114,8 +149,8 @@ void buildContext_nullVendorDetail_doesNotThrow() throws IOException { OrderEmailContext ctx = mapper.buildContext(List.of(order)); OrderLineContext line = ctx.getOrders().get(0).orderLines().get(0).orderLine(); - assertThat(line.getVendorRefNumber()).isNull(); - assertThat(line.getInstructions()).isNull(); + assertThat(line.getVendorRefNumber()).isEmpty(); + assertThat(line.getInstructions()).isEmpty(); } private CompositePurchaseOrder loadOrder(String path) throws IOException { diff --git a/src/test/resources/edifact/acquisitions/composite_purchase_order.json b/src/test/resources/edifact/acquisitions/composite_purchase_order.json index 81c42105f..fac51f780 100644 --- a/src/test/resources/edifact/acquisitions/composite_purchase_order.json +++ b/src/test/resources/edifact/acquisitions/composite_purchase_order.json @@ -1,7 +1,10 @@ { "id": "91103993-acc5-46c4-a2e6-a79cae1959be", + "shipTo": "11111111-1111-1111-1111-111111111111", + "billTo": "22222222-2222-2222-2222-222222222222", + "dateOrdered": "2021-01-15T00:00:00.000+00:00", "approved": false, - "notes": [], + "notes": ["First note", "Second note"], "poNumber": "10000", "orderType": "One-Time", "reEncumber": false, @@ -14,7 +17,8 @@ "poLines": [ { "id": "f7cebba9-900d-4e46-ac96-9dfdb6da214e", - "edition": "", + "edition": "2nd ed.", + "publicationDate": "2021", "checkinItems": false, "instanceId": "69640328-788e-43fc-9c3c-af39e243f3b7", "acquisitionMethod": "306489dd-0053-49ee-a068-c316444a8f55", @@ -29,6 +33,7 @@ ], "cost": { "listUnitPrice": 2.0, + "listUnitPriceElectronic": 3.0, "currency": "USD", "discount": 10.0, "discountType": "percentage", @@ -71,6 +76,7 @@ "paymentStatus": "Pending", "physical": { "createInventory": "Instance, Holding, Item", + "materialType": "1a54b431-2e4f-452d-9cae-9cee66c9a892", "materialSupplier": "50fb6ae0-cdf1-11e8-a8d5-f2801f1b9fd1", "volumes": [] }, @@ -82,7 +88,7 @@ "source": "User", "titleOrPackage": "Futures, biometrics and neuroscience research Luiz Moutinho, Mladen Sokele, editors", "vendorDetail": { - "instructions": "", + "instructions": "Handle with care", "vendorAccount": "BRXXXXX-01", "referenceNumbers": [ { @@ -103,6 +109,7 @@ "metadata": { "createdDate": "2021-12-28T08:16:56.074+00:00", "createdByUserId": "7a626480-284e-5b55-9cf2-db32f93956cf", + "createdByUsername": "jdoe", "updatedDate": "2021-12-28T08:16:56.074+00:00", "updatedByUserId": "7a626480-284e-5b55-9cf2-db32f93956cf" }, From 88e7ec52a1fb3ac7a3bbf48c9d5d1acc7f3230ae Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Wed, 13 May 2026 11:49:47 +0200 Subject: [PATCH 42/76] Resolve order createdBy from userId via new UserService --- .../mapper/OrderEmailContextMapper.java | 18 ++------ .../acquisitions/services/UserService.java | 46 +++++++++++++++++++ .../mapper/OrderEmailContextMapperTest.java | 9 +++- .../composite_purchase_order.json | 1 - 4 files changed, 58 insertions(+), 16 deletions(-) create mode 100644 src/main/java/org/folio/dew/batch/acquisitions/services/UserService.java diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index 4e95f55e3..236117de5 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -5,18 +5,9 @@ import org.apache.commons.lang3.StringUtils; import org.folio.dew.batch.acquisitions.services.ConfigurationService; import org.folio.dew.batch.acquisitions.services.IdentifierTypeService; +import org.folio.dew.batch.acquisitions.services.UserService; import org.folio.dew.batch.acquisitions.utils.ExportUtils; -import org.folio.dew.domain.dto.CompositePurchaseOrder; -import org.folio.dew.domain.dto.Contributor; -import org.folio.dew.domain.dto.Cost; -import org.folio.dew.domain.dto.Details; -import org.folio.dew.domain.dto.Eresource; -import org.folio.dew.domain.dto.FundDistribution; -import org.folio.dew.domain.dto.Physical; -import org.folio.dew.domain.dto.PoLine; -import org.folio.dew.domain.dto.ProductIdentifier; -import org.folio.dew.domain.dto.ReferenceNumberItem; -import org.folio.dew.domain.dto.VendorDetail; +import org.folio.dew.domain.dto.*; import org.folio.dew.domain.dto.templateengine.OrderContext; import org.folio.dew.domain.dto.templateengine.OrderEmailContext; import org.folio.dew.domain.dto.templateengine.OrderLineContext; @@ -35,6 +26,7 @@ public class OrderEmailContextMapper { private final IdentifierTypeService identifierTypeService; private final ConfigurationService configurationService; + private final UserService userService; public OrderEmailContext buildContext(List orders) { var orderWrappers = orders.stream() @@ -52,7 +44,7 @@ private OrderContext mapOrder(CompositePurchaseOrder order) { .poNumber(StringUtils.defaultString(order.getPoNumber())) .orderDate(StringUtils.defaultString(ExportUtils.getFormattedDate(order.getDateOrdered()))) .orderType(Optional.ofNullable(order.getOrderType()).map(CompositePurchaseOrder.OrderTypeEnum::getValue).orElse("")) - .createdBy(Optional.ofNullable(order.getMetadata()).map(m -> m.getCreatedByUsername()).orElse("")) + .createdBy(userService.getUserName(Optional.ofNullable(order.getMetadata()).map(Metadata::getCreatedByUserId).map(Object::toString).orElse(""))) .totalEstimatedPrice(formatDecimal(order.getTotalEstimatedPrice())) .shipTo(configurationService.getAddressConfig(order.getShipTo())) .billTo(configurationService.getAddressConfig(order.getBillTo())) @@ -151,7 +143,7 @@ private String mapVendorRefNumber(VendorDetail vendorDetail) { return Optional.ofNullable(vendorDetail) .map(VendorDetail::getReferenceNumbers) .filter(refs -> !refs.isEmpty()) - .map(refs -> refs.get(0)) + .map(List::getFirst) .map(ReferenceNumberItem::getRefNumber) .orElse(""); } diff --git a/src/main/java/org/folio/dew/batch/acquisitions/services/UserService.java b/src/main/java/org/folio/dew/batch/acquisitions/services/UserService.java new file mode 100644 index 000000000..1bc516074 --- /dev/null +++ b/src/main/java/org/folio/dew/batch/acquisitions/services/UserService.java @@ -0,0 +1,46 @@ +package org.folio.dew.batch.acquisitions.services; + +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.dew.client.UserClient; +import org.folio.dew.domain.dto.Personal; +import org.folio.dew.domain.dto.User; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserService { + + private static final Logger logger = LogManager.getLogger(); + + private final UserClient userClient; + + @Cacheable(cacheNames = "users") + public String getUserName(String userId) { + if (StringUtils.isBlank(userId)) { + return ""; + } + try { + User user = userClient.getUserById(userId); + if (user == null) { + logger.warn("getUserName:: No user found for id '{}'", userId); + return ""; + } + Personal personal = user.getPersonal(); + if (personal != null) { + String name = (StringUtils.defaultString(personal.getFirstName()) + " " + + StringUtils.defaultString(personal.getLastName())).trim(); + if (StringUtils.isNotBlank(name)) { + return name; + } + } + return StringUtils.defaultString(user.getUsername()); + } catch (Exception e) { + logger.warn("getUserName:: Cannot find user by id: '{}'", userId, e); + return ""; + } + } +} diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index 2744c56b1..22bddb6a3 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -13,6 +13,7 @@ import org.folio.dew.batch.acquisitions.services.ConfigurationService; import org.folio.dew.batch.acquisitions.services.IdentifierTypeService; +import org.folio.dew.batch.acquisitions.services.UserService; import org.folio.dew.domain.dto.CompositePurchaseOrder; import org.folio.dew.domain.dto.templateengine.OrderEmailContext; import org.folio.dew.domain.dto.templateengine.OrderLineContext; @@ -37,22 +38,26 @@ class OrderEmailContextMapperTest { private IdentifierTypeService identifierTypeService; @Mock private ConfigurationService configurationService; + @Mock + private UserService userService; private OrderEmailContextMapper mapper; private ObjectMapper objectMapper; @BeforeEach void setUp() { - mapper = new OrderEmailContextMapper(identifierTypeService, configurationService); + mapper = new OrderEmailContextMapper(identifierTypeService, configurationService, userService); objectMapper = new ObjectMapper(); lenient().when(identifierTypeService.getIdentifierTypeName(anyString())).thenReturn("ISBN"); lenient().when(configurationService.getAddressConfig(any())).thenReturn(""); + lenient().when(userService.getUserName(anyString())).thenReturn(""); } @Test void buildContext_mapsOrderFields() throws IOException { when(configurationService.getAddressConfig(SHIP_TO_UUID)).thenReturn(SHIP_TO_ADDRESS); when(configurationService.getAddressConfig(BILL_TO_UUID)).thenReturn(BILL_TO_ADDRESS); + when(userService.getUserName("7a626480-284e-5b55-9cf2-db32f93956cf")).thenReturn("John Doe"); var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); OrderEmailContext ctx = mapper.buildContext(List.of(order)); @@ -62,7 +67,7 @@ void buildContext_mapsOrderFields() throws IOException { assertThat(wrapper.order().getPoNumber()).isEqualTo("10000"); assertThat(wrapper.order().getOrderDate()).isEqualTo("2021-01-15"); assertThat(wrapper.order().getOrderType()).isEqualTo("One-Time"); - assertThat(wrapper.order().getCreatedBy()).isEqualTo("jdoe"); + assertThat(wrapper.order().getCreatedBy()).isEqualTo("John Doe"); assertThat(wrapper.order().getTotalEstimatedPrice()).isEqualTo("1.8"); assertThat(wrapper.order().getShipTo()).isEqualTo(SHIP_TO_ADDRESS); assertThat(wrapper.order().getBillTo()).isEqualTo(BILL_TO_ADDRESS); diff --git a/src/test/resources/edifact/acquisitions/composite_purchase_order.json b/src/test/resources/edifact/acquisitions/composite_purchase_order.json index fac51f780..00cbf2b36 100644 --- a/src/test/resources/edifact/acquisitions/composite_purchase_order.json +++ b/src/test/resources/edifact/acquisitions/composite_purchase_order.json @@ -109,7 +109,6 @@ "metadata": { "createdDate": "2021-12-28T08:16:56.074+00:00", "createdByUserId": "7a626480-284e-5b55-9cf2-db32f93956cf", - "createdByUsername": "jdoe", "updatedDate": "2021-12-28T08:16:56.074+00:00", "updatedByUserId": "7a626480-284e-5b55-9cf2-db32f93956cf" }, From 0d9cbabac712de33e8ea49748838ee7d2f7bf6cc Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Wed, 13 May 2026 12:54:39 +0200 Subject: [PATCH 43/76] Add OrganizationContext (vendor) to OrderEmailContext --- .../mapper/OrderEmailContextMapper.java | 81 ++++++++++++++++- .../services/OrganizationsService.java | 5 +- .../acquisitions/edifact/Organization.java | 5 ++ .../edifact/OrganizationAddress.java | 12 +++ .../edifact/OrganizationEmail.java | 9 ++ .../dto/templateengine/OrderEmailContext.java | 1 + .../OrganizationAddressContext.java | 14 +++ .../templateengine/OrganizationContext.java | 14 +++ .../mapper/OrderEmailContextMapperTest.java | 88 ++++++++++++++++++- 9 files changed, 224 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/OrganizationAddress.java create mode 100644 src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/OrganizationEmail.java create mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationAddressContext.java create mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index 236117de5..384fd2eb8 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -5,19 +5,27 @@ import org.apache.commons.lang3.StringUtils; import org.folio.dew.batch.acquisitions.services.ConfigurationService; import org.folio.dew.batch.acquisitions.services.IdentifierTypeService; +import org.folio.dew.batch.acquisitions.services.OrganizationsService; import org.folio.dew.batch.acquisitions.services.UserService; import org.folio.dew.batch.acquisitions.utils.ExportUtils; import org.folio.dew.domain.dto.*; +import org.folio.dew.domain.dto.acquisitions.edifact.Organization; +import org.folio.dew.domain.dto.acquisitions.edifact.OrganizationAddress; +import org.folio.dew.domain.dto.acquisitions.edifact.OrganizationEmail; import org.folio.dew.domain.dto.templateengine.OrderContext; import org.folio.dew.domain.dto.templateengine.OrderEmailContext; import org.folio.dew.domain.dto.templateengine.OrderLineContext; import org.folio.dew.domain.dto.templateengine.OrderLineWrapper; import org.folio.dew.domain.dto.templateengine.OrderWrapper; +import org.folio.dew.domain.dto.templateengine.OrganizationAddressContext; +import org.folio.dew.domain.dto.templateengine.OrganizationContext; import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.util.List; +import java.util.Objects; import java.util.Optional; +import java.util.function.Function; import java.util.stream.Collectors; @Component @@ -27,6 +35,7 @@ public class OrderEmailContextMapper { private final IdentifierTypeService identifierTypeService; private final ConfigurationService configurationService; private final UserService userService; + private final OrganizationsService organizationsService; public OrderEmailContext buildContext(List orders) { var orderWrappers = orders.stream() @@ -36,7 +45,77 @@ public OrderEmailContext buildContext(List orders) { .map(line -> new OrderLineWrapper(mapOrderLine(line))) .toList())) .toList(); - return OrderEmailContext.builder().orders(orderWrappers).build(); + return OrderEmailContext.builder() + .organization(mapOrganization(orders)) + .orders(orderWrappers) + .build(); + } + + private OrganizationContext mapOrganization(List orders) { + var vendorId = orders.stream() + .map(CompositePurchaseOrder::getVendor) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + if (vendorId == null) { + return emptyOrganizationContext(); + } + Organization org = organizationsService.getOrganizationById(vendorId.toString()); + if (org == null) { + return emptyOrganizationContext(); + } + return OrganizationContext.builder() + .name(StringUtils.defaultString(org.getName())) + .code(StringUtils.defaultString(org.getCode())) + .contactEmail(pickPrimaryEmail(org.getEmails())) + .address(pickPrimaryAddress(org.getAddresses())) + .build(); + } + + private OrganizationContext emptyOrganizationContext() { + return OrganizationContext.builder() + .name("") + .code("") + .contactEmail("") + .address(emptyOrganizationAddressContext()) + .build(); + } + + private OrganizationAddressContext emptyOrganizationAddressContext() { + return OrganizationAddressContext.builder() + .addressLine1("") + .city("") + .zipCode("") + .country("") + .build(); + } + + private String pickPrimaryEmail(List emails) { + return pickPrimary(emails, OrganizationEmail::getIsPrimary) + .map(OrganizationEmail::getValue) + .map(StringUtils::defaultString) + .orElse(""); + } + + private OrganizationAddressContext pickPrimaryAddress(List addresses) { + return pickPrimary(addresses, OrganizationAddress::getIsPrimary) + .map(addr -> OrganizationAddressContext.builder() + .addressLine1(StringUtils.defaultString(addr.getAddressLine1())) + .city(StringUtils.defaultString(addr.getCity())) + .zipCode(StringUtils.defaultString(addr.getZipCode())) + .country(StringUtils.defaultString(addr.getCountry())) + .build()) + .orElseGet(this::emptyOrganizationAddressContext); + } + + private Optional pickPrimary(List items, Function isPrimary) { + if (CollectionUtils.isEmpty(items)) { + return Optional.empty(); + } + return items.stream() + .filter(item -> Boolean.TRUE.equals(isPrimary.apply(item))) + .findFirst() + .or(() -> Optional.of(items.getFirst())); } private OrderContext mapOrder(CompositePurchaseOrder order) { diff --git a/src/main/java/org/folio/dew/batch/acquisitions/services/OrganizationsService.java b/src/main/java/org/folio/dew/batch/acquisitions/services/OrganizationsService.java index 68ffabf93..aef7d6972 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/services/OrganizationsService.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/services/OrganizationsService.java @@ -2,6 +2,7 @@ import org.folio.dew.client.OrganizationsClient; import org.folio.dew.domain.dto.acquisitions.edifact.Organization; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; @@ -13,10 +14,8 @@ public class OrganizationsService { private final OrganizationsClient organizationsClient; + @Cacheable(cacheNames = "organizations") public Organization getOrganizationById(String id) { return organizationsClient.getOrganizationById(id); } - - - } diff --git a/src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/Organization.java b/src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/Organization.java index bdf74975d..930dd0531 100644 --- a/src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/Organization.java +++ b/src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/Organization.java @@ -1,8 +1,13 @@ package org.folio.dew.domain.dto.acquisitions.edifact; +import java.util.List; + import lombok.Data; @Data public class Organization { private String code; + private String name; + private List addresses; + private List emails; } diff --git a/src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/OrganizationAddress.java b/src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/OrganizationAddress.java new file mode 100644 index 000000000..b05206792 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/OrganizationAddress.java @@ -0,0 +1,12 @@ +package org.folio.dew.domain.dto.acquisitions.edifact; + +import lombok.Data; + +@Data +public class OrganizationAddress { + private String addressLine1; + private String city; + private String zipCode; + private String country; + private Boolean isPrimary; +} diff --git a/src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/OrganizationEmail.java b/src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/OrganizationEmail.java new file mode 100644 index 000000000..902d25cfc --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/OrganizationEmail.java @@ -0,0 +1,9 @@ +package org.folio.dew.domain.dto.acquisitions.edifact; + +import lombok.Data; + +@Data +public class OrganizationEmail { + private String value; + private Boolean isPrimary; +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderEmailContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderEmailContext.java index 1afce0482..07a0da567 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderEmailContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderEmailContext.java @@ -9,5 +9,6 @@ @Builder public class OrderEmailContext { + private OrganizationContext organization; private List orders; } diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationAddressContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationAddressContext.java new file mode 100644 index 000000000..b8e883efe --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationAddressContext.java @@ -0,0 +1,14 @@ +package org.folio.dew.domain.dto.templateengine; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class OrganizationAddressContext { + + private String addressLine1; + private String city; + private String zipCode; + private String country; +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java new file mode 100644 index 000000000..2405a0c52 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java @@ -0,0 +1,14 @@ +package org.folio.dew.domain.dto.templateengine; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class OrganizationContext { + + private String name; + private String code; + private String contactEmail; + private OrganizationAddressContext address; +} diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index 22bddb6a3..03b65caed 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -13,8 +13,12 @@ import org.folio.dew.batch.acquisitions.services.ConfigurationService; import org.folio.dew.batch.acquisitions.services.IdentifierTypeService; +import org.folio.dew.batch.acquisitions.services.OrganizationsService; import org.folio.dew.batch.acquisitions.services.UserService; import org.folio.dew.domain.dto.CompositePurchaseOrder; +import org.folio.dew.domain.dto.acquisitions.edifact.Organization; +import org.folio.dew.domain.dto.acquisitions.edifact.OrganizationAddress; +import org.folio.dew.domain.dto.acquisitions.edifact.OrganizationEmail; import org.folio.dew.domain.dto.templateengine.OrderEmailContext; import org.folio.dew.domain.dto.templateengine.OrderLineContext; import org.folio.dew.domain.dto.templateengine.OrderWrapper; @@ -33,6 +37,7 @@ class OrderEmailContextMapperTest { private static final UUID BILL_TO_UUID = UUID.fromString("22222222-2222-2222-2222-222222222222"); private static final String SHIP_TO_ADDRESS = "Library Loading Dock, 123 Main St, Springfield IL"; private static final String BILL_TO_ADDRESS = "Accounts Payable, PO Box 42, Springfield IL"; + private static final String VENDOR_UUID = "50fb6ae0-cdf1-11e8-a8d5-f2801f1b9fd1"; @Mock private IdentifierTypeService identifierTypeService; @@ -40,17 +45,20 @@ class OrderEmailContextMapperTest { private ConfigurationService configurationService; @Mock private UserService userService; + @Mock + private OrganizationsService organizationsService; private OrderEmailContextMapper mapper; private ObjectMapper objectMapper; @BeforeEach void setUp() { - mapper = new OrderEmailContextMapper(identifierTypeService, configurationService, userService); + mapper = new OrderEmailContextMapper(identifierTypeService, configurationService, userService, organizationsService); objectMapper = new ObjectMapper(); lenient().when(identifierTypeService.getIdentifierTypeName(anyString())).thenReturn("ISBN"); lenient().when(configurationService.getAddressConfig(any())).thenReturn(""); lenient().when(userService.getUserName(anyString())).thenReturn(""); + lenient().when(organizationsService.getOrganizationById(anyString())).thenReturn(new Organization()); } @Test @@ -131,6 +139,84 @@ void buildContext_emptyOrders_returnsEmptyList() { OrderEmailContext ctx = mapper.buildContext(List.of()); assertThat(ctx.getOrders()).isEmpty(); + assertThat(ctx.getOrganization().getName()).isEmpty(); + assertThat(ctx.getOrganization().getCode()).isEmpty(); + assertThat(ctx.getOrganization().getContactEmail()).isEmpty(); + assertThat(ctx.getOrganization().getAddress().getAddressLine1()).isEmpty(); + } + + @Test + void buildContext_mapsOrganizationFields() throws IOException { + var org = new Organization(); + org.setName("Acme Books"); + org.setCode("ACME"); + org.setEmails(List.of( + buildEmail("secondary@acme.test", false), + buildEmail("primary@acme.test", true))); + org.setAddresses(List.of( + buildAddress("321 Other St", "Otherville", "99999", "USA", false), + buildAddress("100 Main St", "Springfield", "12345", "USA", true))); + when(organizationsService.getOrganizationById(VENDOR_UUID)).thenReturn(org); + var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); + + OrderEmailContext ctx = mapper.buildContext(List.of(order)); + + var organization = ctx.getOrganization(); + assertThat(organization.getName()).isEqualTo("Acme Books"); + assertThat(organization.getCode()).isEqualTo("ACME"); + assertThat(organization.getContactEmail()).isEqualTo("primary@acme.test"); + assertThat(organization.getAddress().getAddressLine1()).isEqualTo("100 Main St"); + assertThat(organization.getAddress().getCity()).isEqualTo("Springfield"); + assertThat(organization.getAddress().getZipCode()).isEqualTo("12345"); + assertThat(organization.getAddress().getCountry()).isEqualTo("USA"); + } + + @Test + void buildContext_picksFirstWhenNoPrimary() throws IOException { + var org = new Organization(); + org.setEmails(List.of(buildEmail("first@acme.test", null), buildEmail("second@acme.test", null))); + org.setAddresses(List.of(buildAddress("First St", "FirstCity", "11111", "USA", null))); + when(organizationsService.getOrganizationById(VENDOR_UUID)).thenReturn(org); + var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); + + OrderEmailContext ctx = mapper.buildContext(List.of(order)); + + assertThat(ctx.getOrganization().getContactEmail()).isEqualTo("first@acme.test"); + assertThat(ctx.getOrganization().getAddress().getAddressLine1()).isEqualTo("First St"); + } + + @Test + void buildContext_organizationWithEmptyLists_returnsEmptyStrings() throws IOException { + var org = new Organization(); + org.setName("Acme Books"); + when(organizationsService.getOrganizationById(VENDOR_UUID)).thenReturn(org); + var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); + + OrderEmailContext ctx = mapper.buildContext(List.of(order)); + + assertThat(ctx.getOrganization().getName()).isEqualTo("Acme Books"); + assertThat(ctx.getOrganization().getContactEmail()).isEmpty(); + assertThat(ctx.getOrganization().getAddress().getAddressLine1()).isEmpty(); + assertThat(ctx.getOrganization().getAddress().getCity()).isEmpty(); + assertThat(ctx.getOrganization().getAddress().getZipCode()).isEmpty(); + assertThat(ctx.getOrganization().getAddress().getCountry()).isEmpty(); + } + + private OrganizationEmail buildEmail(String value, Boolean isPrimary) { + var email = new OrganizationEmail(); + email.setValue(value); + email.setIsPrimary(isPrimary); + return email; + } + + private OrganizationAddress buildAddress(String line1, String city, String zip, String country, Boolean isPrimary) { + var address = new OrganizationAddress(); + address.setAddressLine1(line1); + address.setCity(city); + address.setZipCode(zip); + address.setCountry(country); + address.setIsPrimary(isPrimary); + return address; } @Test From 4fd9dfc962be38f935779249771a28f3ac76130b Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Wed, 13 May 2026 15:14:59 +0200 Subject: [PATCH 44/76] Render shipTo/billTo line breaks for HTML email output Extract generic helpers (isHtmlOutput, toLineBreaks, formatDecimal) into an abstract EmailContextMapper base class. --- .../acquisitions/jobs/SendToEmailTasklet.java | 2 +- .../mapper/EmailContextMapper.java | 21 ++++++++ .../mapper/OrderEmailContextMapper.java | 18 +++---- .../mapper/OrderEmailContextMapperTest.java | 48 +++++++++++++++---- 4 files changed, 67 insertions(+), 22 deletions(-) create mode 100644 src/main/java/org/folio/dew/batch/acquisitions/mapper/EmailContextMapper.java diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java index ecc0d0fe6..f0ee5fcc4 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java @@ -84,8 +84,8 @@ private String[] resolveTemplate(VendorEdiOrdersExportConfig exportConfig, List< var request = TemplateProcessingRequest.builder() .templateId(templateId) - .context(orderEmailContextMapper.buildContext(orders)) .build(); + request.setContext(orderEmailContextMapper.buildContext(orders, request.getOutputFormat())); try { TemplateProcessingResponse response = templateEngineClient.processTemplate(request); diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/EmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/EmailContextMapper.java new file mode 100644 index 000000000..21706e4e0 --- /dev/null +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/EmailContextMapper.java @@ -0,0 +1,21 @@ +package org.folio.dew.batch.acquisitions.mapper; + +import java.math.BigDecimal; + +import org.apache.commons.lang3.StringUtils; + +public abstract class EmailContextMapper { + + protected static boolean isHtmlOutput(String outputFormat) { + return outputFormat != null && outputFormat.toLowerCase().contains("html"); + } + + protected String toLineBreaks(String value, boolean htmlOutput) { + String safe = StringUtils.defaultString(value); + return htmlOutput ? safe.replace("\n", "
") : safe; + } + + protected String formatDecimal(BigDecimal value) { + return value != null ? value.toPlainString() : ""; + } +} diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index 384fd2eb8..d560ac811 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -21,7 +21,6 @@ import org.folio.dew.domain.dto.templateengine.OrganizationContext; import org.springframework.stereotype.Component; -import java.math.BigDecimal; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -30,17 +29,18 @@ @Component @RequiredArgsConstructor -public class OrderEmailContextMapper { +public class OrderEmailContextMapper extends EmailContextMapper { private final IdentifierTypeService identifierTypeService; private final ConfigurationService configurationService; private final UserService userService; private final OrganizationsService organizationsService; - public OrderEmailContext buildContext(List orders) { + public OrderEmailContext buildContext(List orders, String outputFormat) { + boolean htmlOutput = isHtmlOutput(outputFormat); var orderWrappers = orders.stream() .map(order -> new OrderWrapper( - mapOrder(order), + mapOrder(order, htmlOutput), order.getPoLines().stream() .map(line -> new OrderLineWrapper(mapOrderLine(line))) .toList())) @@ -118,15 +118,15 @@ private Optional pickPrimary(List items, Function isPrimar .or(() -> Optional.of(items.getFirst())); } - private OrderContext mapOrder(CompositePurchaseOrder order) { + private OrderContext mapOrder(CompositePurchaseOrder order, boolean htmlOutput) { return OrderContext.builder() .poNumber(StringUtils.defaultString(order.getPoNumber())) .orderDate(StringUtils.defaultString(ExportUtils.getFormattedDate(order.getDateOrdered()))) .orderType(Optional.ofNullable(order.getOrderType()).map(CompositePurchaseOrder.OrderTypeEnum::getValue).orElse("")) .createdBy(userService.getUserName(Optional.ofNullable(order.getMetadata()).map(Metadata::getCreatedByUserId).map(Object::toString).orElse(""))) .totalEstimatedPrice(formatDecimal(order.getTotalEstimatedPrice())) - .shipTo(configurationService.getAddressConfig(order.getShipTo())) - .billTo(configurationService.getAddressConfig(order.getBillTo())) + .shipTo(toLineBreaks(configurationService.getAddressConfig(order.getShipTo()), htmlOutput)) + .billTo(toLineBreaks(configurationService.getAddressConfig(order.getBillTo()), htmlOutput)) .notes(joinNotes(order.getNotes())) .build(); } @@ -158,10 +158,6 @@ private OrderLineContext mapOrderLine(PoLine line) { .build(); } - private String formatDecimal(BigDecimal value) { - return value != null ? value.toPlainString() : ""; - } - private String joinNotes(List notes) { return CollectionUtils.isEmpty(notes) ? "" : String.join("; ", notes); } diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index 03b65caed..25a398acc 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -68,7 +68,7 @@ void buildContext_mapsOrderFields() throws IOException { when(userService.getUserName("7a626480-284e-5b55-9cf2-db32f93956cf")).thenReturn("John Doe"); var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); - OrderEmailContext ctx = mapper.buildContext(List.of(order)); + OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); assertThat(ctx.getOrders()).hasSize(1); var wrapper = ctx.getOrders().get(0); @@ -82,11 +82,39 @@ void buildContext_mapsOrderFields() throws IOException { assertThat(wrapper.order().getNotes()).isEqualTo("First note; Second note"); } + @Test + void buildContext_shipToBillToNewlines_renderedAsBrTags() throws IOException { + when(configurationService.getAddressConfig(SHIP_TO_UUID)) + .thenReturn("SLUB Dresden\nZellescher Weg 18\n01069 Dresden"); + when(configurationService.getAddressConfig(BILL_TO_UUID)) + .thenReturn("Accounts Payable\nPO Box 42\nSpringfield IL"); + var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); + + OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); + + assertThat(ctx.getOrders().get(0).order().getShipTo()) + .isEqualTo("SLUB Dresden
Zellescher Weg 18
01069 Dresden"); + assertThat(ctx.getOrders().get(0).order().getBillTo()) + .isEqualTo("Accounts Payable
PO Box 42
Springfield IL"); + } + + @Test + void buildContext_shipToBillToNewlines_plainTextKeepsNewlines() throws IOException { + when(configurationService.getAddressConfig(SHIP_TO_UUID)) + .thenReturn("SLUB Dresden\nZellescher Weg 18\n01069 Dresden"); + var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); + + OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/plain"); + + assertThat(ctx.getOrders().get(0).order().getShipTo()) + .isEqualTo("SLUB Dresden\nZellescher Weg 18\n01069 Dresden"); + } + @Test void buildContext_shipToBillToNotResolved_returnsEmptyString() throws IOException { var order = loadOrder("edifact/acquisitions/minimalistic_composite_purchase_order.json"); - OrderEmailContext ctx = mapper.buildContext(List.of(order)); + OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); assertThat(ctx.getOrders().get(0).order().getShipTo()).isEmpty(); assertThat(ctx.getOrders().get(0).order().getBillTo()).isEmpty(); @@ -96,7 +124,7 @@ void buildContext_shipToBillToNotResolved_returnsEmptyString() throws IOExceptio void buildContext_mapsOrderLineFields() throws IOException { var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); - OrderEmailContext ctx = mapper.buildContext(List.of(order)); + OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); assertThat(ctx.getOrders()).hasSize(1); var lines = ctx.getOrders().get(0).orderLines(); @@ -129,14 +157,14 @@ void buildContext_multipleOrders_producesOneWrapperPerOrder() throws IOException var order1 = loadOrder("edifact/acquisitions/composite_purchase_order.json"); var order2 = loadOrder("edifact/acquisitions/minimalistic_composite_purchase_order.json"); - OrderEmailContext ctx = mapper.buildContext(List.of(order1, order2)); + OrderEmailContext ctx = mapper.buildContext(List.of(order1, order2), "text/html"); assertThat(ctx.getOrders()).hasSize(2); } @Test void buildContext_emptyOrders_returnsEmptyList() { - OrderEmailContext ctx = mapper.buildContext(List.of()); + OrderEmailContext ctx = mapper.buildContext(List.of(), "text/html"); assertThat(ctx.getOrders()).isEmpty(); assertThat(ctx.getOrganization().getName()).isEmpty(); @@ -159,7 +187,7 @@ void buildContext_mapsOrganizationFields() throws IOException { when(organizationsService.getOrganizationById(VENDOR_UUID)).thenReturn(org); var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); - OrderEmailContext ctx = mapper.buildContext(List.of(order)); + OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); var organization = ctx.getOrganization(); assertThat(organization.getName()).isEqualTo("Acme Books"); @@ -179,7 +207,7 @@ void buildContext_picksFirstWhenNoPrimary() throws IOException { when(organizationsService.getOrganizationById(VENDOR_UUID)).thenReturn(org); var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); - OrderEmailContext ctx = mapper.buildContext(List.of(order)); + OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); assertThat(ctx.getOrganization().getContactEmail()).isEqualTo("first@acme.test"); assertThat(ctx.getOrganization().getAddress().getAddressLine1()).isEqualTo("First St"); @@ -192,7 +220,7 @@ void buildContext_organizationWithEmptyLists_returnsEmptyStrings() throws IOExce when(organizationsService.getOrganizationById(VENDOR_UUID)).thenReturn(org); var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); - OrderEmailContext ctx = mapper.buildContext(List.of(order)); + OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); assertThat(ctx.getOrganization().getName()).isEqualTo("Acme Books"); assertThat(ctx.getOrganization().getContactEmail()).isEmpty(); @@ -224,7 +252,7 @@ void buildContext_nullCost_doesNotThrow() throws IOException { var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); order.getPoLines().get(0).setCost(null); - OrderEmailContext ctx = mapper.buildContext(List.of(order)); + OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); OrderLineContext line = ctx.getOrders().get(0).orderLines().get(0).orderLine(); assertThat(line.getListUnitPrice()).isEmpty(); @@ -237,7 +265,7 @@ void buildContext_nullVendorDetail_doesNotThrow() throws IOException { var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); order.getPoLines().get(0).setVendorDetail(null); - OrderEmailContext ctx = mapper.buildContext(List.of(order)); + OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); OrderLineContext line = ctx.getOrders().get(0).orderLines().get(0).orderLine(); assertThat(line.getVendorRefNumber()).isEmpty(); From ad4486c6573ffadb07df4335dda04062293b17fa Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Mon, 18 May 2026 11:35:48 +0200 Subject: [PATCH 45/76] Rename organization tokens and require strict isPrimary flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OrganizationContext now exposes primaryEmail and primaryAddress (was contactEmail and address). The picker no longer falls back to the first list entry — only an entry flagged isPrimary qualifies, otherwise the tokens render empty. --- .../mapper/OrderEmailContextMapper.java | 11 +++--- .../templateengine/OrganizationContext.java | 4 +-- .../mapper/OrderEmailContextMapperTest.java | 35 ++++++++++--------- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index d560ac811..c9248731f 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -67,8 +67,8 @@ private OrganizationContext mapOrganization(List orders) return OrganizationContext.builder() .name(StringUtils.defaultString(org.getName())) .code(StringUtils.defaultString(org.getCode())) - .contactEmail(pickPrimaryEmail(org.getEmails())) - .address(pickPrimaryAddress(org.getAddresses())) + .primaryEmail(pickPrimaryEmail(org.getEmails())) + .primaryAddress(pickPrimaryAddress(org.getAddresses())) .build(); } @@ -76,8 +76,8 @@ private OrganizationContext emptyOrganizationContext() { return OrganizationContext.builder() .name("") .code("") - .contactEmail("") - .address(emptyOrganizationAddressContext()) + .primaryEmail("") + .primaryAddress(emptyOrganizationAddressContext()) .build(); } @@ -114,8 +114,7 @@ private Optional pickPrimary(List items, Function isPrimar } return items.stream() .filter(item -> Boolean.TRUE.equals(isPrimary.apply(item))) - .findFirst() - .or(() -> Optional.of(items.getFirst())); + .findFirst(); } private OrderContext mapOrder(CompositePurchaseOrder order, boolean htmlOutput) { diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java index 2405a0c52..996d2624e 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java @@ -9,6 +9,6 @@ public class OrganizationContext { private String name; private String code; - private String contactEmail; - private OrganizationAddressContext address; + private String primaryEmail; + private OrganizationAddressContext primaryAddress; } diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index 25a398acc..5db24b045 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -169,8 +169,8 @@ void buildContext_emptyOrders_returnsEmptyList() { assertThat(ctx.getOrders()).isEmpty(); assertThat(ctx.getOrganization().getName()).isEmpty(); assertThat(ctx.getOrganization().getCode()).isEmpty(); - assertThat(ctx.getOrganization().getContactEmail()).isEmpty(); - assertThat(ctx.getOrganization().getAddress().getAddressLine1()).isEmpty(); + assertThat(ctx.getOrganization().getPrimaryEmail()).isEmpty(); + assertThat(ctx.getOrganization().getPrimaryAddress().getAddressLine1()).isEmpty(); } @Test @@ -192,25 +192,28 @@ void buildContext_mapsOrganizationFields() throws IOException { var organization = ctx.getOrganization(); assertThat(organization.getName()).isEqualTo("Acme Books"); assertThat(organization.getCode()).isEqualTo("ACME"); - assertThat(organization.getContactEmail()).isEqualTo("primary@acme.test"); - assertThat(organization.getAddress().getAddressLine1()).isEqualTo("100 Main St"); - assertThat(organization.getAddress().getCity()).isEqualTo("Springfield"); - assertThat(organization.getAddress().getZipCode()).isEqualTo("12345"); - assertThat(organization.getAddress().getCountry()).isEqualTo("USA"); + assertThat(organization.getPrimaryEmail()).isEqualTo("primary@acme.test"); + assertThat(organization.getPrimaryAddress().getAddressLine1()).isEqualTo("100 Main St"); + assertThat(organization.getPrimaryAddress().getCity()).isEqualTo("Springfield"); + assertThat(organization.getPrimaryAddress().getZipCode()).isEqualTo("12345"); + assertThat(organization.getPrimaryAddress().getCountry()).isEqualTo("USA"); } @Test - void buildContext_picksFirstWhenNoPrimary() throws IOException { + void buildContext_noPrimaryFlagged_returnsEmpty() throws IOException { var org = new Organization(); - org.setEmails(List.of(buildEmail("first@acme.test", null), buildEmail("second@acme.test", null))); + org.setEmails(List.of(buildEmail("first@acme.test", null), buildEmail("second@acme.test", false))); org.setAddresses(List.of(buildAddress("First St", "FirstCity", "11111", "USA", null))); when(organizationsService.getOrganizationById(VENDOR_UUID)).thenReturn(org); var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); - assertThat(ctx.getOrganization().getContactEmail()).isEqualTo("first@acme.test"); - assertThat(ctx.getOrganization().getAddress().getAddressLine1()).isEqualTo("First St"); + assertThat(ctx.getOrganization().getPrimaryEmail()).isEmpty(); + assertThat(ctx.getOrganization().getPrimaryAddress().getAddressLine1()).isEmpty(); + assertThat(ctx.getOrganization().getPrimaryAddress().getCity()).isEmpty(); + assertThat(ctx.getOrganization().getPrimaryAddress().getZipCode()).isEmpty(); + assertThat(ctx.getOrganization().getPrimaryAddress().getCountry()).isEmpty(); } @Test @@ -223,11 +226,11 @@ void buildContext_organizationWithEmptyLists_returnsEmptyStrings() throws IOExce OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); assertThat(ctx.getOrganization().getName()).isEqualTo("Acme Books"); - assertThat(ctx.getOrganization().getContactEmail()).isEmpty(); - assertThat(ctx.getOrganization().getAddress().getAddressLine1()).isEmpty(); - assertThat(ctx.getOrganization().getAddress().getCity()).isEmpty(); - assertThat(ctx.getOrganization().getAddress().getZipCode()).isEmpty(); - assertThat(ctx.getOrganization().getAddress().getCountry()).isEmpty(); + assertThat(ctx.getOrganization().getPrimaryEmail()).isEmpty(); + assertThat(ctx.getOrganization().getPrimaryAddress().getAddressLine1()).isEmpty(); + assertThat(ctx.getOrganization().getPrimaryAddress().getCity()).isEmpty(); + assertThat(ctx.getOrganization().getPrimaryAddress().getZipCode()).isEmpty(); + assertThat(ctx.getOrganization().getPrimaryAddress().getCountry()).isEmpty(); } private OrganizationEmail buildEmail(String value, Boolean isPrimary) { From cf58607cf1360b31b4b901afc84b99c3ddca712a Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Mon, 18 May 2026 14:09:16 +0200 Subject: [PATCH 46/76] Drop organization code and primaryEmail tokens These template tokens are not needed yet, so OrganizationContext now exposes only name and primaryAddress. The OrganizationEmail source DTO, the emails field on Organization, and the pickPrimaryEmail helper are deleted as unused; Organization.code remains for other callers. --- .../mapper/OrderEmailContextMapper.java | 12 ------------ .../dto/acquisitions/edifact/Organization.java | 1 - .../edifact/OrganizationEmail.java | 9 --------- .../templateengine/OrganizationContext.java | 2 -- .../mapper/OrderEmailContextMapperTest.java | 18 ------------------ 5 files changed, 42 deletions(-) delete mode 100644 src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/OrganizationEmail.java diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index c9248731f..444a315e7 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -11,7 +11,6 @@ import org.folio.dew.domain.dto.*; import org.folio.dew.domain.dto.acquisitions.edifact.Organization; import org.folio.dew.domain.dto.acquisitions.edifact.OrganizationAddress; -import org.folio.dew.domain.dto.acquisitions.edifact.OrganizationEmail; import org.folio.dew.domain.dto.templateengine.OrderContext; import org.folio.dew.domain.dto.templateengine.OrderEmailContext; import org.folio.dew.domain.dto.templateengine.OrderLineContext; @@ -66,8 +65,6 @@ private OrganizationContext mapOrganization(List orders) } return OrganizationContext.builder() .name(StringUtils.defaultString(org.getName())) - .code(StringUtils.defaultString(org.getCode())) - .primaryEmail(pickPrimaryEmail(org.getEmails())) .primaryAddress(pickPrimaryAddress(org.getAddresses())) .build(); } @@ -75,8 +72,6 @@ private OrganizationContext mapOrganization(List orders) private OrganizationContext emptyOrganizationContext() { return OrganizationContext.builder() .name("") - .code("") - .primaryEmail("") .primaryAddress(emptyOrganizationAddressContext()) .build(); } @@ -90,13 +85,6 @@ private OrganizationAddressContext emptyOrganizationAddressContext() { .build(); } - private String pickPrimaryEmail(List emails) { - return pickPrimary(emails, OrganizationEmail::getIsPrimary) - .map(OrganizationEmail::getValue) - .map(StringUtils::defaultString) - .orElse(""); - } - private OrganizationAddressContext pickPrimaryAddress(List addresses) { return pickPrimary(addresses, OrganizationAddress::getIsPrimary) .map(addr -> OrganizationAddressContext.builder() diff --git a/src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/Organization.java b/src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/Organization.java index 930dd0531..674fb5afc 100644 --- a/src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/Organization.java +++ b/src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/Organization.java @@ -9,5 +9,4 @@ public class Organization { private String code; private String name; private List addresses; - private List emails; } diff --git a/src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/OrganizationEmail.java b/src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/OrganizationEmail.java deleted file mode 100644 index 902d25cfc..000000000 --- a/src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/OrganizationEmail.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.folio.dew.domain.dto.acquisitions.edifact; - -import lombok.Data; - -@Data -public class OrganizationEmail { - private String value; - private Boolean isPrimary; -} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java index 996d2624e..1e6d3a60e 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java @@ -8,7 +8,5 @@ public class OrganizationContext { private String name; - private String code; - private String primaryEmail; private OrganizationAddressContext primaryAddress; } diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index 5db24b045..50114a507 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -18,7 +18,6 @@ import org.folio.dew.domain.dto.CompositePurchaseOrder; import org.folio.dew.domain.dto.acquisitions.edifact.Organization; import org.folio.dew.domain.dto.acquisitions.edifact.OrganizationAddress; -import org.folio.dew.domain.dto.acquisitions.edifact.OrganizationEmail; import org.folio.dew.domain.dto.templateengine.OrderEmailContext; import org.folio.dew.domain.dto.templateengine.OrderLineContext; import org.folio.dew.domain.dto.templateengine.OrderWrapper; @@ -168,8 +167,6 @@ void buildContext_emptyOrders_returnsEmptyList() { assertThat(ctx.getOrders()).isEmpty(); assertThat(ctx.getOrganization().getName()).isEmpty(); - assertThat(ctx.getOrganization().getCode()).isEmpty(); - assertThat(ctx.getOrganization().getPrimaryEmail()).isEmpty(); assertThat(ctx.getOrganization().getPrimaryAddress().getAddressLine1()).isEmpty(); } @@ -178,9 +175,6 @@ void buildContext_mapsOrganizationFields() throws IOException { var org = new Organization(); org.setName("Acme Books"); org.setCode("ACME"); - org.setEmails(List.of( - buildEmail("secondary@acme.test", false), - buildEmail("primary@acme.test", true))); org.setAddresses(List.of( buildAddress("321 Other St", "Otherville", "99999", "USA", false), buildAddress("100 Main St", "Springfield", "12345", "USA", true))); @@ -191,8 +185,6 @@ void buildContext_mapsOrganizationFields() throws IOException { var organization = ctx.getOrganization(); assertThat(organization.getName()).isEqualTo("Acme Books"); - assertThat(organization.getCode()).isEqualTo("ACME"); - assertThat(organization.getPrimaryEmail()).isEqualTo("primary@acme.test"); assertThat(organization.getPrimaryAddress().getAddressLine1()).isEqualTo("100 Main St"); assertThat(organization.getPrimaryAddress().getCity()).isEqualTo("Springfield"); assertThat(organization.getPrimaryAddress().getZipCode()).isEqualTo("12345"); @@ -202,14 +194,12 @@ void buildContext_mapsOrganizationFields() throws IOException { @Test void buildContext_noPrimaryFlagged_returnsEmpty() throws IOException { var org = new Organization(); - org.setEmails(List.of(buildEmail("first@acme.test", null), buildEmail("second@acme.test", false))); org.setAddresses(List.of(buildAddress("First St", "FirstCity", "11111", "USA", null))); when(organizationsService.getOrganizationById(VENDOR_UUID)).thenReturn(org); var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); - assertThat(ctx.getOrganization().getPrimaryEmail()).isEmpty(); assertThat(ctx.getOrganization().getPrimaryAddress().getAddressLine1()).isEmpty(); assertThat(ctx.getOrganization().getPrimaryAddress().getCity()).isEmpty(); assertThat(ctx.getOrganization().getPrimaryAddress().getZipCode()).isEmpty(); @@ -226,20 +216,12 @@ void buildContext_organizationWithEmptyLists_returnsEmptyStrings() throws IOExce OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); assertThat(ctx.getOrganization().getName()).isEqualTo("Acme Books"); - assertThat(ctx.getOrganization().getPrimaryEmail()).isEmpty(); assertThat(ctx.getOrganization().getPrimaryAddress().getAddressLine1()).isEmpty(); assertThat(ctx.getOrganization().getPrimaryAddress().getCity()).isEmpty(); assertThat(ctx.getOrganization().getPrimaryAddress().getZipCode()).isEmpty(); assertThat(ctx.getOrganization().getPrimaryAddress().getCountry()).isEmpty(); } - private OrganizationEmail buildEmail(String value, Boolean isPrimary) { - var email = new OrganizationEmail(); - email.setValue(value); - email.setIsPrimary(isPrimary); - return email; - } - private OrganizationAddress buildAddress(String line1, String city, String zip, String country, Boolean isPrimary) { var address = new OrganizationAddress(); address.setAddressLine1(line1); From d20b79014f25c8b9f3dead39ce20c13352b0a1ed Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Wed, 20 May 2026 10:18:05 +0200 Subject: [PATCH 47/76] Restore organization code token on OrderEmailContext Brings back the code field on OrganizationContext and its mapping in OrderEmailContextMapper so email templates can reference the vendor code. --- .../dew/batch/acquisitions/mapper/OrderEmailContextMapper.java | 2 ++ .../dew/domain/dto/templateengine/OrganizationContext.java | 1 + .../batch/acquisitions/mapper/OrderEmailContextMapperTest.java | 2 ++ 3 files changed, 5 insertions(+) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index 444a315e7..e3534c14e 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -65,6 +65,7 @@ private OrganizationContext mapOrganization(List orders) } return OrganizationContext.builder() .name(StringUtils.defaultString(org.getName())) + .code(StringUtils.defaultString(org.getCode())) .primaryAddress(pickPrimaryAddress(org.getAddresses())) .build(); } @@ -72,6 +73,7 @@ private OrganizationContext mapOrganization(List orders) private OrganizationContext emptyOrganizationContext() { return OrganizationContext.builder() .name("") + .code("") .primaryAddress(emptyOrganizationAddressContext()) .build(); } diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java index 1e6d3a60e..7615a21ea 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java @@ -8,5 +8,6 @@ public class OrganizationContext { private String name; + private String code; private OrganizationAddressContext primaryAddress; } diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index 50114a507..c829ab7dc 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -167,6 +167,7 @@ void buildContext_emptyOrders_returnsEmptyList() { assertThat(ctx.getOrders()).isEmpty(); assertThat(ctx.getOrganization().getName()).isEmpty(); + assertThat(ctx.getOrganization().getCode()).isEmpty(); assertThat(ctx.getOrganization().getPrimaryAddress().getAddressLine1()).isEmpty(); } @@ -185,6 +186,7 @@ void buildContext_mapsOrganizationFields() throws IOException { var organization = ctx.getOrganization(); assertThat(organization.getName()).isEqualTo("Acme Books"); + assertThat(organization.getCode()).isEqualTo("ACME"); assertThat(organization.getPrimaryAddress().getAddressLine1()).isEqualTo("100 Main St"); assertThat(organization.getPrimaryAddress().getCity()).isEqualTo("Springfield"); assertThat(organization.getPrimaryAddress().getZipCode()).isEqualTo("12345"); From 88330866b8414f14567605d2d5f893cdbcd1a9da Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Wed, 20 May 2026 10:27:02 +0200 Subject: [PATCH 48/76] Drop notes token from OrderContext Removes the order-level notes field on OrderContext along with its mapping and helper in OrderEmailContextMapper, as the token is no longer needed by email templates. --- .../batch/acquisitions/mapper/OrderEmailContextMapper.java | 5 ----- .../folio/dew/domain/dto/templateengine/OrderContext.java | 1 - .../acquisitions/mapper/OrderEmailContextMapperTest.java | 1 - 3 files changed, 7 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index e3534c14e..79f9d1bed 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -116,7 +116,6 @@ private OrderContext mapOrder(CompositePurchaseOrder order, boolean htmlOutput) .totalEstimatedPrice(formatDecimal(order.getTotalEstimatedPrice())) .shipTo(toLineBreaks(configurationService.getAddressConfig(order.getShipTo()), htmlOutput)) .billTo(toLineBreaks(configurationService.getAddressConfig(order.getBillTo()), htmlOutput)) - .notes(joinNotes(order.getNotes())) .build(); } @@ -147,10 +146,6 @@ private OrderLineContext mapOrderLine(PoLine line) { .build(); } - private String joinNotes(List notes) { - return CollectionUtils.isEmpty(notes) ? "" : String.join("; ", notes); - } - private String mapContributors(List contributors) { if (CollectionUtils.isEmpty(contributors)) { return ""; diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java index e9282a9e8..b1a98ee68 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java @@ -14,5 +14,4 @@ public class OrderContext { private String totalEstimatedPrice; private String shipTo; private String billTo; - private String notes; } diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index c829ab7dc..976a9e876 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -78,7 +78,6 @@ void buildContext_mapsOrderFields() throws IOException { assertThat(wrapper.order().getTotalEstimatedPrice()).isEqualTo("1.8"); assertThat(wrapper.order().getShipTo()).isEqualTo(SHIP_TO_ADDRESS); assertThat(wrapper.order().getBillTo()).isEqualTo(BILL_TO_ADDRESS); - assertThat(wrapper.order().getNotes()).isEqualTo("First note; Second note"); } @Test From 292e6a5a392af7afb416367e5e614a1571ec59d4 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Wed, 27 May 2026 16:02:31 +0200 Subject: [PATCH 49/76] Trim unused tokens from order email context Drop orderType/totalEstimatedPrice, organization code, and the contributors/publisher/materialType/fundCodes/vendorRefNumber/instructions order-line tokens, along with their mapper helpers and test assertions. --- .../mapper/OrderEmailContextMapper.java | 59 ++----------------- .../dto/templateengine/OrderContext.java | 2 - .../dto/templateengine/OrderLineContext.java | 6 -- .../templateengine/OrganizationContext.java | 1 - .../mapper/OrderEmailContextMapperTest.java | 25 -------- 5 files changed, 5 insertions(+), 88 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index 79f9d1bed..824fa3998 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -65,7 +65,6 @@ private OrganizationContext mapOrganization(List orders) } return OrganizationContext.builder() .name(StringUtils.defaultString(org.getName())) - .code(StringUtils.defaultString(org.getCode())) .primaryAddress(pickPrimaryAddress(org.getAddresses())) .build(); } @@ -73,7 +72,6 @@ private OrganizationContext mapOrganization(List orders) private OrganizationContext emptyOrganizationContext() { return OrganizationContext.builder() .name("") - .code("") .primaryAddress(emptyOrganizationAddressContext()) .build(); } @@ -111,9 +109,7 @@ private OrderContext mapOrder(CompositePurchaseOrder order, boolean htmlOutput) return OrderContext.builder() .poNumber(StringUtils.defaultString(order.getPoNumber())) .orderDate(StringUtils.defaultString(ExportUtils.getFormattedDate(order.getDateOrdered()))) - .orderType(Optional.ofNullable(order.getOrderType()).map(CompositePurchaseOrder.OrderTypeEnum::getValue).orElse("")) .createdBy(userService.getUserName(Optional.ofNullable(order.getMetadata()).map(Metadata::getCreatedByUserId).map(Object::toString).orElse(""))) - .totalEstimatedPrice(formatDecimal(order.getTotalEstimatedPrice())) .shipTo(toLineBreaks(configurationService.getAddressConfig(order.getShipTo()), htmlOutput)) .billTo(toLineBreaks(configurationService.getAddressConfig(order.getBillTo()), htmlOutput)) .build(); @@ -121,41 +117,25 @@ private OrderContext mapOrder(CompositePurchaseOrder order, boolean htmlOutput) private OrderLineContext mapOrderLine(PoLine line) { var cost = line.getCost(); - var physQty = Optional.ofNullable(cost).map(Cost::getQuantityPhysical).orElse(0); - var elecQty = Optional.ofNullable(cost).map(Cost::getQuantityElectronic).orElse(0); + var quantityPhysical = Optional.ofNullable(cost).map(Cost::getQuantityPhysical).orElse(0); + var quantityElectronic = Optional.ofNullable(cost).map(Cost::getQuantityElectronic).orElse(0); return OrderLineContext.builder() .poLineNumber(StringUtils.defaultString(line.getPoLineNumber())) .title(StringUtils.defaultString(line.getTitleOrPackage())) - .contributors(mapContributors(line.getContributors())) - .publisher(StringUtils.defaultString(line.getPublisher())) .publicationDate(StringUtils.defaultString(line.getPublicationDate())) .edition(StringUtils.defaultString(line.getEdition())) .productIdentifier(mapProductIdentifiers(line.getDetails())) .productIdentifierType(mapProductIdentifierTypes(line.getDetails())) - .materialType(resolveMaterialType(line.getPhysical(), line.getEresource())) .listUnitPrice(formatDecimal(Optional.ofNullable(cost).map(Cost::getListUnitPrice).orElse(null))) .listUnitPriceElectronic(formatDecimal(Optional.ofNullable(cost).map(Cost::getListUnitPriceElectronic).orElse(null))) - .quantityPhysical(physQty) - .quantityElectronic(elecQty) - .quantity(physQty + elecQty) + .quantityPhysical(quantityPhysical) + .quantityElectronic(quantityElectronic) + .quantity(quantityPhysical + quantityElectronic) .estimatedPrice(formatDecimal(Optional.ofNullable(cost).map(Cost::getPoLineEstimatedPrice).orElse(null))) .currency(Optional.ofNullable(cost).map(Cost::getCurrency).orElse("")) - .fundCodes(mapFundCodes(line.getFundDistribution())) - .vendorRefNumber(mapVendorRefNumber(line.getVendorDetail())) - .instructions(Optional.ofNullable(line.getVendorDetail()).map(VendorDetail::getInstructions).orElse("")) .build(); } - private String mapContributors(List contributors) { - if (CollectionUtils.isEmpty(contributors)) { - return ""; - } - return contributors.stream() - .map(Contributor::getContributor) - .filter(StringUtils::isNotBlank) - .collect(Collectors.joining("; ")); - } - private String mapProductIdentifiers(Details details) { if (details == null || CollectionUtils.isEmpty(details.getProductIds())) { return ""; @@ -177,33 +157,4 @@ private String mapProductIdentifierTypes(Details details) { .filter(StringUtils::isNotBlank) .collect(Collectors.joining("; ")); } - - private String resolveMaterialType(Physical physical, Eresource eresource) { - if (physical != null && StringUtils.isNotBlank(physical.getMaterialType())) { - return physical.getMaterialType(); - } - if (eresource != null && StringUtils.isNotBlank(eresource.getMaterialType())) { - return eresource.getMaterialType(); - } - return ""; - } - - private String mapFundCodes(List fundDistributions) { - if (CollectionUtils.isEmpty(fundDistributions)) { - return ""; - } - return fundDistributions.stream() - .map(FundDistribution::getCode) - .filter(StringUtils::isNotBlank) - .collect(Collectors.joining(", ")); - } - - private String mapVendorRefNumber(VendorDetail vendorDetail) { - return Optional.ofNullable(vendorDetail) - .map(VendorDetail::getReferenceNumbers) - .filter(refs -> !refs.isEmpty()) - .map(List::getFirst) - .map(ReferenceNumberItem::getRefNumber) - .orElse(""); - } } diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java index b1a98ee68..fedb9a4f1 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java @@ -9,9 +9,7 @@ public class OrderContext { private String poNumber; private String orderDate; - private String orderType; private String createdBy; - private String totalEstimatedPrice; private String shipTo; private String billTo; } diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java index f99ca039e..a7a60589e 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java @@ -9,13 +9,10 @@ public class OrderLineContext { private String poLineNumber; private String title; - private String contributors; - private String publisher; private String publicationDate; private String edition; private String productIdentifier; private String productIdentifierType; - private String materialType; private String listUnitPrice; private String listUnitPriceElectronic; private Integer quantityPhysical; @@ -23,7 +20,4 @@ public class OrderLineContext { private Integer quantity; private String estimatedPrice; private String currency; - private String fundCodes; - private String vendorRefNumber; - private String instructions; } diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java index 7615a21ea..1e6d3a60e 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java @@ -8,6 +8,5 @@ public class OrganizationContext { private String name; - private String code; private OrganizationAddressContext primaryAddress; } diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index 976a9e876..b1970412b 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -73,9 +73,7 @@ void buildContext_mapsOrderFields() throws IOException { var wrapper = ctx.getOrders().get(0); assertThat(wrapper.order().getPoNumber()).isEqualTo("10000"); assertThat(wrapper.order().getOrderDate()).isEqualTo("2021-01-15"); - assertThat(wrapper.order().getOrderType()).isEqualTo("One-Time"); assertThat(wrapper.order().getCreatedBy()).isEqualTo("John Doe"); - assertThat(wrapper.order().getTotalEstimatedPrice()).isEqualTo("1.8"); assertThat(wrapper.order().getShipTo()).isEqualTo(SHIP_TO_ADDRESS); assertThat(wrapper.order().getBillTo()).isEqualTo(BILL_TO_ADDRESS); } @@ -131,23 +129,14 @@ void buildContext_mapsOrderLineFields() throws IOException { OrderLineContext line = lines.get(0).orderLine(); assertThat(line.getPoLineNumber()).isEqualTo("10000-1"); assertThat(line.getTitle()).isEqualTo("Futures, biometrics and neuroscience research Luiz Moutinho, Mladen Sokele, editors"); - assertThat(line.getContributors()).isEqualTo("Moutinho, Luiz"); - assertThat(line.getPublisher()).isEqualTo("Palgrave Macmillan"); assertThat(line.getPublicationDate()).isEqualTo("2021"); assertThat(line.getEdition()).isEqualTo("2nd ed."); assertThat(line.getProductIdentifier()).isEqualTo("9783319643991"); assertThat(line.getProductIdentifierType()).isEqualTo("ISBN"); - assertThat(line.getMaterialType()).isEqualTo("1a54b431-2e4f-452d-9cae-9cee66c9a892"); assertThat(line.getListUnitPrice()).isEqualTo("2.0"); - assertThat(line.getListUnitPriceElectronic()).isEqualTo("3.0"); assertThat(line.getCurrency()).isEqualTo("USD"); - assertThat(line.getQuantityPhysical()).isEqualTo(1); - assertThat(line.getQuantityElectronic()).isEqualTo(0); assertThat(line.getQuantity()).isEqualTo(1); assertThat(line.getEstimatedPrice()).isEqualTo("1.8"); - assertThat(line.getFundCodes()).isEqualTo("USHIST"); - assertThat(line.getVendorRefNumber()).isEqualTo("ORD1000"); - assertThat(line.getInstructions()).isEqualTo("Handle with care"); } @Test @@ -166,7 +155,6 @@ void buildContext_emptyOrders_returnsEmptyList() { assertThat(ctx.getOrders()).isEmpty(); assertThat(ctx.getOrganization().getName()).isEmpty(); - assertThat(ctx.getOrganization().getCode()).isEmpty(); assertThat(ctx.getOrganization().getPrimaryAddress().getAddressLine1()).isEmpty(); } @@ -185,7 +173,6 @@ void buildContext_mapsOrganizationFields() throws IOException { var organization = ctx.getOrganization(); assertThat(organization.getName()).isEqualTo("Acme Books"); - assertThat(organization.getCode()).isEqualTo("ACME"); assertThat(organization.getPrimaryAddress().getAddressLine1()).isEqualTo("100 Main St"); assertThat(organization.getPrimaryAddress().getCity()).isEqualTo("Springfield"); assertThat(organization.getPrimaryAddress().getZipCode()).isEqualTo("12345"); @@ -246,18 +233,6 @@ void buildContext_nullCost_doesNotThrow() throws IOException { assertThat(line.getQuantity()).isEqualTo(0); } - @Test - void buildContext_nullVendorDetail_doesNotThrow() throws IOException { - var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); - order.getPoLines().get(0).setVendorDetail(null); - - OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); - - OrderLineContext line = ctx.getOrders().get(0).orderLines().get(0).orderLine(); - assertThat(line.getVendorRefNumber()).isEmpty(); - assertThat(line.getInstructions()).isEmpty(); - } - private CompositePurchaseOrder loadOrder(String path) throws IOException { return objectMapper.readValue(getMockData(path), CompositePurchaseOrder.class); } From 212e8daf7f47700e973d3130bfe8cb8b35e43334 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Wed, 27 May 2026 18:20:34 +0200 Subject: [PATCH 50/76] Split email-context test fixture from EDIFACT/CSV mapper fixture Restores composite_purchase_order.json to its master content so EdifactMapperTest and CsvMapperTest golden outputs match again, and moves the email-context-specific fields into a new composite_purchase_order_email_context.json consumed only by OrderEmailContextMapperTest. --- .../mapper/OrderEmailContextMapperTest.java | 18 +-- .../composite_purchase_order.json | 12 +- ...omposite_purchase_order_email_context.json | 116 ++++++++++++++++++ 3 files changed, 128 insertions(+), 18 deletions(-) create mode 100644 src/test/resources/edifact/acquisitions/composite_purchase_order_email_context.json diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index b1970412b..e21dfdc08 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -65,7 +65,7 @@ void buildContext_mapsOrderFields() throws IOException { when(configurationService.getAddressConfig(SHIP_TO_UUID)).thenReturn(SHIP_TO_ADDRESS); when(configurationService.getAddressConfig(BILL_TO_UUID)).thenReturn(BILL_TO_ADDRESS); when(userService.getUserName("7a626480-284e-5b55-9cf2-db32f93956cf")).thenReturn("John Doe"); - var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); + var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); @@ -84,7 +84,7 @@ void buildContext_shipToBillToNewlines_renderedAsBrTags() throws IOException { .thenReturn("SLUB Dresden\nZellescher Weg 18\n01069 Dresden"); when(configurationService.getAddressConfig(BILL_TO_UUID)) .thenReturn("Accounts Payable\nPO Box 42\nSpringfield IL"); - var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); + var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); @@ -98,7 +98,7 @@ void buildContext_shipToBillToNewlines_renderedAsBrTags() throws IOException { void buildContext_shipToBillToNewlines_plainTextKeepsNewlines() throws IOException { when(configurationService.getAddressConfig(SHIP_TO_UUID)) .thenReturn("SLUB Dresden\nZellescher Weg 18\n01069 Dresden"); - var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); + var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/plain"); @@ -118,7 +118,7 @@ void buildContext_shipToBillToNotResolved_returnsEmptyString() throws IOExceptio @Test void buildContext_mapsOrderLineFields() throws IOException { - var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); + var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); @@ -141,7 +141,7 @@ void buildContext_mapsOrderLineFields() throws IOException { @Test void buildContext_multipleOrders_producesOneWrapperPerOrder() throws IOException { - var order1 = loadOrder("edifact/acquisitions/composite_purchase_order.json"); + var order1 = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); var order2 = loadOrder("edifact/acquisitions/minimalistic_composite_purchase_order.json"); OrderEmailContext ctx = mapper.buildContext(List.of(order1, order2), "text/html"); @@ -167,7 +167,7 @@ void buildContext_mapsOrganizationFields() throws IOException { buildAddress("321 Other St", "Otherville", "99999", "USA", false), buildAddress("100 Main St", "Springfield", "12345", "USA", true))); when(organizationsService.getOrganizationById(VENDOR_UUID)).thenReturn(org); - var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); + var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); @@ -184,7 +184,7 @@ void buildContext_noPrimaryFlagged_returnsEmpty() throws IOException { var org = new Organization(); org.setAddresses(List.of(buildAddress("First St", "FirstCity", "11111", "USA", null))); when(organizationsService.getOrganizationById(VENDOR_UUID)).thenReturn(org); - var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); + var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); @@ -199,7 +199,7 @@ void buildContext_organizationWithEmptyLists_returnsEmptyStrings() throws IOExce var org = new Organization(); org.setName("Acme Books"); when(organizationsService.getOrganizationById(VENDOR_UUID)).thenReturn(org); - var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); + var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); @@ -222,7 +222,7 @@ private OrganizationAddress buildAddress(String line1, String city, String zip, @Test void buildContext_nullCost_doesNotThrow() throws IOException { - var order = loadOrder("edifact/acquisitions/composite_purchase_order.json"); + var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); order.getPoLines().get(0).setCost(null); OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); diff --git a/src/test/resources/edifact/acquisitions/composite_purchase_order.json b/src/test/resources/edifact/acquisitions/composite_purchase_order.json index 00cbf2b36..81c42105f 100644 --- a/src/test/resources/edifact/acquisitions/composite_purchase_order.json +++ b/src/test/resources/edifact/acquisitions/composite_purchase_order.json @@ -1,10 +1,7 @@ { "id": "91103993-acc5-46c4-a2e6-a79cae1959be", - "shipTo": "11111111-1111-1111-1111-111111111111", - "billTo": "22222222-2222-2222-2222-222222222222", - "dateOrdered": "2021-01-15T00:00:00.000+00:00", "approved": false, - "notes": ["First note", "Second note"], + "notes": [], "poNumber": "10000", "orderType": "One-Time", "reEncumber": false, @@ -17,8 +14,7 @@ "poLines": [ { "id": "f7cebba9-900d-4e46-ac96-9dfdb6da214e", - "edition": "2nd ed.", - "publicationDate": "2021", + "edition": "", "checkinItems": false, "instanceId": "69640328-788e-43fc-9c3c-af39e243f3b7", "acquisitionMethod": "306489dd-0053-49ee-a068-c316444a8f55", @@ -33,7 +29,6 @@ ], "cost": { "listUnitPrice": 2.0, - "listUnitPriceElectronic": 3.0, "currency": "USD", "discount": 10.0, "discountType": "percentage", @@ -76,7 +71,6 @@ "paymentStatus": "Pending", "physical": { "createInventory": "Instance, Holding, Item", - "materialType": "1a54b431-2e4f-452d-9cae-9cee66c9a892", "materialSupplier": "50fb6ae0-cdf1-11e8-a8d5-f2801f1b9fd1", "volumes": [] }, @@ -88,7 +82,7 @@ "source": "User", "titleOrPackage": "Futures, biometrics and neuroscience research Luiz Moutinho, Mladen Sokele, editors", "vendorDetail": { - "instructions": "Handle with care", + "instructions": "", "vendorAccount": "BRXXXXX-01", "referenceNumbers": [ { diff --git a/src/test/resources/edifact/acquisitions/composite_purchase_order_email_context.json b/src/test/resources/edifact/acquisitions/composite_purchase_order_email_context.json new file mode 100644 index 000000000..00cbf2b36 --- /dev/null +++ b/src/test/resources/edifact/acquisitions/composite_purchase_order_email_context.json @@ -0,0 +1,116 @@ +{ + "id": "91103993-acc5-46c4-a2e6-a79cae1959be", + "shipTo": "11111111-1111-1111-1111-111111111111", + "billTo": "22222222-2222-2222-2222-222222222222", + "dateOrdered": "2021-01-15T00:00:00.000+00:00", + "approved": false, + "notes": ["First note", "Second note"], + "poNumber": "10000", + "orderType": "One-Time", + "reEncumber": false, + "totalEstimatedPrice": 1.8, + "totalEncumbered": 0.0, + "totalExpended": 0.0, + "totalItems": 1, + "vendor": "50fb6ae0-cdf1-11e8-a8d5-f2801f1b9fd1", + "workflowStatus": "Open", + "poLines": [ + { + "id": "f7cebba9-900d-4e46-ac96-9dfdb6da214e", + "edition": "2nd ed.", + "publicationDate": "2021", + "checkinItems": false, + "instanceId": "69640328-788e-43fc-9c3c-af39e243f3b7", + "acquisitionMethod": "306489dd-0053-49ee-a068-c316444a8f55", + "automaticExport": true, + "claims": [], + "collection": false, + "contributors": [ + { + "contributor": "Moutinho, Luiz", + "contributorNameTypeId": "2b94c631-fca9-4892-a730-03ee529ffe2a" + } + ], + "cost": { + "listUnitPrice": 2.0, + "listUnitPriceElectronic": 3.0, + "currency": "USD", + "discount": 10.0, + "discountType": "percentage", + "quantityPhysical": 1, + "poLineEstimatedPrice": 1.8 + }, + "details": { + "productIds": [ + { + "productId": "9783319643991", + "productIdType": "8261054f-be78-422d-bd51-4ed9f33c3422", + "qualifier": "" + } + ], + "subscriptionInterval": 0 + }, + "eresource": { + "activated": false, + "createInventory": "Instance, Holding", + "trial": false, + "accessProvider": "50fb6ae0-cdf1-11e8-a8d5-f2801f1b9fd1" + }, + "fundDistribution": [ + { + "code": "USHIST", + "fundId": "65032151-39a5-4cef-8810-5350eb316300", + "distributionType": "percentage", + "value": 100.0 + } + ], + "isPackage": false, + "locations": [ + { + "holdingId": "c4a15834-0184-4a6f-9c0c-0ca5bad8286d", + "quantity": 1, + "quantityPhysical": 1 + } + ], + "orderFormat": "Physical Resource", + "paymentStatus": "Pending", + "physical": { + "createInventory": "Instance, Holding, Item", + "materialType": "1a54b431-2e4f-452d-9cae-9cee66c9a892", + "materialSupplier": "50fb6ae0-cdf1-11e8-a8d5-f2801f1b9fd1", + "volumes": [] + }, + "poLineNumber": "10000-1", + "publisher": "Palgrave Macmillan", + "purchaseOrderId": "91103993-acc5-46c4-a2e6-a79cae1959be", + "receiptStatus": "Pending", + "rush": false, + "source": "User", + "titleOrPackage": "Futures, biometrics and neuroscience research Luiz Moutinho, Mladen Sokele, editors", + "vendorDetail": { + "instructions": "Handle with care", + "vendorAccount": "BRXXXXX-01", + "referenceNumbers": [ + { + "refNumber": "ORD1000", + "refNumberType": "Vendor order reference number" + } + ] + }, + "metadata": { + "createdDate": "2021-12-28T08:17:02.171+00:00", + "createdByUserId": "7a626480-284e-5b55-9cf2-db32f93956cf", + "updatedDate": "2021-12-28T08:17:02.171+00:00", + "updatedByUserId": "7a626480-284e-5b55-9cf2-db32f93956cf" + } + } + ], + "acqUnitIds": [], + "metadata": { + "createdDate": "2021-12-28T08:16:56.074+00:00", + "createdByUserId": "7a626480-284e-5b55-9cf2-db32f93956cf", + "updatedDate": "2021-12-28T08:16:56.074+00:00", + "updatedByUserId": "7a626480-284e-5b55-9cf2-db32f93956cf" + }, + "needReEncumber": false +} From 9746e7b6aa485f76186007ed6b63e0385573b89c Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Thu, 28 May 2026 10:35:00 +0200 Subject: [PATCH 51/76] Fix SonarCloud issues --- .../mapper/OrderEmailContextMapper.java | 8 +++--- .../service/JobCommandsReceiverService.java | 11 ++++---- .../mapper/OrderEmailContextMapperTest.java | 26 +++++++++---------- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index 824fa3998..a01b0b80b 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -23,7 +23,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; @Component @@ -86,7 +86,7 @@ private OrganizationAddressContext emptyOrganizationAddressContext() { } private OrganizationAddressContext pickPrimaryAddress(List addresses) { - return pickPrimary(addresses, OrganizationAddress::getIsPrimary) + return pickPrimary(addresses, addr -> Boolean.TRUE.equals(addr.getIsPrimary())) .map(addr -> OrganizationAddressContext.builder() .addressLine1(StringUtils.defaultString(addr.getAddressLine1())) .city(StringUtils.defaultString(addr.getCity())) @@ -96,12 +96,12 @@ private OrganizationAddressContext pickPrimaryAddress(List .orElseGet(this::emptyOrganizationAddressContext); } - private Optional pickPrimary(List items, Function isPrimary) { + private Optional pickPrimary(List items, Predicate isPrimary) { if (CollectionUtils.isEmpty(items)) { return Optional.empty(); } return items.stream() - .filter(item -> Boolean.TRUE.equals(isPrimary.apply(item))) + .filter(isPrimary) .findFirst(); } diff --git a/src/main/java/org/folio/dew/service/JobCommandsReceiverService.java b/src/main/java/org/folio/dew/service/JobCommandsReceiverService.java index 0643eeeae..43e0d060a 100644 --- a/src/main/java/org/folio/dew/service/JobCommandsReceiverService.java +++ b/src/main/java/org/folio/dew/service/JobCommandsReceiverService.java @@ -6,15 +6,14 @@ import static org.folio.dew.utils.Constants.getWorkingDirectory; import jakarta.annotation.PostConstruct; -import java.net.MalformedURLException; -import java.net.URL; +import java.net.URI; +import java.net.URISyntaxException; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.StringUtils; @@ -163,12 +162,12 @@ private boolean deleteOldFiles(JobCommand jobCommand) { List objects = Arrays.stream(filesStr.split(";")).distinct().map(f -> { try { - return StringUtils.stripStart(new URL(f).getPath(), "/"); - } catch (MalformedURLException e) { + return StringUtils.stripStart(new URI(f).getPath(), "/"); + } catch (URISyntaxException e) { log.error(e.getMessage(), e); return null; } - }).filter(StringUtils::isNotBlank).distinct().collect(Collectors.toList()); + }).filter(StringUtils::isNotBlank).distinct().toList(); if (!objects.isEmpty()) { remoteFilesStorage.delete(objects); } diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index e21dfdc08..fee0126bc 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -1,16 +1,6 @@ package org.folio.dew.batch.acquisitions.mapper; -import static org.assertj.core.api.Assertions.assertThat; -import static org.folio.dew.utils.TestUtils.getMockData; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.util.List; -import java.util.UUID; - +import com.fasterxml.jackson.databind.ObjectMapper; import org.folio.dew.batch.acquisitions.services.ConfigurationService; import org.folio.dew.batch.acquisitions.services.IdentifierTypeService; import org.folio.dew.batch.acquisitions.services.OrganizationsService; @@ -20,14 +10,22 @@ import org.folio.dew.domain.dto.acquisitions.edifact.OrganizationAddress; import org.folio.dew.domain.dto.templateengine.OrderEmailContext; import org.folio.dew.domain.dto.templateengine.OrderLineContext; -import org.folio.dew.domain.dto.templateengine.OrderWrapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.folio.dew.utils.TestUtils.getMockData; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class OrderEmailContextMapperTest { @@ -230,7 +228,7 @@ void buildContext_nullCost_doesNotThrow() throws IOException { OrderLineContext line = ctx.getOrders().get(0).orderLines().get(0).orderLine(); assertThat(line.getListUnitPrice()).isEmpty(); assertThat(line.getCurrency()).isEmpty(); - assertThat(line.getQuantity()).isEqualTo(0); + assertThat(line.getQuantity()).isZero(); } private CompositePurchaseOrder loadOrder(String path) throws IOException { From 647da7252502bbff206df24e2a2e868e57831ff3 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Thu, 28 May 2026 10:54:34 +0200 Subject: [PATCH 52/76] Collapse mapOrganization null checks into Optional chain --- .../mapper/OrderEmailContextMapper.java | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index a01b0b80b..225c67d2b 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -9,7 +9,6 @@ import org.folio.dew.batch.acquisitions.services.UserService; import org.folio.dew.batch.acquisitions.utils.ExportUtils; import org.folio.dew.domain.dto.*; -import org.folio.dew.domain.dto.acquisitions.edifact.Organization; import org.folio.dew.domain.dto.acquisitions.edifact.OrganizationAddress; import org.folio.dew.domain.dto.templateengine.OrderContext; import org.folio.dew.domain.dto.templateengine.OrderEmailContext; @@ -51,22 +50,16 @@ public OrderEmailContext buildContext(List orders, Strin } private OrganizationContext mapOrganization(List orders) { - var vendorId = orders.stream() + return orders.stream() .map(CompositePurchaseOrder::getVendor) .filter(Objects::nonNull) .findFirst() - .orElse(null); - if (vendorId == null) { - return emptyOrganizationContext(); - } - Organization org = organizationsService.getOrganizationById(vendorId.toString()); - if (org == null) { - return emptyOrganizationContext(); - } - return OrganizationContext.builder() - .name(StringUtils.defaultString(org.getName())) - .primaryAddress(pickPrimaryAddress(org.getAddresses())) - .build(); + .map(vendorId -> organizationsService.getOrganizationById(vendorId.toString())) + .map(org -> OrganizationContext.builder() + .name(StringUtils.defaultString(org.getName())) + .primaryAddress(pickPrimaryAddress(org.getAddresses())) + .build()) + .orElseGet(this::emptyOrganizationContext); } private OrganizationContext emptyOrganizationContext() { From 517dd647f630a9d889ea3ddcc8a19714a8534c99 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Mon, 22 Jun 2026 17:36:35 +0200 Subject: [PATCH 53/76] Restructure order email context into nested sub-contexts Group PO line price, vendor, and identifier fields under cost, vendorDetail, and details sub-contexts, and expose details.productIds as a typed array with resolved type names. Add orderType to the order context. --- .../mapper/OrderEmailContextMapper.java | 64 ++++++++++++------- .../dto/templateengine/CostContext.java | 17 +++++ .../dto/templateengine/DetailsContext.java | 13 ++++ .../dto/templateengine/OrderContext.java | 1 + .../dto/templateengine/OrderLineContext.java | 12 +--- .../dto/templateengine/ProductIdContext.java | 14 ++++ .../templateengine/VendorDetailContext.java | 11 ++++ .../mapper/OrderEmailContextMapperTest.java | 57 ++++++++++++++--- ...omposite_purchase_order_email_context.json | 2 +- 9 files changed, 149 insertions(+), 42 deletions(-) create mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/CostContext.java create mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/DetailsContext.java create mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/ProductIdContext.java create mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/VendorDetailContext.java diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index 225c67d2b..5e4e1493f 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -10,6 +10,8 @@ import org.folio.dew.batch.acquisitions.utils.ExportUtils; import org.folio.dew.domain.dto.*; import org.folio.dew.domain.dto.acquisitions.edifact.OrganizationAddress; +import org.folio.dew.domain.dto.templateengine.CostContext; +import org.folio.dew.domain.dto.templateengine.DetailsContext; import org.folio.dew.domain.dto.templateengine.OrderContext; import org.folio.dew.domain.dto.templateengine.OrderEmailContext; import org.folio.dew.domain.dto.templateengine.OrderLineContext; @@ -17,13 +19,14 @@ import org.folio.dew.domain.dto.templateengine.OrderWrapper; import org.folio.dew.domain.dto.templateengine.OrganizationAddressContext; import org.folio.dew.domain.dto.templateengine.OrganizationContext; +import org.folio.dew.domain.dto.templateengine.ProductIdContext; +import org.folio.dew.domain.dto.templateengine.VendorDetailContext; import org.springframework.stereotype.Component; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; -import java.util.stream.Collectors; @Component @RequiredArgsConstructor @@ -40,7 +43,7 @@ public OrderEmailContext buildContext(List orders, Strin .map(order -> new OrderWrapper( mapOrder(order, htmlOutput), order.getPoLines().stream() - .map(line -> new OrderLineWrapper(mapOrderLine(line))) + .map(line -> new OrderLineWrapper(mapOrderLine(line, htmlOutput))) .toList())) .toList(); return OrderEmailContext.builder() @@ -101,6 +104,7 @@ private Optional pickPrimary(List items, Predicate isPrimary) { private OrderContext mapOrder(CompositePurchaseOrder order, boolean htmlOutput) { return OrderContext.builder() .poNumber(StringUtils.defaultString(order.getPoNumber())) + .orderType(Optional.ofNullable(order.getOrderType()).map(CompositePurchaseOrder.OrderTypeEnum::getValue).orElse("")) .orderDate(StringUtils.defaultString(ExportUtils.getFormattedDate(order.getDateOrdered()))) .createdBy(userService.getUserName(Optional.ofNullable(order.getMetadata()).map(Metadata::getCreatedByUserId).map(Object::toString).orElse(""))) .shipTo(toLineBreaks(configurationService.getAddressConfig(order.getShipTo()), htmlOutput)) @@ -108,17 +112,22 @@ private OrderContext mapOrder(CompositePurchaseOrder order, boolean htmlOutput) .build(); } - private OrderLineContext mapOrderLine(PoLine line) { - var cost = line.getCost(); - var quantityPhysical = Optional.ofNullable(cost).map(Cost::getQuantityPhysical).orElse(0); - var quantityElectronic = Optional.ofNullable(cost).map(Cost::getQuantityElectronic).orElse(0); + private OrderLineContext mapOrderLine(PoLine line, boolean htmlOutput) { return OrderLineContext.builder() .poLineNumber(StringUtils.defaultString(line.getPoLineNumber())) .title(StringUtils.defaultString(line.getTitleOrPackage())) .publicationDate(StringUtils.defaultString(line.getPublicationDate())) .edition(StringUtils.defaultString(line.getEdition())) - .productIdentifier(mapProductIdentifiers(line.getDetails())) - .productIdentifierType(mapProductIdentifierTypes(line.getDetails())) + .details(mapDetails(line.getDetails())) + .cost(mapCost(line.getCost())) + .vendorDetail(mapVendorDetail(line.getVendorDetail(), htmlOutput)) + .build(); + } + + private CostContext mapCost(Cost cost) { + var quantityPhysical = Optional.ofNullable(cost).map(Cost::getQuantityPhysical).orElse(0); + var quantityElectronic = Optional.ofNullable(cost).map(Cost::getQuantityElectronic).orElse(0); + return CostContext.builder() .listUnitPrice(formatDecimal(Optional.ofNullable(cost).map(Cost::getListUnitPrice).orElse(null))) .listUnitPriceElectronic(formatDecimal(Optional.ofNullable(cost).map(Cost::getListUnitPriceElectronic).orElse(null))) .quantityPhysical(quantityPhysical) @@ -129,25 +138,34 @@ private OrderLineContext mapOrderLine(PoLine line) { .build(); } - private String mapProductIdentifiers(Details details) { - if (details == null || CollectionUtils.isEmpty(details.getProductIds())) { - return ""; - } - return details.getProductIds().stream() - .map(ProductIdentifier::getProductId) - .filter(StringUtils::isNotBlank) - .collect(Collectors.joining("; ")); + private VendorDetailContext mapVendorDetail(VendorDetail vendorDetail, boolean htmlOutput) { + return VendorDetailContext.builder() + .instructions(toLineBreaks(Optional.ofNullable(vendorDetail).map(VendorDetail::getInstructions).orElse(""), htmlOutput)) + .build(); + } + + private DetailsContext mapDetails(Details details) { + return DetailsContext.builder() + .productIds(mapProductIds(details)) + .build(); } - private String mapProductIdentifierTypes(Details details) { + private List mapProductIds(Details details) { if (details == null || CollectionUtils.isEmpty(details.getProductIds())) { - return ""; + return List.of(); } return details.getProductIds().stream() - .map(ProductIdentifier::getProductIdType) - .filter(StringUtils::isNotBlank) - .map(identifierTypeService::getIdentifierTypeName) - .filter(StringUtils::isNotBlank) - .collect(Collectors.joining("; ")); + .map(this::mapProductId) + .toList(); + } + + private ProductIdContext mapProductId(ProductIdentifier productId) { + var productIdType = StringUtils.defaultString(productId.getProductIdType()); + return ProductIdContext.builder() + .productId(StringUtils.defaultString(productId.getProductId())) + .qualifier(StringUtils.defaultString(productId.getQualifier())) + .productIdType(productIdType) + .productIdTypeName(StringUtils.isBlank(productIdType) ? "" : StringUtils.defaultString(identifierTypeService.getIdentifierTypeName(productIdType))) + .build(); } } diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/CostContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/CostContext.java new file mode 100644 index 000000000..21a9e6f67 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/CostContext.java @@ -0,0 +1,17 @@ +package org.folio.dew.domain.dto.templateengine; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class CostContext { + + private String listUnitPrice; + private String listUnitPriceElectronic; + private Integer quantityPhysical; + private Integer quantityElectronic; + private Integer quantity; + private String estimatedPrice; + private String currency; +} \ No newline at end of file diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/DetailsContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/DetailsContext.java new file mode 100644 index 000000000..6c8252013 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/DetailsContext.java @@ -0,0 +1,13 @@ +package org.folio.dew.domain.dto.templateengine; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class DetailsContext { + + private List productIds; +} \ No newline at end of file diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java index fedb9a4f1..e25da4336 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java @@ -8,6 +8,7 @@ public class OrderContext { private String poNumber; + private String orderType; private String orderDate; private String createdBy; private String shipTo; diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java index a7a60589e..e95cb5dae 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java @@ -11,13 +11,7 @@ public class OrderLineContext { private String title; private String publicationDate; private String edition; - private String productIdentifier; - private String productIdentifierType; - private String listUnitPrice; - private String listUnitPriceElectronic; - private Integer quantityPhysical; - private Integer quantityElectronic; - private Integer quantity; - private String estimatedPrice; - private String currency; + private DetailsContext details; + private CostContext cost; + private VendorDetailContext vendorDetail; } diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/ProductIdContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/ProductIdContext.java new file mode 100644 index 000000000..80b2d936b --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/ProductIdContext.java @@ -0,0 +1,14 @@ +package org.folio.dew.domain.dto.templateengine; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ProductIdContext { + + private String productId; + private String qualifier; + private String productIdType; + private String productIdTypeName; +} \ No newline at end of file diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/VendorDetailContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/VendorDetailContext.java new file mode 100644 index 000000000..a57d39b22 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/VendorDetailContext.java @@ -0,0 +1,11 @@ +package org.folio.dew.domain.dto.templateengine; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class VendorDetailContext { + + private String instructions; +} \ No newline at end of file diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index fee0126bc..dad825752 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -70,6 +70,7 @@ void buildContext_mapsOrderFields() throws IOException { assertThat(ctx.getOrders()).hasSize(1); var wrapper = ctx.getOrders().get(0); assertThat(wrapper.order().getPoNumber()).isEqualTo("10000"); + assertThat(wrapper.order().getOrderType()).isEqualTo("One-Time"); assertThat(wrapper.order().getOrderDate()).isEqualTo("2021-01-15"); assertThat(wrapper.order().getCreatedBy()).isEqualTo("John Doe"); assertThat(wrapper.order().getShipTo()).isEqualTo(SHIP_TO_ADDRESS); @@ -129,12 +130,17 @@ void buildContext_mapsOrderLineFields() throws IOException { assertThat(line.getTitle()).isEqualTo("Futures, biometrics and neuroscience research Luiz Moutinho, Mladen Sokele, editors"); assertThat(line.getPublicationDate()).isEqualTo("2021"); assertThat(line.getEdition()).isEqualTo("2nd ed."); - assertThat(line.getProductIdentifier()).isEqualTo("9783319643991"); - assertThat(line.getProductIdentifierType()).isEqualTo("ISBN"); - assertThat(line.getListUnitPrice()).isEqualTo("2.0"); - assertThat(line.getCurrency()).isEqualTo("USD"); - assertThat(line.getQuantity()).isEqualTo(1); - assertThat(line.getEstimatedPrice()).isEqualTo("1.8"); + assertThat(line.getDetails().getProductIds()).hasSize(1); + var productId = line.getDetails().getProductIds().get(0); + assertThat(productId.getProductId()).isEqualTo("9783319643991"); + assertThat(productId.getQualifier()).isEqualTo("(paperback)"); + assertThat(productId.getProductIdType()).isEqualTo("8261054f-be78-422d-bd51-4ed9f33c3422"); + assertThat(productId.getProductIdTypeName()).isEqualTo("ISBN"); + assertThat(line.getCost().getListUnitPrice()).isEqualTo("2.0"); + assertThat(line.getCost().getCurrency()).isEqualTo("USD"); + assertThat(line.getCost().getQuantity()).isEqualTo(1); + assertThat(line.getCost().getEstimatedPrice()).isEqualTo("1.8"); + assertThat(line.getVendorDetail().getInstructions()).isEqualTo("Handle with care"); } @Test @@ -226,9 +232,42 @@ void buildContext_nullCost_doesNotThrow() throws IOException { OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); OrderLineContext line = ctx.getOrders().get(0).orderLines().get(0).orderLine(); - assertThat(line.getListUnitPrice()).isEmpty(); - assertThat(line.getCurrency()).isEmpty(); - assertThat(line.getQuantity()).isZero(); + assertThat(line.getCost().getListUnitPrice()).isEmpty(); + assertThat(line.getCost().getCurrency()).isEmpty(); + assertThat(line.getCost().getQuantity()).isZero(); + } + + @Test + void buildContext_instructionsNewlines_renderedAsBrTags() throws IOException { + var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); + order.getPoLines().get(0).getVendorDetail().setInstructions("Line 1\nLine 2"); + + OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); + + OrderLineContext line = ctx.getOrders().get(0).orderLines().get(0).orderLine(); + assertThat(line.getVendorDetail().getInstructions()).isEqualTo("Line 1
Line 2"); + } + + @Test + void buildContext_instructionsNewlines_plainTextKeepsNewlines() throws IOException { + var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); + order.getPoLines().get(0).getVendorDetail().setInstructions("Line 1\nLine 2"); + + OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/plain"); + + OrderLineContext line = ctx.getOrders().get(0).orderLines().get(0).orderLine(); + assertThat(line.getVendorDetail().getInstructions()).isEqualTo("Line 1\nLine 2"); + } + + @Test + void buildContext_nullVendorDetail_doesNotThrow() throws IOException { + var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); + order.getPoLines().get(0).setVendorDetail(null); + + OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); + + OrderLineContext line = ctx.getOrders().get(0).orderLines().get(0).orderLine(); + assertThat(line.getVendorDetail().getInstructions()).isEmpty(); } private CompositePurchaseOrder loadOrder(String path) throws IOException { diff --git a/src/test/resources/edifact/acquisitions/composite_purchase_order_email_context.json b/src/test/resources/edifact/acquisitions/composite_purchase_order_email_context.json index 00cbf2b36..e78db4aaa 100644 --- a/src/test/resources/edifact/acquisitions/composite_purchase_order_email_context.json +++ b/src/test/resources/edifact/acquisitions/composite_purchase_order_email_context.json @@ -45,7 +45,7 @@ { "productId": "9783319643991", "productIdType": "8261054f-be78-422d-bd51-4ed9f33c3422", - "qualifier": "" + "qualifier": "(paperback)" } ], "subscriptionInterval": 0 From 0a5433f23e0d9f8505186784544b50c31dfb103d Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Tue, 23 Jun 2026 17:32:45 +0200 Subject: [PATCH 54/76] Update submodule --- folio-export-common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/folio-export-common b/folio-export-common index 2df1f861e..982ad7a55 160000 --- a/folio-export-common +++ b/folio-export-common @@ -1 +1 @@ -Subproject commit 2df1f861eafcf7ce582a3e4f99abc44540456ddd +Subproject commit 982ad7a5552f27a3e836c435b47ac8da1991196a From 89fb1d4fbc16e58c9800f5248ab67596d434c9e6 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Tue, 23 Jun 2026 18:06:26 +0200 Subject: [PATCH 55/76] Add contributors context to order-line email templates Expose each PO line's contributors in the email template context with the contributor name, name-type UUID, and resolved type name, resolved via a cached ContributorNameTypeService mirroring the identifier-types lookup. --- descriptors/ModuleDescriptor-template.json | 4 ++ .../mapper/OrderEmailContextMapper.java | 22 ++++++++++ .../services/ContributorNameTypeService.java | 32 ++++++++++++++ .../dew/client/ContributorNameTypeClient.java | 15 +++++++ .../dew/config/HttpClientConfiguration.java | 6 +++ .../edifact/ContributorNameType.java | 8 ++++ .../templateengine/ContributorContext.java | 13 ++++++ .../dto/templateengine/OrderLineContext.java | 3 ++ .../SaveToFileStorageTaskletDeciderTest.java | 2 +- .../mapper/OrderEmailContextMapperTest.java | 11 ++++- .../ContributorNameTypeServiceTest.java | 42 +++++++++++++++++++ 11 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeService.java create mode 100644 src/main/java/org/folio/dew/client/ContributorNameTypeClient.java create mode 100644 src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/ContributorNameType.java create mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/ContributorContext.java create mode 100644 src/test/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeServiceTest.java diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 4c961ea1a..b08b304e9 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -54,6 +54,10 @@ "id": "identifier-types", "version": "1.2" }, + { + "id": "contributor-name-types", + "version": "1.2" + }, { "id": "ill-policies", "version": "1.0" diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index 5e4e1493f..f9671e40a 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -4,12 +4,14 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.folio.dew.batch.acquisitions.services.ConfigurationService; +import org.folio.dew.batch.acquisitions.services.ContributorNameTypeService; import org.folio.dew.batch.acquisitions.services.IdentifierTypeService; import org.folio.dew.batch.acquisitions.services.OrganizationsService; import org.folio.dew.batch.acquisitions.services.UserService; import org.folio.dew.batch.acquisitions.utils.ExportUtils; import org.folio.dew.domain.dto.*; import org.folio.dew.domain.dto.acquisitions.edifact.OrganizationAddress; +import org.folio.dew.domain.dto.templateengine.ContributorContext; import org.folio.dew.domain.dto.templateengine.CostContext; import org.folio.dew.domain.dto.templateengine.DetailsContext; import org.folio.dew.domain.dto.templateengine.OrderContext; @@ -33,6 +35,7 @@ public class OrderEmailContextMapper extends EmailContextMapper { private final IdentifierTypeService identifierTypeService; + private final ContributorNameTypeService contributorNameTypeService; private final ConfigurationService configurationService; private final UserService userService; private final OrganizationsService organizationsService; @@ -118,6 +121,7 @@ private OrderLineContext mapOrderLine(PoLine line, boolean htmlOutput) { .title(StringUtils.defaultString(line.getTitleOrPackage())) .publicationDate(StringUtils.defaultString(line.getPublicationDate())) .edition(StringUtils.defaultString(line.getEdition())) + .contributors(mapContributors(line.getContributors())) .details(mapDetails(line.getDetails())) .cost(mapCost(line.getCost())) .vendorDetail(mapVendorDetail(line.getVendorDetail(), htmlOutput)) @@ -144,6 +148,24 @@ private VendorDetailContext mapVendorDetail(VendorDetail vendorDetail, boolean h .build(); } + private List mapContributors(List contributors) { + if (CollectionUtils.isEmpty(contributors)) { + return List.of(); + } + return contributors.stream() + .map(this::mapContributor) + .toList(); + } + + private ContributorContext mapContributor(Contributor contributor) { + var contributorNameType = StringUtils.defaultString(contributor.getContributorNameTypeId()); + return ContributorContext.builder() + .contributor(StringUtils.defaultString(contributor.getContributor())) + .contributorNameType(contributorNameType) + .contributorNameTypeName(StringUtils.isBlank(contributorNameType) ? "" : StringUtils.defaultString(contributorNameTypeService.getContributorNameTypeName(contributorNameType))) + .build(); + } + private DetailsContext mapDetails(Details details) { return DetailsContext.builder() .productIds(mapProductIds(details)) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeService.java b/src/main/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeService.java new file mode 100644 index 000000000..b9b317758 --- /dev/null +++ b/src/main/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeService.java @@ -0,0 +1,32 @@ +package org.folio.dew.batch.acquisitions.services; + +import org.folio.dew.client.ContributorNameTypeClient; +import org.folio.dew.domain.dto.acquisitions.edifact.ContributorNameType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ContributorNameTypeService { + @Autowired + private final ContributorNameTypeClient contributorNameTypeClient; + + private ContributorNameType getContributorNameType(String id) { + return contributorNameTypeClient.getContributorNameType(id); + } + + @Cacheable(cacheNames = "contributorNameTypes") + public String getContributorNameTypeName(String id) { + ContributorNameType contributorNameType = getContributorNameType(id); + String name = ""; + + if (contributorNameType != null) { + name = contributorNameType.getName(); + } + + return name; + } +} \ No newline at end of file diff --git a/src/main/java/org/folio/dew/client/ContributorNameTypeClient.java b/src/main/java/org/folio/dew/client/ContributorNameTypeClient.java new file mode 100644 index 000000000..24f18f928 --- /dev/null +++ b/src/main/java/org/folio/dew/client/ContributorNameTypeClient.java @@ -0,0 +1,15 @@ +package org.folio.dew.client; + +import org.folio.dew.domain.dto.acquisitions.edifact.ContributorNameType; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PathVariable; + +import org.springframework.web.service.annotation.GetExchange; +import org.springframework.web.service.annotation.HttpExchange; + + +@HttpExchange(url = "contributor-name-types") +public interface ContributorNameTypeClient { + @GetExchange(value = "/{contributorNameTypeId}", accept = MediaType.APPLICATION_JSON_VALUE) + ContributorNameType getContributorNameType(@PathVariable String contributorNameTypeId); +} \ No newline at end of file diff --git a/src/main/java/org/folio/dew/config/HttpClientConfiguration.java b/src/main/java/org/folio/dew/config/HttpClientConfiguration.java index bbcd034c6..af5eb7db1 100644 --- a/src/main/java/org/folio/dew/config/HttpClientConfiguration.java +++ b/src/main/java/org/folio/dew/config/HttpClientConfiguration.java @@ -6,6 +6,7 @@ import org.folio.dew.client.AccountClient; import org.folio.dew.client.AgreementClient; import org.folio.dew.client.AuditClient; +import org.folio.dew.client.ContributorNameTypeClient; import org.folio.dew.client.DataExportSpringClient; import org.folio.dew.client.EmailClient; import org.folio.dew.client.EntitiesLinksStatsClient; @@ -114,6 +115,11 @@ public IdentifierTypeClient identifierTypeClient(HttpServiceProxyFactory factory return factory.createClient(IdentifierTypeClient.class); } + @Bean + public ContributorNameTypeClient contributorNameTypeClient(HttpServiceProxyFactory factory) { + return factory.createClient(ContributorNameTypeClient.class); + } + @Bean public InventoryClient inventoryClient(HttpServiceProxyFactory factory) { return factory.createClient(InventoryClient.class); diff --git a/src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/ContributorNameType.java b/src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/ContributorNameType.java new file mode 100644 index 000000000..497d6f039 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/ContributorNameType.java @@ -0,0 +1,8 @@ +package org.folio.dew.domain.dto.acquisitions.edifact; + +import lombok.Data; + +@Data +public class ContributorNameType { + private String name; +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/ContributorContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/ContributorContext.java new file mode 100644 index 000000000..8aa694b3d --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/ContributorContext.java @@ -0,0 +1,13 @@ +package org.folio.dew.domain.dto.templateengine; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ContributorContext { + + private String contributor; + private String contributorNameType; + private String contributorNameTypeName; +} \ No newline at end of file diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java index e95cb5dae..d3b5aa15d 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java @@ -3,6 +3,8 @@ import lombok.Builder; import lombok.Data; +import java.util.List; + @Data @Builder public class OrderLineContext { @@ -11,6 +13,7 @@ public class OrderLineContext { private String title; private String publicationDate; private String edition; + private List contributors; private DetailsContext details; private CostContext cost; private VendorDetailContext vendorDetail; diff --git a/src/test/java/org/folio/dew/batch/acquisitions/jobs/decider/SaveToFileStorageTaskletDeciderTest.java b/src/test/java/org/folio/dew/batch/acquisitions/jobs/decider/SaveToFileStorageTaskletDeciderTest.java index f3cc9fd9b..42a86121e 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/jobs/decider/SaveToFileStorageTaskletDeciderTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/jobs/decider/SaveToFileStorageTaskletDeciderTest.java @@ -75,4 +75,4 @@ void decide_shouldSkip_whenTransmissionMethodIsNotFtp() { assertEquals(ExportStepDecision.SKIP, decision); } -} \ No newline at end of file +} diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index dad825752..4fe526e1d 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.folio.dew.batch.acquisitions.services.ConfigurationService; +import org.folio.dew.batch.acquisitions.services.ContributorNameTypeService; import org.folio.dew.batch.acquisitions.services.IdentifierTypeService; import org.folio.dew.batch.acquisitions.services.OrganizationsService; import org.folio.dew.batch.acquisitions.services.UserService; @@ -39,6 +40,8 @@ class OrderEmailContextMapperTest { @Mock private IdentifierTypeService identifierTypeService; @Mock + private ContributorNameTypeService contributorNameTypeService; + @Mock private ConfigurationService configurationService; @Mock private UserService userService; @@ -50,9 +53,10 @@ class OrderEmailContextMapperTest { @BeforeEach void setUp() { - mapper = new OrderEmailContextMapper(identifierTypeService, configurationService, userService, organizationsService); + mapper = new OrderEmailContextMapper(identifierTypeService, contributorNameTypeService, configurationService, userService, organizationsService); objectMapper = new ObjectMapper(); lenient().when(identifierTypeService.getIdentifierTypeName(anyString())).thenReturn("ISBN"); + lenient().when(contributorNameTypeService.getContributorNameTypeName(anyString())).thenReturn("Personal name"); lenient().when(configurationService.getAddressConfig(any())).thenReturn(""); lenient().when(userService.getUserName(anyString())).thenReturn(""); lenient().when(organizationsService.getOrganizationById(anyString())).thenReturn(new Organization()); @@ -130,6 +134,11 @@ void buildContext_mapsOrderLineFields() throws IOException { assertThat(line.getTitle()).isEqualTo("Futures, biometrics and neuroscience research Luiz Moutinho, Mladen Sokele, editors"); assertThat(line.getPublicationDate()).isEqualTo("2021"); assertThat(line.getEdition()).isEqualTo("2nd ed."); + assertThat(line.getContributors()).hasSize(1); + var contributor = line.getContributors().get(0); + assertThat(contributor.getContributor()).isEqualTo("Moutinho, Luiz"); + assertThat(contributor.getContributorNameType()).isEqualTo("2b94c631-fca9-4892-a730-03ee529ffe2a"); + assertThat(contributor.getContributorNameTypeName()).isEqualTo("Personal name"); assertThat(line.getDetails().getProductIds()).hasSize(1); var productId = line.getDetails().getProductIds().get(0); assertThat(productId.getProductId()).isEqualTo("9783319643991"); diff --git a/src/test/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeServiceTest.java b/src/test/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeServiceTest.java new file mode 100644 index 000000000..6f6161393 --- /dev/null +++ b/src/test/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeServiceTest.java @@ -0,0 +1,42 @@ +package org.folio.dew.batch.acquisitions.services; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +import org.folio.dew.client.ContributorNameTypeClient; +import org.folio.dew.domain.dto.acquisitions.edifact.ContributorNameType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.fasterxml.jackson.databind.ObjectMapper; + +@ExtendWith(MockitoExtension.class) +class ContributorNameTypeServiceTest { + + @InjectMocks + private ContributorNameTypeService contributorNameTypeService; + @Mock + private ContributorNameTypeClient client; + @Spy + private ObjectMapper objectMapper; + + @Test + void getContributorNameTypeName() { + String contributorNameTypeName = contributorNameTypeService.getContributorNameTypeName("2b94c631-fca9-4892-a730-03ee529ffe2a"); + assertEquals("", contributorNameTypeName); + } + + @Test + void getContributorNameTypeNameFromJson() { + var contributorNameType = new ContributorNameType(); + contributorNameType.setName("Personal name"); + when(client.getContributorNameType(anyString())).thenReturn(contributorNameType); + String contributorNameTypeName = contributorNameTypeService.getContributorNameTypeName("2b94c631-fca9-4892-a730-03ee529ffe2a"); + assertEquals("Personal name", contributorNameTypeName); + } +} \ No newline at end of file From 3b68340884987460db6c64b639cfe8f9b860919f Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Wed, 24 Jun 2026 17:18:37 +0200 Subject: [PATCH 56/76] Restructure order email context into nested typed sub-contexts Resolve shipTo/billTo to TenantAddressContext and createdBy to a full UserContext, share a TypeContext for contributor/product types, and add publisher/rush/poLineEstimatedPrice. Move context DTOs into a templateengine.context subpackage. --- .../mapper/OrderEmailContextMapper.java | 78 ++++++++++++++----- .../services/ConfigurationService.java | 23 ++++-- .../acquisitions/services/UserService.java | 61 +++++++++++---- .../templateengine/ContributorContext.java | 13 ---- .../dto/templateengine/OrderContext.java | 16 ---- .../context/ContributorContext.java | 12 +++ .../{ => context}/CostContext.java | 3 +- .../{ => context}/DetailsContext.java | 2 +- .../templateengine/context/OrderContext.java | 16 ++++ .../{ => context}/OrderEmailContext.java | 2 +- .../{ => context}/OrderLineContext.java | 4 +- .../{ => context}/OrderLineWrapper.java | 2 +- .../context/OrderMetadataContext.java | 11 +++ .../{ => context}/OrderWrapper.java | 2 +- .../OrganizationAddressContext.java | 2 +- .../{ => context}/OrganizationContext.java | 2 +- .../{ => context}/ProductIdContext.java | 5 +- .../context/TenantAddressContext.java | 12 +++ .../templateengine/context/TypeContext.java | 12 +++ .../templateengine/context/UserContext.java | 18 +++++ .../{ => context}/VendorDetailContext.java | 2 +- .../mapper/OrderEmailContextMapperTest.java | 67 ++++++++++------ 22 files changed, 254 insertions(+), 111 deletions(-) delete mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/ContributorContext.java delete mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java create mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/context/ContributorContext.java rename src/main/java/org/folio/dew/domain/dto/templateengine/{ => context}/CostContext.java (77%) rename src/main/java/org/folio/dew/domain/dto/templateengine/{ => context}/DetailsContext.java (73%) create mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderContext.java rename src/main/java/org/folio/dew/domain/dto/templateengine/{ => context}/OrderEmailContext.java (77%) rename src/main/java/org/folio/dew/domain/dto/templateengine/{ => context}/OrderLineContext.java (78%) rename src/main/java/org/folio/dew/domain/dto/templateengine/{ => context}/OrderLineWrapper.java (52%) create mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderMetadataContext.java rename src/main/java/org/folio/dew/domain/dto/templateengine/{ => context}/OrderWrapper.java (66%) rename src/main/java/org/folio/dew/domain/dto/templateengine/{ => context}/OrganizationAddressContext.java (78%) rename src/main/java/org/folio/dew/domain/dto/templateengine/{ => context}/OrganizationContext.java (75%) rename src/main/java/org/folio/dew/domain/dto/templateengine/{ => context}/ProductIdContext.java (56%) create mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/context/TenantAddressContext.java create mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/context/TypeContext.java create mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/context/UserContext.java rename src/main/java/org/folio/dew/domain/dto/templateengine/{ => context}/VendorDetailContext.java (69%) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index f9671e40a..8672ed3df 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -11,23 +11,27 @@ import org.folio.dew.batch.acquisitions.utils.ExportUtils; import org.folio.dew.domain.dto.*; import org.folio.dew.domain.dto.acquisitions.edifact.OrganizationAddress; -import org.folio.dew.domain.dto.templateengine.ContributorContext; -import org.folio.dew.domain.dto.templateengine.CostContext; -import org.folio.dew.domain.dto.templateengine.DetailsContext; -import org.folio.dew.domain.dto.templateengine.OrderContext; -import org.folio.dew.domain.dto.templateengine.OrderEmailContext; -import org.folio.dew.domain.dto.templateengine.OrderLineContext; -import org.folio.dew.domain.dto.templateengine.OrderLineWrapper; -import org.folio.dew.domain.dto.templateengine.OrderWrapper; -import org.folio.dew.domain.dto.templateengine.OrganizationAddressContext; -import org.folio.dew.domain.dto.templateengine.OrganizationContext; -import org.folio.dew.domain.dto.templateengine.ProductIdContext; -import org.folio.dew.domain.dto.templateengine.VendorDetailContext; +import org.folio.dew.domain.dto.templateengine.context.ContributorContext; +import org.folio.dew.domain.dto.templateengine.context.CostContext; +import org.folio.dew.domain.dto.templateengine.context.DetailsContext; +import org.folio.dew.domain.dto.templateengine.context.OrderContext; +import org.folio.dew.domain.dto.templateengine.context.OrderEmailContext; +import org.folio.dew.domain.dto.templateengine.context.OrderLineContext; +import org.folio.dew.domain.dto.templateengine.context.OrderLineWrapper; +import org.folio.dew.domain.dto.templateengine.context.OrderMetadataContext; +import org.folio.dew.domain.dto.templateengine.context.OrderWrapper; +import org.folio.dew.domain.dto.templateengine.context.OrganizationAddressContext; +import org.folio.dew.domain.dto.templateengine.context.OrganizationContext; +import org.folio.dew.domain.dto.templateengine.context.ProductIdContext; +import org.folio.dew.domain.dto.templateengine.context.TenantAddressContext; +import org.folio.dew.domain.dto.templateengine.context.TypeContext; +import org.folio.dew.domain.dto.templateengine.context.VendorDetailContext; import org.springframework.stereotype.Component; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.UUID; import java.util.function.Predicate; @Component @@ -109,9 +113,34 @@ private OrderContext mapOrder(CompositePurchaseOrder order, boolean htmlOutput) .poNumber(StringUtils.defaultString(order.getPoNumber())) .orderType(Optional.ofNullable(order.getOrderType()).map(CompositePurchaseOrder.OrderTypeEnum::getValue).orElse("")) .orderDate(StringUtils.defaultString(ExportUtils.getFormattedDate(order.getDateOrdered()))) - .createdBy(userService.getUserName(Optional.ofNullable(order.getMetadata()).map(Metadata::getCreatedByUserId).map(Object::toString).orElse(""))) - .shipTo(toLineBreaks(configurationService.getAddressConfig(order.getShipTo()), htmlOutput)) - .billTo(toLineBreaks(configurationService.getAddressConfig(order.getBillTo()), htmlOutput)) + .metadata(mapOrderMetadata(order.getMetadata())) + .shipTo(mapTenantAddress(order.getShipTo(), htmlOutput)) + .billTo(mapTenantAddress(order.getBillTo(), htmlOutput)) + .build(); + } + + private String toUuidString(UUID id) { + return Optional.ofNullable(id).map(UUID::toString).orElse(""); + } + + private TenantAddressContext mapTenantAddress(UUID addressId, boolean htmlOutput) { + var tenantAddress = configurationService.getTenantAddress(addressId); + if (tenantAddress == null) { + return TenantAddressContext.builder().id("").address("").build(); + } + return TenantAddressContext.builder() + .id(toUuidString(tenantAddress.getId())) + .address(toLineBreaks(StringUtils.defaultString(tenantAddress.getAddress()), htmlOutput)) + .build(); + } + + private OrderMetadataContext mapOrderMetadata(Metadata metadata) { + var createdByUserId = Optional.ofNullable(metadata) + .map(Metadata::getCreatedByUserId) + .map(Object::toString) + .orElse(""); + return OrderMetadataContext.builder() + .createdByUser(userService.getUserContext(createdByUserId)) .build(); } @@ -119,8 +148,10 @@ private OrderLineContext mapOrderLine(PoLine line, boolean htmlOutput) { return OrderLineContext.builder() .poLineNumber(StringUtils.defaultString(line.getPoLineNumber())) .title(StringUtils.defaultString(line.getTitleOrPackage())) + .publisher(StringUtils.defaultString(line.getPublisher())) .publicationDate(StringUtils.defaultString(line.getPublicationDate())) .edition(StringUtils.defaultString(line.getEdition())) + .rush(Optional.ofNullable(line.getRush()).orElse(false)) .contributors(mapContributors(line.getContributors())) .details(mapDetails(line.getDetails())) .cost(mapCost(line.getCost())) @@ -138,6 +169,7 @@ private CostContext mapCost(Cost cost) { .quantityElectronic(quantityElectronic) .quantity(quantityPhysical + quantityElectronic) .estimatedPrice(formatDecimal(Optional.ofNullable(cost).map(Cost::getPoLineEstimatedPrice).orElse(null))) + .poLineEstimatedPrice(formatDecimal(Optional.ofNullable(cost).map(Cost::getPoLineEstimatedPrice).orElse(null))) .currency(Optional.ofNullable(cost).map(Cost::getCurrency).orElse("")) .build(); } @@ -158,11 +190,13 @@ private List mapContributors(List contributors) } private ContributorContext mapContributor(Contributor contributor) { - var contributorNameType = StringUtils.defaultString(contributor.getContributorNameTypeId()); + var contributorNameTypeId = StringUtils.defaultString(contributor.getContributorNameTypeId()); return ContributorContext.builder() .contributor(StringUtils.defaultString(contributor.getContributor())) - .contributorNameType(contributorNameType) - .contributorNameTypeName(StringUtils.isBlank(contributorNameType) ? "" : StringUtils.defaultString(contributorNameTypeService.getContributorNameTypeName(contributorNameType))) + .contributorNameType(TypeContext.builder() + .id(contributorNameTypeId) + .name(StringUtils.isBlank(contributorNameTypeId) ? "" : StringUtils.defaultString(contributorNameTypeService.getContributorNameTypeName(contributorNameTypeId))) + .build()) .build(); } @@ -182,12 +216,14 @@ private List mapProductIds(Details details) { } private ProductIdContext mapProductId(ProductIdentifier productId) { - var productIdType = StringUtils.defaultString(productId.getProductIdType()); + var productIdTypeId = StringUtils.defaultString(productId.getProductIdType()); return ProductIdContext.builder() .productId(StringUtils.defaultString(productId.getProductId())) .qualifier(StringUtils.defaultString(productId.getQualifier())) - .productIdType(productIdType) - .productIdTypeName(StringUtils.isBlank(productIdType) ? "" : StringUtils.defaultString(identifierTypeService.getIdentifierTypeName(productIdType))) + .productIdType(TypeContext.builder() + .id(productIdTypeId) + .name(StringUtils.isBlank(productIdTypeId) ? "" : StringUtils.defaultString(identifierTypeService.getIdentifierTypeName(productIdTypeId))) + .build()) .build(); } } diff --git a/src/main/java/org/folio/dew/batch/acquisitions/services/ConfigurationService.java b/src/main/java/org/folio/dew/batch/acquisitions/services/ConfigurationService.java index ed90e0f6c..8be99dafb 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/services/ConfigurationService.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/services/ConfigurationService.java @@ -25,23 +25,30 @@ public class ConfigurationService { @Cacheable(cacheNames = "addressConfiguration") public String getAddressConfig(UUID shipToConfigId) { + return ofNullable(getTenantAddress(shipToConfigId)) + .map(TenantAddress::getAddress) + .orElse(""); + } + + @Cacheable(cacheNames = "tenantAddress") + public TenantAddress getTenantAddress(UUID shipToConfigId) { if (shipToConfigId == null) { - logger.warn("getAddressConfig:: shipToConfigId is null"); - return ""; + logger.warn("getTenantAddress:: shipToConfigId is null"); + return null; } try { TenantAddress addressResponse = tenantAddressesClient.getById(shipToConfigId.toString()); if (addressResponse == null) { - logger.warn("getAddressConfig:: No address found for id '{}'", shipToConfigId); - return ""; + logger.warn("getTenantAddress:: No address found for id '{}'", shipToConfigId); + return null; } - logger.info("getAddressConfig:: Found address with id '{}'", shipToConfigId); - return ofNullable(addressResponse.getAddress()).orElse(""); + logger.info("getTenantAddress:: Found address with id '{}'", shipToConfigId); + return addressResponse; } catch (Exception e) { - logger.warn("getAddressConfig:: Cannot find address by id: '{}'", shipToConfigId, e); - return ""; + logger.warn("getTenantAddress:: Cannot find address by id: '{}'", shipToConfigId, e); + return null; } } } diff --git a/src/main/java/org/folio/dew/batch/acquisitions/services/UserService.java b/src/main/java/org/folio/dew/batch/acquisitions/services/UserService.java index 1bc516074..78931548d 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/services/UserService.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/services/UserService.java @@ -7,6 +7,7 @@ import org.folio.dew.client.UserClient; import org.folio.dew.domain.dto.Personal; import org.folio.dew.domain.dto.User; +import org.folio.dew.domain.dto.templateengine.context.UserContext; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @@ -19,28 +20,56 @@ public class UserService { private final UserClient userClient; @Cacheable(cacheNames = "users") - public String getUserName(String userId) { + public UserContext getUserContext(String userId) { if (StringUtils.isBlank(userId)) { - return ""; + return emptyUserContext(); } try { User user = userClient.getUserById(userId); if (user == null) { - logger.warn("getUserName:: No user found for id '{}'", userId); - return ""; + logger.warn("getUserContext:: No user found for id '{}'", userId); + return emptyUserContext(); } - Personal personal = user.getPersonal(); - if (personal != null) { - String name = (StringUtils.defaultString(personal.getFirstName()) + " " - + StringUtils.defaultString(personal.getLastName())).trim(); - if (StringUtils.isNotBlank(name)) { - return name; - } - } - return StringUtils.defaultString(user.getUsername()); + return toUserContext(user); } catch (Exception e) { - logger.warn("getUserName:: Cannot find user by id: '{}'", userId, e); - return ""; + logger.warn("getUserContext:: Cannot find user by id: '{}'", userId, e); + return emptyUserContext(); + } + } + + private UserContext toUserContext(User user) { + Personal personal = user.getPersonal(); + String firstName = personal != null ? StringUtils.defaultString(personal.getFirstName()) : ""; + String lastName = personal != null ? StringUtils.defaultString(personal.getLastName()) : ""; + String middleName = personal != null ? StringUtils.defaultString(personal.getMiddleName()) : ""; + String preferredFirstName = personal != null ? StringUtils.defaultString(personal.getPreferredFirstName()) : ""; + String email = personal != null ? StringUtils.defaultString(personal.getEmail()) : ""; + String fullName = (firstName + " " + lastName).trim(); + if (StringUtils.isBlank(fullName)) { + fullName = StringUtils.defaultString(user.getUsername()); } + return UserContext.builder() + .id(StringUtils.defaultString(user.getId())) + .username(StringUtils.defaultString(user.getUsername())) + .firstName(firstName) + .lastName(lastName) + .middleName(middleName) + .preferredFirstName(preferredFirstName) + .email(email) + .fullName(fullName) + .build(); + } + + private UserContext emptyUserContext() { + return UserContext.builder() + .id("") + .username("") + .firstName("") + .lastName("") + .middleName("") + .preferredFirstName("") + .email("") + .fullName("") + .build(); } -} +} \ No newline at end of file diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/ContributorContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/ContributorContext.java deleted file mode 100644 index 8aa694b3d..000000000 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/ContributorContext.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.folio.dew.domain.dto.templateengine; - -import lombok.Builder; -import lombok.Data; - -@Data -@Builder -public class ContributorContext { - - private String contributor; - private String contributorNameType; - private String contributorNameTypeName; -} \ No newline at end of file diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java deleted file mode 100644 index e25da4336..000000000 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderContext.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.folio.dew.domain.dto.templateengine; - -import lombok.Builder; -import lombok.Data; - -@Data -@Builder -public class OrderContext { - - private String poNumber; - private String orderType; - private String orderDate; - private String createdBy; - private String shipTo; - private String billTo; -} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/ContributorContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/ContributorContext.java new file mode 100644 index 000000000..a83dd439e --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/ContributorContext.java @@ -0,0 +1,12 @@ +package org.folio.dew.domain.dto.templateengine.context; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ContributorContext { + + private String contributor; + private TypeContext contributorNameType; +} \ No newline at end of file diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/CostContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/CostContext.java similarity index 77% rename from src/main/java/org/folio/dew/domain/dto/templateengine/CostContext.java rename to src/main/java/org/folio/dew/domain/dto/templateengine/context/CostContext.java index 21a9e6f67..3dc4f5e19 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/CostContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/CostContext.java @@ -1,4 +1,4 @@ -package org.folio.dew.domain.dto.templateengine; +package org.folio.dew.domain.dto.templateengine.context; import lombok.Builder; import lombok.Data; @@ -13,5 +13,6 @@ public class CostContext { private Integer quantityElectronic; private Integer quantity; private String estimatedPrice; + private String poLineEstimatedPrice; private String currency; } \ No newline at end of file diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/DetailsContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/DetailsContext.java similarity index 73% rename from src/main/java/org/folio/dew/domain/dto/templateengine/DetailsContext.java rename to src/main/java/org/folio/dew/domain/dto/templateengine/context/DetailsContext.java index 6c8252013..76b9a23b9 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/DetailsContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/DetailsContext.java @@ -1,4 +1,4 @@ -package org.folio.dew.domain.dto.templateengine; +package org.folio.dew.domain.dto.templateengine.context; import lombok.Builder; import lombok.Data; diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderContext.java new file mode 100644 index 000000000..ccdb5520b --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderContext.java @@ -0,0 +1,16 @@ +package org.folio.dew.domain.dto.templateengine.context; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class OrderContext { + + private String poNumber; + private String orderType; + private String orderDate; + private OrderMetadataContext metadata; + private TenantAddressContext shipTo; + private TenantAddressContext billTo; +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderEmailContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderEmailContext.java similarity index 77% rename from src/main/java/org/folio/dew/domain/dto/templateengine/OrderEmailContext.java rename to src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderEmailContext.java index 07a0da567..837cc52d7 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderEmailContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderEmailContext.java @@ -1,4 +1,4 @@ -package org.folio.dew.domain.dto.templateengine; +package org.folio.dew.domain.dto.templateengine.context; import lombok.Builder; import lombok.Data; diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineContext.java similarity index 78% rename from src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java rename to src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineContext.java index d3b5aa15d..fb1c3d470 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineContext.java @@ -1,4 +1,4 @@ -package org.folio.dew.domain.dto.templateengine; +package org.folio.dew.domain.dto.templateengine.context; import lombok.Builder; import lombok.Data; @@ -11,8 +11,10 @@ public class OrderLineContext { private String poLineNumber; private String title; + private String publisher; private String publicationDate; private String edition; + private Boolean rush; private List contributors; private DetailsContext details; private CostContext cost; diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineWrapper.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineWrapper.java similarity index 52% rename from src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineWrapper.java rename to src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineWrapper.java index f4e3bfede..d435e1bf3 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderLineWrapper.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineWrapper.java @@ -1,4 +1,4 @@ -package org.folio.dew.domain.dto.templateengine; +package org.folio.dew.domain.dto.templateengine.context; public record OrderLineWrapper(OrderLineContext orderLine) { } diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderMetadataContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderMetadataContext.java new file mode 100644 index 000000000..437b913a4 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderMetadataContext.java @@ -0,0 +1,11 @@ +package org.folio.dew.domain.dto.templateengine.context; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class OrderMetadataContext { + + private UserContext createdByUser; +} \ No newline at end of file diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderWrapper.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderWrapper.java similarity index 66% rename from src/main/java/org/folio/dew/domain/dto/templateengine/OrderWrapper.java rename to src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderWrapper.java index 9c4d3ae30..57a64440e 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/OrderWrapper.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderWrapper.java @@ -1,4 +1,4 @@ -package org.folio.dew.domain.dto.templateengine; +package org.folio.dew.domain.dto.templateengine.context; import java.util.List; diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationAddressContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrganizationAddressContext.java similarity index 78% rename from src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationAddressContext.java rename to src/main/java/org/folio/dew/domain/dto/templateengine/context/OrganizationAddressContext.java index b8e883efe..db6f5bb4e 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationAddressContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrganizationAddressContext.java @@ -1,4 +1,4 @@ -package org.folio.dew.domain.dto.templateengine; +package org.folio.dew.domain.dto.templateengine.context; import lombok.Builder; import lombok.Data; diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrganizationContext.java similarity index 75% rename from src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java rename to src/main/java/org/folio/dew/domain/dto/templateengine/context/OrganizationContext.java index 1e6d3a60e..bc7001fa6 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/OrganizationContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrganizationContext.java @@ -1,4 +1,4 @@ -package org.folio.dew.domain.dto.templateengine; +package org.folio.dew.domain.dto.templateengine.context; import lombok.Builder; import lombok.Data; diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/ProductIdContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/ProductIdContext.java similarity index 56% rename from src/main/java/org/folio/dew/domain/dto/templateengine/ProductIdContext.java rename to src/main/java/org/folio/dew/domain/dto/templateengine/context/ProductIdContext.java index 80b2d936b..2c77f447f 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/ProductIdContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/ProductIdContext.java @@ -1,4 +1,4 @@ -package org.folio.dew.domain.dto.templateengine; +package org.folio.dew.domain.dto.templateengine.context; import lombok.Builder; import lombok.Data; @@ -9,6 +9,5 @@ public class ProductIdContext { private String productId; private String qualifier; - private String productIdType; - private String productIdTypeName; + private TypeContext productIdType; } \ No newline at end of file diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/TenantAddressContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/TenantAddressContext.java new file mode 100644 index 000000000..3cc656f79 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/TenantAddressContext.java @@ -0,0 +1,12 @@ +package org.folio.dew.domain.dto.templateengine.context; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class TenantAddressContext { + + private String id; + private String address; +} \ No newline at end of file diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/TypeContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/TypeContext.java new file mode 100644 index 000000000..eb8e657f5 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/TypeContext.java @@ -0,0 +1,12 @@ +package org.folio.dew.domain.dto.templateengine.context; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class TypeContext { + + private String id; + private String name; +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/UserContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/UserContext.java new file mode 100644 index 000000000..5ac0b61b4 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/UserContext.java @@ -0,0 +1,18 @@ +package org.folio.dew.domain.dto.templateengine.context; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class UserContext { + + private String id; + private String username; + private String firstName; + private String lastName; + private String middleName; + private String preferredFirstName; + private String email; + private String fullName; +} \ No newline at end of file diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/VendorDetailContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/VendorDetailContext.java similarity index 69% rename from src/main/java/org/folio/dew/domain/dto/templateengine/VendorDetailContext.java rename to src/main/java/org/folio/dew/domain/dto/templateengine/context/VendorDetailContext.java index a57d39b22..29d19a74f 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/VendorDetailContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/VendorDetailContext.java @@ -1,4 +1,4 @@ -package org.folio.dew.domain.dto.templateengine; +package org.folio.dew.domain.dto.templateengine.context; import lombok.Builder; import lombok.Data; diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index 4fe526e1d..03022145f 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -9,8 +9,10 @@ import org.folio.dew.domain.dto.CompositePurchaseOrder; import org.folio.dew.domain.dto.acquisitions.edifact.Organization; import org.folio.dew.domain.dto.acquisitions.edifact.OrganizationAddress; -import org.folio.dew.domain.dto.templateengine.OrderEmailContext; -import org.folio.dew.domain.dto.templateengine.OrderLineContext; +import org.folio.dew.domain.dto.acquisitions.edifact.TenantAddress; +import org.folio.dew.domain.dto.templateengine.context.OrderEmailContext; +import org.folio.dew.domain.dto.templateengine.context.OrderLineContext; +import org.folio.dew.domain.dto.templateengine.context.UserContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -57,16 +59,17 @@ void setUp() { objectMapper = new ObjectMapper(); lenient().when(identifierTypeService.getIdentifierTypeName(anyString())).thenReturn("ISBN"); lenient().when(contributorNameTypeService.getContributorNameTypeName(anyString())).thenReturn("Personal name"); - lenient().when(configurationService.getAddressConfig(any())).thenReturn(""); - lenient().when(userService.getUserName(anyString())).thenReturn(""); + lenient().when(configurationService.getTenantAddress(any())).thenReturn(null); + lenient().when(userService.getUserContext(anyString())).thenReturn(UserContext.builder().build()); lenient().when(organizationsService.getOrganizationById(anyString())).thenReturn(new Organization()); } @Test void buildContext_mapsOrderFields() throws IOException { - when(configurationService.getAddressConfig(SHIP_TO_UUID)).thenReturn(SHIP_TO_ADDRESS); - when(configurationService.getAddressConfig(BILL_TO_UUID)).thenReturn(BILL_TO_ADDRESS); - when(userService.getUserName("7a626480-284e-5b55-9cf2-db32f93956cf")).thenReturn("John Doe"); + when(configurationService.getTenantAddress(SHIP_TO_UUID)).thenReturn(tenantAddress(SHIP_TO_UUID, SHIP_TO_ADDRESS)); + when(configurationService.getTenantAddress(BILL_TO_UUID)).thenReturn(tenantAddress(BILL_TO_UUID, BILL_TO_ADDRESS)); + var johnDoe = UserContext.builder().id("7a626480-284e-5b55-9cf2-db32f93956cf").firstName("John").lastName("Doe").fullName("John Doe").build(); + when(userService.getUserContext("7a626480-284e-5b55-9cf2-db32f93956cf")).thenReturn(johnDoe); var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); @@ -76,36 +79,40 @@ void buildContext_mapsOrderFields() throws IOException { assertThat(wrapper.order().getPoNumber()).isEqualTo("10000"); assertThat(wrapper.order().getOrderType()).isEqualTo("One-Time"); assertThat(wrapper.order().getOrderDate()).isEqualTo("2021-01-15"); - assertThat(wrapper.order().getCreatedBy()).isEqualTo("John Doe"); - assertThat(wrapper.order().getShipTo()).isEqualTo(SHIP_TO_ADDRESS); - assertThat(wrapper.order().getBillTo()).isEqualTo(BILL_TO_ADDRESS); + assertThat(wrapper.order().getMetadata().getCreatedByUser().getId()).isEqualTo("7a626480-284e-5b55-9cf2-db32f93956cf"); + assertThat(wrapper.order().getMetadata().getCreatedByUser()).isEqualTo(johnDoe); + assertThat(wrapper.order().getMetadata().getCreatedByUser().getFullName()).isEqualTo("John Doe"); + assertThat(wrapper.order().getShipTo().getId()).isEqualTo(SHIP_TO_UUID.toString()); + assertThat(wrapper.order().getShipTo().getAddress()).isEqualTo(SHIP_TO_ADDRESS); + assertThat(wrapper.order().getBillTo().getId()).isEqualTo(BILL_TO_UUID.toString()); + assertThat(wrapper.order().getBillTo().getAddress()).isEqualTo(BILL_TO_ADDRESS); } @Test void buildContext_shipToBillToNewlines_renderedAsBrTags() throws IOException { - when(configurationService.getAddressConfig(SHIP_TO_UUID)) - .thenReturn("SLUB Dresden\nZellescher Weg 18\n01069 Dresden"); - when(configurationService.getAddressConfig(BILL_TO_UUID)) - .thenReturn("Accounts Payable\nPO Box 42\nSpringfield IL"); + when(configurationService.getTenantAddress(SHIP_TO_UUID)) + .thenReturn(tenantAddress(SHIP_TO_UUID, "SLUB Dresden\nZellescher Weg 18\n01069 Dresden")); + when(configurationService.getTenantAddress(BILL_TO_UUID)) + .thenReturn(tenantAddress(BILL_TO_UUID, "Accounts Payable\nPO Box 42\nSpringfield IL")); var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); - assertThat(ctx.getOrders().get(0).order().getShipTo()) + assertThat(ctx.getOrders().get(0).order().getShipTo().getAddress()) .isEqualTo("SLUB Dresden
Zellescher Weg 18
01069 Dresden"); - assertThat(ctx.getOrders().get(0).order().getBillTo()) + assertThat(ctx.getOrders().get(0).order().getBillTo().getAddress()) .isEqualTo("Accounts Payable
PO Box 42
Springfield IL"); } @Test void buildContext_shipToBillToNewlines_plainTextKeepsNewlines() throws IOException { - when(configurationService.getAddressConfig(SHIP_TO_UUID)) - .thenReturn("SLUB Dresden\nZellescher Weg 18\n01069 Dresden"); + when(configurationService.getTenantAddress(SHIP_TO_UUID)) + .thenReturn(tenantAddress(SHIP_TO_UUID, "SLUB Dresden\nZellescher Weg 18\n01069 Dresden")); var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/plain"); - assertThat(ctx.getOrders().get(0).order().getShipTo()) + assertThat(ctx.getOrders().get(0).order().getShipTo().getAddress()) .isEqualTo("SLUB Dresden\nZellescher Weg 18\n01069 Dresden"); } @@ -115,8 +122,8 @@ void buildContext_shipToBillToNotResolved_returnsEmptyString() throws IOExceptio OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); - assertThat(ctx.getOrders().get(0).order().getShipTo()).isEmpty(); - assertThat(ctx.getOrders().get(0).order().getBillTo()).isEmpty(); + assertThat(ctx.getOrders().get(0).order().getShipTo().getAddress()).isEmpty(); + assertThat(ctx.getOrders().get(0).order().getBillTo().getAddress()).isEmpty(); } @Test @@ -132,23 +139,26 @@ void buildContext_mapsOrderLineFields() throws IOException { OrderLineContext line = lines.get(0).orderLine(); assertThat(line.getPoLineNumber()).isEqualTo("10000-1"); assertThat(line.getTitle()).isEqualTo("Futures, biometrics and neuroscience research Luiz Moutinho, Mladen Sokele, editors"); + assertThat(line.getPublisher()).isEqualTo("Palgrave Macmillan"); assertThat(line.getPublicationDate()).isEqualTo("2021"); assertThat(line.getEdition()).isEqualTo("2nd ed."); + assertThat(line.getRush()).isFalse(); assertThat(line.getContributors()).hasSize(1); var contributor = line.getContributors().get(0); assertThat(contributor.getContributor()).isEqualTo("Moutinho, Luiz"); - assertThat(contributor.getContributorNameType()).isEqualTo("2b94c631-fca9-4892-a730-03ee529ffe2a"); - assertThat(contributor.getContributorNameTypeName()).isEqualTo("Personal name"); + assertThat(contributor.getContributorNameType().getId()).isEqualTo("2b94c631-fca9-4892-a730-03ee529ffe2a"); + assertThat(contributor.getContributorNameType().getName()).isEqualTo("Personal name"); assertThat(line.getDetails().getProductIds()).hasSize(1); var productId = line.getDetails().getProductIds().get(0); assertThat(productId.getProductId()).isEqualTo("9783319643991"); assertThat(productId.getQualifier()).isEqualTo("(paperback)"); - assertThat(productId.getProductIdType()).isEqualTo("8261054f-be78-422d-bd51-4ed9f33c3422"); - assertThat(productId.getProductIdTypeName()).isEqualTo("ISBN"); + assertThat(productId.getProductIdType().getId()).isEqualTo("8261054f-be78-422d-bd51-4ed9f33c3422"); + assertThat(productId.getProductIdType().getName()).isEqualTo("ISBN"); assertThat(line.getCost().getListUnitPrice()).isEqualTo("2.0"); assertThat(line.getCost().getCurrency()).isEqualTo("USD"); assertThat(line.getCost().getQuantity()).isEqualTo(1); assertThat(line.getCost().getEstimatedPrice()).isEqualTo("1.8"); + assertThat(line.getCost().getPoLineEstimatedPrice()).isEqualTo("1.8"); assertThat(line.getVendorDetail().getInstructions()).isEqualTo("Handle with care"); } @@ -282,4 +292,11 @@ void buildContext_nullVendorDetail_doesNotThrow() throws IOException { private CompositePurchaseOrder loadOrder(String path) throws IOException { return objectMapper.readValue(getMockData(path), CompositePurchaseOrder.class); } + + private TenantAddress tenantAddress(UUID id, String address) { + var tenantAddress = new TenantAddress(); + tenantAddress.setId(id); + tenantAddress.setAddress(address); + return tenantAddress; + } } From fdb4269e469d31411ca9f1aff549032e58780e58 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Thu, 25 Jun 2026 12:34:31 +0200 Subject: [PATCH 57/76] Add fund distribution code to order-line email context Expose each PO line's fund distribution as a fundDistribution list in OrderLineContext, surfacing the fund code for email templates. --- .../mapper/OrderEmailContextMapper.java | 17 +++++++++++++++++ .../context/FundDistributionContext.java | 11 +++++++++++ .../context/OrderLineContext.java | 1 + .../mapper/OrderEmailContextMapperTest.java | 3 +++ 4 files changed, 32 insertions(+) create mode 100644 src/main/java/org/folio/dew/domain/dto/templateengine/context/FundDistributionContext.java diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index 8672ed3df..40bb54dc9 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -14,6 +14,7 @@ import org.folio.dew.domain.dto.templateengine.context.ContributorContext; import org.folio.dew.domain.dto.templateengine.context.CostContext; import org.folio.dew.domain.dto.templateengine.context.DetailsContext; +import org.folio.dew.domain.dto.templateengine.context.FundDistributionContext; import org.folio.dew.domain.dto.templateengine.context.OrderContext; import org.folio.dew.domain.dto.templateengine.context.OrderEmailContext; import org.folio.dew.domain.dto.templateengine.context.OrderLineContext; @@ -155,10 +156,26 @@ private OrderLineContext mapOrderLine(PoLine line, boolean htmlOutput) { .contributors(mapContributors(line.getContributors())) .details(mapDetails(line.getDetails())) .cost(mapCost(line.getCost())) + .fundDistribution(mapFundDistribution(line.getFundDistribution())) .vendorDetail(mapVendorDetail(line.getVendorDetail(), htmlOutput)) .build(); } + private List mapFundDistribution(List fundDistribution) { + if (CollectionUtils.isEmpty(fundDistribution)) { + return List.of(); + } + return fundDistribution.stream() + .map(this::mapFundDistribution) + .toList(); + } + + private FundDistributionContext mapFundDistribution(FundDistribution fundDistribution) { + return FundDistributionContext.builder() + .code(StringUtils.defaultString(fundDistribution.getCode())) + .build(); + } + private CostContext mapCost(Cost cost) { var quantityPhysical = Optional.ofNullable(cost).map(Cost::getQuantityPhysical).orElse(0); var quantityElectronic = Optional.ofNullable(cost).map(Cost::getQuantityElectronic).orElse(0); diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/FundDistributionContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/FundDistributionContext.java new file mode 100644 index 000000000..dbdf1a1ea --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/FundDistributionContext.java @@ -0,0 +1,11 @@ +package org.folio.dew.domain.dto.templateengine.context; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class FundDistributionContext { + + private String code; +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineContext.java index fb1c3d470..ec4abca45 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineContext.java @@ -18,5 +18,6 @@ public class OrderLineContext { private List contributors; private DetailsContext details; private CostContext cost; + private List fundDistribution; private VendorDetailContext vendorDetail; } diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index 03022145f..3c7415d98 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -159,6 +159,9 @@ void buildContext_mapsOrderLineFields() throws IOException { assertThat(line.getCost().getQuantity()).isEqualTo(1); assertThat(line.getCost().getEstimatedPrice()).isEqualTo("1.8"); assertThat(line.getCost().getPoLineEstimatedPrice()).isEqualTo("1.8"); + assertThat(line.getFundDistribution()).hasSize(1); + var fund = line.getFundDistribution().get(0); + assertThat(fund.getCode()).isEqualTo("USHIST"); assertThat(line.getVendorDetail().getInstructions()).isEqualTo("Handle with care"); } From b6d351a94f518f091f9284b7a0152fdbf95fbd13 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Thu, 25 Jun 2026 13:07:25 +0200 Subject: [PATCH 58/76] Add createdAt timestamp to order email context Stamp the order email context with the build time as a top-level createdAt field, formatted as ISO-8601 UTC with milliseconds. --- .../batch/acquisitions/mapper/OrderEmailContextMapper.java | 7 +++++++ .../dto/templateengine/context/OrderEmailContext.java | 1 + 2 files changed, 8 insertions(+) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index 40bb54dc9..3c70e29f4 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -29,6 +29,9 @@ import org.folio.dew.domain.dto.templateengine.context.VendorDetailContext; import org.springframework.stereotype.Component; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -39,6 +42,9 @@ @RequiredArgsConstructor public class OrderEmailContextMapper extends EmailContextMapper { + private static final DateTimeFormatter CREATED_AT_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(ZoneOffset.UTC); + private final IdentifierTypeService identifierTypeService; private final ContributorNameTypeService contributorNameTypeService; private final ConfigurationService configurationService; @@ -55,6 +61,7 @@ public OrderEmailContext buildContext(List orders, Strin .toList())) .toList(); return OrderEmailContext.builder() + .createdAt(CREATED_AT_FORMATTER.format(Instant.now())) .organization(mapOrganization(orders)) .orders(orderWrappers) .build(); diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderEmailContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderEmailContext.java index 837cc52d7..47f54a8b9 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderEmailContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderEmailContext.java @@ -9,6 +9,7 @@ @Builder public class OrderEmailContext { + private String createdAt; private OrganizationContext organization; private List orders; } From 0a51a62e5f112f7b0b325491a61ce6b9d6e5b733 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Thu, 25 Jun 2026 13:46:38 +0200 Subject: [PATCH 59/76] Trim order email context fields for createdByUser, order, and order line Remove orderDate from OrderContext and username/middleName/preferredFirstName/email from the createdByUser UserContext. Rename OrderLineContext.title to titleOrPackage to match its source field. --- .../acquisitions/mapper/OrderEmailContextMapper.java | 4 +--- .../dew/batch/acquisitions/services/UserService.java | 11 ----------- .../dto/templateengine/context/OrderContext.java | 1 - .../dto/templateengine/context/OrderLineContext.java | 2 +- .../dto/templateengine/context/UserContext.java | 4 ---- 5 files changed, 2 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index 3c70e29f4..f968d6be3 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -8,7 +8,6 @@ import org.folio.dew.batch.acquisitions.services.IdentifierTypeService; import org.folio.dew.batch.acquisitions.services.OrganizationsService; import org.folio.dew.batch.acquisitions.services.UserService; -import org.folio.dew.batch.acquisitions.utils.ExportUtils; import org.folio.dew.domain.dto.*; import org.folio.dew.domain.dto.acquisitions.edifact.OrganizationAddress; import org.folio.dew.domain.dto.templateengine.context.ContributorContext; @@ -120,7 +119,6 @@ private OrderContext mapOrder(CompositePurchaseOrder order, boolean htmlOutput) return OrderContext.builder() .poNumber(StringUtils.defaultString(order.getPoNumber())) .orderType(Optional.ofNullable(order.getOrderType()).map(CompositePurchaseOrder.OrderTypeEnum::getValue).orElse("")) - .orderDate(StringUtils.defaultString(ExportUtils.getFormattedDate(order.getDateOrdered()))) .metadata(mapOrderMetadata(order.getMetadata())) .shipTo(mapTenantAddress(order.getShipTo(), htmlOutput)) .billTo(mapTenantAddress(order.getBillTo(), htmlOutput)) @@ -155,7 +153,7 @@ private OrderMetadataContext mapOrderMetadata(Metadata metadata) { private OrderLineContext mapOrderLine(PoLine line, boolean htmlOutput) { return OrderLineContext.builder() .poLineNumber(StringUtils.defaultString(line.getPoLineNumber())) - .title(StringUtils.defaultString(line.getTitleOrPackage())) + .titleOrPackage(StringUtils.defaultString(line.getTitleOrPackage())) .publisher(StringUtils.defaultString(line.getPublisher())) .publicationDate(StringUtils.defaultString(line.getPublicationDate())) .edition(StringUtils.defaultString(line.getEdition())) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/services/UserService.java b/src/main/java/org/folio/dew/batch/acquisitions/services/UserService.java index 78931548d..1cc8e69e0 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/services/UserService.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/services/UserService.java @@ -41,21 +41,14 @@ private UserContext toUserContext(User user) { Personal personal = user.getPersonal(); String firstName = personal != null ? StringUtils.defaultString(personal.getFirstName()) : ""; String lastName = personal != null ? StringUtils.defaultString(personal.getLastName()) : ""; - String middleName = personal != null ? StringUtils.defaultString(personal.getMiddleName()) : ""; - String preferredFirstName = personal != null ? StringUtils.defaultString(personal.getPreferredFirstName()) : ""; - String email = personal != null ? StringUtils.defaultString(personal.getEmail()) : ""; String fullName = (firstName + " " + lastName).trim(); if (StringUtils.isBlank(fullName)) { fullName = StringUtils.defaultString(user.getUsername()); } return UserContext.builder() .id(StringUtils.defaultString(user.getId())) - .username(StringUtils.defaultString(user.getUsername())) .firstName(firstName) .lastName(lastName) - .middleName(middleName) - .preferredFirstName(preferredFirstName) - .email(email) .fullName(fullName) .build(); } @@ -63,12 +56,8 @@ private UserContext toUserContext(User user) { private UserContext emptyUserContext() { return UserContext.builder() .id("") - .username("") .firstName("") .lastName("") - .middleName("") - .preferredFirstName("") - .email("") .fullName("") .build(); } diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderContext.java index ccdb5520b..3df1959b4 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderContext.java @@ -9,7 +9,6 @@ public class OrderContext { private String poNumber; private String orderType; - private String orderDate; private OrderMetadataContext metadata; private TenantAddressContext shipTo; private TenantAddressContext billTo; diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineContext.java index ec4abca45..076d472ad 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineContext.java @@ -10,7 +10,7 @@ public class OrderLineContext { private String poLineNumber; - private String title; + private String titleOrPackage; private String publisher; private String publicationDate; private String edition; diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/UserContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/UserContext.java index 5ac0b61b4..966a02f17 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/UserContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/UserContext.java @@ -8,11 +8,7 @@ public class UserContext { private String id; - private String username; private String firstName; private String lastName; - private String middleName; - private String preferredFirstName; - private String email; private String fullName; } \ No newline at end of file From 95a7e8e3db6485af7ad6cba99a86f037e75d2b1c Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Fri, 26 Jun 2026 10:24:24 +0200 Subject: [PATCH 60/76] Align OrderEmailContextMapperTest with renamed context fields Drop the removed orderDate assertion and use getTitleOrPackage in place of getTitle so the test compiles against the current OrderContext and OrderLineContext DTOs. --- .../batch/acquisitions/mapper/OrderEmailContextMapperTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index 3c7415d98..b3c069d25 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -78,7 +78,6 @@ void buildContext_mapsOrderFields() throws IOException { var wrapper = ctx.getOrders().get(0); assertThat(wrapper.order().getPoNumber()).isEqualTo("10000"); assertThat(wrapper.order().getOrderType()).isEqualTo("One-Time"); - assertThat(wrapper.order().getOrderDate()).isEqualTo("2021-01-15"); assertThat(wrapper.order().getMetadata().getCreatedByUser().getId()).isEqualTo("7a626480-284e-5b55-9cf2-db32f93956cf"); assertThat(wrapper.order().getMetadata().getCreatedByUser()).isEqualTo(johnDoe); assertThat(wrapper.order().getMetadata().getCreatedByUser().getFullName()).isEqualTo("John Doe"); @@ -138,7 +137,7 @@ void buildContext_mapsOrderLineFields() throws IOException { OrderLineContext line = lines.get(0).orderLine(); assertThat(line.getPoLineNumber()).isEqualTo("10000-1"); - assertThat(line.getTitle()).isEqualTo("Futures, biometrics and neuroscience research Luiz Moutinho, Mladen Sokele, editors"); + assertThat(line.getTitleOrPackage()).isEqualTo("Futures, biometrics and neuroscience research Luiz Moutinho, Mladen Sokele, editors"); assertThat(line.getPublisher()).isEqualTo("Palgrave Macmillan"); assertThat(line.getPublicationDate()).isEqualTo("2021"); assertThat(line.getEdition()).isEqualTo("2nd ed."); From 464fd61988a9868e8464bfc4bcc1ca09b13f9977 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Fri, 26 Jun 2026 12:52:53 +0200 Subject: [PATCH 61/76] Pass email-context line breaks through verbatim and drop base mapper Remove the toLineBreaks/isHtmlOutput HTML conversion so address and vendor-instruction newlines reach the context unchanged for the frontend to render. Delete the now-redundant EmailContextMapper base class, inlining formatDecimal into OrderEmailContextMapper. --- .../acquisitions/jobs/SendToEmailTasklet.java | 2 +- .../mapper/EmailContextMapper.java | 21 ------- .../mapper/OrderEmailContextMapper.java | 32 ++++++----- .../mapper/OrderEmailContextMapperTest.java | 55 ++++++------------- 4 files changed, 35 insertions(+), 75 deletions(-) delete mode 100644 src/main/java/org/folio/dew/batch/acquisitions/mapper/EmailContextMapper.java diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java index 8eda3cbd8..52342e318 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java @@ -89,7 +89,7 @@ private String[] resolveTemplate(VendorEdiOrdersExportConfig exportConfig, List< var request = TemplateProcessingRequest.builder() .templateId(templateId) .build(); - request.setContext(orderEmailContextMapper.buildContext(orders, request.getOutputFormat())); + request.setContext(orderEmailContextMapper.buildContext(orders)); try { TemplateProcessingResponse response = templateEngineClient.processTemplate(request); diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/EmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/EmailContextMapper.java deleted file mode 100644 index 21706e4e0..000000000 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/EmailContextMapper.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.folio.dew.batch.acquisitions.mapper; - -import java.math.BigDecimal; - -import org.apache.commons.lang3.StringUtils; - -public abstract class EmailContextMapper { - - protected static boolean isHtmlOutput(String outputFormat) { - return outputFormat != null && outputFormat.toLowerCase().contains("html"); - } - - protected String toLineBreaks(String value, boolean htmlOutput) { - String safe = StringUtils.defaultString(value); - return htmlOutput ? safe.replace("\n", "
") : safe; - } - - protected String formatDecimal(BigDecimal value) { - return value != null ? value.toPlainString() : ""; - } -} diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index f968d6be3..1343089fc 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -28,6 +28,7 @@ import org.folio.dew.domain.dto.templateengine.context.VendorDetailContext; import org.springframework.stereotype.Component; +import java.math.BigDecimal; import java.time.Instant; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; @@ -39,7 +40,7 @@ @Component @RequiredArgsConstructor -public class OrderEmailContextMapper extends EmailContextMapper { +public class OrderEmailContextMapper { private static final DateTimeFormatter CREATED_AT_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(ZoneOffset.UTC); @@ -50,13 +51,12 @@ public class OrderEmailContextMapper extends EmailContextMapper { private final UserService userService; private final OrganizationsService organizationsService; - public OrderEmailContext buildContext(List orders, String outputFormat) { - boolean htmlOutput = isHtmlOutput(outputFormat); + public OrderEmailContext buildContext(List orders) { var orderWrappers = orders.stream() .map(order -> new OrderWrapper( - mapOrder(order, htmlOutput), + mapOrder(order), order.getPoLines().stream() - .map(line -> new OrderLineWrapper(mapOrderLine(line, htmlOutput))) + .map(line -> new OrderLineWrapper(mapOrderLine(line))) .toList())) .toList(); return OrderEmailContext.builder() @@ -115,13 +115,13 @@ private Optional pickPrimary(List items, Predicate isPrimary) { .findFirst(); } - private OrderContext mapOrder(CompositePurchaseOrder order, boolean htmlOutput) { + private OrderContext mapOrder(CompositePurchaseOrder order) { return OrderContext.builder() .poNumber(StringUtils.defaultString(order.getPoNumber())) .orderType(Optional.ofNullable(order.getOrderType()).map(CompositePurchaseOrder.OrderTypeEnum::getValue).orElse("")) .metadata(mapOrderMetadata(order.getMetadata())) - .shipTo(mapTenantAddress(order.getShipTo(), htmlOutput)) - .billTo(mapTenantAddress(order.getBillTo(), htmlOutput)) + .shipTo(mapTenantAddress(order.getShipTo())) + .billTo(mapTenantAddress(order.getBillTo())) .build(); } @@ -129,14 +129,14 @@ private String toUuidString(UUID id) { return Optional.ofNullable(id).map(UUID::toString).orElse(""); } - private TenantAddressContext mapTenantAddress(UUID addressId, boolean htmlOutput) { + private TenantAddressContext mapTenantAddress(UUID addressId) { var tenantAddress = configurationService.getTenantAddress(addressId); if (tenantAddress == null) { return TenantAddressContext.builder().id("").address("").build(); } return TenantAddressContext.builder() .id(toUuidString(tenantAddress.getId())) - .address(toLineBreaks(StringUtils.defaultString(tenantAddress.getAddress()), htmlOutput)) + .address(StringUtils.defaultString(tenantAddress.getAddress())) .build(); } @@ -150,7 +150,7 @@ private OrderMetadataContext mapOrderMetadata(Metadata metadata) { .build(); } - private OrderLineContext mapOrderLine(PoLine line, boolean htmlOutput) { + private OrderLineContext mapOrderLine(PoLine line) { return OrderLineContext.builder() .poLineNumber(StringUtils.defaultString(line.getPoLineNumber())) .titleOrPackage(StringUtils.defaultString(line.getTitleOrPackage())) @@ -162,7 +162,7 @@ private OrderLineContext mapOrderLine(PoLine line, boolean htmlOutput) { .details(mapDetails(line.getDetails())) .cost(mapCost(line.getCost())) .fundDistribution(mapFundDistribution(line.getFundDistribution())) - .vendorDetail(mapVendorDetail(line.getVendorDetail(), htmlOutput)) + .vendorDetail(mapVendorDetail(line.getVendorDetail())) .build(); } @@ -196,9 +196,13 @@ private CostContext mapCost(Cost cost) { .build(); } - private VendorDetailContext mapVendorDetail(VendorDetail vendorDetail, boolean htmlOutput) { + private String formatDecimal(BigDecimal value) { + return value != null ? value.toPlainString() : ""; + } + + private VendorDetailContext mapVendorDetail(VendorDetail vendorDetail) { return VendorDetailContext.builder() - .instructions(toLineBreaks(Optional.ofNullable(vendorDetail).map(VendorDetail::getInstructions).orElse(""), htmlOutput)) + .instructions(Optional.ofNullable(vendorDetail).map(VendorDetail::getInstructions).orElse("")) .build(); } diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index b3c069d25..4bc031177 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -72,7 +72,7 @@ void buildContext_mapsOrderFields() throws IOException { when(userService.getUserContext("7a626480-284e-5b55-9cf2-db32f93956cf")).thenReturn(johnDoe); var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); - OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); + OrderEmailContext ctx = mapper.buildContext(List.of(order)); assertThat(ctx.getOrders()).hasSize(1); var wrapper = ctx.getOrders().get(0); @@ -88,38 +88,26 @@ void buildContext_mapsOrderFields() throws IOException { } @Test - void buildContext_shipToBillToNewlines_renderedAsBrTags() throws IOException { + void buildContext_shipToBillToNewlines_keptVerbatim() throws IOException { when(configurationService.getTenantAddress(SHIP_TO_UUID)) .thenReturn(tenantAddress(SHIP_TO_UUID, "SLUB Dresden\nZellescher Weg 18\n01069 Dresden")); when(configurationService.getTenantAddress(BILL_TO_UUID)) .thenReturn(tenantAddress(BILL_TO_UUID, "Accounts Payable\nPO Box 42\nSpringfield IL")); var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); - OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); - - assertThat(ctx.getOrders().get(0).order().getShipTo().getAddress()) - .isEqualTo("SLUB Dresden
Zellescher Weg 18
01069 Dresden"); - assertThat(ctx.getOrders().get(0).order().getBillTo().getAddress()) - .isEqualTo("Accounts Payable
PO Box 42
Springfield IL"); - } - - @Test - void buildContext_shipToBillToNewlines_plainTextKeepsNewlines() throws IOException { - when(configurationService.getTenantAddress(SHIP_TO_UUID)) - .thenReturn(tenantAddress(SHIP_TO_UUID, "SLUB Dresden\nZellescher Weg 18\n01069 Dresden")); - var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); - - OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/plain"); + OrderEmailContext ctx = mapper.buildContext(List.of(order)); assertThat(ctx.getOrders().get(0).order().getShipTo().getAddress()) .isEqualTo("SLUB Dresden\nZellescher Weg 18\n01069 Dresden"); + assertThat(ctx.getOrders().get(0).order().getBillTo().getAddress()) + .isEqualTo("Accounts Payable\nPO Box 42\nSpringfield IL"); } @Test void buildContext_shipToBillToNotResolved_returnsEmptyString() throws IOException { var order = loadOrder("edifact/acquisitions/minimalistic_composite_purchase_order.json"); - OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); + OrderEmailContext ctx = mapper.buildContext(List.of(order)); assertThat(ctx.getOrders().get(0).order().getShipTo().getAddress()).isEmpty(); assertThat(ctx.getOrders().get(0).order().getBillTo().getAddress()).isEmpty(); @@ -129,7 +117,7 @@ void buildContext_shipToBillToNotResolved_returnsEmptyString() throws IOExceptio void buildContext_mapsOrderLineFields() throws IOException { var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); - OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); + OrderEmailContext ctx = mapper.buildContext(List.of(order)); assertThat(ctx.getOrders()).hasSize(1); var lines = ctx.getOrders().get(0).orderLines(); @@ -169,14 +157,14 @@ void buildContext_multipleOrders_producesOneWrapperPerOrder() throws IOException var order1 = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); var order2 = loadOrder("edifact/acquisitions/minimalistic_composite_purchase_order.json"); - OrderEmailContext ctx = mapper.buildContext(List.of(order1, order2), "text/html"); + OrderEmailContext ctx = mapper.buildContext(List.of(order1, order2)); assertThat(ctx.getOrders()).hasSize(2); } @Test void buildContext_emptyOrders_returnsEmptyList() { - OrderEmailContext ctx = mapper.buildContext(List.of(), "text/html"); + OrderEmailContext ctx = mapper.buildContext(List.of()); assertThat(ctx.getOrders()).isEmpty(); assertThat(ctx.getOrganization().getName()).isEmpty(); @@ -194,7 +182,7 @@ void buildContext_mapsOrganizationFields() throws IOException { when(organizationsService.getOrganizationById(VENDOR_UUID)).thenReturn(org); var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); - OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); + OrderEmailContext ctx = mapper.buildContext(List.of(order)); var organization = ctx.getOrganization(); assertThat(organization.getName()).isEqualTo("Acme Books"); @@ -211,7 +199,7 @@ void buildContext_noPrimaryFlagged_returnsEmpty() throws IOException { when(organizationsService.getOrganizationById(VENDOR_UUID)).thenReturn(org); var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); - OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); + OrderEmailContext ctx = mapper.buildContext(List.of(order)); assertThat(ctx.getOrganization().getPrimaryAddress().getAddressLine1()).isEmpty(); assertThat(ctx.getOrganization().getPrimaryAddress().getCity()).isEmpty(); @@ -226,7 +214,7 @@ void buildContext_organizationWithEmptyLists_returnsEmptyStrings() throws IOExce when(organizationsService.getOrganizationById(VENDOR_UUID)).thenReturn(org); var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); - OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); + OrderEmailContext ctx = mapper.buildContext(List.of(order)); assertThat(ctx.getOrganization().getName()).isEqualTo("Acme Books"); assertThat(ctx.getOrganization().getPrimaryAddress().getAddressLine1()).isEmpty(); @@ -250,7 +238,7 @@ void buildContext_nullCost_doesNotThrow() throws IOException { var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); order.getPoLines().get(0).setCost(null); - OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); + OrderEmailContext ctx = mapper.buildContext(List.of(order)); OrderLineContext line = ctx.getOrders().get(0).orderLines().get(0).orderLine(); assertThat(line.getCost().getListUnitPrice()).isEmpty(); @@ -259,22 +247,11 @@ void buildContext_nullCost_doesNotThrow() throws IOException { } @Test - void buildContext_instructionsNewlines_renderedAsBrTags() throws IOException { - var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); - order.getPoLines().get(0).getVendorDetail().setInstructions("Line 1\nLine 2"); - - OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); - - OrderLineContext line = ctx.getOrders().get(0).orderLines().get(0).orderLine(); - assertThat(line.getVendorDetail().getInstructions()).isEqualTo("Line 1
Line 2"); - } - - @Test - void buildContext_instructionsNewlines_plainTextKeepsNewlines() throws IOException { + void buildContext_instructionsNewlines_keptVerbatim() throws IOException { var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); order.getPoLines().get(0).getVendorDetail().setInstructions("Line 1\nLine 2"); - OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/plain"); + OrderEmailContext ctx = mapper.buildContext(List.of(order)); OrderLineContext line = ctx.getOrders().get(0).orderLines().get(0).orderLine(); assertThat(line.getVendorDetail().getInstructions()).isEqualTo("Line 1\nLine 2"); @@ -285,7 +262,7 @@ void buildContext_nullVendorDetail_doesNotThrow() throws IOException { var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); order.getPoLines().get(0).setVendorDetail(null); - OrderEmailContext ctx = mapper.buildContext(List.of(order), "text/html"); + OrderEmailContext ctx = mapper.buildContext(List.of(order)); OrderLineContext line = ctx.getOrders().get(0).orderLines().get(0).orderLine(); assertThat(line.getVendorDetail().getInstructions()).isEmpty(); From bfdef21b60000773cdd1ea339718feddbb4089ae Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Fri, 26 Jun 2026 17:02:27 +0200 Subject: [PATCH 62/76] Extract generic mapList helper and simplify createdAt formatting Replace the three duplicated empty-check/stream/map methods (contributors, fund distribution, product IDs) with one generic mapList helper, and build createdAt via Instant.toString() instead of a custom DateTimeFormatter. --- .../mapper/OrderEmailContextMapper.java | 40 +++++-------------- .../services/ContributorNameTypeService.java | 2 +- .../acquisitions/services/UserService.java | 2 +- .../dew/client/ContributorNameTypeClient.java | 2 +- .../context/ContributorContext.java | 2 +- .../templateengine/context/CostContext.java | 2 +- .../context/DetailsContext.java | 2 +- .../context/OrderMetadataContext.java | 2 +- .../context/ProductIdContext.java | 2 +- .../context/TenantAddressContext.java | 2 +- .../templateengine/context/UserContext.java | 2 +- .../ContributorNameTypeServiceTest.java | 2 +- 12 files changed, 20 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index 1343089fc..c5509d290 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -30,21 +30,17 @@ import java.math.BigDecimal; import java.time.Instant; -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.UUID; +import java.util.function.Function; import java.util.function.Predicate; @Component @RequiredArgsConstructor public class OrderEmailContextMapper { - private static final DateTimeFormatter CREATED_AT_FORMATTER = - DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(ZoneOffset.UTC); - private final IdentifierTypeService identifierTypeService; private final ContributorNameTypeService contributorNameTypeService; private final ConfigurationService configurationService; @@ -60,7 +56,7 @@ public OrderEmailContext buildContext(List orders) { .toList())) .toList(); return OrderEmailContext.builder() - .createdAt(CREATED_AT_FORMATTER.format(Instant.now())) + .createdAt(Instant.now().toString()) .organization(mapOrganization(orders)) .orders(orderWrappers) .build(); @@ -158,20 +154,20 @@ private OrderLineContext mapOrderLine(PoLine line) { .publicationDate(StringUtils.defaultString(line.getPublicationDate())) .edition(StringUtils.defaultString(line.getEdition())) .rush(Optional.ofNullable(line.getRush()).orElse(false)) - .contributors(mapContributors(line.getContributors())) + .contributors(mapList(line.getContributors(), this::mapContributor)) .details(mapDetails(line.getDetails())) .cost(mapCost(line.getCost())) - .fundDistribution(mapFundDistribution(line.getFundDistribution())) + .fundDistribution(mapList(line.getFundDistribution(), this::mapFundDistribution)) .vendorDetail(mapVendorDetail(line.getVendorDetail())) .build(); } - private List mapFundDistribution(List fundDistribution) { - if (CollectionUtils.isEmpty(fundDistribution)) { + private List mapList(List source, Function mapper) { + if (CollectionUtils.isEmpty(source)) { return List.of(); } - return fundDistribution.stream() - .map(this::mapFundDistribution) + return source.stream() + .map(mapper) .toList(); } @@ -206,15 +202,6 @@ private VendorDetailContext mapVendorDetail(VendorDetail vendorDetail) { .build(); } - private List mapContributors(List contributors) { - if (CollectionUtils.isEmpty(contributors)) { - return List.of(); - } - return contributors.stream() - .map(this::mapContributor) - .toList(); - } - private ContributorContext mapContributor(Contributor contributor) { var contributorNameTypeId = StringUtils.defaultString(contributor.getContributorNameTypeId()); return ContributorContext.builder() @@ -228,19 +215,10 @@ private ContributorContext mapContributor(Contributor contributor) { private DetailsContext mapDetails(Details details) { return DetailsContext.builder() - .productIds(mapProductIds(details)) + .productIds(mapList(details == null ? null : details.getProductIds(), this::mapProductId)) .build(); } - private List mapProductIds(Details details) { - if (details == null || CollectionUtils.isEmpty(details.getProductIds())) { - return List.of(); - } - return details.getProductIds().stream() - .map(this::mapProductId) - .toList(); - } - private ProductIdContext mapProductId(ProductIdentifier productId) { var productIdTypeId = StringUtils.defaultString(productId.getProductIdType()); return ProductIdContext.builder() diff --git a/src/main/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeService.java b/src/main/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeService.java index b9b317758..95b7d4db5 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeService.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeService.java @@ -29,4 +29,4 @@ public String getContributorNameTypeName(String id) { return name; } -} \ No newline at end of file +} diff --git a/src/main/java/org/folio/dew/batch/acquisitions/services/UserService.java b/src/main/java/org/folio/dew/batch/acquisitions/services/UserService.java index 1cc8e69e0..e1ad4d916 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/services/UserService.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/services/UserService.java @@ -61,4 +61,4 @@ private UserContext emptyUserContext() { .fullName("") .build(); } -} \ No newline at end of file +} diff --git a/src/main/java/org/folio/dew/client/ContributorNameTypeClient.java b/src/main/java/org/folio/dew/client/ContributorNameTypeClient.java index 24f18f928..20124d5d0 100644 --- a/src/main/java/org/folio/dew/client/ContributorNameTypeClient.java +++ b/src/main/java/org/folio/dew/client/ContributorNameTypeClient.java @@ -12,4 +12,4 @@ public interface ContributorNameTypeClient { @GetExchange(value = "/{contributorNameTypeId}", accept = MediaType.APPLICATION_JSON_VALUE) ContributorNameType getContributorNameType(@PathVariable String contributorNameTypeId); -} \ No newline at end of file +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/ContributorContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/ContributorContext.java index a83dd439e..f0a81c5b8 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/ContributorContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/ContributorContext.java @@ -9,4 +9,4 @@ public class ContributorContext { private String contributor; private TypeContext contributorNameType; -} \ No newline at end of file +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/CostContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/CostContext.java index 3dc4f5e19..37606a669 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/CostContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/CostContext.java @@ -15,4 +15,4 @@ public class CostContext { private String estimatedPrice; private String poLineEstimatedPrice; private String currency; -} \ No newline at end of file +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/DetailsContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/DetailsContext.java index 76b9a23b9..f205617dc 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/DetailsContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/DetailsContext.java @@ -10,4 +10,4 @@ public class DetailsContext { private List productIds; -} \ No newline at end of file +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderMetadataContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderMetadataContext.java index 437b913a4..17019e829 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderMetadataContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderMetadataContext.java @@ -8,4 +8,4 @@ public class OrderMetadataContext { private UserContext createdByUser; -} \ No newline at end of file +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/ProductIdContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/ProductIdContext.java index 2c77f447f..4b4ebcd8d 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/ProductIdContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/ProductIdContext.java @@ -10,4 +10,4 @@ public class ProductIdContext { private String productId; private String qualifier; private TypeContext productIdType; -} \ No newline at end of file +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/TenantAddressContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/TenantAddressContext.java index 3cc656f79..cd90a8d70 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/TenantAddressContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/TenantAddressContext.java @@ -9,4 +9,4 @@ public class TenantAddressContext { private String id; private String address; -} \ No newline at end of file +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/UserContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/UserContext.java index 966a02f17..ed31cbc67 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/UserContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/UserContext.java @@ -11,4 +11,4 @@ public class UserContext { private String firstName; private String lastName; private String fullName; -} \ No newline at end of file +} diff --git a/src/test/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeServiceTest.java b/src/test/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeServiceTest.java index 6f6161393..218e82b85 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeServiceTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeServiceTest.java @@ -39,4 +39,4 @@ void getContributorNameTypeNameFromJson() { String contributorNameTypeName = contributorNameTypeService.getContributorNameTypeName("2b94c631-fca9-4892-a730-03ee529ffe2a"); assertEquals("Personal name", contributorNameTypeName); } -} \ No newline at end of file +} From a2603daac58e45ddca1e90a3b2798ab62819f23f Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Fri, 26 Jun 2026 17:08:44 +0200 Subject: [PATCH 63/76] Remove blank line before fields in email context DTOs Normalize the templateengine context classes by dropping the leading blank line after each class declaration and fixing a missing trailing newline in VendorDetailContext. --- .../domain/dto/templateengine/context/ContributorContext.java | 1 - .../dew/domain/dto/templateengine/context/CostContext.java | 1 - .../dew/domain/dto/templateengine/context/DetailsContext.java | 1 - .../dto/templateengine/context/FundDistributionContext.java | 1 - .../dew/domain/dto/templateengine/context/OrderContext.java | 1 - .../domain/dto/templateengine/context/OrderEmailContext.java | 1 - .../domain/dto/templateengine/context/OrderLineContext.java | 1 - .../dto/templateengine/context/OrderMetadataContext.java | 1 - .../dto/templateengine/context/OrganizationAddressContext.java | 1 - .../domain/dto/templateengine/context/OrganizationContext.java | 1 - .../domain/dto/templateengine/context/ProductIdContext.java | 1 - .../dto/templateengine/context/TenantAddressContext.java | 1 - .../dew/domain/dto/templateengine/context/TypeContext.java | 1 - .../dew/domain/dto/templateengine/context/UserContext.java | 1 - .../domain/dto/templateengine/context/VendorDetailContext.java | 3 +-- 15 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/ContributorContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/ContributorContext.java index f0a81c5b8..2c006272f 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/ContributorContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/ContributorContext.java @@ -6,7 +6,6 @@ @Data @Builder public class ContributorContext { - private String contributor; private TypeContext contributorNameType; } diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/CostContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/CostContext.java index 37606a669..5352ad8b7 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/CostContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/CostContext.java @@ -6,7 +6,6 @@ @Data @Builder public class CostContext { - private String listUnitPrice; private String listUnitPriceElectronic; private Integer quantityPhysical; diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/DetailsContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/DetailsContext.java index f205617dc..6965a5c83 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/DetailsContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/DetailsContext.java @@ -8,6 +8,5 @@ @Data @Builder public class DetailsContext { - private List productIds; } diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/FundDistributionContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/FundDistributionContext.java index dbdf1a1ea..c2459bcef 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/FundDistributionContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/FundDistributionContext.java @@ -6,6 +6,5 @@ @Data @Builder public class FundDistributionContext { - private String code; } diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderContext.java index 3df1959b4..c646fd96b 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderContext.java @@ -6,7 +6,6 @@ @Data @Builder public class OrderContext { - private String poNumber; private String orderType; private OrderMetadataContext metadata; diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderEmailContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderEmailContext.java index 47f54a8b9..c0a484664 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderEmailContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderEmailContext.java @@ -8,7 +8,6 @@ @Data @Builder public class OrderEmailContext { - private String createdAt; private OrganizationContext organization; private List orders; diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineContext.java index 076d472ad..45ab2a84b 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineContext.java @@ -8,7 +8,6 @@ @Data @Builder public class OrderLineContext { - private String poLineNumber; private String titleOrPackage; private String publisher; diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderMetadataContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderMetadataContext.java index 17019e829..d0087aa2c 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderMetadataContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderMetadataContext.java @@ -6,6 +6,5 @@ @Data @Builder public class OrderMetadataContext { - private UserContext createdByUser; } diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrganizationAddressContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrganizationAddressContext.java index db6f5bb4e..0936ae55b 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrganizationAddressContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrganizationAddressContext.java @@ -6,7 +6,6 @@ @Data @Builder public class OrganizationAddressContext { - private String addressLine1; private String city; private String zipCode; diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrganizationContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrganizationContext.java index bc7001fa6..059ea35c8 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrganizationContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrganizationContext.java @@ -6,7 +6,6 @@ @Data @Builder public class OrganizationContext { - private String name; private OrganizationAddressContext primaryAddress; } diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/ProductIdContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/ProductIdContext.java index 4b4ebcd8d..473553d9b 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/ProductIdContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/ProductIdContext.java @@ -6,7 +6,6 @@ @Data @Builder public class ProductIdContext { - private String productId; private String qualifier; private TypeContext productIdType; diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/TenantAddressContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/TenantAddressContext.java index cd90a8d70..6971f76b6 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/TenantAddressContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/TenantAddressContext.java @@ -6,7 +6,6 @@ @Data @Builder public class TenantAddressContext { - private String id; private String address; } diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/TypeContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/TypeContext.java index eb8e657f5..7d504b767 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/TypeContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/TypeContext.java @@ -6,7 +6,6 @@ @Data @Builder public class TypeContext { - private String id; private String name; } diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/UserContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/UserContext.java index ed31cbc67..380ee6dff 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/UserContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/UserContext.java @@ -6,7 +6,6 @@ @Data @Builder public class UserContext { - private String id; private String firstName; private String lastName; diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/VendorDetailContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/VendorDetailContext.java index 29d19a74f..f73f3ee81 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/VendorDetailContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/VendorDetailContext.java @@ -6,6 +6,5 @@ @Data @Builder public class VendorDetailContext { - private String instructions; -} \ No newline at end of file +} From 99434e29f317f4f2de67f49933603b6fff5314e7 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Fri, 26 Jun 2026 17:25:32 +0200 Subject: [PATCH 64/76] Degrade order-email lookups gracefully and stop caching nulls Wrap organization, contributor-name-type, and identifier-type lookups in try/catch so a failed enrichment call no longer aborts the whole email export, and add unless="#result == null" on those plus tenantAddress so transient failures are not pinned in the cache. --- .../services/ConfigurationService.java | 2 +- .../services/ContributorNameTypeService.java | 24 ++++++++----------- .../services/IdentifierTypeService.java | 24 ++++++++----------- .../services/OrganizationsService.java | 9 +++++-- 4 files changed, 28 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/services/ConfigurationService.java b/src/main/java/org/folio/dew/batch/acquisitions/services/ConfigurationService.java index 8be99dafb..0cd7f2fa7 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/services/ConfigurationService.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/services/ConfigurationService.java @@ -30,7 +30,7 @@ public String getAddressConfig(UUID shipToConfigId) { .orElse(""); } - @Cacheable(cacheNames = "tenantAddress") + @Cacheable(cacheNames = "tenantAddress", unless = "#result == null") public TenantAddress getTenantAddress(UUID shipToConfigId) { if (shipToConfigId == null) { logger.warn("getTenantAddress:: shipToConfigId is null"); diff --git a/src/main/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeService.java b/src/main/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeService.java index 95b7d4db5..b2c8d0708 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeService.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeService.java @@ -2,31 +2,27 @@ import org.folio.dew.client.ContributorNameTypeClient; import org.folio.dew.domain.dto.acquisitions.edifact.ContributorNameType; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; @Service @RequiredArgsConstructor +@Log4j2 public class ContributorNameTypeService { - @Autowired - private final ContributorNameTypeClient contributorNameTypeClient; - private ContributorNameType getContributorNameType(String id) { - return contributorNameTypeClient.getContributorNameType(id); - } + private final ContributorNameTypeClient contributorNameTypeClient; - @Cacheable(cacheNames = "contributorNameTypes") + @Cacheable(cacheNames = "contributorNameTypes", unless = "#result == null") public String getContributorNameTypeName(String id) { - ContributorNameType contributorNameType = getContributorNameType(id); - String name = ""; - - if (contributorNameType != null) { - name = contributorNameType.getName(); + try { + ContributorNameType contributorNameType = contributorNameTypeClient.getContributorNameType(id); + return contributorNameType != null ? contributorNameType.getName() : ""; + } catch (Exception e) { + log.warn("getContributorNameTypeName:: Cannot find contributor name type by id: '{}'", id, e); + return null; } - - return name; } } diff --git a/src/main/java/org/folio/dew/batch/acquisitions/services/IdentifierTypeService.java b/src/main/java/org/folio/dew/batch/acquisitions/services/IdentifierTypeService.java index d255f0e84..bec85a63a 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/services/IdentifierTypeService.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/services/IdentifierTypeService.java @@ -2,31 +2,27 @@ import org.folio.dew.client.IdentifierTypeClient; import org.folio.dew.domain.dto.acquisitions.edifact.IdentifierType; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; @Service @RequiredArgsConstructor +@Log4j2 public class IdentifierTypeService { - @Autowired - private final IdentifierTypeClient identifierTypeClient; - private IdentifierType getIdentifierType(String id) { - return identifierTypeClient.getIdentifierType(id); - } + private final IdentifierTypeClient identifierTypeClient; - @Cacheable(cacheNames = "identifierTypes") + @Cacheable(cacheNames = "identifierTypes", unless = "#result == null") public String getIdentifierTypeName(String id) { - IdentifierType identifierType = getIdentifierType(id); - String name = ""; - - if (identifierType != null) { - name = identifierType.getName(); + try { + IdentifierType identifierType = identifierTypeClient.getIdentifierType(id); + return identifierType != null ? identifierType.getName() : ""; + } catch (Exception e) { + log.warn("getIdentifierTypeName:: Cannot find identifier type by id: '{}'", id, e); + return null; } - - return name; } } diff --git a/src/main/java/org/folio/dew/batch/acquisitions/services/OrganizationsService.java b/src/main/java/org/folio/dew/batch/acquisitions/services/OrganizationsService.java index aef7d6972..25728fa81 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/services/OrganizationsService.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/services/OrganizationsService.java @@ -14,8 +14,13 @@ public class OrganizationsService { private final OrganizationsClient organizationsClient; - @Cacheable(cacheNames = "organizations") + @Cacheable(cacheNames = "organizations", unless = "#result == null") public Organization getOrganizationById(String id) { - return organizationsClient.getOrganizationById(id); + try { + return organizationsClient.getOrganizationById(id); + } catch (Exception e) { + log.warn("getOrganizationById:: Cannot find organization by id: '{}'", id, e); + return null; + } } } From 5bb76a5a2ede36a450935a81259c987a670168f8 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Fri, 26 Jun 2026 17:54:29 +0200 Subject: [PATCH 65/76] Confine order-email lookup resilience to the email mapper Revert OrganizationsService and IdentifierTypeService to propagate lookup failures so the shared EDIFACT path keeps failing loud instead of NPEing, and degrade gracefully only in OrderEmailContextMapper via resolveOrganization/resolveIdentifierTypeName. Add tests for the throwing lookup paths. --- .../mapper/OrderEmailContextMapper.java | 25 +++++++++++++++-- .../services/IdentifierTypeService.java | 11 ++------ .../services/OrganizationsService.java | 9 +------ .../mapper/OrderEmailContextMapperTest.java | 27 +++++++++++++++++++ 4 files changed, 53 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index c5509d290..cdf904e77 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -1,6 +1,7 @@ package org.folio.dew.batch.acquisitions.mapper; import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.folio.dew.batch.acquisitions.services.ConfigurationService; @@ -9,6 +10,7 @@ import org.folio.dew.batch.acquisitions.services.OrganizationsService; import org.folio.dew.batch.acquisitions.services.UserService; import org.folio.dew.domain.dto.*; +import org.folio.dew.domain.dto.acquisitions.edifact.Organization; import org.folio.dew.domain.dto.acquisitions.edifact.OrganizationAddress; import org.folio.dew.domain.dto.templateengine.context.ContributorContext; import org.folio.dew.domain.dto.templateengine.context.CostContext; @@ -39,6 +41,7 @@ @Component @RequiredArgsConstructor +@Log4j2 public class OrderEmailContextMapper { private final IdentifierTypeService identifierTypeService; @@ -67,7 +70,7 @@ private OrganizationContext mapOrganization(List orders) .map(CompositePurchaseOrder::getVendor) .filter(Objects::nonNull) .findFirst() - .map(vendorId -> organizationsService.getOrganizationById(vendorId.toString())) + .map(this::resolveOrganization) .map(org -> OrganizationContext.builder() .name(StringUtils.defaultString(org.getName())) .primaryAddress(pickPrimaryAddress(org.getAddresses())) @@ -75,6 +78,15 @@ private OrganizationContext mapOrganization(List orders) .orElseGet(this::emptyOrganizationContext); } + private Organization resolveOrganization(UUID vendorId) { + try { + return organizationsService.getOrganizationById(vendorId.toString()); + } catch (Exception e) { + log.warn("resolveOrganization:: Cannot resolve vendor organization '{}' for email context", vendorId, e); + return null; + } + } + private OrganizationContext emptyOrganizationContext() { return OrganizationContext.builder() .name("") @@ -226,8 +238,17 @@ private ProductIdContext mapProductId(ProductIdentifier productId) { .qualifier(StringUtils.defaultString(productId.getQualifier())) .productIdType(TypeContext.builder() .id(productIdTypeId) - .name(StringUtils.isBlank(productIdTypeId) ? "" : StringUtils.defaultString(identifierTypeService.getIdentifierTypeName(productIdTypeId))) + .name(StringUtils.isBlank(productIdTypeId) ? "" : resolveIdentifierTypeName(productIdTypeId)) .build()) .build(); } + + private String resolveIdentifierTypeName(String productIdTypeId) { + try { + return StringUtils.defaultString(identifierTypeService.getIdentifierTypeName(productIdTypeId)); + } catch (Exception e) { + log.warn("resolveIdentifierTypeName:: Cannot resolve identifier type '{}' for email context", productIdTypeId, e); + return ""; + } + } } diff --git a/src/main/java/org/folio/dew/batch/acquisitions/services/IdentifierTypeService.java b/src/main/java/org/folio/dew/batch/acquisitions/services/IdentifierTypeService.java index bec85a63a..ce5989bf3 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/services/IdentifierTypeService.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/services/IdentifierTypeService.java @@ -6,23 +6,16 @@ import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; @Service @RequiredArgsConstructor -@Log4j2 public class IdentifierTypeService { private final IdentifierTypeClient identifierTypeClient; @Cacheable(cacheNames = "identifierTypes", unless = "#result == null") public String getIdentifierTypeName(String id) { - try { - IdentifierType identifierType = identifierTypeClient.getIdentifierType(id); - return identifierType != null ? identifierType.getName() : ""; - } catch (Exception e) { - log.warn("getIdentifierTypeName:: Cannot find identifier type by id: '{}'", id, e); - return null; - } + IdentifierType identifierType = identifierTypeClient.getIdentifierType(id); + return identifierType != null ? identifierType.getName() : ""; } } diff --git a/src/main/java/org/folio/dew/batch/acquisitions/services/OrganizationsService.java b/src/main/java/org/folio/dew/batch/acquisitions/services/OrganizationsService.java index 25728fa81..49bc1c1f2 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/services/OrganizationsService.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/services/OrganizationsService.java @@ -6,21 +6,14 @@ import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; @Service @RequiredArgsConstructor -@Log4j2 public class OrganizationsService { private final OrganizationsClient organizationsClient; @Cacheable(cacheNames = "organizations", unless = "#result == null") public Organization getOrganizationById(String id) { - try { - return organizationsClient.getOrganizationById(id); - } catch (Exception e) { - log.warn("getOrganizationById:: Cannot find organization by id: '{}'", id, e); - return null; - } + return organizationsClient.getOrganizationById(id); } } diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index 4bc031177..70a6beff1 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -268,6 +268,33 @@ void buildContext_nullVendorDetail_doesNotThrow() throws IOException { assertThat(line.getVendorDetail().getInstructions()).isEmpty(); } + @Test + void buildContext_organizationLookupThrows_returnsEmptyOrganization() throws IOException { + when(organizationsService.getOrganizationById(VENDOR_UUID)).thenThrow(new RuntimeException("boom")); + var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); + + OrderEmailContext ctx = mapper.buildContext(List.of(order)); + + assertThat(ctx.getOrganization().getName()).isEmpty(); + assertThat(ctx.getOrganization().getPrimaryAddress().getAddressLine1()).isEmpty(); + assertThat(ctx.getOrganization().getPrimaryAddress().getCity()).isEmpty(); + assertThat(ctx.getOrganization().getPrimaryAddress().getZipCode()).isEmpty(); + assertThat(ctx.getOrganization().getPrimaryAddress().getCountry()).isEmpty(); + } + + @Test + void buildContext_identifierTypeLookupThrows_returnsEmptyName() throws IOException { + when(identifierTypeService.getIdentifierTypeName(anyString())).thenThrow(new RuntimeException("boom")); + var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); + + OrderEmailContext ctx = mapper.buildContext(List.of(order)); + + var productIdType = ctx.getOrders().get(0).orderLines().get(0).orderLine() + .getDetails().getProductIds().get(0).getProductIdType(); + assertThat(productIdType.getName()).isEmpty(); + assertThat(productIdType.getId()).isEqualTo("8261054f-be78-422d-bd51-4ed9f33c3422"); + } + private CompositePurchaseOrder loadOrder(String path) throws IOException { return objectMapper.readValue(getMockData(path), CompositePurchaseOrder.class); } From 75e325ea8df2b6520df89ab017abe7fe3b1e307b Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Fri, 26 Jun 2026 18:33:09 +0200 Subject: [PATCH 66/76] Drop duplicate estimatedPrice token and tidy ediEmail null handling Remove the order-email estimatedPrice cost token, which duplicated poLineEstimatedPrice. Resolve ediEmail once in SendToEmailTasklet with a clear "no email configuration" failure before use, so the template-id guard stays reachable and a missing config no longer NPEs. --- .../acquisitions/jobs/SendToEmailTasklet.java | 18 ++++++++++++------ .../mapper/OrderEmailContextMapper.java | 1 - .../templateengine/context/CostContext.java | 1 - .../mapper/OrderEmailContextMapperTest.java | 1 - 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java index 52342e318..83b9a6e9e 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTasklet.java @@ -41,6 +41,7 @@ public class SendToEmailTasklet implements Tasklet { private static final String EMAIL_ERROR_MESSAGE = "Failed to send the email"; private static final String TEMPLATE_ENGINE_ERROR_MESSAGE = "Failed to retrieve the email template"; + private static final String EMAIL_NOT_CONFIGURED_ERROR_MESSAGE = "Failed because no email configuration has been provided"; private static final String TEMPLATE_NOT_CONFIGURED_ERROR_MESSAGE = "Failed because no email template has been configured"; private final ObjectMapper ediObjectMapper; @@ -62,12 +63,18 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon var ordersJson = (String) ExecutionContextUtils.getExecutionVariable(stepExecution, ACQ_EXPORT_ORDERS); var orders = ediObjectMapper.readValue(ordersJson, new TypeReference>() {}); + var ediEmail = Optional.ofNullable(exportConfig.getEdiEmail()) + .orElseThrow(() -> { + log.error(EMAIL_NOT_CONFIGURED_ERROR_MESSAGE); + return new EdifactException(EMAIL_NOT_CONFIGURED_ERROR_MESSAGE); + }); + var emailEntity = new EmailEntity(); emailEntity.setNotificationId(jobId); - emailEntity.setFrom(exportConfig.getEdiEmail().getEmailFrom()); - emailEntity.setTo(exportConfig.getEdiEmail().getEmailTo()); + emailEntity.setFrom(ediEmail.getEmailFrom()); + emailEntity.setTo(ediEmail.getEmailTo()); - var templateResult = resolveTemplate(exportConfig, orders); + var templateResult = resolveTemplate(ediEmail, orders); emailEntity.setHeader(templateResult[0]); emailEntity.setBody(templateResult[1]); emailEntity.setOutputFormat(templateResult[2]); @@ -78,9 +85,8 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon return RepeatStatus.FINISHED; } - private String[] resolveTemplate(VendorEdiOrdersExportConfig exportConfig, List orders) { - UUID templateId = Optional.ofNullable(exportConfig.getEdiEmail()) - .map(EdiEmail::getEmailTemplate) + private String[] resolveTemplate(EdiEmail ediEmail, List orders) { + UUID templateId = Optional.ofNullable(ediEmail.getEmailTemplate()) .orElseThrow(() -> { log.error(TEMPLATE_NOT_CONFIGURED_ERROR_MESSAGE); return new EdifactException(TEMPLATE_NOT_CONFIGURED_ERROR_MESSAGE); diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index cdf904e77..eff951e48 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -198,7 +198,6 @@ private CostContext mapCost(Cost cost) { .quantityPhysical(quantityPhysical) .quantityElectronic(quantityElectronic) .quantity(quantityPhysical + quantityElectronic) - .estimatedPrice(formatDecimal(Optional.ofNullable(cost).map(Cost::getPoLineEstimatedPrice).orElse(null))) .poLineEstimatedPrice(formatDecimal(Optional.ofNullable(cost).map(Cost::getPoLineEstimatedPrice).orElse(null))) .currency(Optional.ofNullable(cost).map(Cost::getCurrency).orElse("")) .build(); diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/CostContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/CostContext.java index 5352ad8b7..12d2747bb 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/CostContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/CostContext.java @@ -11,7 +11,6 @@ public class CostContext { private Integer quantityPhysical; private Integer quantityElectronic; private Integer quantity; - private String estimatedPrice; private String poLineEstimatedPrice; private String currency; } diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index 70a6beff1..7b0d3219c 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -144,7 +144,6 @@ void buildContext_mapsOrderLineFields() throws IOException { assertThat(line.getCost().getListUnitPrice()).isEqualTo("2.0"); assertThat(line.getCost().getCurrency()).isEqualTo("USD"); assertThat(line.getCost().getQuantity()).isEqualTo(1); - assertThat(line.getCost().getEstimatedPrice()).isEqualTo("1.8"); assertThat(line.getCost().getPoLineEstimatedPrice()).isEqualTo("1.8"); assertThat(line.getFundDistribution()).hasSize(1); var fund = line.getFundDistribution().get(0); From a63051000b57c27404530b2d29f297cc172162c0 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Mon, 29 Jun 2026 12:07:53 +0200 Subject: [PATCH 67/76] Document order email template context payload in README Add an Order Email context-payload reference under Additional information: an annotated structure tree and an example JSON payload for the EDIFACT_ORDERS_EXPORT email transmission. --- README.md | 163 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 162 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d97b0eddf..29e528616 100644 --- a/README.md +++ b/README.md @@ -60,4 +60,165 @@ This value should be `true` if AWS S3 is used as storage. | CHUNKS | 100 | Number of items being passed to write at once | | CORE_POOL_SIZE | 10 | Maximum number of threads being created for each task before the queue is utilized | | MAX_POOL_SIZE | 10 | Maximum number of threads that can be created after the queue is full and before rejecting the new tasks | -| BUCKET_SIZE | 50 | Size of the bucket used in partitioning parameters | +| BUCKET_SIZE | 50 | Size of the bucket used in partitioning parameters | + +### Order Email — Template Context Payload + +This is the `context` payload sent to **mod-template-engine** when an `EDIFACT_ORDERS_EXPORT` job is transmitted by **Email**. + +#### Structure overview + +``` +OrderEmailContext +├── createdAt # context build time, ISO-8601 UTC with millis (e.g. 2026-03-30T16:22:13.284Z) +├── organization +│ ├── name +│ └── primaryAddress # address flagged isPrimary +│ ├── addressLine1 +│ ├── city +│ ├── zipCode +│ └── country +└── orders[] # multiple entries + ├── order + │ ├── poNumber + │ ├── orderType + │ ├── metadata + │ │ └── createdByUser # resolved from metadata.createdByUserId via mod-users + │ │ ├── id + │ │ ├── firstName + │ │ ├── lastName + │ │ └── fullName + │ ├── shipTo # resolved from shipTo UUID via tenant-addresses + │ │ ├── id + │ │ └── address + │ └── billTo # resolved from billTo UUID via tenant-addresses + │ ├── id + │ └── address + └── orderLines[] # multiple entries + └── orderLine + ├── poLineNumber + ├── titleOrPackage + ├── publisher + ├── publicationDate + ├── edition + ├── rush + ├── contributors[] # multiple entries + │ ├── contributor + │ └── contributorNameType # resolved type name (e.g. Personal name, Corporate name) + │ ├── id + │ └── name + ├── details + │ └── productIds[] # multiple entries + │ ├── productId + │ ├── qualifier + │ └── productIdType # resolved type name (e.g. ISBN, ASIN) + │ ├── id + │ └── name + ├── cost + │ ├── listUnitPrice + │ ├── listUnitPriceElectronic + │ ├── quantityPhysical + │ ├── quantityElectronic + │ ├── quantity + │ ├── estimatedPrice + │ ├── poLineEstimatedPrice + │ └── currency + ├── fundDistribution[] # multiple entries + │ └── code # fund distribution code + └── vendorDetail + └── instructions # vendor instructions +``` +> **Null/empty policy:** missing values are rendered as safe defaults rather than +> `null`, so templates can reference any field without null checks. + +#### Example payload + +```json +{ + "createdAt": "2026-03-30T16:22:13.284Z", + "organization": { + "name": "GOBI Library Solutions", + "primaryAddress": { + "addressLine1": "1 Innovation Way", + "city": "Contoocook", + "zipCode": "03229", + "country": "USA" + } + }, + "orders": [ + { + "order": { + "poNumber": "10000", + "orderType": "One-Time", + "metadata": { + "createdByUser": { + "id": "7a626480-284e-5b55-9cf2-db32f93956cf", + "firstName": "John", + "lastName": "Doe", + "fullName": "John Doe" + } + }, + "shipTo": { + "id": "11111111-1111-1111-1111-111111111111", + "address": "SLUB Dresden, Zellescher Weg 18, 01069 Dresden" + }, + "billTo": { + "id": "22222222-2222-2222-2222-222222222222", + "address": "Accounts Payable, PO Box 42, Springfield IL" + } + }, + "orderLines": [ + { + "orderLine": { + "poLineNumber": "10000-1", + "titleOrPackage": "Introduction to FOLIO", + "publisher": "FOLIO Press", + "publicationDate": "2020", + "edition": "2nd", + "rush": false, + "contributors": [ + { + "contributor": "Jane Author", + "contributorNameType": { + "id": "2b94c631-fca9-4892-a730-03ee529ffe2a", + "name": "Personal name" + } + } + ], + "details": { + "productIds": [ + { + "productId": "978-3-16-148410-0", + "qualifier": "", + "productIdType": { + "id": "8261054f-be78-422d-bd51-4ed9f33c3422", + "name": "ISBN" + } + } + ] + }, + "cost": { + "listUnitPrice": "49.99", + "listUnitPriceElectronic": "0.00", + "quantityPhysical": 2, + "quantityElectronic": 0, + "quantity": 2, + "estimatedPrice": "99.98", + "poLineEstimatedPrice": "99.98", + "currency": "USD" + }, + "fundDistribution": [ + { + "code": "USHIST" + } + ], + "vendorDetail": { + "instructions": "Deliver to loading dock, ring bell on arrival" + } + } + } + ] + } + ] +} +``` | From 4c3fa7e4b14e54fa7621b4b70de2d66acf294f09 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Mon, 29 Jun 2026 12:21:21 +0200 Subject: [PATCH 68/76] Drop derived quantity field from order email cost context The cost context exposed a quantity equal to quantityPhysical plus quantityElectronic, which is not a real PO line field. Remove it from the DTO, mapper, tests, and README, and clarify that fundDistribution code is taken verbatim from the PO line without resolving fundId. --- README.md | 4 +--- .../batch/acquisitions/mapper/OrderEmailContextMapper.java | 1 - .../dew/domain/dto/templateengine/context/CostContext.java | 1 - .../acquisitions/mapper/OrderEmailContextMapperTest.java | 2 -- 4 files changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index 29e528616..e75eeda84 100644 --- a/README.md +++ b/README.md @@ -119,12 +119,11 @@ OrderEmailContext │ ├── listUnitPriceElectronic │ ├── quantityPhysical │ ├── quantityElectronic - │ ├── quantity │ ├── estimatedPrice │ ├── poLineEstimatedPrice │ └── currency ├── fundDistribution[] # multiple entries - │ └── code # fund distribution code + │ └── code # code taken as-is from the PO line; fundId is not resolved └── vendorDetail └── instructions # vendor instructions ``` @@ -202,7 +201,6 @@ OrderEmailContext "listUnitPriceElectronic": "0.00", "quantityPhysical": 2, "quantityElectronic": 0, - "quantity": 2, "estimatedPrice": "99.98", "poLineEstimatedPrice": "99.98", "currency": "USD" diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index eff951e48..15593f751 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -197,7 +197,6 @@ private CostContext mapCost(Cost cost) { .listUnitPriceElectronic(formatDecimal(Optional.ofNullable(cost).map(Cost::getListUnitPriceElectronic).orElse(null))) .quantityPhysical(quantityPhysical) .quantityElectronic(quantityElectronic) - .quantity(quantityPhysical + quantityElectronic) .poLineEstimatedPrice(formatDecimal(Optional.ofNullable(cost).map(Cost::getPoLineEstimatedPrice).orElse(null))) .currency(Optional.ofNullable(cost).map(Cost::getCurrency).orElse("")) .build(); diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/CostContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/CostContext.java index 12d2747bb..89368217c 100644 --- a/src/main/java/org/folio/dew/domain/dto/templateengine/context/CostContext.java +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/CostContext.java @@ -10,7 +10,6 @@ public class CostContext { private String listUnitPriceElectronic; private Integer quantityPhysical; private Integer quantityElectronic; - private Integer quantity; private String poLineEstimatedPrice; private String currency; } diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index 7b0d3219c..6ce75857a 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -143,7 +143,6 @@ void buildContext_mapsOrderLineFields() throws IOException { assertThat(productId.getProductIdType().getName()).isEqualTo("ISBN"); assertThat(line.getCost().getListUnitPrice()).isEqualTo("2.0"); assertThat(line.getCost().getCurrency()).isEqualTo("USD"); - assertThat(line.getCost().getQuantity()).isEqualTo(1); assertThat(line.getCost().getPoLineEstimatedPrice()).isEqualTo("1.8"); assertThat(line.getFundDistribution()).hasSize(1); var fund = line.getFundDistribution().get(0); @@ -242,7 +241,6 @@ void buildContext_nullCost_doesNotThrow() throws IOException { OrderLineContext line = ctx.getOrders().get(0).orderLines().get(0).orderLine(); assertThat(line.getCost().getListUnitPrice()).isEmpty(); assertThat(line.getCost().getCurrency()).isEmpty(); - assertThat(line.getCost().getQuantity()).isZero(); } @Test From e3d8639dac251b0e06359affacecc0ed1ee59bec Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Mon, 29 Jun 2026 14:33:53 +0200 Subject: [PATCH 69/76] Fix ConfigurationService SonarCloud findings Drop the unused JsonNode import and ADDRESS constant, and route both cacheable methods through a private fetchTenantAddress helper so the @Cacheable getTenantAddress is no longer self-invoked via this. --- .../services/ConfigurationService.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/services/ConfigurationService.java b/src/main/java/org/folio/dew/batch/acquisitions/services/ConfigurationService.java index 0cd7f2fa7..f03071fca 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/services/ConfigurationService.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/services/ConfigurationService.java @@ -2,8 +2,6 @@ import java.util.UUID; -import com.fasterxml.jackson.databind.JsonNode; - import lombok.RequiredArgsConstructor; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -19,35 +17,38 @@ public class ConfigurationService { private static final Logger logger = LogManager.getLogger(); - private static final String ADDRESS = "address"; private final TenantAddressesClient tenantAddressesClient; @Cacheable(cacheNames = "addressConfiguration") public String getAddressConfig(UUID shipToConfigId) { - return ofNullable(getTenantAddress(shipToConfigId)) + return ofNullable(fetchTenantAddress(shipToConfigId)) .map(TenantAddress::getAddress) .orElse(""); } @Cacheable(cacheNames = "tenantAddress", unless = "#result == null") public TenantAddress getTenantAddress(UUID shipToConfigId) { + return fetchTenantAddress(shipToConfigId); + } + + private TenantAddress fetchTenantAddress(UUID shipToConfigId) { if (shipToConfigId == null) { - logger.warn("getTenantAddress:: shipToConfigId is null"); + logger.warn("fetchTenantAddress:: shipToConfigId is null"); return null; } try { TenantAddress addressResponse = tenantAddressesClient.getById(shipToConfigId.toString()); if (addressResponse == null) { - logger.warn("getTenantAddress:: No address found for id '{}'", shipToConfigId); + logger.warn("fetchTenantAddress:: No address found for id '{}'", shipToConfigId); return null; } - logger.info("getTenantAddress:: Found address with id '{}'", shipToConfigId); + logger.info("fetchTenantAddress:: Found address with id '{}'", shipToConfigId); return addressResponse; } catch (Exception e) { - logger.warn("getTenantAddress:: Cannot find address by id: '{}'", shipToConfigId, e); + logger.warn("fetchTenantAddress:: Cannot find address by id: '{}'", shipToConfigId, e); return null; } } From 62491700af41d5c105577cccbd49a62f682d0799 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Mon, 29 Jun 2026 14:44:18 +0200 Subject: [PATCH 70/76] Add UserServiceTest for order email user context Cover getUserContext: resolved name, username fallback when names are blank or personal is absent, and empty context on null response or client exception. --- .../services/UserServiceTest.java | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/test/java/org/folio/dew/batch/acquisitions/services/UserServiceTest.java diff --git a/src/test/java/org/folio/dew/batch/acquisitions/services/UserServiceTest.java b/src/test/java/org/folio/dew/batch/acquisitions/services/UserServiceTest.java new file mode 100644 index 000000000..8871c1a93 --- /dev/null +++ b/src/test/java/org/folio/dew/batch/acquisitions/services/UserServiceTest.java @@ -0,0 +1,97 @@ +package org.folio.dew.batch.acquisitions.services; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.folio.dew.client.UserClient; +import org.folio.dew.domain.dto.Personal; +import org.folio.dew.domain.dto.User; +import org.folio.dew.domain.dto.templateengine.context.UserContext; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class UserServiceTest { + + private static final String USER_ID = "7a626480-284e-5b55-9cf2-db32f93956cf"; + + @Mock + private UserClient userClient; + + @InjectMocks + private UserService userService; + + @Test + void getUserContext_userWithPersonal_returnsResolvedContext() { + when(userClient.getUserById(USER_ID)).thenReturn(user("jdoe", "John", "Doe")); + + UserContext context = userService.getUserContext(USER_ID); + + assertThat(context.getId()).isEqualTo(USER_ID); + assertThat(context.getFirstName()).isEqualTo("John"); + assertThat(context.getLastName()).isEqualTo("Doe"); + assertThat(context.getFullName()).isEqualTo("John Doe"); + } + + @Test + void getUserContext_blankNames_fullNameFallsBackToUsername() { + when(userClient.getUserById(USER_ID)).thenReturn(user("jdoe", " ", " ")); + + UserContext context = userService.getUserContext(USER_ID); + + assertThat(context.getFullName()).isEqualTo("jdoe"); + } + + @Test + void getUserContext_nullPersonal_fullNameFallsBackToUsername() { + User user = new User(); + user.setId(USER_ID); + user.setUsername("jdoe"); + when(userClient.getUserById(USER_ID)).thenReturn(user); + + UserContext context = userService.getUserContext(USER_ID); + + assertThat(context.getFirstName()).isEmpty(); + assertThat(context.getLastName()).isEmpty(); + assertThat(context.getFullName()).isEqualTo("jdoe"); + } + + @Test + void getUserContext_nullResponse_returnsEmptyContext() { + when(userClient.getUserById(USER_ID)).thenReturn(null); + + assertEmpty(userService.getUserContext(USER_ID)); + } + + @Test + void getUserContext_clientThrowsException_returnsEmptyContext() { + when(userClient.getUserById(USER_ID)).thenThrow(new RuntimeException("Connection error")); + + assertEmpty(userService.getUserContext(USER_ID)); + } + + private static void assertEmpty(UserContext context) { + assertThat(context.getId()).isEmpty(); + assertThat(context.getFirstName()).isEmpty(); + assertThat(context.getLastName()).isEmpty(); + assertThat(context.getFullName()).isEmpty(); + } + + private static User user(String username, String firstName, String lastName) { + Personal personal = new Personal(); + personal.setFirstName(firstName); + personal.setLastName(lastName); + + User user = new User(); + user.setId(USER_ID); + user.setUsername(username); + user.setPersonal(personal); + return user; + } +} From d0df2ec9d28a2ab1388c6d13b13400614dee47c7 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Mon, 29 Jun 2026 14:57:33 +0200 Subject: [PATCH 71/76] Remove unused imports from UserServiceTest --- .../folio/dew/batch/acquisitions/services/UserServiceTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/test/java/org/folio/dew/batch/acquisitions/services/UserServiceTest.java b/src/test/java/org/folio/dew/batch/acquisitions/services/UserServiceTest.java index 8871c1a93..fa6b64dec 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/services/UserServiceTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/services/UserServiceTest.java @@ -1,9 +1,6 @@ package org.folio.dew.batch.acquisitions.services; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.folio.dew.client.UserClient; From e1660a43af73ffe2a55eb41efc56d419db785368 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Tue, 30 Jun 2026 09:31:04 +0200 Subject: [PATCH 72/76] Filter null elements in order email context list mapping Skip null source entries before mapping in the shared list helper to avoid NPEs. Also drop the obsolete estimatedPrice field from the documented order-line payload in README. --- README.md | 2 -- .../dew/batch/acquisitions/mapper/OrderEmailContextMapper.java | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index e75eeda84..9e45033ab 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,6 @@ OrderEmailContext │ ├── listUnitPriceElectronic │ ├── quantityPhysical │ ├── quantityElectronic - │ ├── estimatedPrice │ ├── poLineEstimatedPrice │ └── currency ├── fundDistribution[] # multiple entries @@ -201,7 +200,6 @@ OrderEmailContext "listUnitPriceElectronic": "0.00", "quantityPhysical": 2, "quantityElectronic": 0, - "estimatedPrice": "99.98", "poLineEstimatedPrice": "99.98", "currency": "USD" }, diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index 15593f751..7c8ae5ec2 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -179,6 +179,7 @@ private List mapList(List source, Function mapper) { return List.of(); } return source.stream() + .filter(Objects::nonNull) .map(mapper) .toList(); } From 3eca8ed4e2be7d538ec65dd322c7d42029652694 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Tue, 30 Jun 2026 09:38:03 +0200 Subject: [PATCH 73/76] Use Lombok @Log4j2 in UserService Replace the manual LogManager.getLogger() field with Lombok @Log4j2 to match the other acquisitions services in this branch. --- .../dew/batch/acquisitions/services/UserService.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/services/UserService.java b/src/main/java/org/folio/dew/batch/acquisitions/services/UserService.java index e1ad4d916..4d44f619d 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/services/UserService.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/services/UserService.java @@ -1,9 +1,8 @@ package org.folio.dew.batch.acquisitions.services; import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.folio.dew.client.UserClient; import org.folio.dew.domain.dto.Personal; import org.folio.dew.domain.dto.User; @@ -12,11 +11,10 @@ import org.springframework.stereotype.Service; @Service +@Log4j2 @RequiredArgsConstructor public class UserService { - private static final Logger logger = LogManager.getLogger(); - private final UserClient userClient; @Cacheable(cacheNames = "users") @@ -27,12 +25,12 @@ public UserContext getUserContext(String userId) { try { User user = userClient.getUserById(userId); if (user == null) { - logger.warn("getUserContext:: No user found for id '{}'", userId); + log.warn("getUserContext:: No user found for id '{}'", userId); return emptyUserContext(); } return toUserContext(user); } catch (Exception e) { - logger.warn("getUserContext:: Cannot find user by id: '{}'", userId, e); + log.warn("getUserContext:: Cannot find user by id: '{}'", userId, e); return emptyUserContext(); } } From 2d5b0b463bacfe186428be6ae1763756ff4e15d7 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Tue, 30 Jun 2026 09:49:05 +0200 Subject: [PATCH 74/76] Truncate order email createdAt to millisecond precision Instant.now().toString() emits a variable number of fractional digits depending on clock resolution, breaking the required millisecond format. Truncate to millis and add a regression test guarding the pattern. --- .../batch/acquisitions/mapper/OrderEmailContextMapper.java | 3 ++- .../acquisitions/mapper/OrderEmailContextMapperTest.java | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index 7c8ae5ec2..d07e4129f 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -32,6 +32,7 @@ import java.math.BigDecimal; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -59,7 +60,7 @@ public OrderEmailContext buildContext(List orders) { .toList())) .toList(); return OrderEmailContext.builder() - .createdAt(Instant.now().toString()) + .createdAt(Instant.now().truncatedTo(ChronoUnit.MILLIS).toString()) .organization(mapOrganization(orders)) .orders(orderWrappers) .build(); diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index 6ce75857a..83f85ca77 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -169,6 +169,13 @@ void buildContext_emptyOrders_returnsEmptyList() { assertThat(ctx.getOrganization().getPrimaryAddress().getAddressLine1()).isEmpty(); } + @Test + void buildContext_setsCreatedAt() { + OrderEmailContext ctx = mapper.buildContext(List.of()); + + assertThat(ctx.getCreatedAt()).matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z"); + } + @Test void buildContext_mapsOrganizationFields() throws IOException { var org = new Organization(); From e407fdf44b304be4240fa10ca3db33dc9a0ab154 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Tue, 30 Jun 2026 14:43:08 +0200 Subject: [PATCH 75/76] Move contributor name type lookup error handling to email mapper Let ContributorNameTypeService return the raw cached result and handle lookup failures in OrderEmailContextMapper, so a failed type resolution degrades to an empty name without breaking the email context build. --- .../mapper/OrderEmailContextMapper.java | 11 ++++++++++- .../services/ContributorNameTypeService.java | 11 ++--------- .../mapper/OrderEmailContextMapperTest.java | 13 +++++++++++++ 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java index d07e4129f..060e73b5d 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -220,7 +220,7 @@ private ContributorContext mapContributor(Contributor contributor) { .contributor(StringUtils.defaultString(contributor.getContributor())) .contributorNameType(TypeContext.builder() .id(contributorNameTypeId) - .name(StringUtils.isBlank(contributorNameTypeId) ? "" : StringUtils.defaultString(contributorNameTypeService.getContributorNameTypeName(contributorNameTypeId))) + .name(StringUtils.isBlank(contributorNameTypeId) ? "" : resolveContributorNameTypeName(contributorNameTypeId)) .build()) .build(); } @@ -251,4 +251,13 @@ private String resolveIdentifierTypeName(String productIdTypeId) { return ""; } } + + private String resolveContributorNameTypeName(String contributorNameTypeId) { + try { + return StringUtils.defaultString(contributorNameTypeService.getContributorNameTypeName(contributorNameTypeId)); + } catch (Exception e) { + log.warn("resolveContributorNameTypeName:: Cannot resolve contributor name type '{}' for email context", contributorNameTypeId, e); + return ""; + } + } } diff --git a/src/main/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeService.java b/src/main/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeService.java index b2c8d0708..0a692137d 100644 --- a/src/main/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeService.java +++ b/src/main/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeService.java @@ -6,23 +6,16 @@ import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; @Service @RequiredArgsConstructor -@Log4j2 public class ContributorNameTypeService { private final ContributorNameTypeClient contributorNameTypeClient; @Cacheable(cacheNames = "contributorNameTypes", unless = "#result == null") public String getContributorNameTypeName(String id) { - try { - ContributorNameType contributorNameType = contributorNameTypeClient.getContributorNameType(id); - return contributorNameType != null ? contributorNameType.getName() : ""; - } catch (Exception e) { - log.warn("getContributorNameTypeName:: Cannot find contributor name type by id: '{}'", id, e); - return null; - } + ContributorNameType contributorNameType = contributorNameTypeClient.getContributorNameType(id); + return contributorNameType != null ? contributorNameType.getName() : ""; } } diff --git a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java index 83f85ca77..e27a49e0e 100644 --- a/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -299,6 +299,19 @@ void buildContext_identifierTypeLookupThrows_returnsEmptyName() throws IOExcepti assertThat(productIdType.getId()).isEqualTo("8261054f-be78-422d-bd51-4ed9f33c3422"); } + @Test + void buildContext_contributorNameTypeLookupThrows_returnsEmptyName() throws IOException { + when(contributorNameTypeService.getContributorNameTypeName(anyString())).thenThrow(new RuntimeException("boom")); + var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.json"); + + OrderEmailContext ctx = mapper.buildContext(List.of(order)); + + var contributorNameType = ctx.getOrders().get(0).orderLines().get(0).orderLine() + .getContributors().get(0).getContributorNameType(); + assertThat(contributorNameType.getName()).isEmpty(); + assertThat(contributorNameType.getId()).isEqualTo("2b94c631-fca9-4892-a730-03ee529ffe2a"); + } + private CompositePurchaseOrder loadOrder(String path) throws IOException { return objectMapper.readValue(getMockData(path), CompositePurchaseOrder.class); } From c8bdd3ce5abc386ec25cd47a84edcf99e64db891 Mon Sep 17 00:00:00 2001 From: Markus Weigelt Date: Thu, 2 Jul 2026 14:52:09 +0200 Subject: [PATCH 76/76] Fix formatting issue in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e45033ab..d41b892da 100644 --- a/README.md +++ b/README.md @@ -217,4 +217,4 @@ OrderEmailContext } ] } -``` | +```