Files
votianng/backend/src/test/java/de/stadtbote/votian/OrderRecalculationTest.java
T
Sven 363bc27c26 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.
2026-10-13 20:31:31 +02:00

75 lines
5.7 KiB
Java

package de.stadtbote.votian;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.stadtbote.votian.database.DatabaseService;
import jakarta.validation.Validation;
import org.junit.jupiter.api.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
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.OrderEntryFixtures.USER;
import static de.stadtbote.votian.OrderEntryModels.*;
class OrderRecalculationTest {
final DatabaseService db=mock(DatabaseService.class);
final OrderEntryCatalog catalog=mock(OrderEntryCatalog.class);
final LegacyOrderPricing prices=mock(LegacyOrderPricing.class);
final OrderRoutingService routing=mock(OrderRoutingService.class);
final OrderOperations orders=mock(OrderOperations.class);
final BusinessSupport support=new BusinessSupport(db,new ObjectMapper().findAndRegisterModules());
final OrderRecalculation service=new OrderRecalculation(db,support,orders,catalog,prices,routing,Validation.buildDefaultValidatorFactory().getValidator());
final Map<String,Object> job=fields("jb_id",5L,"hq_id",7,"hq_id_dispo",7,"jb_ordertime",LocalDateTime.of(2026,10,10,10,0),"csc_id_payer",41,"jb_totalprice",100,"jb_cr_price",70,"jb_status",9,"jb_version",2,"jb_weight",0);
OrderOperations.Snapshot snapshot;
OrderRecalculation.Input input=new OrderRecalculation.Input(new Pricing(PriceMode.FIXED,new BigDecimal("100"),BigDecimal.TEN,null,null,null,null,false,4L),2,List.of(),false,false);
@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))).thenReturn(List.of(fields("hq_id",7)));
when(db.lockRows(startsWith("SELECT jb_id,hq_id FROM job WHERE jb_id_parent="),any(Object[].class))).thenReturn(List.of());
when(catalog.costCenter(USER,41,true)).thenReturn(fields("cs_id",31));
snapshot=new OrderOperations.Snapshot(job,List.of(fields("tr_id",6,"tr_sort",1,"tr_ware_from_to",3,"tr_comp","Test","ad_street","Teststraße","tr_hsno","1","ad_city","Berlin","ad_zipcode","10115","ad_country","DE")),List.of(),List.of(),List.of(),List.of());
when(orders.snapshot(eq(USER),eq(5L),anyBoolean())).thenAnswer(c->snapshot);
when(orders.view(USER,5)).thenReturn(fields("id",5));
when(prices.calculate(eq(USER),any(),anyMap(),any(),any(),anyList())).thenReturn(OrderEntryFixtures.quote());
}
OrderRecalculation.Command command(String fingerprint) {return new OrderRecalculation.Command(UUID.randomUUID().toString(),AdminSupport.revision(snapshot),input,fingerprint);}
@Test void previewIsReadOnlyAndUsesSavedDateStationAndPayer() {
var result=service.preview(USER,5,command(null));assertThat(result).containsEntry("previousNet",new BigDecimal("100"));
verify(prices).calculate(eq(USER),argThat(r->r.stops().size()==1&&r.payerCostCenterId()==41&&r.pricing().mode()==PriceMode.FIXED),anyMap(),eq(LocalDateTime.of(2026,10,10,10,0)),isNull(),anyList());
verify(db,never()).update(anyString(),any(Object[].class));verify(db,never()).insert(anyString(),anyMap());
}
@Test void staleRevisionAndChangedTariffsCannotBeSaved() {
assertThatThrownBy(()->service.preview(USER,5,new OrderRecalculation.Command(null,"old",input,null))).hasMessageContaining("geändert");
assertThatThrownBy(()->service.save(USER,5,command("old"))).hasMessageContaining("Preisvorschau");
verify(db,never()).update(anyString(),any(Object[].class));
}
@Test void writesOnlyPricesAndMetadataWithoutReplayingOperationalState() {
var c=command(null);var preview=service.preview(USER,5,c);
service.save(USER,5,new OrderRecalculation.Command(c.requestId(),c.revision(),input,(String)preview.get("fingerprint")));
verify(db).update(eq("DELETE FROM tourservice WHERE jb_id=?"),eq(5L));
verify(db).update(startsWith("UPDATE job SET"),any(Object[].class));
verify(db,never()).insert(eq("job"),anyMap());verify(db,never()).insert(eq("tour"),anyMap());
verify(db,never()).update(contains("stock"),any(Object[].class));
verify(db,never()).update(contains("mt_sort=16"),any(Object[].class));
verify(db,never()).update(contains("jb_incomplete"),any(Object[].class));
}
@Test void paymentsAndBrokenChildLinksAreNotSilentlyOverwritten() {
snapshot=new OrderOperations.Snapshot(job,snapshot.stops(),List.of(),List.of(),List.of(fields("jbp_id",1)),List.of());
assertThatThrownBy(()->service.preview(USER,5,command(null))).hasMessageContaining("Einnahmenbuchungen");
snapshot=new OrderOperations.Snapshot(job,snapshot.stops(),List.of(),List.of(),List.of(),List.of());job.put("jb_id_parent",-1);
assertThatThrownBy(()->service.preview(USER,5,command(null))).hasMessageContaining("fehlende Unteraufträge");
}
@Test void invalidInputsAndMissingRightsFailBeforeExternalCalls() {
var none=new LegacyUser(11,12,7,"no-rights","0".repeat(40));
assertThatThrownBy(()->service.preview(none,5,command(null))).isInstanceOf(org.springframework.security.access.AccessDeniedException.class);
input=new OrderRecalculation.Input(new Pricing(PriceMode.FIXED,new BigDecimal("-1"),null,null,null,null,null,false,4L),2,List.of(),false,false);
assertThatThrownBy(()->service.preview(USER,5,command(null))).hasMessageContaining("Kalkulationswerte");
verifyNoInteractions(routing);
}
}