FreelancerCommandService.java
package de.mirkosertic.powerstaff.freelancer.command;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@Transactional
public class FreelancerCommandService {
private final FreelancerRepository freelancerRepository;
private final FreelancerContactRepository contactRepository;
private final FreelancerHistoryRepository historyRepository;
private final FreelancerTagCommandService tagCommandService;
private final JdbcClient jdbcClient;
public FreelancerCommandService(final FreelancerRepository freelancerRepository,
final FreelancerContactRepository contactRepository,
final FreelancerHistoryRepository historyRepository,
final FreelancerTagCommandService tagCommandService,
final JdbcClient jdbcClient) {
this.freelancerRepository = freelancerRepository;
this.contactRepository = contactRepository;
this.historyRepository = historyRepository;
this.tagCommandService = tagCommandService;
this.jdbcClient = jdbcClient;
}
/**
* Speichert Freiberufler-Stammdaten, Kontakte, Historie und Tags via Delta-Commands.
* Nur Einträge mit op="ADD" oder op="DELETE" werden verarbeitet;
* unveränderte Einträge erhalten keinen neuen Audit-Timestamp.
* Leerer Code wird als NULL gespeichert. Wirft {@link DuplicateCodeException}
* wenn der Code bereits von einem anderen Freiberufler verwendet wird.
*/
/** Convenience-Methode für Tests und interne Aufrufe ohne Delta-Listen. */
public Freelancer save(final Freelancer freelancer) {
return save(freelancer, List.of(), List.of(), List.of());
}
public Freelancer save(final Freelancer freelancer,
final List<FreelancerContactEntry> contactChanges,
final List<FreelancerHistoryEntry> historyChanges,
final List<FreelancerTagEntry> tagChanges) {
// partner_id wird ausschließlich über das Partner-Modul gesetzt – beim Speichern
// des Freiberufler-Formulars bestehenden Wert aus der DB übernehmen.
if (freelancer.getId() != null) {
freelancerRepository.findById(freelancer.getId())
.ifPresent(existing -> freelancer.setPartnerId(existing.getPartnerId()));
}
// Leeren Code → NULL normalisieren (verhindert UNIQUE-Constraint-Verletzung)
if (freelancer.getCode() != null && freelancer.getCode().isBlank()) {
freelancer.setCode(null);
}
// Dubletten-Prüfung nur wenn Code gesetzt
if (freelancer.getCode() != null) {
freelancerRepository.findByCode(freelancer.getCode()).ifPresent(existing -> {
if (!existing.getId().equals(freelancer.getId())) {
throw new DuplicateCodeException(freelancer.getCode());
}
});
}
final Freelancer saved = freelancerRepository.save(freelancer);
final long freelancerId = saved.getId();
// Kontakt-Delta verarbeiten
for (final FreelancerContactEntry cmd : contactChanges) {
if ("ADD".equals(cmd.op())) {
final FreelancerContact contact = new FreelancerContact();
contact.setType(cmd.type());
contact.setValue(cmd.value());
contact.setFreelancerId(freelancerId);
contactRepository.save(contact);
} else if ("DELETE".equals(cmd.op())) {
if (cmd.id() == null) {
throw new IllegalArgumentException("DELETE Kontakt erfordert eine ID");
}
contactRepository.deleteById(cmd.id());
}
}
// Historie-Delta verarbeiten
for (final FreelancerHistoryEntry cmd : historyChanges) {
if ("ADD".equals(cmd.op())) {
final FreelancerHistory history = new FreelancerHistory();
history.setDescription(cmd.description());
history.setTypeId(cmd.typeId());
history.setFreelancerId(freelancerId);
historyRepository.save(history);
} else if ("UPDATE".equals(cmd.op())) {
if (cmd.id() == null) {
throw new IllegalArgumentException("UPDATE Kontakthistorie erfordert eine ID");
}
historyRepository.findById(cmd.id()).ifPresent(history -> {
history.setDescription(cmd.description());
history.setTypeId(cmd.typeId());
historyRepository.save(history);
});
} else if ("DELETE".equals(cmd.op())) {
if (cmd.id() == null) {
throw new IllegalArgumentException("DELETE Kontakthistorie erfordert eine ID");
}
historyRepository.deleteById(cmd.id());
}
}
// Tag-Delta verarbeiten
for (final FreelancerTagEntry cmd : tagChanges) {
if ("ADD".equals(cmd.op()) && cmd.tagId() != null) {
try {
tagCommandService.addTag(freelancerId, cmd.tagId());
} catch (final DuplicateTagException ignored) {
// Tag bereits zugeordnet – ignorieren (idempotent)
}
} else if ("DELETE".equals(cmd.op()) && cmd.tagId() != null) {
tagCommandService.removeTagByTagId(freelancerId, cmd.tagId());
}
}
return saved;
}
@Transactional(readOnly = true)
public Optional<Freelancer> findById(final long id) {
return freelancerRepository.findById(id);
}
/**
* Sucht einen Freiberufler anhand seines anonymisierten Codes.
* Liefert ein öffentliches Lookup-Ergebnis für Cross-Modul-Nutzung.
*/
@Transactional(readOnly = true)
public Optional<FreelancerLookupResult> findByCode(final String code) {
return freelancerRepository.findByCode(code)
.map(f -> new FreelancerLookupResult(f.getId(), f.getPartnerId(), f.getCompany()));
}
/**
* Löscht einen Freiberufler. Wirft {@link FreelancerHasPositionsException} wenn aktive
* Projektpositionen auf diesen Freiberufler verweisen.
*/
public void deleteById(final long id) {
final List<Long> linkedProjectIds = jdbcClient
.sql("SELECT project_id FROM project_position WHERE freelancer_id = :freelancerId")
.param("freelancerId", id)
.query(Long.class)
.list();
if (!linkedProjectIds.isEmpty()) {
throw new FreelancerHasPositionsException(linkedProjectIds);
}
freelancerRepository.deleteById(id);
}
/**
* Ordnet einen Freiberufler einem Partner zu.
* Targeted UPDATE — kein vollständiges Aggregate-Save, um den Audit-Trail zu schützen.
*/
public void assignToPartner(final long freelancerId, final long partnerId) {
jdbcClient.sql("UPDATE freelancer SET partner_id = :partnerId WHERE id = :freelancerId")
.param("partnerId", partnerId)
.param("freelancerId", freelancerId)
.update();
}
/**
* Löst die Zuordnung eines Freiberuflers zu einem Partner auf.
*/
public void removeFromPartner(final long freelancerId, final long partnerId) {
jdbcClient.sql("UPDATE freelancer SET partner_id = NULL WHERE id = :freelancerId AND partner_id = :partnerId")
.param("freelancerId", freelancerId)
.param("partnerId", partnerId)
.update();
}
}