feat: Digest sender job, safety cleanup and email assembly - EXO-89539_EXO-89982_EXO-89486_EXO-89487_EXO-90020 - eXIP7.3.0.22 - #775
Conversation
One hourly Spring scheduled job (started by the social webapp context) serves every user whose daily or weekly digest is due in his own timezone: claim by guarded watermark update, email built from the waiting items through the addons' line providers, mail queue, deletion of the covered items, safety cleanup first. The administrator switch gates only the email production: the job never stops and the watermarks keep moving. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- due rule: a watermark from the occurrence's own day blocks it (no midnight send)
- daily and weekly of one user served in sequence by one task
- unreadable template is an error, never an empty email
- PARAMS shrinks by dropping the longest values, keeps the ids
- {i} plain placeholder replacement like the other notification bundles
- watermarks in whole seconds, release outcome logged
- JPQL statements shared as constants and run on the engine in tests
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- ITEM_DATE is the insert moment: a row written during a run belongs to the next window - the run leaves the Spring scheduler thread, one run at a time - DigestLine.of tolerates a null argument - placeholders replaced in one pass, arguments are never re-expanded - the per-user grouping pin now counts the tasks - one stack trace per failure, at debug level Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- job glue tested: skip while running, failure never blocks the next run - oversized placeholder index is text, rejected submission resets the flag - DigestItem date documented as the capture moment Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Addon PRs implementing the |
… - eXIP7.3.0.22 A work list row whose settings show no enabled frequency has no owner any more (the settings vanish with the account): at his next occurrence the job deletes the row and every waiting item instead of serving a ghost forever. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Added (EXO-89982, from Ayoub's test of an account deletion): a user whose digest settings are gone (both frequencies read off while his work list row still exists — the settings are purged with the account) is forgotten by the job at his next occurrence: the NTF_DIGEST_USERS row and every NTF_DIGEST_ITEMS row of the user are deleted, nothing is built. No listener, no separate PR: it is part of the cleanup mechanism of the job. Tests: |
…-89539 - eXIP7.3.0.22 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
….0.22 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
…9_EXO-89982_EXO-89486_EXO-89487 - eXIP7.3.0.22 (#775)
…- eXIP7.3.0.22 (#779) eXIP 7.3.0.22 Digest Mail Notifications — US08 (EXO-89539), spec §4 Timezone: "When a timezone is saved, that service broadcasts an event, and a small digest listener refreshes TIMEZONE in NTF_DIGEST_USERS." This listener was missing from #775; the functional tests showed the copy only refreshed on Apply. ## What it adds - `DigestTimeZoneListener`, a Kernel listener on `social.timeZone.saved` (event broadcast by social's `UserTimeZoneService`, source = username, data = zone id), asynchronous and fail-safe: it hands the new zone to `DigestService.updateTimeZone`, and a failure is logged without affecting the timezone save. commons doesn't depend on social: the event name is the contract, like the capture listener. - `DigestService.updateTimeZone(username, timeZone)` → `DigestEnrollmentStorage.updateTimeZone`: refreshes the copy of an enrolled user, writes nothing for a user without digest or an unchanged zone. - Declared in `conf/portal/configuration.xml` next to the capture listener. Tests: `DigestTimeZoneListenerTest` (event handed over, failure contained), `DigestEnrollmentStorageTest` (refreshed / ignored without digest / unchanged zone). Digest tests green. Note: `DigestEnrollmentStorageTest` is also created by #778 with other methods; the second one to merge takes a trivial file merge, I will handle it. Classification: **N2** (glue listener + one storage method inside the digest engine). Knowledge: none. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…9_EXO-89982_EXO-89486_EXO-89487 - eXIP7.3.0.22 (#775)
…- eXIP7.3.0.22 (#779) eXIP 7.3.0.22 Digest Mail Notifications — US08 (EXO-89539), spec §4 Timezone: "When a timezone is saved, that service broadcasts an event, and a small digest listener refreshes TIMEZONE in NTF_DIGEST_USERS." This listener was missing from #775; the functional tests showed the copy only refreshed on Apply. ## What it adds - `DigestTimeZoneListener`, a Kernel listener on `social.timeZone.saved` (event broadcast by social's `UserTimeZoneService`, source = username, data = zone id), asynchronous and fail-safe: it hands the new zone to `DigestService.updateTimeZone`, and a failure is logged without affecting the timezone save. commons doesn't depend on social: the event name is the contract, like the capture listener. - `DigestService.updateTimeZone(username, timeZone)` → `DigestEnrollmentStorage.updateTimeZone`: refreshes the copy of an enrolled user, writes nothing for a user without digest or an unchanged zone. - Declared in `conf/portal/configuration.xml` next to the capture listener. Tests: `DigestTimeZoneListenerTest` (event handed over, failure contained), `DigestEnrollmentStorageTest` (refreshed / ignored without digest / unchanged zone). Digest tests green. Note: `DigestEnrollmentStorageTest` is also created by #778 with other methods; the second one to merge takes a trivial file merge, I will handle it. Classification: **N2** (glue listener + one storage method inside the digest engine). Knowledge: none. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…9_EXO-89982_EXO-89486_EXO-89487 - eXIP7.3.0.22 (#775)
…- eXIP7.3.0.22 (#779) eXIP 7.3.0.22 Digest Mail Notifications — US08 (EXO-89539), spec §4 Timezone: "When a timezone is saved, that service broadcasts an event, and a small digest listener refreshes TIMEZONE in NTF_DIGEST_USERS." This listener was missing from #775; the functional tests showed the copy only refreshed on Apply. ## What it adds - `DigestTimeZoneListener`, a Kernel listener on `social.timeZone.saved` (event broadcast by social's `UserTimeZoneService`, source = username, data = zone id), asynchronous and fail-safe: it hands the new zone to `DigestService.updateTimeZone`, and a failure is logged without affecting the timezone save. commons doesn't depend on social: the event name is the contract, like the capture listener. - `DigestService.updateTimeZone(username, timeZone)` → `DigestEnrollmentStorage.updateTimeZone`: refreshes the copy of an enrolled user, writes nothing for a user without digest or an unchanged zone. - Declared in `conf/portal/configuration.xml` next to the capture listener. Tests: `DigestTimeZoneListenerTest` (event handed over, failure contained), `DigestEnrollmentStorageTest` (refreshed / ignored without digest / unchanged zone). Digest tests green. Note: `DigestEnrollmentStorageTest` is also created by #778 with other methods; the second one to merge takes a trivial file merge, I will handle it. Classification: **N2** (glue listener + one storage method inside the digest engine). Knowledge: none. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…9_EXO-89982_EXO-89486_EXO-89487 - eXIP7.3.0.22 (#775)
…9_EXO-89982_EXO-89486_EXO-89487 - eXIP7.3.0.22 (#775)




eXIP 7.3.0.22 Digest Mail Notifications — US08 "Digest sending schedule and coverage" (EXO-89539), with the email assembly shared by US06 (daily) and US07 (weekly). Spec: Tribe note 50469, §4 Scheduling / Assembly / Data.
What it does
DigestSenderJob, cronexo.notification.digest.job.expression, default0 0 * * * ?), glue only, run on its own thread, one run at a time. It never stops: the admin switch OFF stops only the email production, the claims, watermarks, deletions and cleanup go on (settled decision, spec §4 Admin switch).DigestSender): safety cleanup (orphans + retentionexo.notification.digest.retention.days, default 8) → indexed candidates → exact "due now?" in the user's timezone (DigestDueCalculator:daily.hour18,weekly.dayFRIDAY; at most one per local day / week, catch-up after a missed run, first digest the day after opt-in, no double email around a timezone change) → claim by guarded watermark UPDATE (the cross-node protection) → email → mail queue → deletion of the items covered by every enabled frequency. The two frequencies of one user are served in sequence by one task. A failure after the claim gives the occurrence back.DigestMailBuilder+DigestMail.gtmpl): categories of the frequency only (second half of the double check), display order, capdaily.cap5 /weekly.cap10 with "+ N more →", subject / greeting / intro / footer links per the board texts, everything escaped, translations text-only with{i}placeholders (platform convention). Zero lines ⇒ no email, the watermark still moves.DigestLinePlugin(Kernel plugin declared in XML next to the category,addLineProvideronDigestCategoryRegistry) builds oneDigestLine(label key + args + link) from the stored ids; a vanished object gives no line and is not counted. The five addon PRs (social, content, task, agenda, processes) follow.ITEM_DATEis now the capture moment; PARAMS shrinks longest-value-first instead of being dropped.Tests
93 digest tests green in
commons-component-common, includingDigestUserDAOTestandDigestItemDAOTest(the hand-written JPQL run on HSQLDB through the kernel persistence unit),DigestDueCalculatorTest(15 calendar scenarios incl. the spec's timezone edge cases),DigestMailBuilderTest(renders the real template),DigestSenderTest,DigestSenderJobTest.Self-review close-out (3 independent rounds + refute pass)
{i}· 🟡 JPQL not engine-run · 🟡 spec drifts · 🟡 duplicate label resolver · 🟢 nitsVerified conform by the reviewers: job = glue calling the Service layer with a property cron; Service → Storage → DAO layering, constructor injection; every hand-written JPQL executed by the engine; admin-OFF semantics as settled; double check of categories; caps, counts and never-send-empty; escaping of every value put in the HTML;
war:template resolution fromcommons-extension-webapp; mail queue contract (pluginId"digest", sender/recipient from the existing helpers).Accepted trade-offs for the approver: no platform "one node only" job guard exists (spec assumption A2 is false) — the watermark claim is the protection; claim (Spring tx) and queueing (Kernel tx) are two transactions, release compensates; derived-name Spring Data queries are validated at the social webapp start, not in this suite.
Spec resync to propose before merge (note 50469 §4): first digest the day after opt-in; ITEM_DATE = capture moment; cleanup = orphans + retention (the category step is redundant by construction); no
MailTemplateContentTransformerServicepass (links absolute, escaping upstream); line providers as a second Kernel plugin; PARAMS shrinks longest-first; A2 false.Classification: N1 (core notification path, new DAO surface and plugin SPI in commons, mass email). Human approval required, author ≠ approver.
Knowledge: none — the
commonsdomain doc fragment (digest engine,DigestLinePluginSPI) is drafted with the last PR of this lot, once the five addon PRs have landed the same SPI.🤖 Generated with Claude Code