From a53cae7c377905ee821ded4f38ed2be7fab186ea Mon Sep 17 00:00:00 2001 From: Przemek Zatorski Date: Wed, 23 Sep 2026 16:39:15 +0200 Subject: [PATCH] Istotna edycja zatwierdzonego ogloszenia wraca do moderacji - Zmiana tytulu, opisu, zdjec, danych kontaktowych lub linku spaceru w ogloszeniu opublikowanym albo wstrzymanym ustawia status PENDING; cena i parametry zmieniaja sie od razu. - Frontend nie koduje ponownie zapisanych zdjec JPEG przy edycji (bez utraty jakosci i bez falszywej zmiany zdjec); informacja o weryfikacji w formularzu. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../listing/ListingService.java | 20 +++++- .../listing/ListingEditModerationTest.java | 69 +++++++++++++++++++ frontend/src/App.tsx | 11 +++ 3 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 backend/src/test/java/pl/polskalokalnie/listing/ListingEditModerationTest.java diff --git a/backend/src/main/java/pl/polskalokalnie/listing/ListingService.java b/backend/src/main/java/pl/polskalokalnie/listing/ListingService.java index 61da0f7..0ee9f42 100644 --- a/backend/src/main/java/pl/polskalokalnie/listing/ListingService.java +++ b/backend/src/main/java/pl/polskalokalnie/listing/ListingService.java @@ -216,8 +216,8 @@ public class ListingService { return detailOf(saved); } - // Edycja wlasnego ogloszenia. Waliduje tresc filtrem, ale NIE zmienia statusu - - // zatwierdzone ogloszenie pozostaje widoczne od razu po zapisie zmian. + // Edycja wlasnego ogloszenia. Waliduje tresc filtrem; istotna zmiana tresci odsyla ogloszenie + // do ponownej weryfikacji, a drobne zmiany (cena, metraz, parametry) sa widoczne od razu. @Transactional public ListingDetailResponse updateOwn(Long id, String ownerEmail, ListingCreateRequest request) { PropertyListing listing = listingRepository.findById(id) @@ -228,13 +228,27 @@ public class ListingService { request.title(), request.description(), request.city(), request.address(), request.district(), request.street(), request.contactName()); + List before = moderatedContent(listing); applyRequest(listing, request); - // ownerEmail i status pozostaja bez zmian. + // Zmiana tresci widocznej dla kupujacych (tytul, opis, zdjecia, kontakt, link spaceru) wraca do + // weryfikacji - inaczej zatwierdzone ogloszenie mozna bylo podmienic na dowolne, np. oszustwo. + // Dotyczy tez wstrzymanego, bo wlasciciel sam je wznawia. Cena, metraz itp. zmieniaja sie od razu. + boolean visible = listing.getStatus() == ListingStatus.APPROVED || listing.getStatus() == ListingStatus.PAUSED; + if (visible && !before.equals(moderatedContent(listing))) { + listing.setStatus(ListingStatus.PENDING); + } PropertyListing saved = listingRepository.save(listing); return detailOf(saved); } + // Pola, ktorych zmiana wymaga ponownej akceptacji administracji. + static List moderatedContent(PropertyListing listing) { + return java.util.Arrays.asList(listing.getTitle(), listing.getDescription(), List.copyOf(listing.getPhotos()), + listing.getContactName(), listing.getContactPhone(), listing.getContactEmail(), + listing.getVirtualTourUrl()); + } + // Wspolne mapowanie pol requestu na encje (uzywane przez create i updateOwn). private void applyRequest(PropertyListing listing, ListingCreateRequest request) { listing.setTitle(request.title().trim()); diff --git a/backend/src/test/java/pl/polskalokalnie/listing/ListingEditModerationTest.java b/backend/src/test/java/pl/polskalokalnie/listing/ListingEditModerationTest.java new file mode 100644 index 0000000..ec996b4 --- /dev/null +++ b/backend/src/test/java/pl/polskalokalnie/listing/ListingEditModerationTest.java @@ -0,0 +1,69 @@ +package pl.polskalokalnie.listing; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import pl.polskalokalnie.moderation.TextModerationService; +import pl.polskalokalnie.notification.NotificationService; +import pl.polskalokalnie.user.UserRepository; +import pl.polskalokalnie.user.UserSettingsService; + +class ListingEditModerationTest { + + private static final String OWNER = "wlasciciel@example.com"; + private static final String PHOTO = "data:image/jpeg;base64,/9j/4AAQSkZJRg=="; + + private final ListingRepository repository = mock(ListingRepository.class); + private final ListingService service = new ListingService(repository, mock(TextModerationService.class), + mock(NotificationService.class), mock(UserRepository.class), mock(UserSettingsService.class)); + private PropertyListing listing; + + private ListingCreateRequest request(String title, BigDecimal price, String phone) { + return new ListingCreateRequest(title, "Opis mieszkania", OfferType.SALE, PropertyType.APARTMENT, "Kraków", + null, price, 50.0, 2, null, null, null, null, null, null, null, null, null, + "Jan", phone, OWNER, null, null, null, null, null, null, null, null, + null, null, List.of(), List.of(), List.of(PHOTO), null, false); + } + + @BeforeEach + void setUp() { + listing = new PropertyListing(); + listing.setOwnerEmail(OWNER); + when(repository.findById(1L)).thenReturn(Optional.of(listing)); + when(repository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + // Stan po pierwszym zapisie i akceptacji przez administracje. + service.updateOwn(1L, OWNER, request("Mieszkanie 2 pokoje", new BigDecimal("500000"), "500600700")); + listing.setStatus(ListingStatus.APPROVED); + } + + @Test + void priceChangeStaysPublished() { + service.updateOwn(1L, OWNER, request("Mieszkanie 2 pokoje", new BigDecimal("480000"), "500600700")); + + assertThat(listing.getStatus()).isEqualTo(ListingStatus.APPROVED); + } + + @Test + void changedContentGoesBackToModeration() { + // Regresja: zatwierdzone ogloszenie mozna bylo podmienic na dowolna tresc bez weryfikacji. + service.updateOwn(1L, OWNER, request("Okazja! Zadzwon teraz", new BigDecimal("500000"), "500600700")); + + assertThat(listing.getStatus()).isEqualTo(ListingStatus.PENDING); + } + + @Test + void changedPhoneOfPausedListingGoesBackToModeration() { + listing.setStatus(ListingStatus.PAUSED); + + service.updateOwn(1L, OWNER, request("Mieszkanie 2 pokoje", new BigDecimal("500000"), "999888777")); + + assertThat(listing.getStatus()).isEqualTo(ListingStatus.PENDING); + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d6de0c0..11dd27f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -986,6 +986,11 @@ type PriceFormatters = { * `maxDim` px po dluzszym boku - dzieki temu zdjecia da sie utrwalic w bazie bez rozdmuchiwania jej. */ async function imageUrlToDataUrl(url: string, maxDim = 1280, quality = 0.72): Promise { + // Zdjecie juz zapisane w ogloszeniu (skompresowany JPEG) zostaje bez zmian. Ponowne kodowanie przy + // kazdej edycji pogarszalo jakosc i zmienialo bajty - serwer uznawal to za podmiane zdjec. + if (url.startsWith('data:image/jpeg;base64,')) { + return url; + } try { const blob = await (await fetch(url)).blob(); const bitmap = await createImageBitmap(blob); @@ -15054,6 +15059,12 @@ function AddListingPage({ editListingId }: { editListingId?: number }) { {publishState === 'error' && (

{publishError}

)} + {isEdit && ( +

+ Zmiana tytułu, opisu, zdjęć, danych kontaktowych lub wirtualnego spaceru wymaga ponownej + akceptacji - do tego czasu ogłoszenie nie będzie widoczne. Cena i parametry zmieniają się od razu. +

+ )}