diff --git a/scripts/migrate-event-address-nullable.sql b/scripts/migrate-event-address-nullable.sql new file mode 100644 index 0000000..d64b984 --- /dev/null +++ b/scripts/migrate-event-address-nullable.sql @@ -0,0 +1,16 @@ +-- Migration: allow events.address to be NULL +-- Run on any environment with strict schema management (staging / production). +-- Dev uses ddl-auto=update, but Hibernate's "update" mode never DROPS an existing NOT NULL +-- constraint, so this must be applied by hand even on dev DBs created before this change. +-- +-- WHY: +-- Scraped events sometimes arrive without an address. Import previously failed the whole scrape +-- batch on the events.address NOT NULL constraint — one address-less row rolled back every event +-- in that cycle, so nothing reached the ops portal. address is now optional in the entity: the +-- event is kept for admin review and geocoded later once an admin supplies an address via the +-- ops portal edit endpoint (PUT /admin/event/{id}). +-- +-- ROLLBACK (only safe once no rows have address IS NULL): +-- ALTER TABLE events ALTER COLUMN address SET NOT NULL; + +ALTER TABLE events ALTER COLUMN address DROP NOT NULL; diff --git a/src/main/java/org/smalltech/hashtaglocal_backend/entity/EventEntity.java b/src/main/java/org/smalltech/hashtaglocal_backend/entity/EventEntity.java index 6e310ce..3426813 100644 --- a/src/main/java/org/smalltech/hashtaglocal_backend/entity/EventEntity.java +++ b/src/main/java/org/smalltech/hashtaglocal_backend/entity/EventEntity.java @@ -79,8 +79,12 @@ public class EventEntity { /** * Raw address string copied directly from the event data source (e.g., the Excel sheet). Kept as * a human-readable fallback while `location_id` is still null. + * + *
Nullable: scraped events sometimes arrive without an address. We keep the event anyway (for + * admin review) rather than dropping it — geocoding skips events with a null address until an + * admin supplies one via the ops portal edit endpoint. */ - @Column(length = 1024, nullable = false) + @Column(length = 1024) private String address; /** URL to the original event page on the source platform. */ diff --git a/src/main/java/org/smalltech/hashtaglocal_backend/service/EventImportService.java b/src/main/java/org/smalltech/hashtaglocal_backend/service/EventImportService.java index ae536a6..4b526d9 100644 --- a/src/main/java/org/smalltech/hashtaglocal_backend/service/EventImportService.java +++ b/src/main/java/org/smalltech/hashtaglocal_backend/service/EventImportService.java @@ -1,22 +1,17 @@ package org.smalltech.hashtaglocal_backend.service; -import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.regex.Pattern; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.smalltech.hashtaglocal_backend.dto.ScrapeEventDTO; -import org.smalltech.hashtaglocal_backend.entity.EventApprovalEntity; import org.smalltech.hashtaglocal_backend.entity.EventEntity; import org.smalltech.hashtaglocal_backend.entity.MediaEntity; -import org.smalltech.hashtaglocal_backend.model.EventApprovalStatus; import org.smalltech.hashtaglocal_backend.model.EventPortalModel; import org.smalltech.hashtaglocal_backend.model.EventTypeModel; -import org.smalltech.hashtaglocal_backend.repository.EventApprovalRepository; import org.smalltech.hashtaglocal_backend.repository.EventRepository; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; /** * Handles importing events from the scrape service JSON response. @@ -52,22 +47,25 @@ public class EventImportService { private final EventService eventService; private final EventRepository eventRepository; private final EventImageService eventImageService; - private final EventApprovalRepository eventApprovalRepository; /** * Processes a list of events from the scrape service, deduplicates against the database, and - * bulk-saves all new events in a single transaction. + * saves each new event (with its {@code PENDING} approval row) in its own transaction. + * + *
Events are persisted independently, so a single bad row — a constraint violation, a failed
+ * image upload, etc. — only skips that event instead of rolling back the whole batch. A missing
+ * address does not drop the event: it is kept for admin review and geocoded later once an
+ * address is supplied via the ops portal.
*
* @param scrapeEvents raw event DTOs from the scrape service response
- * @return the number of events actually saved (duplicates excluded)
+ * @return the number of events actually saved (duplicates and failures excluded)
*/
- @Transactional
public int importFromScrapeResponse(List Used by the scraper import: each event is saved independently, so a single failing row (a
+ * constraint violation, a bad field, etc.) rolls back only that event instead of the whole
+ * import batch.
+ */
+ @Transactional
+ public EventEntity saveWithPendingApproval(EventEntity event) {
+ EventEntity saved = eventRepository.save(event);
+ eventApprovalRepository.save(
+ EventApprovalEntity.builder()
+ .eventId(saved.getId())
+ .status(EventApprovalStatus.PENDING)
+ .build());
+ return saved;
+ }
+
/**
* Returns all {@code APPROVED} events that have a resolved location, mapped to {@link EventData}.
*
diff --git a/src/test/java/org/smalltech/hashtaglocal_backend/service/EventImportServiceTest.java b/src/test/java/org/smalltech/hashtaglocal_backend/service/EventImportServiceTest.java
index 855a25a..1be5c56 100644
--- a/src/test/java/org/smalltech/hashtaglocal_backend/service/EventImportServiceTest.java
+++ b/src/test/java/org/smalltech/hashtaglocal_backend/service/EventImportServiceTest.java
@@ -28,7 +28,6 @@
import org.smalltech.hashtaglocal_backend.model.EventPortalModel;
import org.smalltech.hashtaglocal_backend.model.EventTypeModel;
import org.smalltech.hashtaglocal_backend.model.MediaTypeModel;
-import org.smalltech.hashtaglocal_backend.repository.EventApprovalRepository;
import org.smalltech.hashtaglocal_backend.repository.EventRepository;
/**
@@ -44,7 +43,6 @@ class EventImportServiceTest {
@Mock private EventService eventService;
@Mock private EventRepository eventRepository;
@Mock private EventImageService eventImageService;
- @Mock private EventApprovalRepository eventApprovalRepository;
@InjectMocks private EventImportService eventImportService;
@@ -79,11 +77,11 @@ private ScrapeEventDTO dto(String name, String type, String portal, LocalDateTim
.build();
}
- @SuppressWarnings("unchecked")
private List> captor = ArgumentCaptor.forClass(List.class);
- verify(eventService).saveAll(captor.capture());
- return captor.getValue();
+ ArgumentCaptor