Istotna edycja zatwierdzonego ogloszenia wraca do moderacji
Deploy Polska Lokalnie / deploy (push) Successful in 12s

- 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) <noreply@anthropic.com>
This commit is contained in:
2026-09-23 16:39:15 +02:00
parent b850e4c092
commit a53cae7c37
3 changed files with 97 additions and 3 deletions
@@ -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<Object> 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<Object> 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());
@@ -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);
}
}
+11
View File
@@ -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<string | null> {
// 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' && (
<p className="publish-feedback error" role="alert">{publishError}</p>
)}
{isEdit && (
<p className="publish-feedback">
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.
</p>
)}
<div className="add-location-actions final-actions">
<button className="back-step-button" type="button" onClick={() => goToStep(3)}>
Wstecz