diff --git a/README.md b/README.md index d97b0eddf..d41b892da 100644 --- a/README.md +++ b/README.md @@ -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" + } + } + } + ] + } + ] +} +``` 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/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 c004f53e5..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 @@ -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; @@ -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 @@ -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>() {}); + + 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]); @@ -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 orders) { + UUID templateId = Optional.ofNullable(ediEmail.getEmailTemplate()) .orElseThrow(() -> { log.error(TEMPLATE_NOT_CONFIGURED_ERROR_MESSAGE); return new EdifactException(TEMPLATE_NOT_CONFIGURED_ERROR_MESSAGE); @@ -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); 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..060e73b5d --- /dev/null +++ b/src/main/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapper.java @@ -0,0 +1,263 @@ +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; +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.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; +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; +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.math.BigDecimal; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +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 +@Log4j2 +public class OrderEmailContextMapper { + + private final IdentifierTypeService identifierTypeService; + private final ContributorNameTypeService contributorNameTypeService; + private final ConfigurationService configurationService; + private final UserService userService; + private final OrganizationsService organizationsService; + + 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() + .createdAt(Instant.now().truncatedTo(ChronoUnit.MILLIS).toString()) + .organization(mapOrganization(orders)) + .orders(orderWrappers) + .build(); + } + + private OrganizationContext mapOrganization(List orders) { + return orders.stream() + .map(CompositePurchaseOrder::getVendor) + .filter(Objects::nonNull) + .findFirst() + .map(this::resolveOrganization) + .map(org -> OrganizationContext.builder() + .name(StringUtils.defaultString(org.getName())) + .primaryAddress(pickPrimaryAddress(org.getAddresses())) + .build()) + .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("") + .primaryAddress(emptyOrganizationAddressContext()) + .build(); + } + + private OrganizationAddressContext emptyOrganizationAddressContext() { + return OrganizationAddressContext.builder() + .addressLine1("") + .city("") + .zipCode("") + .country("") + .build(); + } + + private OrganizationAddressContext pickPrimaryAddress(List addresses) { + return pickPrimary(addresses, addr -> Boolean.TRUE.equals(addr.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, Predicate isPrimary) { + if (CollectionUtils.isEmpty(items)) { + return Optional.empty(); + } + return items.stream() + .filter(isPrimary) + .findFirst(); + } + + 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())) + .billTo(mapTenantAddress(order.getBillTo())) + .build(); + } + + private String toUuidString(UUID id) { + return Optional.ofNullable(id).map(UUID::toString).orElse(""); + } + + 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(StringUtils.defaultString(tenantAddress.getAddress())) + .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(); + } + + private OrderLineContext mapOrderLine(PoLine line) { + return OrderLineContext.builder() + .poLineNumber(StringUtils.defaultString(line.getPoLineNumber())) + .titleOrPackage(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(mapList(line.getContributors(), this::mapContributor)) + .details(mapDetails(line.getDetails())) + .cost(mapCost(line.getCost())) + .fundDistribution(mapList(line.getFundDistribution(), this::mapFundDistribution)) + .vendorDetail(mapVendorDetail(line.getVendorDetail())) + .build(); + } + + private List mapList(List source, Function mapper) { + if (CollectionUtils.isEmpty(source)) { + return List.of(); + } + return source.stream() + .filter(Objects::nonNull) + .map(mapper) + .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); + 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) + .quantityElectronic(quantityElectronic) + .poLineEstimatedPrice(formatDecimal(Optional.ofNullable(cost).map(Cost::getPoLineEstimatedPrice).orElse(null))) + .currency(Optional.ofNullable(cost).map(Cost::getCurrency).orElse("")) + .build(); + } + + private String formatDecimal(BigDecimal value) { + return value != null ? value.toPlainString() : ""; + } + + private VendorDetailContext mapVendorDetail(VendorDetail vendorDetail) { + return VendorDetailContext.builder() + .instructions(Optional.ofNullable(vendorDetail).map(VendorDetail::getInstructions).orElse("")) + .build(); + } + + private ContributorContext mapContributor(Contributor contributor) { + var contributorNameTypeId = StringUtils.defaultString(contributor.getContributorNameTypeId()); + return ContributorContext.builder() + .contributor(StringUtils.defaultString(contributor.getContributor())) + .contributorNameType(TypeContext.builder() + .id(contributorNameTypeId) + .name(StringUtils.isBlank(contributorNameTypeId) ? "" : resolveContributorNameTypeName(contributorNameTypeId)) + .build()) + .build(); + } + + private DetailsContext mapDetails(Details details) { + return DetailsContext.builder() + .productIds(mapList(details == null ? null : details.getProductIds(), this::mapProductId)) + .build(); + } + + private ProductIdContext mapProductId(ProductIdentifier productId) { + var productIdTypeId = StringUtils.defaultString(productId.getProductIdType()); + return ProductIdContext.builder() + .productId(StringUtils.defaultString(productId.getProductId())) + .qualifier(StringUtils.defaultString(productId.getQualifier())) + .productIdType(TypeContext.builder() + .id(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 ""; + } + } + + 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/ConfigurationService.java b/src/main/java/org/folio/dew/batch/acquisitions/services/ConfigurationService.java index ed90e0f6c..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,29 +17,39 @@ 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(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("getAddressConfig:: shipToConfigId is null"); - return ""; + logger.warn("fetchTenantAddress:: 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("fetchTenantAddress:: No address found for id '{}'", shipToConfigId); + return null; } - logger.info("getAddressConfig:: Found address with id '{}'", shipToConfigId); - return ofNullable(addressResponse.getAddress()).orElse(""); + logger.info("fetchTenantAddress:: Found address with id '{}'", shipToConfigId); + return addressResponse; } catch (Exception e) { - logger.warn("getAddressConfig:: Cannot find address by id: '{}'", shipToConfigId, e); - return ""; + logger.warn("fetchTenantAddress:: Cannot find address by id: '{}'", shipToConfigId, e); + return 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 new file mode 100644 index 000000000..0a692137d --- /dev/null +++ b/src/main/java/org/folio/dew/batch/acquisitions/services/ContributorNameTypeService.java @@ -0,0 +1,21 @@ +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.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ContributorNameTypeService { + + private final ContributorNameTypeClient contributorNameTypeClient; + + @Cacheable(cacheNames = "contributorNameTypes", unless = "#result == null") + public String getContributorNameTypeName(String id) { + ContributorNameType contributorNameType = contributorNameTypeClient.getContributorNameType(id); + return contributorNameType != null ? contributorNameType.getName() : ""; + } +} 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..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 @@ -2,7 +2,6 @@ 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; @@ -11,22 +10,12 @@ @Service @RequiredArgsConstructor 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(); - } - - return name; + 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 68ffabf93..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 @@ -2,21 +2,18 @@ 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; -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) { return organizationsClient.getOrganizationById(id); } - - - } 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..4d44f619d --- /dev/null +++ b/src/main/java/org/folio/dew/batch/acquisitions/services/UserService.java @@ -0,0 +1,62 @@ +package org.folio.dew.batch.acquisitions.services; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; +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; + +@Service +@Log4j2 +@RequiredArgsConstructor +public class UserService { + + private final UserClient userClient; + + @Cacheable(cacheNames = "users") + public UserContext getUserContext(String userId) { + if (StringUtils.isBlank(userId)) { + return emptyUserContext(); + } + try { + User user = userClient.getUserById(userId); + if (user == null) { + log.warn("getUserContext:: No user found for id '{}'", userId); + return emptyUserContext(); + } + return toUserContext(user); + } catch (Exception e) { + log.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 fullName = (firstName + " " + lastName).trim(); + if (StringUtils.isBlank(fullName)) { + fullName = StringUtils.defaultString(user.getUsername()); + } + return UserContext.builder() + .id(StringUtils.defaultString(user.getId())) + .firstName(firstName) + .lastName(lastName) + .fullName(fullName) + .build(); + } + + private UserContext emptyUserContext() { + return UserContext.builder() + .id("") + .firstName("") + .lastName("") + .fullName("") + .build(); + } +} 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..20124d5d0 --- /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); +} 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/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/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/acquisitions/edifact/Organization.java b/src/main/java/org/folio/dew/domain/dto/acquisitions/edifact/Organization.java index bdf74975d..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 @@ -1,8 +1,12 @@ 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; } 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/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/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..2c006272f --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/ContributorContext.java @@ -0,0 +1,11 @@ +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; +} 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 new file mode 100644 index 000000000..89368217c --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/CostContext.java @@ -0,0 +1,15 @@ +package org.folio.dew.domain.dto.templateengine.context; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class CostContext { + private String listUnitPrice; + private String listUnitPriceElectronic; + private Integer quantityPhysical; + private Integer quantityElectronic; + private String poLineEstimatedPrice; + private String currency; +} 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 new file mode 100644 index 000000000..6965a5c83 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/DetailsContext.java @@ -0,0 +1,12 @@ +package org.folio.dew.domain.dto.templateengine.context; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@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 new file mode 100644 index 000000000..c2459bcef --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/FundDistributionContext.java @@ -0,0 +1,10 @@ +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/OrderContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderContext.java new file mode 100644 index 000000000..c646fd96b --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderContext.java @@ -0,0 +1,14 @@ +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 OrderMetadataContext metadata; + private TenantAddressContext shipTo; + private TenantAddressContext billTo; +} 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 new file mode 100644 index 000000000..c0a484664 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderEmailContext.java @@ -0,0 +1,14 @@ +package org.folio.dew.domain.dto.templateengine.context; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@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 new file mode 100644 index 000000000..45ab2a84b --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineContext.java @@ -0,0 +1,22 @@ +package org.folio.dew.domain.dto.templateengine.context; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class OrderLineContext { + private String poLineNumber; + private String titleOrPackage; + private String publisher; + private String publicationDate; + private String edition; + private Boolean rush; + private List contributors; + private DetailsContext details; + private CostContext cost; + private List fundDistribution; + private VendorDetailContext vendorDetail; +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineWrapper.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineWrapper.java new file mode 100644 index 000000000..d435e1bf3 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderLineWrapper.java @@ -0,0 +1,4 @@ +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..d0087aa2c --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderMetadataContext.java @@ -0,0 +1,10 @@ +package org.folio.dew.domain.dto.templateengine.context; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class OrderMetadataContext { + private UserContext createdByUser; +} diff --git a/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderWrapper.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderWrapper.java new file mode 100644 index 000000000..57a64440e --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrderWrapper.java @@ -0,0 +1,6 @@ +package org.folio.dew.domain.dto.templateengine.context; + +import java.util.List; + +public record OrderWrapper(OrderContext order, List orderLines) { +} 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 new file mode 100644 index 000000000..0936ae55b --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrganizationAddressContext.java @@ -0,0 +1,13 @@ +package org.folio.dew.domain.dto.templateengine.context; + +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/context/OrganizationContext.java b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrganizationContext.java new file mode 100644 index 000000000..059ea35c8 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/OrganizationContext.java @@ -0,0 +1,11 @@ +package org.folio.dew.domain.dto.templateengine.context; + +import lombok.Builder; +import lombok.Data; + +@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 new file mode 100644 index 000000000..473553d9b --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/ProductIdContext.java @@ -0,0 +1,12 @@ +package org.folio.dew.domain.dto.templateengine.context; + +import lombok.Builder; +import lombok.Data; + +@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 new file mode 100644 index 000000000..6971f76b6 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/TenantAddressContext.java @@ -0,0 +1,11 @@ +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; +} 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..7d504b767 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/TypeContext.java @@ -0,0 +1,11 @@ +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..380ee6dff --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/UserContext.java @@ -0,0 +1,13 @@ +package org.folio.dew.domain.dto.templateengine.context; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class UserContext { + private String id; + private String firstName; + private String lastName; + private String fullName; +} 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 new file mode 100644 index 000000000..f73f3ee81 --- /dev/null +++ b/src/main/java/org/folio/dew/domain/dto/templateengine/context/VendorDetailContext.java @@ -0,0 +1,10 @@ +package org.folio.dew.domain.dto.templateengine.context; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class VendorDetailContext { + private String instructions; +} 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/jobs/SendToEmailTaskletTest.java b/src/test/java/org/folio/dew/batch/acquisitions/jobs/SendToEmailTaskletTest.java index 2f5471038..10adf83a7 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; @@ -150,6 +151,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..e27a49e0e --- /dev/null +++ b/src/test/java/org/folio/dew/batch/acquisitions/mapper/OrderEmailContextMapperTest.java @@ -0,0 +1,325 @@ +package org.folio.dew.batch.acquisitions.mapper; + +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; +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.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; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +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 { + + 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"; + private static final String VENDOR_UUID = "50fb6ae0-cdf1-11e8-a8d5-f2801f1b9fd1"; + + @Mock + private IdentifierTypeService identifierTypeService; + @Mock + private ContributorNameTypeService contributorNameTypeService; + @Mock + private ConfigurationService configurationService; + @Mock + private UserService userService; + @Mock + private OrganizationsService organizationsService; + + private OrderEmailContextMapper mapper; + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + 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.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.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)); + + 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().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_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)); + + 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)); + + assertThat(ctx.getOrders().get(0).order().getShipTo().getAddress()).isEmpty(); + assertThat(ctx.getOrders().get(0).order().getBillTo().getAddress()).isEmpty(); + } + + @Test + void buildContext_mapsOrderLineFields() throws IOException { + var order = loadOrder("edifact/acquisitions/composite_purchase_order_email_context.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.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."); + assertThat(line.getRush()).isFalse(); + assertThat(line.getContributors()).hasSize(1); + var contributor = line.getContributors().get(0); + assertThat(contributor.getContributor()).isEqualTo("Moutinho, Luiz"); + 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().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().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"); + } + + @Test + 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)); + + assertThat(ctx.getOrders()).hasSize(2); + } + + @Test + void buildContext_emptyOrders_returnsEmptyList() { + OrderEmailContext ctx = mapper.buildContext(List.of()); + + assertThat(ctx.getOrders()).isEmpty(); + assertThat(ctx.getOrganization().getName()).isEmpty(); + 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(); + org.setName("Acme Books"); + org.setCode("ACME"); + 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_email_context.json"); + + OrderEmailContext ctx = mapper.buildContext(List.of(order)); + + var organization = ctx.getOrganization(); + assertThat(organization.getName()).isEqualTo("Acme Books"); + 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_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_email_context.json"); + + OrderEmailContext ctx = mapper.buildContext(List.of(order)); + + 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_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_email_context.json"); + + OrderEmailContext ctx = mapper.buildContext(List.of(order)); + + assertThat(ctx.getOrganization().getName()).isEqualTo("Acme Books"); + 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 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 + 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)); + + OrderLineContext line = ctx.getOrders().get(0).orderLines().get(0).orderLine(); + assertThat(line.getCost().getListUnitPrice()).isEmpty(); + assertThat(line.getCost().getCurrency()).isEmpty(); + } + + @Test + 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)); + + 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)); + + OrderLineContext line = ctx.getOrders().get(0).orderLines().get(0).orderLine(); + 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"); + } + + @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); + } + + private TenantAddress tenantAddress(UUID id, String address) { + var tenantAddress = new TenantAddress(); + tenantAddress.setId(id); + tenantAddress.setAddress(address); + return tenantAddress; + } +} 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..218e82b85 --- /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); + } +} 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..fa6b64dec --- /dev/null +++ b/src/test/java/org/folio/dew/batch/acquisitions/services/UserServiceTest.java @@ -0,0 +1,94 @@ +package org.folio.dew.batch.acquisitions.services; + +import static org.assertj.core.api.Assertions.assertThat; +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; + } +} 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..e78db4aaa --- /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": "(paperback)" + } + ], + "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 +}