Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
a01605b
Update submodule
markusweigelt Mar 11, 2026
4dff439
Add implementation to send emails
markusweigelt Mar 11, 2026
b4d94e7
Update submodule
markusweigelt Mar 11, 2026
055fa21
Improvement to compile module
markusweigelt Mar 11, 2026
1dbf08c
Improvement to compile module
markusweigelt Mar 11, 2026
eeff6fd
Improvement to compile module
markusweigelt Mar 11, 2026
8cdaf43
Improve email implementation
markusweigelt Mar 12, 2026
3e32a52
Override Kafka-propagated x-okapi-url with worker's own Eureka sidec…
markusweigelt Mar 12, 2026
fae19f8
add logging
markusweigelt Mar 12, 2026
c64c9b5
add configuration
markusweigelt Mar 12, 2026
d403eba
Update Client
markusweigelt Mar 12, 2026
cce9aef
Update client config
markusweigelt Mar 12, 2026
98ec7c9
Update emailclient
markusweigelt Mar 13, 2026
3b860bd
Add template request
markusweigelt Mar 13, 2026
b11579b
Update template engline
markusweigelt Mar 13, 2026
0348c22
fix(template-engine): change default outputFormat to text/html
markusweigelt Mar 13, 2026
39425f0
propagate template outputFormat to email entity
markusweigelt Mar 13, 2026
d4d70e7
Add SendToEmailTasklet error handling and tests
markusweigelt Mar 27, 2026
f8b55a9
Support email sending for ORDERING integration type
markusweigelt Mar 27, 2026
a0a85cd
Merge master into send-mail
markusweigelt Mar 27, 2026
a099ee8
Fix Spring Batch and Feign client migration for email send step
markusweigelt Mar 27, 2026
9774da8
Register EmailClient and TemplateEngineClient beans in HttpClientConf…
markusweigelt Mar 27, 2026
1f66e3d
Add @RequestBody to EmailClient and TemplateEngineClient parameters
markusweigelt Mar 27, 2026
a931b58
Fix Jackson version conflict in TemplateEngineClient response deseria…
markusweigelt Mar 27, 2026
b2391e5
Replace String/JsonNode workaround with concrete TemplateProcessingRe…
markusweigelt Mar 27, 2026
690e88d
Update SendToEmailTaskletTest mock to use TemplateProcessingResponse DTO
markusweigelt Mar 27, 2026
db458f6
Fix missing newline at end of file in several source files
markusweigelt Mar 30, 2026
e2e3dd8
Fix SendToEmailTaskletDecider to trigger on transmissionMethod only
markusweigelt Mar 30, 2026
050fb92
Update folio-export-common submodule branch to MODEXPW-625
markusweigelt Mar 30, 2026
de25a6c
Remove stale Feign client debug logging from application.yml
markusweigelt Mar 30, 2026
6d76013
Fix SendToEmailTaskletDeciderTest to correctly cover transmissionMethod
markusweigelt Mar 30, 2026
c205964
Add email config validation to MapToEdifactTasklet
markusweigelt Mar 30, 2026
8835ae0
Reset folio.okapi-url default to okapi:9130
markusweigelt Mar 31, 2026
5a8e077
Add email and template-engine as optional interfaces in ModuleDescriptor
markusweigelt Mar 31, 2026
4770c8e
Refactor EdifactExportJobConfig to use lazy constructor injection for…
markusweigelt Apr 8, 2026
5d0a856
Fix SaveToFileStorageTaskletDecider to skip FTP step when transmissio…
markusweigelt Apr 8, 2026
d258084
Add @Qualifier to EdifactExportJobConfig constructor to resolve Step …
markusweigelt Apr 8, 2026
449f7e7
Revert EdifactExportJobConfig to @Bean method parameter injection
markusweigelt Apr 8, 2026
40bb493
Merge branch 'master' into MODEXPW-625
markusweigelt Apr 8, 2026
0956b3d
Merge branch 'master' into MODEXPW-625
markusweigelt Apr 24, 2026
76eb73d
Update submodule
markusweigelt Apr 24, 2026
0d37e63
Add order/line template context for EDIFACT email export
markusweigelt Apr 29, 2026
2782921
Add contributors and productIdentifierType to order email context
markusweigelt May 11, 2026
1763611
Resolve shipTo/billTo UUIDs to addresses in OrderEmailContext
markusweigelt May 12, 2026
88e7ec5
Resolve order createdBy from userId via new UserService
markusweigelt May 13, 2026
0d9cbab
Add OrganizationContext (vendor) to OrderEmailContext
markusweigelt May 13, 2026
4fd9dfc
Render shipTo/billTo line breaks for HTML email output
markusweigelt May 13, 2026
ad4486c
Rename organization tokens and require strict isPrimary flag
markusweigelt May 18, 2026
cf58607
Drop organization code and primaryEmail tokens
markusweigelt May 18, 2026
d20b790
Restore organization code token on OrderEmailContext
markusweigelt May 20, 2026
8833086
Drop notes token from OrderContext
markusweigelt May 20, 2026
292e6a5
Trim unused tokens from order email context
markusweigelt May 27, 2026
212e8da
Split email-context test fixture from EDIFACT/CSV mapper fixture
markusweigelt May 27, 2026
9746e7b
Fix SonarCloud issues
markusweigelt May 28, 2026
647da72
Collapse mapOrganization null checks into Optional chain
markusweigelt May 28, 2026
517dd64
Restructure order email context into nested sub-contexts
markusweigelt Jun 22, 2026
e5fc816
Merge branch 'master' into MODEXPW-635
markusweigelt Jun 23, 2026
81e0e30
Merge branch 'master' into MODEXPW-635
markusweigelt Jun 23, 2026
0a5433f
Update submodule
markusweigelt Jun 23, 2026
89fb1d4
Add contributors context to order-line email templates
markusweigelt Jun 23, 2026
3b68340
Restructure order email context into nested typed sub-contexts
markusweigelt Jun 24, 2026
fdb4269
Add fund distribution code to order-line email context
markusweigelt Jun 25, 2026
b6d351a
Add createdAt timestamp to order email context
markusweigelt Jun 25, 2026
0a51a62
Trim order email context fields for createdByUser, order, and order line
markusweigelt Jun 25, 2026
95a7e8e
Align OrderEmailContextMapperTest with renamed context fields
markusweigelt Jun 26, 2026
464fd61
Pass email-context line breaks through verbatim and drop base mapper
markusweigelt Jun 26, 2026
bfdef21
Extract generic mapList helper and simplify createdAt formatting
markusweigelt Jun 26, 2026
a2603da
Remove blank line before fields in email context DTOs
markusweigelt Jun 26, 2026
99434e2
Degrade order-email lookups gracefully and stop caching nulls
markusweigelt Jun 26, 2026
5bb76a5
Confine order-email lookup resilience to the email mapper
markusweigelt Jun 26, 2026
75e325e
Drop duplicate estimatedPrice token and tidy ediEmail null handling
markusweigelt Jun 26, 2026
a630510
Document order email template context payload in README
markusweigelt Jun 29, 2026
4c3fa7e
Drop derived quantity field from order email cost context
markusweigelt Jun 29, 2026
e3d8639
Fix ConfigurationService SonarCloud findings
markusweigelt Jun 29, 2026
6249170
Add UserServiceTest for order email user context
markusweigelt Jun 29, 2026
d0df2ec
Remove unused imports from UserServiceTest
markusweigelt Jun 29, 2026
e1660a4
Filter null elements in order email context list mapping
markusweigelt Jun 30, 2026
3eca8ed
Use Lombok @Log4j2 in UserService
markusweigelt Jun 30, 2026
2d5b0b4
Truncate order email createdAt to millisecond precision
markusweigelt Jun 30, 2026
e407fdf
Move contributor name type lookup error handling to email mapper
markusweigelt Jun 30, 2026
c8bdd3c
Fix formatting issue in README.md
markusweigelt Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 158 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,161 @@ 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
│ ├── poLineEstimatedPrice
│ └── currency
├── fundDistribution[] # multiple entries
│ └── code # code taken as-is from the PO line; fundId is not resolved
└── 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,
"poLineEstimatedPrice": "99.98",
"currency": "USD"
},
"fundDistribution": [
{
"code": "USHIST"
}
],
"vendorDetail": {
"instructions": "Deliver to loading dock, ring bell on arrival"
}
}
}
]
}
]
}
```
4 changes: 4 additions & 0 deletions descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@
"id": "identifier-types",
"version": "1.2"
},
{
"id": "contributor-name-types",
"version": "1.2"
},
{
"id": "ill-policies",
"version": "1.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package org.folio.dew.batch.acquisitions.jobs;

import com.fasterxml.jackson.core.type.TypeReference;
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.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;
Expand Down Expand Up @@ -38,11 +41,13 @@ 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;
private final EmailClient emailClient;
private final TemplateEngineClient templateEngineClient;
private final OrderEmailContextMapper orderEmailContextMapper;

@Override
@SneakyThrows
Expand All @@ -55,13 +60,21 @@ 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<List<CompositePurchaseOrder>>() {});

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);
var templateResult = resolveTemplate(ediEmail, orders);
emailEntity.setHeader(templateResult[0]);
emailEntity.setBody(templateResult[1]);
emailEntity.setOutputFormat(templateResult[2]);
Expand All @@ -72,9 +85,8 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon
return RepeatStatus.FINISHED;
}

private String[] resolveTemplate(VendorEdiOrdersExportConfig exportConfig) {
UUID templateId = Optional.ofNullable(exportConfig.getEdiEmail())
.map(EdiEmail::getEmailTemplate)
private String[] resolveTemplate(EdiEmail ediEmail, List<CompositePurchaseOrder> orders) {
UUID templateId = Optional.ofNullable(ediEmail.getEmailTemplate())
.orElseThrow(() -> {
log.error(TEMPLATE_NOT_CONFIGURED_ERROR_MESSAGE);
return new EdifactException(TEMPLATE_NOT_CONFIGURED_ERROR_MESSAGE);
Expand All @@ -83,6 +95,7 @@ private String[] resolveTemplate(VendorEdiOrdersExportConfig exportConfig) {
var request = TemplateProcessingRequest.builder()
.templateId(templateId)
.build();
request.setContext(orderEmailContextMapper.buildContext(orders));

try {
TemplateProcessingResponse response = templateEngineClient.processTemplate(request);
Expand Down
Loading
Loading