feat: Frachtanzeige und Stationsabschluss für Version 0.9.23

Fracht mit Maßen und Gewicht anzeigen und Stationsabschlüsse gegen Wiederholung absichern. Backend-Abschluss atomar ausführen und mit Regressionstests abdecken. Optionale Aufgaben beim Kopieren erhalten und Kunden mit Adressen anzeigen. App und Backend auf Version 0.9.23 aktualisieren.
This commit is contained in:
Sven committed 2026-09-28 11:30:36 +02:00
1 parent d7c0173493
commit 7325c77739
11 files changed
+277 -65

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@
<packaging>jar</packaging>
<properties>
<revision>0.9.22</revision>
<revision>0.9.23</revision>
<java.version>21</java.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
@@ -31,6 +31,10 @@ import de.assecutor.votianlt.model.JobStatus;
import lombok.extern.slf4j.Slf4j;
import de.assecutor.votianlt.messaging.MessagingPublisher;
import org.springframework.stereotype.Component;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import java.time.LocalDateTime;
import java.util.List;
@@ -53,6 +57,7 @@ public class MessageController {
private final AppUserService appUserService;
private final JobRepository jobRepository;
private final MongoTemplate mongoTemplate;
private final CargoItemRepository cargoItemRepository;
@@ -72,11 +77,12 @@ public class MessageController {
TaskRepository taskRepository, PhotoRepository photoRepository, BarcodeRepository barcodeRepository,
SignatureRepository signatureRepository, CommentRepository commentRepository,
JobHistoryService jobHistoryService, JobUpdateBroadcaster jobUpdateBroadcaster, EmailService emailService,
MessageService messageService, TaskAssignmentService taskAssignmentService) {
MessageService messageService, TaskAssignmentService taskAssignmentService, MongoTemplate mongoTemplate) {
this.messagingPublisher = messagingPublisher;
this.appUserRepository = appUserRepository;
this.appUserService = appUserService;
this.jobRepository = jobRepository;
this.mongoTemplate = mongoTemplate;
this.cargoItemRepository = cargoItemRepository;
this.taskRepository = taskRepository;
this.photoRepository = photoRepository;
@@ -420,7 +426,9 @@ public class MessageController {
boolean allCompleted = mandatoryTasks.stream().allMatch(task -> task.isCompleted());
if (allCompleted) {
updateJobStatusToCompleted(jobId);
if (!updateJobStatusToCompleted(jobId)) {
return;
}
try {
emailService.sendJobCompletionNotification(jobId, completedBy);
} catch (Exception e) {
@@ -438,23 +446,18 @@ public class MessageController {
}
}
private void updateJobStatusToCompleted(ObjectId jobId) {
private boolean updateJobStatusToCompleted(ObjectId jobId) {
try {
Optional<Job> jobOpt = jobRepository.findById(jobId);
if (jobOpt.isEmpty()) {
log.warn("[JOB] Cannot mark job {} as completed: job not found", jobId);
return;
}
Job job = jobOpt.get();
if (job.getStatus() != JobStatus.COMPLETED) {
job.setStatus(JobStatus.COMPLETED);
job.setUpdatedAt(LocalDateTime.now());
jobRepository.save(job);
Query query = Query.query(Criteria.where("_id").is(jobId).and("status").ne(JobStatus.COMPLETED));
Update update = new Update().set("status", JobStatus.COMPLETED).set("updatedAt", LocalDateTime.now());
boolean completed = mongoTemplate.updateFirst(query, update, Job.class).getModifiedCount() > 0;
if (completed) {
log.info("[JOB] Job {} marked as COMPLETED", jobId);
}
return completed;
} catch (Exception e) {
log.error("[JOB] Could not update status of job {} to COMPLETED: {}", jobId, e.getMessage(), e);
return false;
}
}
@@ -1600,6 +1600,7 @@ public class DeliveryStationDialog extends Dialog {
if (copy != null) {
copy.setDescription(original.getDescription());
copy.setOptional(original.isOptional());
copy.setTaskOrder(original.getTaskOrder() != null ? original.getTaskOrder() : 0);
copy.setCompleted(original.isCompleted());
copy.setCompletedAt(original.getCompletedAt());
@@ -310,7 +310,7 @@ public class PickupStationDialog extends Dialog {
customerLabelMap.clear();
for (Customer c : customers) {
CustomerAddressLabelHelper.putName(customerLabelMap, c,
CustomerAddressLabelHelper.put(customerLabelMap, c,
translationHelper.getTranslation("addjob.customer.unnamed"));
}
customerComboBox.setItems(new ArrayList<>(customerLabelMap.keySet()));
@@ -243,7 +243,7 @@ public class AddJobView extends Main implements HasDynamicTitle {
List<Customer> ownerCustomers = customerService.findAllForCurrentOwner();
customerLabelToEntity.clear();
for (Customer c : ownerCustomers) {
CustomerAddressLabelHelper.putName(customerLabelToEntity, c, getTranslation("addjob.customer.unnamed"));
CustomerAddressLabelHelper.put(customerLabelToEntity, c, getTranslation("addjob.customer.unnamed"));
}
customerSelection.setItems(new ArrayList<>(customerLabelToEntity.keySet()));
@@ -0,0 +1,112 @@
package de.assecutor.votianlt.controller;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import com.mongodb.client.result.UpdateResult;
import de.assecutor.votianlt.model.Job;
import de.assecutor.votianlt.model.JobStatus;
import de.assecutor.votianlt.model.task.ConfirmationTask;
import de.assecutor.votianlt.repository.JobRepository;
import de.assecutor.votianlt.service.EmailService;
import de.assecutor.votianlt.service.JobUpdateBroadcaster;
import de.assecutor.votianlt.service.TaskAssignmentService;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
@ExtendWith(MockitoExtension.class)
class StationCompletionTest {
@Mock
private JobRepository jobRepository;
@Mock
private TaskAssignmentService taskAssignmentService;
@Mock
private EmailService emailService;
@Mock
private JobUpdateBroadcaster jobUpdateBroadcaster;
@Mock
private MongoTemplate mongoTemplate;
@InjectMocks
private MessageController controller;
private final ObjectId jobId = new ObjectId();
private ConfirmationTask mandatoryTask;
private Map<String, Object> payload;
@BeforeEach
void setUp() {
Job job = new Job();
job.setId(jobId);
job.setJobNumber("JOB-001");
when(jobRepository.findById(jobId)).thenReturn(Optional.of(job));
mandatoryTask = new ConfirmationTask("Lieferung bestätigen");
mandatoryTask.setCompleted(true);
when(taskAssignmentService.findTasksForJob(jobId)).thenReturn(List.of(mandatoryTask));
payload = Map.of("jobId", jobId.toHexString(), "jobNumber", "JOB-001", "stationOrder", 0);
}
@Test
void repeatedStationCompletionSendsOnlyOneEmail() {
when(mongoTemplate.updateFirst(any(Query.class), any(Update.class), eq(Job.class)))
.thenReturn(UpdateResult.acknowledged(1, 1L, null), UpdateResult.acknowledged(0, 0L, null));
controller.handleStationCompleted("driver-1", payload);
controller.handleStationCompleted("driver-1", payload);
verify(emailService).sendJobCompletionNotification(jobId, "driver-1");
}
@Test
void alreadyCompletedJobDoesNotSendAnotherEmail() {
when(mongoTemplate.updateFirst(any(Query.class), any(Update.class), eq(Job.class)))
.thenReturn(UpdateResult.acknowledged(0, 0L, null));
controller.handleStationCompleted("driver-1", payload);
verifyNoInteractions(emailService);
ArgumentCaptor<Query> query = ArgumentCaptor.forClass(Query.class);
verify(mongoTemplate).updateFirst(query.capture(), any(Update.class), eq(Job.class));
assertThat(query.getValue().getQueryObject()).containsEntry("_id", jobId)
.containsEntry("status", new Document("$ne", JobStatus.COMPLETED));
}
@Test
void incompleteMandatoryTaskPreventsJobCompletion() {
mandatoryTask.setCompleted(false);
controller.handleStationCompleted("driver-1", payload);
verifyNoInteractions(mongoTemplate, emailService);
}
@Test
void unfinishedOptionalTaskDoesNotPreventExplicitCompletion() {
ConfirmationTask optionalTask = new ConfirmationTask("Zusätzliche Bestätigung");
optionalTask.setOptional(true);
when(taskAssignmentService.findTasksForJob(jobId)).thenReturn(List.of(mandatoryTask, optionalTask));
when(mongoTemplate.updateFirst(any(Query.class), any(Update.class), eq(Job.class)))
.thenReturn(UpdateResult.acknowledged(1, 1L, null));
controller.handleStationCompleted("driver-1", payload);
verify(emailService).sendJobCompletionNotification(jobId, "driver-1");
}
}