Document legacy workflows and stabilize migrated services
This commit is contained in:
@@ -42,6 +42,16 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-net</groupId>
|
||||
<artifactId>commons-net</artifactId>
|
||||
<version>3.11.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.mwiede</groupId>
|
||||
<artifactId>jsch</artifactId>
|
||||
<version>0.2.20</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.librepdf</groupId>
|
||||
<artifactId>openpdf</artifactId>
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package de.votian.services.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "votian.scheduling")
|
||||
public class LegacySchedulingProperties {
|
||||
|
||||
private String zone = "Europe/Berlin";
|
||||
private boolean enabled = true;
|
||||
private String authSessionCleanupCron = "0 */5 * * * *";
|
||||
private String courierAutoLogoutCron = "0 */30 * * * *";
|
||||
private long courierAutoLogoutHeadquartersId = 3L;
|
||||
private String acceptanceProtocolMailCron = "0 */5 * * * *";
|
||||
private String acceptanceProtocolLetterCron = "30 */5 * * * *";
|
||||
private int acceptanceProtocolMailLookbackDays = 1;
|
||||
private int acceptanceProtocolLetterLookbackDays = 3;
|
||||
private String acceptanceProtocolLetterFtpProfile = "MPS1";
|
||||
|
||||
public String getZone() {
|
||||
return zone;
|
||||
}
|
||||
|
||||
public void setZone(String zone) {
|
||||
this.zone = zone;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getAuthSessionCleanupCron() {
|
||||
return authSessionCleanupCron;
|
||||
}
|
||||
|
||||
public void setAuthSessionCleanupCron(String authSessionCleanupCron) {
|
||||
this.authSessionCleanupCron = authSessionCleanupCron;
|
||||
}
|
||||
|
||||
public String getCourierAutoLogoutCron() {
|
||||
return courierAutoLogoutCron;
|
||||
}
|
||||
|
||||
public void setCourierAutoLogoutCron(String courierAutoLogoutCron) {
|
||||
this.courierAutoLogoutCron = courierAutoLogoutCron;
|
||||
}
|
||||
|
||||
public long getCourierAutoLogoutHeadquartersId() {
|
||||
return courierAutoLogoutHeadquartersId;
|
||||
}
|
||||
|
||||
public void setCourierAutoLogoutHeadquartersId(long courierAutoLogoutHeadquartersId) {
|
||||
this.courierAutoLogoutHeadquartersId = courierAutoLogoutHeadquartersId;
|
||||
}
|
||||
|
||||
public String getAcceptanceProtocolMailCron() {
|
||||
return acceptanceProtocolMailCron;
|
||||
}
|
||||
|
||||
public void setAcceptanceProtocolMailCron(String acceptanceProtocolMailCron) {
|
||||
this.acceptanceProtocolMailCron = acceptanceProtocolMailCron;
|
||||
}
|
||||
|
||||
public String getAcceptanceProtocolLetterCron() {
|
||||
return acceptanceProtocolLetterCron;
|
||||
}
|
||||
|
||||
public void setAcceptanceProtocolLetterCron(String acceptanceProtocolLetterCron) {
|
||||
this.acceptanceProtocolLetterCron = acceptanceProtocolLetterCron;
|
||||
}
|
||||
|
||||
public int getAcceptanceProtocolMailLookbackDays() {
|
||||
return acceptanceProtocolMailLookbackDays;
|
||||
}
|
||||
|
||||
public void setAcceptanceProtocolMailLookbackDays(int acceptanceProtocolMailLookbackDays) {
|
||||
this.acceptanceProtocolMailLookbackDays = acceptanceProtocolMailLookbackDays;
|
||||
}
|
||||
|
||||
public int getAcceptanceProtocolLetterLookbackDays() {
|
||||
return acceptanceProtocolLetterLookbackDays;
|
||||
}
|
||||
|
||||
public void setAcceptanceProtocolLetterLookbackDays(int acceptanceProtocolLetterLookbackDays) {
|
||||
this.acceptanceProtocolLetterLookbackDays = acceptanceProtocolLetterLookbackDays;
|
||||
}
|
||||
|
||||
public String getAcceptanceProtocolLetterFtpProfile() {
|
||||
return acceptanceProtocolLetterFtpProfile;
|
||||
}
|
||||
|
||||
public void setAcceptanceProtocolLetterFtpProfile(String acceptanceProtocolLetterFtpProfile) {
|
||||
this.acceptanceProtocolLetterFtpProfile = acceptanceProtocolLetterFtpProfile;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.votian.services.config;
|
||||
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
@EnableConfigurationProperties(LegacySchedulingProperties.class)
|
||||
public class SchedulingConfig {
|
||||
}
|
||||
@@ -242,6 +242,32 @@ public class JobController {
|
||||
return ResponseEntity.ok(jobService.findTodaysJobs());
|
||||
}
|
||||
|
||||
@GetMapping("/today/count")
|
||||
public ResponseEntity<Map<String, Long>> countTodaysJobs(Authentication authentication) {
|
||||
UserSessionDto user = sessionUser(authentication);
|
||||
if (isCourierUser(user)) {
|
||||
long count = jobService.findByCourierId(user.getCourierId()).stream()
|
||||
.filter(job -> matchesDateRange(job.getOrderTime(),
|
||||
java.time.LocalDate.now().atStartOfDay(),
|
||||
java.time.LocalDate.now().atTime(23, 59, 59)))
|
||||
.count();
|
||||
return ResponseEntity.ok(Map.of("count", count));
|
||||
}
|
||||
if (!accessControlService.canViewJobs(user)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
if (isCustomerUser(user)) {
|
||||
long count = jobService.findByPayerCostCenters(user.getAccessibleCostCenterIds()).stream()
|
||||
.filter(job -> matchesDateRange(job.getOrderTime(),
|
||||
java.time.LocalDate.now().atStartOfDay(),
|
||||
java.time.LocalDate.now().atTime(23, 59, 59)))
|
||||
.filter(job -> user.getHeadquartersId() == null || user.getHeadquartersId().equals(job.getHeadquartersId()))
|
||||
.count();
|
||||
return ResponseEntity.ok(Map.of("count", count));
|
||||
}
|
||||
return ResponseEntity.ok(Map.of("count", jobService.countTodaysJobs(user != null ? user.getHeadquartersId() : null)));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Job> create(@RequestBody JobCreateRequest request, Authentication authentication) {
|
||||
UserSessionDto user = sessionUser(authentication);
|
||||
|
||||
@@ -69,9 +69,11 @@ public class Company {
|
||||
private String logo;
|
||||
|
||||
@Column(name = "cmp_logo_width")
|
||||
@Convert(converter = LegacyBlankableIntegerStringConverter.class)
|
||||
private Integer logoWidth;
|
||||
|
||||
@Column(name = "cmp_logo_height")
|
||||
@Convert(converter = LegacyBlankableIntegerStringConverter.class)
|
||||
private Integer logoHeight;
|
||||
|
||||
@Column(name = "cmp_modify_status")
|
||||
|
||||
@@ -23,18 +23,12 @@ public class CostCenter {
|
||||
@Column(name = "csc_path")
|
||||
private String path;
|
||||
|
||||
@Column(name = "csc_id_payer")
|
||||
private Long payerId;
|
||||
|
||||
@Column(name = "csc_visible")
|
||||
private String visible;
|
||||
|
||||
@Column(name = "csc_is_extern")
|
||||
private String isExtern;
|
||||
|
||||
@Column(name = "emp_id_related")
|
||||
private Long relatedEmployeeId;
|
||||
|
||||
public CostCenter() {}
|
||||
|
||||
public Long getId() { return id; }
|
||||
@@ -47,12 +41,8 @@ public class CostCenter {
|
||||
public void setParentId(Long parentId) { this.parentId = parentId; }
|
||||
public String getPath() { return path; }
|
||||
public void setPath(String path) { this.path = path; }
|
||||
public Long getPayerId() { return payerId; }
|
||||
public void setPayerId(Long payerId) { this.payerId = payerId; }
|
||||
public String getVisible() { return visible; }
|
||||
public void setVisible(String visible) { this.visible = visible; }
|
||||
public String getIsExtern() { return isExtern; }
|
||||
public void setIsExtern(String isExtern) { this.isExtern = isExtern; }
|
||||
public Long getRelatedEmployeeId() { return relatedEmployeeId; }
|
||||
public void setRelatedEmployeeId(Long relatedEmployeeId) { this.relatedEmployeeId = relatedEmployeeId; }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.votian.services.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.Formula;
|
||||
|
||||
@Entity
|
||||
@Table(name = "employee")
|
||||
@@ -14,7 +15,7 @@ public class Employee {
|
||||
@Column(name = "usr_id")
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "hq_id")
|
||||
@Formula("(select u.hq_id from user u where u.usr_id = usr_id)")
|
||||
private Long headquartersId;
|
||||
|
||||
@Column(name = "csc_id")
|
||||
|
||||
@@ -10,7 +10,7 @@ import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "appointment", schema = "phoenix_group")
|
||||
@Table(name = "appointment", catalog = "phoenix_group")
|
||||
public class GroupwareAppointment {
|
||||
|
||||
@Id
|
||||
|
||||
@@ -149,6 +149,7 @@ public class Job {
|
||||
private Integer longhaul;
|
||||
|
||||
@Column(name = "jb_service")
|
||||
@Convert(converter = LegacyBlankableIntegerStringConverter.class)
|
||||
private Integer service;
|
||||
|
||||
@Column(name = "vht_id")
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package de.votian.services.entity;
|
||||
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
import jakarta.persistence.Converter;
|
||||
|
||||
@Converter
|
||||
public class LegacyBlankableIntegerStringConverter implements AttributeConverter<Integer, String> {
|
||||
|
||||
@Override
|
||||
public String convertToDatabaseColumn(Integer attribute) {
|
||||
return attribute != null ? attribute.toString() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer convertToEntityAttribute(String dbData) {
|
||||
if (dbData == null) {
|
||||
return null;
|
||||
}
|
||||
String normalized = dbData.trim();
|
||||
if (normalized.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return Integer.valueOf(normalized);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,15 @@ public interface CostCenterRepository extends JpaRepository<CostCenter, Long> {
|
||||
@Query("SELECT csc FROM CostCenter csc WHERE csc.customerId = :csId AND csc.visible = '1'")
|
||||
List<CostCenter> findVisibleByCustomerId(@Param("csId") Long csId);
|
||||
|
||||
@Query(value = "SELECT c.* " +
|
||||
"FROM costcenter c " +
|
||||
"JOIN customer cs ON cs.cs_id = c.cs_id " +
|
||||
"WHERE cs.hq_id = :hqId " +
|
||||
" AND c.csc_visible = 1 " +
|
||||
" AND COALESCE(c.csc_is_extern, 0) <> 1",
|
||||
nativeQuery = true)
|
||||
List<CostCenter> findVisibleNonExternByHeadquartersId(@Param("hqId") Long hqId);
|
||||
|
||||
@Query("SELECT csc FROM CostCenter csc WHERE csc.path LIKE %:pathFragment% AND csc.customerId = :csId")
|
||||
List<CostCenter> findByPathContainingAndCustomerId(@Param("pathFragment") String pathFragment, @Param("csId") Long csId);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,11 @@ public interface JobRepository extends JpaRepository<Job, Long> {
|
||||
@Query("SELECT j FROM Job j WHERE j.orderTime >= CURRENT_DATE")
|
||||
List<Job> findTodaysJobs();
|
||||
|
||||
@Query("SELECT COUNT(j) FROM Job j WHERE j.headquartersId = :hqId AND j.orderTime >= :from AND j.orderTime < :to")
|
||||
long countByOrderTimeBetweenAndHq(@Param("hqId") Long hqId,
|
||||
@Param("from") LocalDateTime from,
|
||||
@Param("to") LocalDateTime to);
|
||||
|
||||
@Query("SELECT j FROM Job j WHERE j.copyPermanentId IS NOT NULL AND j.orderTime > :since " +
|
||||
"AND (j.costCenterPayerId IN :cscIds OR j.costCenterPayerCashId IN :cscIds) ORDER BY j.id")
|
||||
List<Job> findPermanentCopies(@Param("since") LocalDateTime since, @Param("cscIds") List<Long> cscIds);
|
||||
|
||||
@@ -36,10 +36,11 @@ public interface UserRepository extends JpaRepository<User, Long> {
|
||||
List<User> findByTypeAndHeadquartersIdOrderByNameAscFirstnameAsc(@Param("type") Integer type,
|
||||
@Param("hqId") Long headquartersId);
|
||||
|
||||
@Query("SELECT DISTINCT hq.mnemonic, u.name, u.firstname, u.type " +
|
||||
"FROM User u JOIN Headquarters hq ON u.headquartersId = hq.id " +
|
||||
"WHERE FUNCTION('RIGHT', u.birthdate, 5) = FUNCTION('RIGHT', CURRENT_DATE, 5) " +
|
||||
"ORDER BY u.type, u.name")
|
||||
@Query(value = "SELECT DISTINCT hq.hq_mnemonic, u.usr_name, u.usr_firstname, u.usr_type " +
|
||||
"FROM user u JOIN headquarters hq ON u.hq_id = hq.hq_id " +
|
||||
"WHERE DATE_FORMAT(u.usr_birthdate, '%m-%d') = DATE_FORMAT(CURRENT_DATE, '%m-%d') " +
|
||||
"ORDER BY u.usr_type, u.usr_name",
|
||||
nativeQuery = true)
|
||||
List<Object[]> findBirthdaysToday();
|
||||
|
||||
@Modifying
|
||||
|
||||
@@ -57,6 +57,33 @@ public class AuthSessionService {
|
||||
return Optional.of(state.user());
|
||||
}
|
||||
|
||||
private record SessionState(UserSessionDto user, Instant expiresAt) { }
|
||||
private record PendingTotpState(UserSessionDto user, Instant expiresAt) { }
|
||||
public CleanupResult cleanupExpiredSessions() {
|
||||
Instant now = Instant.now();
|
||||
int expiredAuthenticated = removeExpiredEntries(authSessions, now);
|
||||
int expiredPendingTotp = removeExpiredEntries(pendingTotpSessions, now);
|
||||
return new CleanupResult(expiredAuthenticated, expiredPendingTotp);
|
||||
}
|
||||
|
||||
public record CleanupResult(int expiredAuthenticatedSessions, int expiredPendingTotpSessions) {
|
||||
public int totalExpiredSessions() {
|
||||
return expiredAuthenticatedSessions + expiredPendingTotpSessions;
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends ExpiringState> int removeExpiredEntries(Map<String, T> sessions, Instant now) {
|
||||
int removed = 0;
|
||||
for (Map.Entry<String, T> entry : sessions.entrySet()) {
|
||||
if (entry.getValue().expiresAt().isBefore(now) && sessions.remove(entry.getKey(), entry.getValue())) {
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
private sealed interface ExpiringState permits SessionState, PendingTotpState {
|
||||
Instant expiresAt();
|
||||
}
|
||||
|
||||
private record SessionState(UserSessionDto user, Instant expiresAt) implements ExpiringState { }
|
||||
private record PendingTotpState(UserSessionDto user, Instant expiresAt) implements ExpiringState { }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
package de.votian.services.service;
|
||||
|
||||
import de.votian.services.config.LegacySchedulingProperties;
|
||||
import de.votian.services.config.ParameterKeys;
|
||||
import de.votian.services.dto.JobAcceptanceProtocolDto;
|
||||
import de.votian.services.entity.CostCenter;
|
||||
import de.votian.services.entity.CostCenterAddress;
|
||||
import de.votian.services.entity.Customer;
|
||||
import de.votian.services.entity.GenericDataContainer;
|
||||
import de.votian.services.entity.Job;
|
||||
import de.votian.services.repository.CostCenterAddressRepository;
|
||||
import de.votian.services.repository.CostCenterRepository;
|
||||
import de.votian.services.repository.CustomerRepository;
|
||||
import de.votian.services.repository.GenericDataContainerRepository;
|
||||
import de.votian.services.repository.JobRepository;
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.mail.MailException;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@Service
|
||||
public class AcceptanceProtocolAutomationService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AcceptanceProtocolAutomationService.class);
|
||||
private static final String JOB_OBJECT_TYPE = "jb";
|
||||
private static final String MAIL_FIELD = "mail_srv_acc_prot";
|
||||
private static final String LETTER_FIELD = "letter_srv_acc_prot";
|
||||
private static final String IN_PROGRESS = "SEND_IN_PROGRESS";
|
||||
private static final String MAIL_OK = "MAIL_SENT=OK";
|
||||
private static final String MAIL_NOT_OK = "MAIL_SENT=NOT_OK";
|
||||
private static final String LETTER_OK = "LETTER_SENT=OK";
|
||||
private static final String LETTER_NOT_OK = "LETTER_SENT=NOT_OK";
|
||||
private static final String PDF_BASE_NAME = "ABNAHMEPROTOKOLL";
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final JobRepository jobRepository;
|
||||
private final CostCenterRepository costCenterRepository;
|
||||
private final CustomerRepository customerRepository;
|
||||
private final CostCenterAddressRepository costCenterAddressRepository;
|
||||
private final GenericDataContainerRepository genericDataContainerRepository;
|
||||
private final JobAcceptanceProtocolService jobAcceptanceProtocolService;
|
||||
private final ParameterService parameterService;
|
||||
private final LegacyFileTransferService legacyFileTransferService;
|
||||
private final ObjectProvider<JavaMailSender> mailSenderProvider;
|
||||
private final LegacySchedulingProperties schedulingProperties;
|
||||
private final AtomicBoolean mailRunning = new AtomicBoolean();
|
||||
private final AtomicBoolean letterRunning = new AtomicBoolean();
|
||||
|
||||
public AcceptanceProtocolAutomationService(JdbcTemplate jdbcTemplate,
|
||||
JobRepository jobRepository,
|
||||
CostCenterRepository costCenterRepository,
|
||||
CustomerRepository customerRepository,
|
||||
CostCenterAddressRepository costCenterAddressRepository,
|
||||
GenericDataContainerRepository genericDataContainerRepository,
|
||||
JobAcceptanceProtocolService jobAcceptanceProtocolService,
|
||||
ParameterService parameterService,
|
||||
LegacyFileTransferService legacyFileTransferService,
|
||||
ObjectProvider<JavaMailSender> mailSenderProvider,
|
||||
LegacySchedulingProperties schedulingProperties) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.jobRepository = jobRepository;
|
||||
this.costCenterRepository = costCenterRepository;
|
||||
this.customerRepository = customerRepository;
|
||||
this.costCenterAddressRepository = costCenterAddressRepository;
|
||||
this.genericDataContainerRepository = genericDataContainerRepository;
|
||||
this.jobAcceptanceProtocolService = jobAcceptanceProtocolService;
|
||||
this.parameterService = parameterService;
|
||||
this.legacyFileTransferService = legacyFileTransferService;
|
||||
this.mailSenderProvider = mailSenderProvider;
|
||||
this.schedulingProperties = schedulingProperties;
|
||||
}
|
||||
|
||||
public int dispatchPendingMailProtocols() {
|
||||
if (!isCronEnabled() || !mailRunning.compareAndSet(false, true)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
int processed = 0;
|
||||
for (Long jobId : loadMailCandidates()) {
|
||||
if (dispatchMail(jobId)) {
|
||||
processed++;
|
||||
}
|
||||
}
|
||||
if (processed > 0) {
|
||||
log.info("Acceptance protocol mail automation processed {} job(s).", processed);
|
||||
}
|
||||
return processed;
|
||||
} finally {
|
||||
mailRunning.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
public int dispatchPendingLetterProtocols() {
|
||||
if (!isCronEnabled() || !letterRunning.compareAndSet(false, true)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
int processed = 0;
|
||||
for (Long jobId : loadLetterCandidates()) {
|
||||
if (dispatchLetter(jobId)) {
|
||||
processed++;
|
||||
}
|
||||
}
|
||||
if (processed > 0) {
|
||||
log.info("Acceptance protocol letter automation processed {} job(s).", processed);
|
||||
}
|
||||
return processed;
|
||||
} finally {
|
||||
letterRunning.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean dispatchMail(Long jobId) {
|
||||
DispatchContext context = resolveContext(jobId);
|
||||
if (context == null || !context.protocol().isDataAvailable() || !context.protocol().isSignatureAvailable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MailRecipients recipients = resolveRecipients(context);
|
||||
if (recipients.to().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (!markInProgress(jobId, MAIL_FIELD)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] pdf = jobAcceptanceProtocolService.createPdf(jobId);
|
||||
sendMail(context, recipients, pdf);
|
||||
updateDispatchState(jobId, MAIL_FIELD, MAIL_OK);
|
||||
return true;
|
||||
} catch (RuntimeException exception) {
|
||||
log.warn("Acceptance protocol mail dispatch failed for job {}: {}", jobId, exception.getMessage());
|
||||
updateDispatchState(jobId, MAIL_FIELD, MAIL_NOT_OK);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean dispatchLetter(Long jobId) {
|
||||
DispatchContext context = resolveContext(jobId);
|
||||
if (context == null || !context.protocol().isDataAvailable() || !context.protocol().isSignatureAvailable()) {
|
||||
return false;
|
||||
}
|
||||
if (!markInProgress(jobId, LETTER_FIELD)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] pdf = jobAcceptanceProtocolService.createPdf(jobId);
|
||||
String fileName = PDF_BASE_NAME + "_" + jobId + ".pdf";
|
||||
legacyFileTransferService.upload(
|
||||
schedulingProperties.getAcceptanceProtocolLetterFtpProfile(),
|
||||
fileName,
|
||||
pdf
|
||||
);
|
||||
updateDispatchState(jobId, LETTER_FIELD, LETTER_OK);
|
||||
return true;
|
||||
} catch (RuntimeException exception) {
|
||||
log.warn("Acceptance protocol letter dispatch failed for job {}: {}", jobId, exception.getMessage());
|
||||
updateDispatchState(jobId, LETTER_FIELD, LETTER_NOT_OK);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private DispatchContext resolveContext(Long jobId) {
|
||||
Job job = jobRepository.findById(Objects.requireNonNull(jobId)).orElse(null);
|
||||
if (job == null || job.getHeadquartersId() == null || job.getCostCenterRelatedId() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
CostCenter relatedCostCenter = costCenterRepository.findById(job.getCostCenterRelatedId()).orElse(null);
|
||||
if (relatedCostCenter == null || relatedCostCenter.getCustomerId() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Customer customer = customerRepository.findById(relatedCostCenter.getCustomerId()).orElse(null);
|
||||
if (customer == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
JobAcceptanceProtocolDto protocol = jobAcceptanceProtocolService.getProtocol(jobId);
|
||||
Long groupId = extractFirstNumericGroup(customer.getGroup());
|
||||
return new DispatchContext(job, customer, protocol, groupId);
|
||||
}
|
||||
|
||||
private MailRecipients resolveRecipients(DispatchContext context) {
|
||||
long headquartersId = context.job().getHeadquartersId();
|
||||
Long customerId = context.customer().getId();
|
||||
Long groupId = context.groupId();
|
||||
|
||||
String mailTo = firstNonBlank(
|
||||
parameterService.getObjectBasedParameterValue(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_TO + "_CS", customerId, headquartersId, 0L, true),
|
||||
groupId != null
|
||||
? parameterService.getObjectBasedParameterValue(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_TO + "_GRP", groupId, headquartersId, 0L, true)
|
||||
: "",
|
||||
parameterService.getParameterValueWithFallback(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_TO, headquartersId)
|
||||
);
|
||||
String mailCc = firstNonBlank(
|
||||
parameterService.getObjectBasedParameterValue(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_CC + "_CS", customerId, headquartersId, 0L, true),
|
||||
groupId != null
|
||||
? parameterService.getObjectBasedParameterValue(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_CC + "_GRP", groupId, headquartersId, 0L, true)
|
||||
: "",
|
||||
parameterService.getParameterValueWithFallback(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_CC, headquartersId)
|
||||
);
|
||||
String mailBcc = firstNonBlank(
|
||||
parameterService.getObjectBasedParameterValue(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_BCC + "_CS", customerId, headquartersId, 0L, true),
|
||||
groupId != null
|
||||
? parameterService.getObjectBasedParameterValue(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_BCC + "_GRP", groupId, headquartersId, 0L, true)
|
||||
: "",
|
||||
parameterService.getParameterValueWithFallback(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_BCC, headquartersId)
|
||||
);
|
||||
|
||||
LinkedHashSet<String> toRecipients = splitRecipients(mailTo);
|
||||
resolvePayerMailAddress(context.job().getCostCenterPayerId()).ifPresent(toRecipients::add);
|
||||
return new MailRecipients(
|
||||
List.copyOf(toRecipients),
|
||||
List.copyOf(splitRecipients(mailCc)),
|
||||
List.copyOf(splitRecipients(mailBcc))
|
||||
);
|
||||
}
|
||||
|
||||
private Optional<String> resolvePayerMailAddress(Long payerCostCenterId) {
|
||||
if (payerCostCenterId == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
for (int addressTypeId : List.of(4, 2, 3, 1)) {
|
||||
Optional<String> address = costCenterAddressRepository.findByCostCenterIdAndAddressTypeId(payerCostCenterId, addressTypeId)
|
||||
.map(CostCenterAddress::getEmail)
|
||||
.map(this::normalizeRecipient)
|
||||
.filter(value -> !value.isBlank());
|
||||
if (address.isPresent()) {
|
||||
return address;
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private void sendMail(DispatchContext context, MailRecipients recipients, byte[] pdf) {
|
||||
JavaMailSender mailSender = mailSenderProvider.getIfAvailable();
|
||||
if (mailSender == null) {
|
||||
throw new IllegalStateException("Kein JavaMailSender konfiguriert.");
|
||||
}
|
||||
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
try {
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, StandardCharsets.UTF_8.name());
|
||||
helper.setTo(recipients.to().toArray(String[]::new));
|
||||
if (!recipients.cc().isEmpty()) {
|
||||
helper.setCc(recipients.cc().toArray(String[]::new));
|
||||
}
|
||||
if (!recipients.bcc().isEmpty()) {
|
||||
helper.setBcc(recipients.bcc().toArray(String[]::new));
|
||||
}
|
||||
helper.setSubject(PDF_BASE_NAME + " (" + context.job().getId() + ")");
|
||||
helper.setText(buildMailBody(context), false);
|
||||
|
||||
String senderAddress = parameterService.getParameterValueWithFallback(ParameterKeys.MAIL_SENDER_ADDRESS, context.job().getHeadquartersId());
|
||||
if (!senderAddress.isBlank()) {
|
||||
helper.setFrom(senderAddress);
|
||||
helper.setReplyTo(senderAddress);
|
||||
}
|
||||
|
||||
helper.addAttachment(PDF_BASE_NAME + "_" + context.job().getId() + ".pdf", new ByteArrayResource(pdf));
|
||||
} catch (MessagingException exception) {
|
||||
throw new IllegalStateException("Mail konnte nicht vorbereitet werden.", exception);
|
||||
}
|
||||
|
||||
try {
|
||||
mailSender.send(message);
|
||||
} catch (MailException exception) {
|
||||
throw new IllegalStateException("Mailversand fehlgeschlagen.", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private String buildMailBody(DispatchContext context) {
|
||||
StringBuilder body = new StringBuilder();
|
||||
body.append("Sehr geehrte Damen und Herren,").append("\n\n");
|
||||
body.append("im Anhang erhalten Sie das Abnahmeprotokoll zum Auftrag ")
|
||||
.append(context.job().getId())
|
||||
.append('.')
|
||||
.append("\n\n");
|
||||
if (context.protocol().getProtocolServiceLabel() != null && !context.protocol().getProtocolServiceLabel().isBlank()) {
|
||||
body.append("Protokoll-Service: ")
|
||||
.append(context.protocol().getProtocolServiceLabel())
|
||||
.append("\n");
|
||||
}
|
||||
body.append("\nMit freundlichen Gruessen");
|
||||
return body.toString();
|
||||
}
|
||||
|
||||
private boolean markInProgress(Long jobId, String fieldName) {
|
||||
Optional<GenericDataContainer> existing = genericDataContainerRepository.findByTypeAndIdAndField(JOB_OBJECT_TYPE, jobId, fieldName);
|
||||
if (existing.isPresent() && contains(existing.get().getContext(), IN_PROGRESS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
GenericDataContainer container = existing.orElseGet(GenericDataContainer::new);
|
||||
container.setObjectType(JOB_OBJECT_TYPE);
|
||||
container.setObjectId(jobId);
|
||||
container.setFieldName(fieldName);
|
||||
container.setContent(existing.map(GenericDataContainer::getContent).orElse(""));
|
||||
container.setContext(IN_PROGRESS);
|
||||
genericDataContainerRepository.save(container);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateDispatchState(Long jobId, String fieldName, String context) {
|
||||
GenericDataContainer container = genericDataContainerRepository.findByTypeAndIdAndField(JOB_OBJECT_TYPE, jobId, fieldName)
|
||||
.orElseGet(GenericDataContainer::new);
|
||||
container.setObjectType(JOB_OBJECT_TYPE);
|
||||
container.setObjectId(jobId);
|
||||
container.setFieldName(fieldName);
|
||||
container.setContent(container.getContent() != null ? container.getContent() : "");
|
||||
container.setContext(context);
|
||||
genericDataContainerRepository.save(container);
|
||||
}
|
||||
|
||||
private List<Long> loadMailCandidates() {
|
||||
return jdbcTemplate.queryForList(
|
||||
"""
|
||||
SELECT DISTINCT jb.jb_id
|
||||
FROM job AS jb
|
||||
JOIN costcenteraddress AS cscad ON cscad.csc_id = jb.csc_id_payer AND cscad.adt_id = 2
|
||||
LEFT JOIN genericdatacontainer AS gdc
|
||||
ON gdc.gdc_obj_type = 'jb'
|
||||
AND gdc.gdc_obj_id = jb.jb_id
|
||||
AND gdc.gdc_gen_fieldname = ?
|
||||
WHERE jb.jb_finishtime > ?
|
||||
AND (jb.jb_storno IS NULL OR jb.jb_storno = '0')
|
||||
AND jb.jb_status = 2
|
||||
AND COALESCE(jb.jb_offer, 0) = 0
|
||||
AND (jb.jb_service & 2) = 2
|
||||
AND COALESCE(cscad.cscad_email, '') <> ''
|
||||
AND (gdc.gdc_id IS NULL
|
||||
OR (COALESCE(gdc.gdc_context, '') NOT LIKE '%MAIL_SENT=OK%'
|
||||
AND COALESCE(gdc.gdc_context, '') NOT LIKE '%SEND_IN_PROGRESS%'))
|
||||
ORDER BY jb.jb_id
|
||||
""",
|
||||
Long.class,
|
||||
MAIL_FIELD,
|
||||
Timestamp.valueOf(LocalDateTime.now().minusDays(Math.max(1, schedulingProperties.getAcceptanceProtocolMailLookbackDays())))
|
||||
);
|
||||
}
|
||||
|
||||
private List<Long> loadLetterCandidates() {
|
||||
return jdbcTemplate.queryForList(
|
||||
"""
|
||||
SELECT DISTINCT jb.jb_id
|
||||
FROM job AS jb
|
||||
JOIN costcenteraddress AS cscad ON cscad.csc_id = jb.csc_id_payer AND cscad.adt_id = 2
|
||||
LEFT JOIN genericdatacontainer AS gdc
|
||||
ON gdc.gdc_obj_type = 'jb'
|
||||
AND gdc.gdc_obj_id = jb.jb_id
|
||||
AND gdc.gdc_gen_fieldname = ?
|
||||
WHERE jb.jb_finishtime > ?
|
||||
AND (jb.jb_storno IS NULL OR jb.jb_storno = '0')
|
||||
AND jb.jb_status = 2
|
||||
AND COALESCE(jb.jb_offer, 0) = 0
|
||||
AND (jb.jb_service & 2) = 2
|
||||
AND COALESCE(cscad.cscad_email, '') = ''
|
||||
AND (gdc.gdc_id IS NULL
|
||||
OR (COALESCE(gdc.gdc_context, '') NOT LIKE '%LETTER_SENT=OK%'
|
||||
AND COALESCE(gdc.gdc_context, '') NOT LIKE '%SEND_IN_PROGRESS%'))
|
||||
ORDER BY jb.jb_id
|
||||
""",
|
||||
Long.class,
|
||||
LETTER_FIELD,
|
||||
Timestamp.valueOf(LocalDateTime.now().minusDays(Math.max(1, schedulingProperties.getAcceptanceProtocolLetterLookbackDays())))
|
||||
);
|
||||
}
|
||||
|
||||
private boolean isCronEnabled() {
|
||||
return "1".equals(parameterService.getParameterValueWithFallback(ParameterKeys.GLOBAL_CRON_ENABLED, 0L))
|
||||
&& "1".equals(parameterService.getParameterValueWithFallback(
|
||||
ParameterKeys.CRON_SERVICE_ACCEPTANCE_PROTOCOL_ENABLED,
|
||||
0L
|
||||
));
|
||||
}
|
||||
|
||||
private Long extractFirstNumericGroup(String rawValue) {
|
||||
if (rawValue == null || rawValue.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
for (String token : rawValue.split(",")) {
|
||||
String normalized = token != null ? token.trim() : "";
|
||||
if (normalized.matches("\\d+")) {
|
||||
return Long.valueOf(normalized);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String firstNonBlank(String... values) {
|
||||
for (String value : values) {
|
||||
if (value != null && !value.isBlank()) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private LinkedHashSet<String> splitRecipients(String rawRecipients) {
|
||||
LinkedHashSet<String> recipients = new LinkedHashSet<>();
|
||||
if (rawRecipients == null || rawRecipients.isBlank()) {
|
||||
return recipients;
|
||||
}
|
||||
for (String token : rawRecipients.split("[,;]")) {
|
||||
String normalized = normalizeRecipient(token);
|
||||
if (!normalized.isBlank()) {
|
||||
recipients.add(normalized);
|
||||
}
|
||||
}
|
||||
return recipients;
|
||||
}
|
||||
|
||||
private String normalizeRecipient(String value) {
|
||||
String normalized = value != null ? value.trim() : "";
|
||||
if (normalized.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
return normalized.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private boolean contains(String value, String token) {
|
||||
return value != null && value.contains(token);
|
||||
}
|
||||
|
||||
private record DispatchContext(Job job, Customer customer, JobAcceptanceProtocolDto protocol, Long groupId) {
|
||||
}
|
||||
|
||||
private record MailRecipients(List<String> to, List<String> cc, List<String> bcc) {
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
@@ -90,6 +91,15 @@ public class JobService {
|
||||
return jobRepository.findTodaysJobs();
|
||||
}
|
||||
|
||||
public long countTodaysJobs(Long headquartersId) {
|
||||
if (headquartersId == null) {
|
||||
return 0L;
|
||||
}
|
||||
LocalDateTime from = LocalDate.now().atStartOfDay();
|
||||
LocalDateTime to = from.plusDays(1);
|
||||
return jobRepository.countByOrderTimeBetweenAndHq(headquartersId, from, to);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Job createJob(Job job, List<Tour> tours, List<TourArticle> articles) {
|
||||
return createJob(job, tours, articles, List.of());
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package de.votian.services.service;
|
||||
|
||||
import de.votian.services.config.LegacySchedulingProperties;
|
||||
import de.votian.services.config.ParameterKeys;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@Service
|
||||
public class LegacyCourierAutoLogoutService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LegacyCourierAutoLogoutService.class);
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final ParameterService parameterService;
|
||||
private final LegacySchedulingProperties schedulingProperties;
|
||||
private final AtomicBoolean running = new AtomicBoolean();
|
||||
|
||||
public LegacyCourierAutoLogoutService(JdbcTemplate jdbcTemplate,
|
||||
ParameterService parameterService,
|
||||
LegacySchedulingProperties schedulingProperties) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.parameterService = parameterService;
|
||||
this.schedulingProperties = schedulingProperties;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public int logoutCouriersByAutorevokes() {
|
||||
if (!running.compareAndSet(false, true)) {
|
||||
log.info("Legacy courier auto logout is still running, skipping overlapping execution.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
List<CourierLogState> candidates = loadCandidates(schedulingProperties.getCourierAutoLogoutHeadquartersId());
|
||||
int loggedOut = 0;
|
||||
for (CourierLogState candidate : candidates) {
|
||||
if (candidate.acceptedCount() > 0 || candidate.cancelledCount() > 0 || candidate.revokedCount() < 2) {
|
||||
continue;
|
||||
}
|
||||
if (isOccupied(candidate.courierId())) {
|
||||
continue;
|
||||
}
|
||||
logoutCourier(candidate);
|
||||
loggedOut++;
|
||||
}
|
||||
if (loggedOut > 0) {
|
||||
log.info("Legacy courier auto logout logged out {} courier(s).", loggedOut);
|
||||
}
|
||||
return loggedOut;
|
||||
} finally {
|
||||
running.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
private List<CourierLogState> loadCandidates(long headquartersId) {
|
||||
return jdbcTemplate.query(
|
||||
"""
|
||||
SELECT cr.cr_id,
|
||||
cr.cr_eid,
|
||||
cr.hq_id,
|
||||
SUM(CASE WHEN log.logo_id = 3 THEN 1 ELSE 0 END) AS accepted_count,
|
||||
SUM(CASE WHEN log.logo_id = 8 THEN 1 ELSE 0 END) AS revoked_count,
|
||||
SUM(CASE WHEN log.logo_id = 11 THEN 1 ELSE 0 END) AS cancelled_count
|
||||
FROM courier AS cr
|
||||
JOIN phoenix_log.log AS log ON log.cr_id = cr.cr_id
|
||||
WHERE cr.cr_available = '1'
|
||||
AND cr.hq_id = ?
|
||||
AND cr.cr_locationzipcode REGEXP '^[0-9]+$'
|
||||
AND log.log_createtime >= DATE_SUB(NOW(), INTERVAL 30 MINUTE)
|
||||
AND log.logo_id IN (3, 8, 11)
|
||||
GROUP BY cr.cr_id, cr.cr_eid, cr.hq_id
|
||||
ORDER BY cr.cr_id
|
||||
""",
|
||||
(rs, rowNum) -> new CourierLogState(
|
||||
rs.getLong("cr_id"),
|
||||
rs.getString("cr_eid"),
|
||||
rs.getLong("hq_id"),
|
||||
rs.getInt("accepted_count"),
|
||||
rs.getInt("revoked_count"),
|
||||
rs.getInt("cancelled_count")
|
||||
),
|
||||
headquartersId
|
||||
);
|
||||
}
|
||||
|
||||
private boolean isOccupied(long courierId) {
|
||||
Integer matches = jdbcTemplate.queryForObject(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM courier AS cr
|
||||
JOIN job AS jb ON jb.cr_id_order = cr.cr_id
|
||||
JOIN tour AS tr ON tr.jb_id = jb.jb_id AND tr.tr_sort = 1
|
||||
JOIN address AS ad ON tr.ad_id = ad.ad_id
|
||||
LEFT JOIN serviceplz AS srvp ON ad.ad_zipcode = srvp.srvp_plz
|
||||
LEFT JOIN serviceplztraveltime AS srvpt ON srvp.srvp_id = srvpt.srvp_id
|
||||
WHERE cr.cr_id = ?
|
||||
AND jb.jb_status IN (0, 1)
|
||||
AND DATE_SUB(jb.jb_ordertime, INTERVAL GREATEST(COALESCE(srvpt.srvpt_traveltime, 0), 30) MINUTE) <= NOW()
|
||||
AND (srvpt.hq_id = jb.hq_id OR srvpt.hq_id IS NULL)
|
||||
""",
|
||||
Integer.class,
|
||||
courierId
|
||||
);
|
||||
return matches != null && matches > 0;
|
||||
}
|
||||
|
||||
private void logoutCourier(CourierLogState candidate) {
|
||||
LocalDateTime now = LocalDateTime.now().withNano(0);
|
||||
LocalDateTime executeUntil = now.plusMinutes(16);
|
||||
|
||||
jdbcTemplate.update(
|
||||
"UPDATE courier SET cr_availabletime = ?, cr_locationzipcode = ? WHERE cr_id = ?",
|
||||
Timestamp.valueOf(now),
|
||||
"LOGOUT",
|
||||
candidate.courierId()
|
||||
);
|
||||
jdbcTemplate.update(
|
||||
"""
|
||||
INSERT INTO phoenix_pda.commandexec
|
||||
(cr_id, hq_id, cmd_command, cmde_exec_time, cmde_exec_timelimit, cmde_mode, cmde_param)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
candidate.courierId(),
|
||||
candidate.headquartersId(),
|
||||
"2",
|
||||
Timestamp.valueOf(now),
|
||||
Timestamp.valueOf(executeUntil),
|
||||
"1",
|
||||
""
|
||||
);
|
||||
|
||||
if ("1".equals(parameterService.getParameterValueWithFallback(ParameterKeys.LOG_DB, candidate.headquartersId()))) {
|
||||
jdbcTemplate.update(
|
||||
"""
|
||||
INSERT INTO phoenix_log.log
|
||||
(logo_id, hq_id, jb_id, usr_id, cr_id, cr_sid, cs_id, at_id, pt_id, emp_id, logo_description)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
143,
|
||||
candidate.headquartersId(),
|
||||
0,
|
||||
0,
|
||||
candidate.courierId(),
|
||||
"",
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
"STATUS_LOGOUT=PDA-LOGOUT DURCH AUTOREVOKES"
|
||||
);
|
||||
}
|
||||
|
||||
log.info("Legacy courier auto logout logged out courier {} ({})", candidate.courierId(), candidate.courierEid());
|
||||
}
|
||||
|
||||
private record CourierLogState(long courierId,
|
||||
String courierEid,
|
||||
long headquartersId,
|
||||
int acceptedCount,
|
||||
int revokedCount,
|
||||
int cancelledCount) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package de.votian.services.service;
|
||||
|
||||
import com.jcraft.jsch.ChannelSftp;
|
||||
import com.jcraft.jsch.JSch;
|
||||
import com.jcraft.jsch.JSchException;
|
||||
import com.jcraft.jsch.Session;
|
||||
import com.jcraft.jsch.SftpException;
|
||||
import de.votian.services.config.ParameterKeys;
|
||||
import org.apache.commons.net.ftp.FTP;
|
||||
import org.apache.commons.net.ftp.FTPClient;
|
||||
import org.apache.commons.net.ftp.FTPSClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
@Service
|
||||
public class LegacyFileTransferService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LegacyFileTransferService.class);
|
||||
|
||||
private final ParameterService parameterService;
|
||||
|
||||
public LegacyFileTransferService(ParameterService parameterService) {
|
||||
this.parameterService = parameterService;
|
||||
}
|
||||
|
||||
public void upload(String profile, String fileName, byte[] content) {
|
||||
TransferProfile transferProfile = loadProfile(profile);
|
||||
switch (transferProfile.sslMode()) {
|
||||
case "2" -> uploadViaSftp(transferProfile, fileName, content);
|
||||
case "1" -> uploadViaFtps(transferProfile, fileName, content);
|
||||
default -> uploadViaFtp(transferProfile, fileName, content);
|
||||
}
|
||||
}
|
||||
|
||||
private TransferProfile loadProfile(String profile) {
|
||||
String normalizedProfile = profile != null ? profile.trim().toUpperCase() : "";
|
||||
if (normalizedProfile.isBlank()) {
|
||||
throw new IllegalArgumentException("FTP-Profil fehlt.");
|
||||
}
|
||||
|
||||
String server = loadProfileValue(ParameterKeys.FTP_SERVER_PREFIX, normalizedProfile);
|
||||
String user = loadProfileValue(ParameterKeys.FTP_USER_PREFIX, normalizedProfile);
|
||||
String password = loadProfileValue(ParameterKeys.FTP_PASSWORD_PREFIX, normalizedProfile);
|
||||
String sslMode = blankToDefault(loadProfileValue(ParameterKeys.FTP_SSL, normalizedProfile), "0");
|
||||
String remotePath = blankToDefault(loadProfileValue(ParameterKeys.FTP_REMOTEPATH, normalizedProfile), "/");
|
||||
|
||||
if (server.isBlank() || user.isBlank() || password.isBlank()) {
|
||||
throw new IllegalStateException("FTP-Profil " + normalizedProfile + " ist unvollstaendig konfiguriert.");
|
||||
}
|
||||
|
||||
Endpoint endpoint = parseEndpoint(server, "2".equals(sslMode) ? 22 : 21);
|
||||
return new TransferProfile(normalizedProfile, endpoint.host(), endpoint.port(), user, password, sslMode, normalizeDirectory(remotePath));
|
||||
}
|
||||
|
||||
private String loadProfileValue(String keyPrefix, String profile) {
|
||||
return blankToEmpty(parameterService.getParameterValueWithFallback(keyPrefix + profile, 0L));
|
||||
}
|
||||
|
||||
private void uploadViaFtp(TransferProfile profile, String fileName, byte[] content) {
|
||||
FTPClient client = new FTPClient();
|
||||
try {
|
||||
connectAndLogin(client, profile);
|
||||
storeFile(client, profile.remotePath(), fileName, content);
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("FTP-Upload fuer Profil " + profile.profileName() + " fehlgeschlagen.", exception);
|
||||
} finally {
|
||||
disconnectQuietly(client);
|
||||
}
|
||||
}
|
||||
|
||||
private void uploadViaFtps(TransferProfile profile, String fileName, byte[] content) {
|
||||
FTPSClient client = new FTPSClient(false);
|
||||
try {
|
||||
connectAndLogin(client, profile);
|
||||
client.execPBSZ(0);
|
||||
client.execPROT("P");
|
||||
storeFile(client, profile.remotePath(), fileName, content);
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("FTPS-Upload fuer Profil " + profile.profileName() + " fehlgeschlagen.", exception);
|
||||
} finally {
|
||||
disconnectQuietly(client);
|
||||
}
|
||||
}
|
||||
|
||||
private void connectAndLogin(FTPClient client, TransferProfile profile) throws IOException {
|
||||
client.connect(profile.host(), profile.port());
|
||||
if (!client.login(profile.user(), profile.password())) {
|
||||
throw new IOException("FTP-Login fehlgeschlagen.");
|
||||
}
|
||||
client.enterLocalPassiveMode();
|
||||
client.setFileType(FTP.BINARY_FILE_TYPE);
|
||||
}
|
||||
|
||||
private void storeFile(FTPClient client, String remoteDirectory, String fileName, byte[] content) throws IOException {
|
||||
ensureFtpDirectory(client, remoteDirectory);
|
||||
try (InputStream inputStream = new ByteArrayInputStream(content)) {
|
||||
if (!client.storeFile(fileName, inputStream)) {
|
||||
throw new IOException("Datei konnte nicht gespeichert werden. Reply-Code: " + client.getReplyCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureFtpDirectory(FTPClient client, String remoteDirectory) throws IOException {
|
||||
String[] segments = remoteDirectory.split("/");
|
||||
if (remoteDirectory.startsWith("/")) {
|
||||
client.changeWorkingDirectory("/");
|
||||
}
|
||||
for (String segment : segments) {
|
||||
if (segment == null || segment.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (!client.changeWorkingDirectory(segment)) {
|
||||
if (!client.makeDirectory(segment) || !client.changeWorkingDirectory(segment)) {
|
||||
throw new IOException("FTP-Verzeichnis konnte nicht angelegt werden: " + remoteDirectory);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void disconnectQuietly(FTPClient client) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (client.isConnected()) {
|
||||
client.logout();
|
||||
client.disconnect();
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
log.debug("FTP disconnect failed: {}", exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void uploadViaSftp(TransferProfile profile, String fileName, byte[] content) {
|
||||
Session session = null;
|
||||
ChannelSftp channel = null;
|
||||
try {
|
||||
JSch jsch = new JSch();
|
||||
session = jsch.getSession(profile.user(), profile.host(), profile.port());
|
||||
session.setPassword(profile.password());
|
||||
Properties properties = new Properties();
|
||||
properties.put("StrictHostKeyChecking", "no");
|
||||
session.setConfig(properties);
|
||||
session.connect();
|
||||
|
||||
channel = (ChannelSftp) session.openChannel("sftp");
|
||||
channel.connect();
|
||||
ensureSftpDirectory(channel, profile.remotePath());
|
||||
channel.put(new ByteArrayInputStream(content), fileName);
|
||||
} catch (JSchException | SftpException exception) {
|
||||
throw new IllegalStateException("SFTP-Upload fuer Profil " + profile.profileName() + " fehlgeschlagen.", exception);
|
||||
} finally {
|
||||
if (channel != null) {
|
||||
channel.disconnect();
|
||||
}
|
||||
if (session != null) {
|
||||
session.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureSftpDirectory(ChannelSftp channel, String remoteDirectory) throws SftpException {
|
||||
channel.cd("/");
|
||||
for (String segment : remoteDirectory.split("/")) {
|
||||
if (segment == null || segment.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
channel.cd(segment);
|
||||
} catch (SftpException exception) {
|
||||
channel.mkdir(segment);
|
||||
channel.cd(segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeDirectory(String value) {
|
||||
String normalized = value != null ? value.trim() : "";
|
||||
if (normalized.isBlank()) {
|
||||
return "/";
|
||||
}
|
||||
normalized = normalized.replace('\\', '/');
|
||||
if (!normalized.startsWith("/")) {
|
||||
normalized = "/" + normalized;
|
||||
}
|
||||
while (normalized.endsWith("/") && normalized.length() > 1) {
|
||||
normalized = normalized.substring(0, normalized.length() - 1);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private Endpoint parseEndpoint(String value, int defaultPort) {
|
||||
String normalized = blankToEmpty(value);
|
||||
int separator = normalized.lastIndexOf(':');
|
||||
if (separator > 0 && separator < normalized.length() - 1 && normalized.indexOf(':') == separator) {
|
||||
String host = normalized.substring(0, separator).trim();
|
||||
String portValue = normalized.substring(separator + 1).trim();
|
||||
if (portValue.matches("\\d+")) {
|
||||
return new Endpoint(host, Integer.parseInt(portValue));
|
||||
}
|
||||
}
|
||||
return new Endpoint(normalized, defaultPort);
|
||||
}
|
||||
|
||||
private String blankToEmpty(String value) {
|
||||
return value != null ? value.trim() : "";
|
||||
}
|
||||
|
||||
private String blankToDefault(String value, String fallback) {
|
||||
String normalized = blankToEmpty(value);
|
||||
return normalized.isBlank() ? fallback : normalized;
|
||||
}
|
||||
|
||||
private record TransferProfile(String profileName,
|
||||
String host,
|
||||
int port,
|
||||
String user,
|
||||
String password,
|
||||
String sslMode,
|
||||
String remotePath) {
|
||||
}
|
||||
|
||||
private record Endpoint(String host, int port) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package de.votian.services.service;
|
||||
|
||||
import de.votian.services.config.LegacySchedulingProperties;
|
||||
import de.votian.services.security.AuthSessionService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class LegacyOperationsScheduler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LegacyOperationsScheduler.class);
|
||||
|
||||
private final LegacySchedulingProperties schedulingProperties;
|
||||
private final AuthSessionService authSessionService;
|
||||
private final LegacyCourierAutoLogoutService legacyCourierAutoLogoutService;
|
||||
private final AcceptanceProtocolAutomationService acceptanceProtocolAutomationService;
|
||||
|
||||
public LegacyOperationsScheduler(LegacySchedulingProperties schedulingProperties,
|
||||
AuthSessionService authSessionService,
|
||||
LegacyCourierAutoLogoutService legacyCourierAutoLogoutService,
|
||||
AcceptanceProtocolAutomationService acceptanceProtocolAutomationService) {
|
||||
this.schedulingProperties = schedulingProperties;
|
||||
this.authSessionService = authSessionService;
|
||||
this.legacyCourierAutoLogoutService = legacyCourierAutoLogoutService;
|
||||
this.acceptanceProtocolAutomationService = acceptanceProtocolAutomationService;
|
||||
}
|
||||
|
||||
@Scheduled(cron = "${votian.scheduling.auth-session-cleanup-cron:0 */5 * * * *}",
|
||||
zone = "${votian.scheduling.zone:Europe/Berlin}")
|
||||
public void cleanupExpiredAuthSessions() {
|
||||
if (!schedulingProperties.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
AuthSessionService.CleanupResult result = authSessionService.cleanupExpiredSessions();
|
||||
if (result.totalExpiredSessions() > 0) {
|
||||
log.info("Expired {} auth session(s) and {} pending TOTP session(s).",
|
||||
result.expiredAuthenticatedSessions(),
|
||||
result.expiredPendingTotpSessions());
|
||||
}
|
||||
}
|
||||
|
||||
@Scheduled(cron = "${votian.scheduling.courier-auto-logout-cron:0 */30 * * * *}",
|
||||
zone = "${votian.scheduling.zone:Europe/Berlin}")
|
||||
public void runCourierAutoLogout() {
|
||||
if (!schedulingProperties.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
legacyCourierAutoLogoutService.logoutCouriersByAutorevokes();
|
||||
}
|
||||
|
||||
@Scheduled(cron = "${votian.scheduling.acceptance-protocol-mail-cron:0 */5 * * * *}",
|
||||
zone = "${votian.scheduling.zone:Europe/Berlin}")
|
||||
public void runAcceptanceProtocolMailDispatch() {
|
||||
if (!schedulingProperties.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
acceptanceProtocolAutomationService.dispatchPendingMailProtocols();
|
||||
}
|
||||
|
||||
@Scheduled(cron = "${votian.scheduling.acceptance-protocol-letter-cron:30 */5 * * * *}",
|
||||
zone = "${votian.scheduling.zone:Europe/Berlin}")
|
||||
public void runAcceptanceProtocolLetterDispatch() {
|
||||
if (!schedulingProperties.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
acceptanceProtocolAutomationService.dispatchPendingLetterProtocols();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import de.votian.services.entity.Job;
|
||||
import de.votian.services.entity.ServiceZipCode;
|
||||
import de.votian.services.entity.Tour;
|
||||
import de.votian.services.entity.VehicleDisposition;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -39,6 +40,7 @@ public class LonghaulRemoteDbService {
|
||||
private final ParameterService parameterService;
|
||||
private final ConnectionFactory connectionFactory;
|
||||
|
||||
@Autowired
|
||||
public LonghaulRemoteDbService(ParameterService parameterService) {
|
||||
this(parameterService, new DriverManagerConnectionFactory());
|
||||
}
|
||||
|
||||
@@ -113,15 +113,11 @@ public class ViewDataService {
|
||||
}
|
||||
|
||||
public List<CustomerListItemDto> findCustomersByHeadquarters(Long hqId) {
|
||||
return customerRepository.findByHeadquartersId(hqId).stream()
|
||||
.map(this::toCustomerListItem)
|
||||
.toList();
|
||||
return toCustomerListItems(customerRepository.findByHeadquartersId(hqId));
|
||||
}
|
||||
|
||||
public List<CustomerListItemDto> searchCustomers(Long hqId, String term) {
|
||||
return customerRepository.searchCustomers(hqId, term).stream()
|
||||
.map(this::toCustomerListItem)
|
||||
.toList();
|
||||
return toCustomerListItems(customerRepository.searchCustomers(hqId, term));
|
||||
}
|
||||
|
||||
public Optional<CustomerDetailDto> findCustomerDetail(Long id) {
|
||||
@@ -129,15 +125,11 @@ public class ViewDataService {
|
||||
}
|
||||
|
||||
public List<CourierListItemDto> findCouriersByHeadquarters(Long hqId) {
|
||||
return courierRepository.findByHeadquartersId(hqId).stream()
|
||||
.map(this::toCourierListItem)
|
||||
.toList();
|
||||
return toCourierListItems(courierRepository.findByHeadquartersId(hqId));
|
||||
}
|
||||
|
||||
public List<CourierListItemDto> searchCouriers(Long hqId, String term) {
|
||||
return courierRepository.searchCouriers(hqId, term).stream()
|
||||
.map(this::toCourierListItem)
|
||||
.toList();
|
||||
return toCourierListItems(courierRepository.searchCouriers(hqId, term));
|
||||
}
|
||||
|
||||
public Optional<CourierDetailDto> findCourierDetail(Long id) {
|
||||
@@ -475,10 +467,7 @@ public class ViewDataService {
|
||||
}
|
||||
|
||||
public List<CostCenter> findJobFilterCostCenters(Long hqId, List<Long> accessibleCostCenterIds) {
|
||||
List<CostCenter> costCenters = new ArrayList<>();
|
||||
for (Customer customer : customerRepository.findByHeadquartersId(hqId)) {
|
||||
costCenters.addAll(findVisibleCostCentersSorted(customer.getId()));
|
||||
}
|
||||
List<CostCenter> costCenters = new ArrayList<>(costCenterRepository.findVisibleNonExternByHeadquartersId(hqId));
|
||||
if (accessibleCostCenterIds != null) {
|
||||
costCenters = filterCostCentersByAccess(costCenters, accessibleCostCenterIds);
|
||||
}
|
||||
@@ -525,6 +514,51 @@ public class ViewDataService {
|
||||
return dto;
|
||||
}
|
||||
|
||||
private List<CustomerListItemDto> toCustomerListItems(List<Customer> customers) {
|
||||
Map<Long, Company> companiesById = new LinkedHashMap<>();
|
||||
for (Company company : companyRepository.findAllById(customers.stream()
|
||||
.map(Customer::getCompanyId)
|
||||
.filter(id -> id != null && id > 0)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)))) {
|
||||
companiesById.put(company.getId(), company);
|
||||
}
|
||||
|
||||
Map<Long, Employee> employeesById = new LinkedHashMap<>();
|
||||
for (Employee employee : employeeRepository.findAllById(customers.stream()
|
||||
.map(Customer::getAdminEmployeeId)
|
||||
.filter(id -> id != null && id > 0)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)))) {
|
||||
employeesById.put(employee.getId(), employee);
|
||||
}
|
||||
|
||||
Map<Long, User> usersById = new LinkedHashMap<>();
|
||||
for (User user : userRepository.findAllById(employeesById.values().stream()
|
||||
.map(Employee::getUserId)
|
||||
.filter(id -> id != null && id > 0)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)))) {
|
||||
usersById.put(user.getId(), user);
|
||||
}
|
||||
|
||||
return customers.stream()
|
||||
.map(customer -> {
|
||||
Company company = companiesById.get(customer.getCompanyId());
|
||||
Employee employee = employeesById.get(customer.getAdminEmployeeId());
|
||||
User user = employee != null ? usersById.get(employee.getUserId()) : null;
|
||||
|
||||
CustomerListItemDto dto = new CustomerListItemDto();
|
||||
dto.setId(customer.getId());
|
||||
dto.setEid(customer.getEid());
|
||||
dto.setCompanyName(company != null && company.getComp() != null && !company.getComp().isBlank()
|
||||
? company.getComp()
|
||||
: customer.getEid());
|
||||
dto.setContact(user != null ? joinName(user.getFirstname(), user.getName()) : "");
|
||||
dto.setEmail(user != null && user.getEmail() != null ? user.getEmail() : "");
|
||||
dto.setPhone(user != null && user.getPhone() != null ? user.getPhone() : "");
|
||||
return dto;
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
private CustomerDetailDto toCustomerDetail(Customer customer) {
|
||||
CustomerDetailDto dto = new CustomerDetailDto();
|
||||
dto.setId(customer.getId());
|
||||
@@ -568,6 +602,42 @@ public class ViewDataService {
|
||||
return dto;
|
||||
}
|
||||
|
||||
private List<CourierListItemDto> toCourierListItems(List<Courier> couriers) {
|
||||
Map<Long, Company> companiesById = new LinkedHashMap<>();
|
||||
for (Company company : companyRepository.findAllById(couriers.stream()
|
||||
.map(Courier::getCompanyId)
|
||||
.filter(id -> id != null && id > 0)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)))) {
|
||||
companiesById.put(company.getId(), company);
|
||||
}
|
||||
|
||||
Map<Long, User> usersById = new LinkedHashMap<>();
|
||||
for (User user : userRepository.findAllById(couriers.stream()
|
||||
.map(Courier::getUserId)
|
||||
.filter(id -> id != null && id > 0)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)))) {
|
||||
usersById.put(user.getId(), user);
|
||||
}
|
||||
|
||||
return couriers.stream()
|
||||
.map(courier -> {
|
||||
Company company = companiesById.get(courier.getCompanyId());
|
||||
User user = usersById.get(courier.getUserId());
|
||||
|
||||
CourierListItemDto dto = new CourierListItemDto();
|
||||
dto.setId(courier.getId());
|
||||
dto.setEid(courier.getEid());
|
||||
dto.setSid(courier.getSid());
|
||||
dto.setCompanyName(company != null && company.getComp() != null && !company.getComp().isBlank()
|
||||
? company.getComp()
|
||||
: courier.getSid());
|
||||
dto.setContact(user != null ? joinName(user.getFirstname(), user.getName()) : "");
|
||||
dto.setAvailable("1".equals(courier.getAvailable()) ? "Ja" : "Nein");
|
||||
return dto;
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
private CourierDetailDto toCourierDetail(Courier courier) {
|
||||
CourierDetailDto dto = new CourierDetailDto();
|
||||
dto.setId(courier.getId());
|
||||
|
||||
@@ -28,3 +28,15 @@ votian.storage.job-station-photos-dir=../html/temp/photos
|
||||
# Logging
|
||||
logging.level.de.votian=INFO
|
||||
logging.level.org.hibernate.SQL=WARN
|
||||
|
||||
# Legacy scheduler bridge
|
||||
votian.scheduling.enabled=true
|
||||
votian.scheduling.zone=Europe/Berlin
|
||||
votian.scheduling.auth-session-cleanup-cron=0 */5 * * * *
|
||||
votian.scheduling.courier-auto-logout-cron=0 */30 * * * *
|
||||
votian.scheduling.courier-auto-logout-headquarters-id=3
|
||||
votian.scheduling.acceptance-protocol-mail-cron=0 */5 * * * *
|
||||
votian.scheduling.acceptance-protocol-letter-cron=30 */5 * * * *
|
||||
votian.scheduling.acceptance-protocol-mail-lookback-days=1
|
||||
votian.scheduling.acceptance-protocol-letter-lookback-days=3
|
||||
votian.scheduling.acceptance-protocol-letter-ftp-profile=MPS1
|
||||
|
||||
Reference in New Issue
Block a user