Season end nudge: post once per close, not daily for the lead window

SupplyCheckReceiver re-posted the auto-cancel "Forleng" card on every
daily check for the whole 5-day window, so a dismissed nudge came back
each morning. Remember the season-end epoch day nudged per med (plaintext
pref) and skip re-posting for the same close; a new end date (next year,
or after Forleng) still earns one fresh nudge.
This commit is contained in:
Ole-Morten Duesund 2026-08-17 12:35:20 +02:00
commit 4bbb06d5d0
2 changed files with 26 additions and 9 deletions

View file

@ -12,6 +12,7 @@ import no.naiv.meddetsamme.data.MedDatabase
import no.naiv.meddetsamme.domain.ScheduleEngine import no.naiv.meddetsamme.domain.ScheduleEngine
import no.naiv.meddetsamme.domain.ScheduleText import no.naiv.meddetsamme.domain.ScheduleText
import no.naiv.meddetsamme.notify.Notifications import no.naiv.meddetsamme.notify.Notifications
import no.naiv.meddetsamme.settings.SettingsStore
/** /**
* Daily derived-state check: low stock (inventory ÷ scheduled rate) and * Daily derived-state check: low stock (inventory ÷ scheduled rate) and
@ -96,7 +97,7 @@ class SupplyCheckReceiver : BroadcastReceiver() {
/** /**
* Per-med seasonal nudge, post-or-cancel like the digests above: * Per-med seasonal nudge, post-or-cancel like the digests above:
* - resting and within [SEASON_START_LEAD_DAYS] of the next window "Start nå"; * - resting and within [SEASON_START_LEAD_DAYS] of the next window "Start nå";
* - in season and within [SEASON_END_LEAD_DAYS] of its close "Forleng". * - in season and within [SEASON_END_LEAD_DAYS] of its close "Forleng" (once per close).
* Acting on either (or the season simply passing) clears it the next day. * Acting on either (or the season simply passing) clears it the next day.
*/ */
private fun checkSeason( private fun checkSeason(
@ -130,14 +131,20 @@ class SupplyCheckReceiver : BroadcastReceiver() {
val end = ScheduleEngine.currentSeasonEnd(doseTimes, today) val end = ScheduleEngine.currentSeasonEnd(doseTimes, today)
val days = end?.let { ChronoUnit.DAYS.between(today, it) } val days = end?.let { ChronoUnit.DAYS.between(today, it) }
if (end != null && days != null && days in 0..SEASON_END_LEAD_DAYS) { if (end != null && days != null && days in 0..SEASON_END_LEAD_DAYS) {
Notifications.post( // Once per season close, not daily: a dismissed card stays dismissed.
context, notifId, // Keyed on the end date, so "Forleng" (new end) earns one fresh nudge.
Notifications.buildSeasonNotification( val settings = SettingsStore(context)
context, medId, name, if (settings.seasonEndNudgedEpochDay(medId) != end.toEpochDay()) {
"Sesongen er snart over (${ScheduleText.dateLabel(end)}). Fortsatt plaget? Forleng to uker.", Notifications.post(
starting = false, context, notifId,
), Notifications.buildSeasonNotification(
) context, medId, name,
"Sesongen er snart over (${ScheduleText.dateLabel(end)}). Fortsatt plaget? Forleng to uker.",
starting = false,
),
)
settings.setSeasonEndNudgedEpochDay(medId, end.toEpochDay())
}
} else { } else {
Notifications.cancel(context, notifId) Notifications.cancel(context, notifId)
} }

View file

@ -66,6 +66,16 @@ class SettingsStore(context: Context) {
get() = prefs.getBoolean("repeat_reminders", true) get() = prefs.getBoolean("repeat_reminders", true)
set(v) = prefs.edit { putBoolean("repeat_reminders", v) } set(v) = prefs.edit { putBoolean("repeat_reminders", v) }
/**
* Epoch day of the season close a med was last nudged "Forleng" for. The
* end-of-season card posts once per close, not daily for the whole lead
* window: keyed on the end date so next year's (or an extended) season
* gets a fresh nudge. -1 = never.
*/
fun seasonEndNudgedEpochDay(medId: Long): Long = prefs.getLong("season_end_nudged_$medId", -1)
fun setSeasonEndNudgedEpochDay(medId: Long, endEpochDay: Long) =
prefs.edit { putLong("season_end_nudged_$medId", endEpochDay) }
/** S3/Garage credentials complete — gates the S3 upload path only. */ /** S3/Garage credentials complete — gates the S3 upload path only. */
val isS3Configured: Boolean val isS3Configured: Boolean
get() = !s3Endpoint.isNullOrBlank() && !s3Bucket.isNullOrBlank() && get() = !s3Endpoint.isNullOrBlank() && !s3Bucket.isNullOrBlank() &&