From 8a315c42888d2af9a07fa92d99b000e33e9aafb6 Mon Sep 17 00:00:00 2001 From: Steven Grimm Date: Wed, 15 Nov 2023 08:18:08 -0800 Subject: [PATCH] SW-4411 Rework planting site notification schema (#1474) We're about to add several more reminder notifications for planting sites, and it will start to get a little unwieldy to have a separate column in the planting sites table to keep track of each one. Create a new table `planting_site_notifications` that tracks how many of each type of notification we've sent for a planting site. This models the reminder notifications a bit differently than before: they are considered to have the same notification type but a different "notification number" where the first notification about something is number 1, the first reminder is number 2, etc. --- .../com/terraformation/backend/jooq/Config.kt | 1 + .../backend/tracking/ObservationService.kt | 3 +- .../backend/tracking/db/PlantingSiteStore.kt | 21 ++-- .../tracking/model/NotificationCriteria.kt | 75 +++++++----- .../0200/V225__PlantingSiteNotifications.sql | 55 +++++++++ .../resources/db/migration/R__Comments.sql | 3 + .../resources/db/migration/R__TypeCodes.sql | 3 +- src/main/resources/i18n/Enums_en.properties | 1 + .../terraformation/backend/db/DatabaseTest.kt | 29 +++++ .../backend/db/SchemaDocsGenerator.kt | 1 + .../tracking/ObservationServiceTest.kt | 108 ++++++++++++------ .../tracking/db/PlantingSiteStoreTest.kt | 13 +-- 12 files changed, 231 insertions(+), 82 deletions(-) create mode 100644 src/main/resources/db/migration/0200/V225__PlantingSiteNotifications.sql diff --git a/jooq/src/main/kotlin/com/terraformation/backend/jooq/Config.kt b/jooq/src/main/kotlin/com/terraformation/backend/jooq/Config.kt index 5436ba15c436..63cd8088cc64 100644 --- a/jooq/src/main/kotlin/com/terraformation/backend/jooq/Config.kt +++ b/jooq/src/main/kotlin/com/terraformation/backend/jooq/Config.kt @@ -210,6 +210,7 @@ val ID_WRAPPERS = "planting_sites\\.id", "planting_site_summaries\\.id", ".*\\.planting_site_id")), + IdWrapper("PlantingSiteNotificationId", listOf("planting_site_notifications\\.id")), IdWrapper("PlantingZoneId", listOf("planting_zones\\.id", ".*\\.planting_zone_id")), IdWrapper("PlantingSubzoneId", listOf("planting_subzones\\.id", ".*\\.planting_subzone_id")), IdWrapper("RecordedPlantId", listOf("recorded_plants\\.id")), diff --git a/src/main/kotlin/com/terraformation/backend/tracking/ObservationService.kt b/src/main/kotlin/com/terraformation/backend/tracking/ObservationService.kt index 4b31504ca809..b67a50d93e42 100644 --- a/src/main/kotlin/com/terraformation/backend/tracking/ObservationService.kt +++ b/src/main/kotlin/com/terraformation/backend/tracking/ObservationService.kt @@ -227,7 +227,8 @@ class ObservationService( manageNotifications() } - plantingSiteStore.markNotificationComplete(plantingSiteId, criteria.notificationCompletedField) + plantingSiteStore.markNotificationComplete( + plantingSiteId, criteria.notificationType, criteria.notificationNumber) } /** diff --git a/src/main/kotlin/com/terraformation/backend/tracking/db/PlantingSiteStore.kt b/src/main/kotlin/com/terraformation/backend/tracking/db/PlantingSiteStore.kt index aacc1cb7fc86..6c2b59e244bb 100644 --- a/src/main/kotlin/com/terraformation/backend/tracking/db/PlantingSiteStore.kt +++ b/src/main/kotlin/com/terraformation/backend/tracking/db/PlantingSiteStore.kt @@ -7,6 +7,7 @@ import com.terraformation.backend.customer.model.requirePermissions import com.terraformation.backend.db.ProjectInDifferentOrganizationException import com.terraformation.backend.db.ProjectNotFoundException import com.terraformation.backend.db.asNonNullable +import com.terraformation.backend.db.default_schema.NotificationType import com.terraformation.backend.db.default_schema.OrganizationId import com.terraformation.backend.db.default_schema.ProjectId import com.terraformation.backend.db.forMultiset @@ -21,10 +22,10 @@ import com.terraformation.backend.db.tracking.tables.daos.PlantingZonesDao import com.terraformation.backend.db.tracking.tables.pojos.PlantingSitesRow import com.terraformation.backend.db.tracking.tables.pojos.PlantingSubzonesRow import com.terraformation.backend.db.tracking.tables.pojos.PlantingZonesRow -import com.terraformation.backend.db.tracking.tables.records.PlantingSitesRecord import com.terraformation.backend.db.tracking.tables.references.MONITORING_PLOTS import com.terraformation.backend.db.tracking.tables.references.PLANTINGS import com.terraformation.backend.db.tracking.tables.references.PLANTING_SITES +import com.terraformation.backend.db.tracking.tables.references.PLANTING_SITE_NOTIFICATIONS import com.terraformation.backend.db.tracking.tables.references.PLANTING_SITE_POPULATIONS import com.terraformation.backend.db.tracking.tables.references.PLANTING_SUBZONES import com.terraformation.backend.db.tracking.tables.references.PLANTING_ZONES @@ -47,7 +48,6 @@ import org.jooq.Condition import org.jooq.DSLContext import org.jooq.Field import org.jooq.Record -import org.jooq.TableField import org.jooq.impl.DSL import org.locationtech.jts.geom.MultiPolygon import org.locationtech.jts.geom.Polygon @@ -474,18 +474,23 @@ class PlantingSiteStore( fun markNotificationComplete( plantingSiteId: PlantingSiteId, - notificationProperty: TableField + notificationType: NotificationType, + notificationNumber: Int, ) { requirePermissions { readPlantingSite(plantingSiteId) manageNotifications() } - dslContext - .update(PLANTING_SITES) - .set(notificationProperty, clock.instant()) - .where(PLANTING_SITES.ID.eq(plantingSiteId)) - .execute() + with(PLANTING_SITE_NOTIFICATIONS) { + dslContext + .insertInto(PLANTING_SITE_NOTIFICATIONS) + .set(PLANTING_SITE_ID, plantingSiteId) + .set(NOTIFICATION_TYPE_ID, notificationType) + .set(NOTIFICATION_NUMBER, notificationNumber) + .set(SENT_TIME, clock.instant()) + .execute() + } } /** diff --git a/src/main/kotlin/com/terraformation/backend/tracking/model/NotificationCriteria.kt b/src/main/kotlin/com/terraformation/backend/tracking/model/NotificationCriteria.kt index 42ec944054c4..87d179f86452 100644 --- a/src/main/kotlin/com/terraformation/backend/tracking/model/NotificationCriteria.kt +++ b/src/main/kotlin/com/terraformation/backend/tracking/model/NotificationCriteria.kt @@ -1,74 +1,89 @@ package com.terraformation.backend.tracking.model +import com.terraformation.backend.db.default_schema.NotificationType import com.terraformation.backend.db.tracking.PlantingSiteId -import com.terraformation.backend.db.tracking.tables.records.PlantingSitesRecord import com.terraformation.backend.db.tracking.tables.references.PLANTING_SITES +import com.terraformation.backend.db.tracking.tables.references.PLANTING_SITE_NOTIFICATIONS import com.terraformation.backend.tracking.event.ObservationNotScheduledNotificationEvent import com.terraformation.backend.tracking.event.ObservationSchedulingNotificationEvent import com.terraformation.backend.tracking.event.ScheduleObservationNotificationEvent import com.terraformation.backend.tracking.event.ScheduleObservationReminderNotificationEvent -import java.time.Instant import org.jooq.Condition -import org.jooq.TableField import org.jooq.impl.DSL class NotificationCriteria { - sealed interface ObservationScheduling { val completedTimeElapsedWeeks: Long val firstPlantingElapsedWeeks: Long - val notificationNotCompletedCondition: Condition - val notificationCompletedField: TableField + val notificationType: NotificationType + val notificationNumber: Int fun notificationEvent(plantingSiteId: PlantingSiteId): ObservationSchedulingNotificationEvent + + val notificationNotCompletedCondition: Condition + get() { + val thisNotificationNotSent = + DSL.notExists( + DSL.selectOne() + .from(PLANTING_SITE_NOTIFICATIONS) + .where(PLANTING_SITES.ID.eq(PLANTING_SITE_NOTIFICATIONS.PLANTING_SITE_ID)) + .and(PLANTING_SITE_NOTIFICATIONS.NOTIFICATION_TYPE_ID.eq(notificationType)) + .and(PLANTING_SITE_NOTIFICATIONS.NOTIFICATION_NUMBER.ge(notificationNumber))) + + return if (notificationNumber > 1) { + val previousNotificationSent = + DSL.exists( + DSL.selectOne() + .from(PLANTING_SITE_NOTIFICATIONS) + .where(PLANTING_SITES.ID.eq(PLANTING_SITE_NOTIFICATIONS.PLANTING_SITE_ID)) + .and(PLANTING_SITE_NOTIFICATIONS.NOTIFICATION_TYPE_ID.eq(notificationType)) + .and( + PLANTING_SITE_NOTIFICATIONS.NOTIFICATION_NUMBER.eq( + notificationNumber - 1))) + + DSL.and(previousNotificationSent, thisNotificationNotSent) + } else { + thisNotificationNotSent + } + } } - object ScheduleObservations : ObservationScheduling { + data object ScheduleObservations : ObservationScheduling { override val completedTimeElapsedWeeks: Long = 2 override val firstPlantingElapsedWeeks: Long = 0 - override val notificationNotCompletedCondition: Condition = - DSL.condition(PLANTING_SITES.SCHEDULE_OBSERVATION_NOTIFICATION_SENT_TIME.isNull) - override val notificationCompletedField: TableField = - PLANTING_SITES.SCHEDULE_OBSERVATION_NOTIFICATION_SENT_TIME + override val notificationType: NotificationType = NotificationType.ScheduleObservation + override val notificationNumber: Int = 1 override fun notificationEvent(plantingSiteId: PlantingSiteId) = ScheduleObservationNotificationEvent(plantingSiteId) } - object RemindSchedulingObservations : ObservationScheduling { + data object RemindSchedulingObservations : ObservationScheduling { override val completedTimeElapsedWeeks: Long = 6 override val firstPlantingElapsedWeeks: Long = 4 - override val notificationNotCompletedCondition: Condition = - DSL.condition(PLANTING_SITES.SCHEDULE_OBSERVATION_NOTIFICATION_SENT_TIME.isNotNull) - .and(PLANTING_SITES.SCHEDULE_OBSERVATION_REMINDER_NOTIFICATION_SENT_TIME.isNull) - override val notificationCompletedField: TableField = - PLANTING_SITES.SCHEDULE_OBSERVATION_REMINDER_NOTIFICATION_SENT_TIME + override val notificationType: NotificationType = NotificationType.ScheduleObservation + override val notificationNumber: Int = 2 override fun notificationEvent(plantingSiteId: PlantingSiteId) = ScheduleObservationReminderNotificationEvent(plantingSiteId) } - object ObservationNotScheduledFirstNotification : ObservationScheduling { + data object ObservationNotScheduledFirstNotification : ObservationScheduling { override val completedTimeElapsedWeeks: Long = 8 override val firstPlantingElapsedWeeks: Long = 6 - override val notificationNotCompletedCondition: Condition = - DSL.condition(PLANTING_SITES.OBSERVATION_NOT_SCHEDULED_FIRST_NOTIFICATION_SENT_TIME.isNull) - override val notificationCompletedField: TableField = - PLANTING_SITES.OBSERVATION_NOT_SCHEDULED_FIRST_NOTIFICATION_SENT_TIME + override val notificationType: NotificationType = + NotificationType.ObservationNotScheduledSupport + override val notificationNumber: Int = 1 override fun notificationEvent(plantingSiteId: PlantingSiteId) = ObservationNotScheduledNotificationEvent(plantingSiteId) } - object ObservationNotScheduledSecondNotification : ObservationScheduling { + data object ObservationNotScheduledSecondNotification : ObservationScheduling { override val completedTimeElapsedWeeks: Long = 16 override val firstPlantingElapsedWeeks: Long = 14 - override val notificationNotCompletedCondition: Condition = - DSL.condition( - PLANTING_SITES.OBSERVATION_NOT_SCHEDULED_FIRST_NOTIFICATION_SENT_TIME.isNotNull, - ) - .and(PLANTING_SITES.OBSERVATION_NOT_SCHEDULED_SECOND_NOTIFICATION_SENT_TIME.isNull) - override val notificationCompletedField: TableField = - PLANTING_SITES.OBSERVATION_NOT_SCHEDULED_SECOND_NOTIFICATION_SENT_TIME + override val notificationType: NotificationType = + NotificationType.ObservationNotScheduledSupport + override val notificationNumber: Int = 2 override fun notificationEvent(plantingSiteId: PlantingSiteId) = ObservationNotScheduledNotificationEvent(plantingSiteId) diff --git a/src/main/resources/db/migration/0200/V225__PlantingSiteNotifications.sql b/src/main/resources/db/migration/0200/V225__PlantingSiteNotifications.sql new file mode 100644 index 000000000000..fc0fc4437132 --- /dev/null +++ b/src/main/resources/db/migration/0200/V225__PlantingSiteNotifications.sql @@ -0,0 +1,55 @@ +CREATE TABLE tracking.planting_site_notifications ( + id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + planting_site_id BIGINT NOT NULL REFERENCES tracking.planting_sites ON DELETE CASCADE, + notification_type_id INTEGER NOT NULL REFERENCES notification_types, + notification_number INTEGER NOT NULL, + sent_time TIMESTAMP WITH TIME ZONE NOT NULL, + + UNIQUE (planting_site_id, notification_type_id, notification_number) +); + +-- These are also in R__TypeCodes.sql +INSERT INTO notification_criticalities (id, name) +VALUES (1, 'Info') +ON CONFLICT DO NOTHING; +INSERT INTO notification_types (id, name, notification_criticality_id) +VALUES (21, 'Observation Not Scheduled (Support)', 1); + +INSERT INTO tracking.planting_site_notifications + (planting_site_id, notification_type_id, notification_number, sent_time) +SELECT ps.id, nt.id, 1, ps.schedule_observation_notification_sent_time +FROM tracking.planting_sites ps, notification_types nt +WHERE ps.schedule_observation_notification_sent_time IS NOT NULL +AND nt.name = 'Schedule Observation'; + +INSERT INTO tracking.planting_site_notifications + (planting_site_id, notification_type_id, notification_number, sent_time) +SELECT ps.id, nt.id, 2, ps.schedule_observation_reminder_notification_sent_time +FROM tracking.planting_sites ps, notification_types nt +WHERE ps.schedule_observation_reminder_notification_sent_time IS NOT NULL +AND nt.name = 'Schedule Observation'; + +INSERT INTO tracking.planting_site_notifications + (planting_site_id, notification_type_id, notification_number, sent_time) +SELECT ps.id, nt.id, 1, ps.observation_not_scheduled_first_notification_sent_time +FROM tracking.planting_sites ps, notification_types nt +WHERE ps.observation_not_scheduled_first_notification_sent_time IS NOT NULL +AND nt.name = 'Observation Not Scheduled (Support)'; + +INSERT INTO tracking.planting_site_notifications + (planting_site_id, notification_type_id, notification_number, sent_time) +SELECT ps.id, nt.id, 2, ps.observation_not_scheduled_second_notification_sent_time +FROM tracking.planting_sites ps, notification_types nt +WHERE ps.observation_not_scheduled_second_notification_sent_time IS NOT NULL +AND nt.name = 'Observation Not Scheduled (Support)'; + +-- Usually we would drop columns in a separate migration to avoid breaking the old code while the +-- new release is being rolled out, but in this case we do it all in one migration. +-- +-- If notifications are being generated on another instance while this migration is running, the +-- notification job will fail because we're dropping these columns. The next run will generate +-- any missing notifications. +ALTER TABLE tracking.planting_sites DROP COLUMN observation_not_scheduled_first_notification_sent_time; +ALTER TABLE tracking.planting_sites DROP COLUMN observation_not_scheduled_second_notification_sent_time; +ALTER TABLE tracking.planting_sites DROP COLUMN schedule_observation_notification_sent_time; +ALTER TABLE tracking.planting_sites DROP COLUMN schedule_observation_reminder_notification_sent_time; diff --git a/src/main/resources/db/migration/R__Comments.sql b/src/main/resources/db/migration/R__Comments.sql index 35e1a5a5bc83..b9e6d95599db 100644 --- a/src/main/resources/db/migration/R__Comments.sql +++ b/src/main/resources/db/migration/R__Comments.sql @@ -343,6 +343,9 @@ COMMENT ON COLUMN tracking.observed_zone_species_totals.cumulative_dead IS 'Tota COMMENT ON COLUMN tracking.observed_zone_species_totals.mortality_rate IS 'Percentage of plants of the species observed in permanent monitoring plots in the planting zone, in either the current observation or in previous ones, that were dead.'; COMMENT ON COLUMN tracking.observed_zone_species_totals.permanent_live IS 'The number of live and existing plants observed in permanent monitoring plots.'; +COMMENT ON TABLE tracking.planting_site_notifications IS 'Tracks which notifications have already been sent regarding planting sites.'; +COMMENT ON COLUMN tracking.planting_site_notifications.notification_number IS 'Number of notifications of this type that have been sent, including this one. 1 for initial notification, 2 for reminder, 3 for second reminder, etc.'; + COMMENT ON TABLE tracking.planting_site_populations IS 'Total number of plants of each species in each planting site.'; COMMENT ON TABLE tracking.planting_sites IS 'Top-level information about entire planting sites. Every planting site has at least one planting zone.'; diff --git a/src/main/resources/db/migration/R__TypeCodes.sql b/src/main/resources/db/migration/R__TypeCodes.sql index ca90b75d1696..f5c1b5d11200 100644 --- a/src/main/resources/db/migration/R__TypeCodes.sql +++ b/src/main/resources/db/migration/R__TypeCodes.sql @@ -129,7 +129,8 @@ VALUES (1, 'User Added to Organization', 1), (17, 'Observation Upcoming', 1), (18, 'Observation Started', 1), (19, 'Schedule Observation', 1), - (20, 'Schedule Observation Reminder', 1) + (20, 'Schedule Observation Reminder', 1), + (21, 'Observation Not Scheduled (Support)', 1) ON CONFLICT (id) DO UPDATE SET name = excluded.name, notification_criticality_id = excluded.notification_criticality_id; diff --git a/src/main/resources/i18n/Enums_en.properties b/src/main/resources/i18n/Enums_en.properties index 6805117eb966..42b1ff29a75c 100644 --- a/src/main/resources/i18n/Enums_en.properties +++ b/src/main/resources/i18n/Enums_en.properties @@ -74,6 +74,7 @@ public.NotificationType.DeviceUnresponsive=Device Unresponsive public.NotificationType.FacilityAlertRequested=Facility Alert Requested public.NotificationType.FacilityIdle=Facility Idle public.NotificationType.NurserySeedlingBatchReady=Nursery Seedling Batch Ready +public.NotificationType.ObservationNotScheduledSupport=Observation Not Scheduled (Support) public.NotificationType.ObservationStarted=Observation Started public.NotificationType.ObservationUpcoming=Observation Upcoming public.NotificationType.ReportCreated=Report Created diff --git a/src/test/kotlin/com/terraformation/backend/db/DatabaseTest.kt b/src/test/kotlin/com/terraformation/backend/db/DatabaseTest.kt index 1013bc226f51..6fe3c4d8990f 100644 --- a/src/test/kotlin/com/terraformation/backend/db/DatabaseTest.kt +++ b/src/test/kotlin/com/terraformation/backend/db/DatabaseTest.kt @@ -109,6 +109,7 @@ import com.terraformation.backend.db.tracking.ObservationState import com.terraformation.backend.db.tracking.ObservedPlotCoordinatesId import com.terraformation.backend.db.tracking.PlantingId import com.terraformation.backend.db.tracking.PlantingSiteId +import com.terraformation.backend.db.tracking.PlantingSiteNotificationId import com.terraformation.backend.db.tracking.PlantingSubzoneId import com.terraformation.backend.db.tracking.PlantingType import com.terraformation.backend.db.tracking.PlantingZoneId @@ -119,6 +120,7 @@ import com.terraformation.backend.db.tracking.tables.daos.ObservationPlotConditi import com.terraformation.backend.db.tracking.tables.daos.ObservationPlotsDao import com.terraformation.backend.db.tracking.tables.daos.ObservationsDao import com.terraformation.backend.db.tracking.tables.daos.ObservedPlotCoordinatesDao +import com.terraformation.backend.db.tracking.tables.daos.PlantingSiteNotificationsDao import com.terraformation.backend.db.tracking.tables.daos.PlantingSitePopulationsDao import com.terraformation.backend.db.tracking.tables.daos.PlantingSitesDao import com.terraformation.backend.db.tracking.tables.daos.PlantingSubzonePopulationsDao @@ -133,6 +135,7 @@ import com.terraformation.backend.db.tracking.tables.pojos.ObservationPhotosRow import com.terraformation.backend.db.tracking.tables.pojos.ObservationPlotsRow import com.terraformation.backend.db.tracking.tables.pojos.ObservationsRow import com.terraformation.backend.db.tracking.tables.pojos.ObservedPlotCoordinatesRow +import com.terraformation.backend.db.tracking.tables.pojos.PlantingSiteNotificationsRow import com.terraformation.backend.db.tracking.tables.pojos.PlantingSitePopulationsRow import com.terraformation.backend.db.tracking.tables.pojos.PlantingSitesRow import com.terraformation.backend.db.tracking.tables.pojos.PlantingSubzonePopulationsRow @@ -331,6 +334,7 @@ abstract class DatabaseTest { protected val organizationsDao: OrganizationsDao by lazyDao() protected val organizationUsersDao: OrganizationUsersDao by lazyDao() protected val plantingsDao: PlantingsDao by lazyDao() + protected val plantingSiteNotificationsDao: PlantingSiteNotificationsDao by lazyDao() protected val plantingSitePopulationsDao: PlantingSitePopulationsDao by lazyDao() protected val plantingSitesDao: PlantingSitesDao by lazyDao() protected val plantingSubzonePopulationsDao: PlantingSubzonePopulationsDao by lazyDao() @@ -1001,6 +1005,28 @@ abstract class DatabaseTest { return rowWithDefaults.id!!.also { inserted.plantingSiteIds.add(it) } } + fun insertPlantingSiteNotification( + row: PlantingSiteNotificationsRow = PlantingSiteNotificationsRow(), + id: Any? = row.id, + number: Int = row.notificationNumber ?: 1, + plantingSiteId: Any = row.plantingSiteId ?: inserted.plantingSiteId, + sentTime: Instant = row.sentTime ?: Instant.EPOCH, + type: NotificationType, + ): PlantingSiteNotificationId { + val rowWithDefaults = + row.copy( + id = id?.toIdWrapper { PlantingSiteNotificationId(it) }, + notificationNumber = number, + notificationTypeId = type, + plantingSiteId = plantingSiteId.toIdWrapper { PlantingSiteId(it) }, + sentTime = sentTime, + ) + + plantingSiteNotificationsDao.insert(rowWithDefaults) + + return rowWithDefaults.id!!.also { inserted.plantingSiteNotificationIds.add(it) } + } + private var nextPlantingZoneNumber: Int = 1 fun insertPlantingZone( @@ -1460,6 +1486,7 @@ abstract class DatabaseTest { val organizationIds = mutableListOf() val plantingIds = mutableListOf() val plantingSiteIds = mutableListOf() + val plantingSiteNotificationIds = mutableListOf() val plantingSubzoneIds = mutableListOf() val plantingZoneIds = mutableListOf() val projectIds = mutableListOf() @@ -1496,6 +1523,8 @@ abstract class DatabaseTest { get() = plantingIds.last() val plantingSiteId get() = plantingSiteIds.last() + val plantingSiteNotificationId + get() = plantingSiteNotificationIds.last() val plantingSubzoneId get() = plantingSubzoneIds.last() val plantingZoneId diff --git a/src/test/kotlin/com/terraformation/backend/db/SchemaDocsGenerator.kt b/src/test/kotlin/com/terraformation/backend/db/SchemaDocsGenerator.kt index 601cd5ef3666..05b87833d371 100644 --- a/src/test/kotlin/com/terraformation/backend/db/SchemaDocsGenerator.kt +++ b/src/test/kotlin/com/terraformation/backend/db/SchemaDocsGenerator.kt @@ -239,6 +239,7 @@ class SchemaDocsGenerator : DatabaseTest() { "observed_site_species_totals" to setOf(ALL, TRACKING), "observed_zone_species_totals" to setOf(ALL, TRACKING), "planting_types" to setOf(ALL, TRACKING), + "planting_site_notifications" to setOf(ALL, TRACKING), "planting_site_populations" to setOf(ALL, TRACKING), "planting_sites" to setOf(ALL, TRACKING), "planting_zone_populations" to setOf(ALL, TRACKING), diff --git a/src/test/kotlin/com/terraformation/backend/tracking/ObservationServiceTest.kt b/src/test/kotlin/com/terraformation/backend/tracking/ObservationServiceTest.kt index 4f433a1f08bb..8055a1e4415f 100644 --- a/src/test/kotlin/com/terraformation/backend/tracking/ObservationServiceTest.kt +++ b/src/test/kotlin/com/terraformation/backend/tracking/ObservationServiceTest.kt @@ -9,6 +9,7 @@ import com.terraformation.backend.db.DatabaseTest import com.terraformation.backend.db.FileNotFoundException import com.terraformation.backend.db.default_schema.FacilityType import com.terraformation.backend.db.default_schema.FileId +import com.terraformation.backend.db.default_schema.NotificationType import com.terraformation.backend.db.tracking.MonitoringPlotId import com.terraformation.backend.db.tracking.ObservationId import com.terraformation.backend.db.tracking.ObservationPlotPosition @@ -16,7 +17,6 @@ import com.terraformation.backend.db.tracking.ObservationState import com.terraformation.backend.db.tracking.PlantingSiteId import com.terraformation.backend.db.tracking.tables.pojos.ObservationPhotosRow import com.terraformation.backend.db.tracking.tables.pojos.ObservationsRow -import com.terraformation.backend.db.tracking.tables.pojos.PlantingSitesRow import com.terraformation.backend.db.tracking.tables.pojos.PlantingsRow import com.terraformation.backend.file.FileService import com.terraformation.backend.file.FileStore @@ -875,9 +875,8 @@ class ObservationServiceTest : DatabaseTest(), RunsAsUser { fun `returns site with subzone plantings planted earlier than 4 weeks and no completed observations`() { insertFacility(type = FacilityType.Nursery) insertSpecies() - val insertedPlantingSiteId = - insertPlantingSite( - PlantingSitesRow(scheduleObservationNotificationSentTime = Instant.EPOCH)) + val insertedPlantingSiteId = insertPlantingSite() + insertPlantingSiteNotification(type = NotificationType.ScheduleObservation) insertWithdrawal() insertDelivery() @@ -894,7 +893,8 @@ class ObservationServiceTest : DatabaseTest(), RunsAsUser { fun `returns empty results with observations completed more recent than six weeks`() { insertFacility(type = FacilityType.Nursery) insertSpecies() - insertPlantingSite(PlantingSitesRow(scheduleObservationNotificationSentTime = Instant.EPOCH)) + insertPlantingSite() + insertPlantingSiteNotification(type = NotificationType.ScheduleObservation) insertWithdrawal() insertDelivery() @@ -912,7 +912,8 @@ class ObservationServiceTest : DatabaseTest(), RunsAsUser { fun `returns empty results with observations completed earlier than six weeks but with other observations scheduled`() { insertFacility(type = FacilityType.Nursery) insertSpecies() - insertPlantingSite(PlantingSitesRow(scheduleObservationNotificationSentTime = Instant.EPOCH)) + insertPlantingSite() + insertPlantingSiteNotification(type = NotificationType.ScheduleObservation) insertWithdrawal() insertDelivery() @@ -933,9 +934,8 @@ class ObservationServiceTest : DatabaseTest(), RunsAsUser { insertFacility(type = FacilityType.Nursery) insertSpecies() - val plantingSiteIdWithCompletedObservation = - insertPlantingSite( - PlantingSitesRow(scheduleObservationNotificationSentTime = Instant.EPOCH)) + val plantingSiteIdWithCompletedObservation = insertPlantingSite() + insertPlantingSiteNotification(type = NotificationType.ScheduleObservation) insertWithdrawal() insertDelivery() @@ -947,9 +947,8 @@ class ObservationServiceTest : DatabaseTest(), RunsAsUser { ObservationsRow(completedTime = Instant.EPOCH.minus(6 * 7, ChronoUnit.DAYS)), state = ObservationState.Completed) - val anotherPlantingSiteIdWithCompletedObservation = - insertPlantingSite( - PlantingSitesRow(scheduleObservationNotificationSentTime = Instant.EPOCH)) + val anotherPlantingSiteIdWithCompletedObservation = insertPlantingSite() + insertPlantingSiteNotification(type = NotificationType.ScheduleObservation) insertWithdrawal() insertDelivery() @@ -962,7 +961,8 @@ class ObservationServiceTest : DatabaseTest(), RunsAsUser { state = ObservationState.Completed) // planting site with a more recent completion - insertPlantingSite(PlantingSitesRow(scheduleObservationNotificationSentTime = Instant.EPOCH)) + insertPlantingSite() + insertPlantingSiteNotification(type = NotificationType.ScheduleObservation) insertWithdrawal() insertDelivery() @@ -973,9 +973,8 @@ class ObservationServiceTest : DatabaseTest(), RunsAsUser { insertObservation( ObservationsRow(completedTime = Instant.EPOCH), state = ObservationState.Completed) - val plantingSiteIdWithPlantings = - insertPlantingSite( - PlantingSitesRow(scheduleObservationNotificationSentTime = Instant.EPOCH)) + val plantingSiteIdWithPlantings = insertPlantingSite() + insertPlantingSiteNotification(type = NotificationType.ScheduleObservation) insertWithdrawal() insertDelivery() @@ -1056,6 +1055,28 @@ class ObservationServiceTest : DatabaseTest(), RunsAsUser { assert(service.fetchNonNotifiedSitesToNotifySchedulingObservations(criteria).isEmpty()) } + @Test + fun `returns empty results if notification already sent`() { + insertFacility(type = FacilityType.Nursery) + insertSpecies() + + insertPlantingSite() + insertPlantingSiteNotification(type = NotificationType.ObservationNotScheduledSupport) + insertWithdrawal() + insertDelivery() + + insertPlantingZone(numPermanentClusters = 2, numTemporaryPlots = 3) + insertPlantingSubzone() + insertPlanting() + + insertObservation( + ObservationsRow(completedTime = Instant.EPOCH.minus(8 * 7, ChronoUnit.DAYS)), + state = ObservationState.Completed) + + assertEquals( + emptyList(), service.fetchNonNotifiedSitesToNotifySchedulingObservations(criteria)) + } + @Test fun `returns sites with observations completed earlier than eight weeks or have sub zone plantings`() { insertFacility(type = FacilityType.Nursery) @@ -1142,9 +1163,8 @@ class ObservationServiceTest : DatabaseTest(), RunsAsUser { fun `returns site with subzone plantings planted earlier than 14 weeks and no completed observations`() { insertFacility(type = FacilityType.Nursery) insertSpecies() - val insertedPlantingSiteId = - insertPlantingSite( - PlantingSitesRow(observationNotScheduledFirstNotificationSentTime = Instant.EPOCH)) + val insertedPlantingSiteId = insertPlantingSite() + insertPlantingSiteNotification(type = NotificationType.ObservationNotScheduledSupport) insertWithdrawal() insertDelivery() @@ -1161,8 +1181,8 @@ class ObservationServiceTest : DatabaseTest(), RunsAsUser { fun `returns empty results with observations completed more recent than sixteen weeks`() { insertFacility(type = FacilityType.Nursery) insertSpecies() - insertPlantingSite( - PlantingSitesRow(observationNotScheduledFirstNotificationSentTime = Instant.EPOCH)) + insertPlantingSite() + insertPlantingSiteNotification(type = NotificationType.ObservationNotScheduledSupport) insertWithdrawal() insertDelivery() @@ -1180,8 +1200,8 @@ class ObservationServiceTest : DatabaseTest(), RunsAsUser { fun `returns empty results with observations completed earlier than sixteen weeks but with other observations scheduled`() { insertFacility(type = FacilityType.Nursery) insertSpecies() - insertPlantingSite( - PlantingSitesRow(observationNotScheduledFirstNotificationSentTime = Instant.EPOCH)) + insertPlantingSite() + insertPlantingSiteNotification(type = NotificationType.ObservationNotScheduledSupport) insertWithdrawal() insertDelivery() @@ -1197,14 +1217,36 @@ class ObservationServiceTest : DatabaseTest(), RunsAsUser { assert(service.fetchNonNotifiedSitesToNotifySchedulingObservations(criteria).isEmpty()) } + @Test + fun `returns empty results if notification already sent`() { + insertFacility(type = FacilityType.Nursery) + insertSpecies() + + insertPlantingSite() + insertPlantingSiteNotification( + type = NotificationType.ObservationNotScheduledSupport, number = 2) + insertWithdrawal() + insertDelivery() + + insertPlantingZone(numPermanentClusters = 2, numTemporaryPlots = 3) + insertPlantingSubzone() + insertPlanting() + + insertObservation( + ObservationsRow(completedTime = Instant.EPOCH.minus(16 * 7, ChronoUnit.DAYS)), + state = ObservationState.Completed) + + assertEquals( + emptyList(), service.fetchNonNotifiedSitesToNotifySchedulingObservations(criteria)) + } + @Test fun `returns sites with observations completed earlier than sixteen weeks or have sub zone plantings`() { insertFacility(type = FacilityType.Nursery) insertSpecies() - val plantingSiteIdWithCompletedObservation = - insertPlantingSite( - PlantingSitesRow(observationNotScheduledFirstNotificationSentTime = Instant.EPOCH)) + val plantingSiteIdWithCompletedObservation = insertPlantingSite() + insertPlantingSiteNotification(type = NotificationType.ObservationNotScheduledSupport) insertWithdrawal() insertDelivery() @@ -1216,9 +1258,8 @@ class ObservationServiceTest : DatabaseTest(), RunsAsUser { ObservationsRow(completedTime = Instant.EPOCH.minus(16 * 7, ChronoUnit.DAYS)), state = ObservationState.Completed) - val anotherPlantingSiteIdWithCompletedObservation = - insertPlantingSite( - PlantingSitesRow(observationNotScheduledFirstNotificationSentTime = Instant.EPOCH)) + val anotherPlantingSiteIdWithCompletedObservation = insertPlantingSite() + insertPlantingSiteNotification(type = NotificationType.ObservationNotScheduledSupport) insertWithdrawal() insertDelivery() @@ -1231,8 +1272,8 @@ class ObservationServiceTest : DatabaseTest(), RunsAsUser { state = ObservationState.Completed) // planting site with a more recent completion - insertPlantingSite( - PlantingSitesRow(observationNotScheduledFirstNotificationSentTime = Instant.EPOCH)) + insertPlantingSite() + insertPlantingSiteNotification(type = NotificationType.ObservationNotScheduledSupport) insertWithdrawal() insertDelivery() @@ -1243,9 +1284,8 @@ class ObservationServiceTest : DatabaseTest(), RunsAsUser { insertObservation( ObservationsRow(completedTime = Instant.EPOCH), state = ObservationState.Completed) - val plantingSiteIdWithPlantings = - insertPlantingSite( - PlantingSitesRow(observationNotScheduledFirstNotificationSentTime = Instant.EPOCH)) + val plantingSiteIdWithPlantings = insertPlantingSite() + insertPlantingSiteNotification(type = NotificationType.ObservationNotScheduledSupport) insertWithdrawal() insertDelivery() diff --git a/src/test/kotlin/com/terraformation/backend/tracking/db/PlantingSiteStoreTest.kt b/src/test/kotlin/com/terraformation/backend/tracking/db/PlantingSiteStoreTest.kt index 3555c36dbf75..aacd090f0570 100644 --- a/src/test/kotlin/com/terraformation/backend/tracking/db/PlantingSiteStoreTest.kt +++ b/src/test/kotlin/com/terraformation/backend/tracking/db/PlantingSiteStoreTest.kt @@ -8,6 +8,7 @@ import com.terraformation.backend.customer.event.PlantingSiteTimeZoneChangedEven import com.terraformation.backend.db.DatabaseTest import com.terraformation.backend.db.ProjectInDifferentOrganizationException import com.terraformation.backend.db.default_schema.FacilityType +import com.terraformation.backend.db.default_schema.NotificationType import com.terraformation.backend.db.default_schema.OrganizationId import com.terraformation.backend.db.default_schema.UserId import com.terraformation.backend.db.nursery.WithdrawalPurpose @@ -818,7 +819,7 @@ internal class PlantingSiteStoreTest : DatabaseTest(), RunsAsUser { @Nested inner class MarkSchedulingObservationsNotificationComplete { @Test - fun `updates notification sent timestamp`() { + fun `records notification sent time`() { val plantingSiteId = insertPlantingSite() every { user.canReadPlantingSite(plantingSiteId) } returns true @@ -826,12 +827,9 @@ internal class PlantingSiteStoreTest : DatabaseTest(), RunsAsUser { clock.instant = Instant.ofEpochSecond(1234) - store.markNotificationComplete( - plantingSiteId, PLANTING_SITES.SCHEDULE_OBSERVATION_NOTIFICATION_SENT_TIME) + store.markNotificationComplete(plantingSiteId, NotificationType.ScheduleObservation, 1) - assertEquals( - clock.instant, - plantingSitesDao.fetchOneById(plantingSiteId)?.scheduleObservationNotificationSentTime) + assertEquals(clock.instant, plantingSiteNotificationsDao.findAll().single().sentTime) } @Test @@ -842,8 +840,7 @@ internal class PlantingSiteStoreTest : DatabaseTest(), RunsAsUser { every { user.canManageNotifications() } returns false assertThrows { - store.markNotificationComplete( - plantingSiteId, PLANTING_SITES.SCHEDULE_OBSERVATION_NOTIFICATION_SENT_TIME) + store.markNotificationComplete(plantingSiteId, NotificationType.ScheduleObservation, 1) } } }