Erweitere VoTiAn NG um Auftrags-, Stamm- und Verwaltungsfunktionen

Ergänzt Auftragserfassung und -bearbeitung, Unternehmerbelege, Stornos, Lagerkorrekturen, Stammdaten, Abrechnung, CRM, Formulare, Berichte und Preisverwaltung auf Basis der aktuellen PHP-Anwendung.

Enthält Integrations- und Oberflächentests sowie dokumentierte Abgleiche und Prüfstände.
This commit is contained in:
Sven committed 2026-10-13 20:31:31 +02:00
1 parent 42c87282ee
commit 363bc27c26
190 files changed
+14815 -1857

No files matched your search

@@ -0,0 +1,85 @@
package de.stadtbote.votian;
import de.stadtbote.votian.database.DatabaseService;
import org.junit.jupiter.api.*;
import org.springframework.security.access.AccessDeniedException;
import java.time.*;
import java.util.*;
import java.util.function.Supplier;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;
import static de.stadtbote.votian.OrderEntryService.fields;
import static de.stadtbote.votian.CalendarAdministration.*;
class CalendarAdministrationTest {
final DatabaseService db=mock(DatabaseService.class);
final LegacyUser user=new LegacyUser(11,12,7,"test","1".repeat(70));
final CalendarAdministration service=new CalendarAdministration(db,Clock.fixed(Instant.parse("2026-10-12T09:00:00Z"),ZoneId.of("Europe/Berlin")));
final List<Map<String,Object>> holidays=new ArrayList<>();
final Map<String,Object> travel=fields("id",1L,"postalCode","10115","country","DE","globalMinutes",30,"localMinutes",null);
@BeforeEach void setup(){
when(db.readTransaction(any())).thenAnswer(c->((Supplier<?>)c.getArgument(0)).get());
when(db.writeTransaction(any())).thenAnswer(c->((Supplier<?>)c.getArgument(0)).get());
when(db.lockRows(anyString(),any(Object[].class))).thenAnswer(c->((String)c.getArgument(0)).startsWith("SELECT ph_id")?holidays:List.of(fields("hq_id",7)));
when(db.queryForList(anyString(),any(Object[].class))).thenAnswer(c->{String sql=c.getArgument(0);if(sql.startsWith("SELECT ph_id"))return holidays;if(sql.startsWith("SELECT p.srvp_id"))return List.of(travel);return List.of();});
when(db.update(anyString(),any(Object[].class))).thenReturn(1);
}
@Test void rightsAreCheckedBeforeAnyDatabaseAccess(){
var denied=new LegacyUser(11,12,7,"test","0".repeat(70));
assertThatThrownBy(()->service.holidays(denied,2026)).isInstanceOf(AccessDeniedException.class);
assertThatThrownBy(()->service.saveTravelTime(denied,TravelMode.BWV,1,new TravelSave("x",20))).isInstanceOf(AccessDeniedException.class);
verifyNoInteractions(db);
}
@Test void historicalRowsAndTimeWindowsSurviveAnEmptyFutureCalendar(){
holidays.add(fields("ph_id",1,"ph_month",1,"ph_day",1,"ph_name","Neujahr","ph_time_from","10:00","ph_time_to","12:00"));
holidays.add(fields("ph_id",2,"ph_month",12,"ph_day",25,"ph_name","Weihnachten","ph_time_from","00:00","ph_time_to","00:00"));
service.saveHolidays(user,2026,new HolidaySave(AdminSupport.revision(holidays),List.of()));
verify(db).update("DELETE FROM publicholiday WHERE ph_id=? AND hq_id=?",2L,7L);
verify(db,never()).update(eq("DELETE FROM publicholiday WHERE ph_id=? AND hq_id=?"),eq(1L),any());
}
@Test void staleCalendarCannotOverwriteAnotherEditorsChanges(){
assertThatThrownBy(()->service.saveHolidays(user,2026,new HolidaySave("outdated",List.of()))).hasMessageContaining("409");
verify(db,never()).update(anyString(),any(Object[].class));verify(db,never()).insert(anyString(),anyMap());
}
@Test void validatesDateAndRejectsModificationOfToday(){
assertThatThrownBy(()->service.saveHolidays(user,2026,new HolidaySave("x",List.of(new Holiday(2,29,"x"))))).hasMessageContaining("Feiertagsdatum");
assertThatThrownBy(()->service.saveHolidays(user,2026,new HolidaySave(AdminSupport.revision(holidays),List.of(new Holiday(10,12,"x"))))).hasMessageContaining("heute");
verify(db,never()).insert(anyString(),anyMap());
}
@Test void futureNamesAreUpdatedWithoutReplacingTimeWindows(){
holidays.add(fields("ph_id",2,"ph_month",12,"ph_day",25,"ph_name","alt","ph_time_from","10:00","ph_time_to","12:00"));
service.saveHolidays(user,2026,new HolidaySave(AdminSupport.revision(holidays),List.of(new Holiday(12,25,"neu"))));
verify(db).update("UPDATE publicholiday SET ph_name=? WHERE ph_id=? AND hq_id=?","neu",2L,7L);
verify(db,never()).insert(anyString(),anyMap());
}
@Test void multipleTimeWindowsRepresentOneDayAndSurviveAnUnchangedSave(){
holidays.add(fields("ph_id",2,"ph_month",12,"ph_day",25,"ph_name","vormittags","ph_time_from","10:00","ph_time_to","12:00"));
holidays.add(fields("ph_id",3,"ph_month",12,"ph_day",25,"ph_name","nachmittags","ph_time_from","14:00","ph_time_to","16:00"));
assertThat((List<?>)service.holidays(user,2026).get("days")).hasSize(1);
service.saveHolidays(user,2026,new HolidaySave(AdminSupport.revision(holidays),List.of(new Holiday(12,25,"vormittags"))));
verify(db,never()).update(anyString(),any(Object[].class));
}
@Test void classicCreatesOnlyThisHeadquartersOverride(){
service.saveTravelTime(user,TravelMode.CLASSIC,1,new TravelSave(AdminSupport.revision(Arrays.asList(1L,TravelMode.CLASSIC,null)),45));
verify(db).update("INSERT INTO serviceplztraveltime (srvp_id,hq_id,srvpt_traveltime) VALUES (?,?,?)",1L,7L,45);
verify(db,never()).update(startsWith("UPDATE serviceplz SET"),any(Object[].class));
}
@Test void bwvIsOneConditionalWriteAndReportsConcurrentUpdates(){
when(db.update(startsWith("UPDATE serviceplz SET"),any(Object[].class))).thenReturn(0);
assertThatThrownBy(()->service.saveTravelTime(user,TravelMode.BWV,1,new TravelSave(AdminSupport.revision(Arrays.asList(1L,TravelMode.BWV,30)),45))).hasMessageContaining("409");
verify(db).update("UPDATE serviceplz SET srvp_traveltime=? WHERE srvp_id=? AND srvp_valid=1 AND srvp_traveltime <=> ?",45,1L,30);
verify(db,never()).insert(anyString(),anyMap());
}
@Test void zeroAndEmptyTravelTimeUseTheConfiguredHeadquartersDefault() {
when(db.queryForList(startsWith("SELECT par_value FROM parameter"),eq(7L),eq("LATEST_TAKETIME_IN_MINUTES"))).thenReturn(List.of(fields("par_value","35")));
service.saveTravelTime(user,TravelMode.CLASSIC,1,new TravelSave(AdminSupport.revision(Arrays.asList(1L,TravelMode.CLASSIC,null)),0));
verify(db).update("INSERT INTO serviceplztraveltime (srvp_id,hq_id,srvpt_traveltime) VALUES (?,?,?)",1L,7L,35);
service.saveTravelTime(user,TravelMode.BWV,1,new TravelSave(AdminSupport.revision(Arrays.asList(1L,TravelMode.BWV,30)),null));
verify(db).update(startsWith("UPDATE serviceplz SET"),eq(35),eq(1L),eq(30));
}
@Test void missingDefaultNeverWritesAnInventedTravelTime() {
assertThatThrownBy(()->service.saveTravelTime(user,TravelMode.CLASSIC,1,new TravelSave("",0))).hasMessageContaining("LATEST_TAKETIME_IN_MINUTES");
verify(db,never()).update(anyString(),any(Object[].class));
}
}