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 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); } }