Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
# v1.2.0

## Added

- EmailDigestProvider collects Notifications into independent named digests using `toEmailDigest()`, `notify()`, and captured notification plans.
- Immediate subscriptions create one-item batches through the same digest policy, selection, renderer, and durable email transport, with no separate ordinary-email route. Immediate-only definitions require no calendar schedule.
- EmailDigestService manages per-Notifiable Off, Immediate email, and Digest subscriptions with native ColdBox schedules and confirmed timezones.
- Durable digest subscriptions, items, batches, and event admissions preserve stable identities, deduplicate overlapping publication, and freeze membership at atomic cutoffs.
- The module ColdBox scheduler registers named digest tasks at startup and supports runtime digest and timezone registration. Native shortcuts, configuration closures, and unstarted task templates use ColdBox scheduling and timezone behavior.
- Configurable item selection keeps all eligible items by default, discards omitted pre-cutoff candidates permanently, and never carries overflow into future digests.
- `itemKey()` and `occurredAt()` helpers produce stable logical identities and integer UTC epoch milliseconds.
- Batch preparation and dispatch reuse the existing receipt-aware email delivery, claim, attempt, retry, and recovery APIs.
- Named policy and renderer contracts integrate application consent, current item eligibility, content, and digest-specific unsubscribe links. Consent and eligibility are checked again after rendering.
- NotificationPublication exposes collection admissions separately through `getCollections()`; collection is never recorded as mail acceptance.
- `reconcileNotAccepted()` permits retry of an ambiguous delivery only after authoritative nonacceptance evidence, preserving transport counts. `recoverExpired()` accepts optional maintenance constraints.

## Configuration

Configure definitions under `digests` and persistence under `storage.digests`. A definition supplies its schedule, policy, renderer, and receipt-aware email channel. Apply the additive digest migration on the same datasource as notification events and deliveries before collecting items.

Existing synchronous Notifications and ordinary immediate email continue to use their configured channels. Subscription changes apply prospectively and do not replay old collected items. See [Named email digests](docs/email-digests.md) for integration examples.

# v1.1.0

## Requirements
Expand Down
10 changes: 8 additions & 2 deletions ModuleConfig.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ component {
"events": {},
"deliveries": {},
"preferences": {},
"subscriptions": {}
"subscriptions": {},
"digests": {}
},
"digests": {},
"queue": {
"queueAdapter": "CBQNotificationQueue@megaphone",
"defaultConnection": "",
Expand Down Expand Up @@ -49,7 +51,8 @@ component {
"events": "EventStore",
"deliveries": "DeliveryStore",
"preferences": "PreferenceStore",
"subscriptions": "SubscriptionStore"
"subscriptions": "SubscriptionStore",
"digests": "EmailDigestStore"
};
for ( var key in stores ) {
var properties = settings.storage[ key ] ?: {};
Expand All @@ -65,6 +68,9 @@ component {
.initArg( name = "properties", value = properties );
}
variables.wirebox.getInstance( "NotificationService@megaphone" ).registerChannels( settings.channels ?: {} );
variables.wirebox
.getInstance( "NotificationService@megaphone" )
.registerChannels( { "emailDigestBatch": { "provider": "EmailDigestBatchProvider@megaphone", "properties": {} } } );
}

}
6 changes: 3 additions & 3 deletions box.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
"shortDescription":"A protocol-based library for sending Notifications in ColdBox",
"description":"A protocol-based library for sending Notifications in ColdBox",
"scripts":{
"format":"cfformat run models/**/*.cfc,dsl/**/*.cfc,resources/**/*.cfc,tests/specs/**/*.cfc,tasks/**/*.cfc,tests/resources/**/*.cfc --overwrite",
"format:check":"cfformat check models/**/*.cfc,dsl/**/*.cfc,resources/**/*.cfc,tests/specs/**/*.cfc,tasks/**/*.cfc,tests/resources/**/*.cfc --verbose",
"format:watch":"cfformat watch models/**/*.cfc,dsl/**/*.cfc,resources/**/*.cfc,tests/specs/**/*.cfc,tasks/**/*.cfc,tests/resources/**/*.cfc",
"format":"cfformat run models/**/*.cfc,config/**/*.cfc,dsl/**/*.cfc,resources/**/*.cfc,tests/specs/**/*.cfc,tasks/**/*.cfc,tests/resources/**/*.cfc --overwrite",
"format:check":"cfformat check models/**/*.cfc,config/**/*.cfc,dsl/**/*.cfc,resources/**/*.cfc,tests/specs/**/*.cfc,tasks/**/*.cfc,tests/resources/**/*.cfc --verbose",
"format:watch":"cfformat watch models/**/*.cfc,config/**/*.cfc,dsl/**/*.cfc,resources/**/*.cfc,tests/specs/**/*.cfc,tasks/**/*.cfc,tests/resources/**/*.cfc",
"install:cfpm":"cfpm install mysql,debugger",
"onServerInitialInstall":"run-script install:cfpm"
},
Expand Down
32 changes: 32 additions & 0 deletions config/Scheduler.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/** ColdBox discovers and owns this module scheduler as cbScheduler@megaphone. */
component {

function configure() {
// Module schedulers are configured before ModuleConfig.onLoad maps configured stores.
task( "emailDigest-deliveries" )
.call( () => {
if ( structIsEmpty( getModuleSettings( "megaphone" ).digests ?: {} ) ) {
return;
}
var digests = getInstance( "EmailDigestService@megaphone" );
digests.refreshSchedules();
digests.dispatchDue();
} )
.everyMinute()
.delay( 1, "minutes" )
.withNoOverlaps();
}

function onStartup() {
var digests = getInstance( "EmailDigestService@megaphone" );
digests.useScheduler( this );
if ( !structIsEmpty( getModuleSettings( "megaphone" ).digests ?: {} ) ) {
digests.refreshSchedules();
}
}

function onAnyTaskError( required task, required exception ) {
variables.log.error( "Email digest task [#arguments.task.getName()#] failed", arguments.exception );
}

}
4 changes: 4 additions & 0 deletions docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,3 +432,7 @@ Use `getRecoveryVersion(delivery)` for an optimistic command token. `requestReco
Apply `2026_10_12_180000_add_megaphone_notification_mapping.cfc` after the existing migrations. Backfill reconstruction mappings for existing events without changing payloads, hashes, delivery IDs, or queue references. Application readers of saved RC admissions must translate their old field names into the version 1 plan shape without resolving a new audience or preferences.

The unreleased `DurableNotificationService`, `DeliveryDispatcher`, and struct-returning `getNotificationSlice()` API have been replaced. Stores remain persistence implementation details; consumers use the services above. Merging, package publication, consumer installation, and deployment are separate verification steps.

## Named email digests

See [Named email digests](docs/email-digests.md) for collection through the `emailDigest` channel, independent recipient schedules, consent and eligibility integration, durable batch dispatch, and digest-specific unsubscribe.
Loading
Loading