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
7 changes: 7 additions & 0 deletions docs/unified-daily-notification.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,13 @@ stored delivery receipt; it does not recollect late candidates or prove receiver
receipt. Do not postpone the central read or widen the business window to hide
late producer jobs.

The Azure deadline check also saves its first result under
`<recipient-scope>/deadline-checks/<report-date>.json` within the delivery
prefix. The record is create-only. It can be read after the job's console
logs expire and records the check result, the original slot and check time.
A retry keeps the first recorded result. The check reads the delivery
record and never sends a notification or modifies that record.

Each expected producer target has an archive-only entrypoint,
`python -m scripts.publish_digest_before_read`, in its platform repository.
The proposed trigger is a dedicated Cloud Run Job at 06:10 Asia/Shanghai,
Expand Down
26 changes: 25 additions & 1 deletion python/scripts/azure_timer_digest.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,32 @@ def execute_scheduled(scheduled, configuration, store, send, *, now=core.utc_now

def _execute_validated(scheduled, configuration, store, send, *, now, platform_config, provenance):
report_date, day, check = core.business_slot(scheduled)
return core.execute_slot(
status, result = core.execute_slot(
report_date, day, check,
{'scheduled_at': scheduled.isoformat(), 'trigger_provenance': provenance},
configuration, store, send, now=now, platform_config=platform_config,
)
if result.get('phase') != 'deadline_check':
return status, result
key = f'{configuration.recipient_scope}/deadline-checks/{report_date.isoformat()}.json'
receipt = {
'schema_version': 'qsl.daily_digest_deadline_check.v1',
'report_date': report_date.isoformat(),
'business_day': day,
'scheduled_at': scheduled.isoformat(),
'checked_at': now().isoformat(),
'delivery': result['delivery'],
'trigger_provenance': provenance,
}
try:
created, existing = store.reserve(key, receipt)
if not created:
identity_fields = ('schema_version', 'report_date', 'business_day')
if (not isinstance(existing, dict)
or any(existing.get(field) != receipt[field] for field in identity_fields)
or existing.get('delivery') not in ('confirmed', 'receipt_unverified')):
raise core.StoreUnavailable()
result = {**result, 'delivery': existing['delivery']}
except core.StoreUnavailable:
return 503, {**result, 'deadline_receipt': 'unverified'}
return status, {**result, 'deadline_receipt': 'recorded' if created else 'already_recorded'}
34 changes: 34 additions & 0 deletions python/tests/test_azure_timer_digest.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,37 @@ def test_container_window_supplies_original_time_at_the_boundary(self):
ValueError, 'unverified_sliding_window'
):
window_slot(bad_start, bad_end, 86400, NOW)

def test_deadline_receipt_is_separate_create_only_and_keeps_first_result(self):
store, send = Mock(), Mock()
store.delivery.return_value = {
'report_date': '2026-11-10', 'business_day': '2026-11-09',
'recipient_scope': 'synthetic', 'delivery': 'accepted', 'late': False,
}
configuration = Mock(recipient_scope='synthetic', cutover_date='2026-11-10', enabled=True)
slot = datetime.fromisoformat('2026-11-09T23:30:00Z')
store.reserve.return_value = (True, 1)
code, result = execute_scheduled(slot, configuration, store, send, now=lambda: NOW)
self.assertEqual((code, result['delivery'], result['deadline_receipt']), (200, 'confirmed', 'recorded'))
key, receipt = store.reserve.call_args.args
self.assertEqual(key, 'synthetic/deadline-checks/2026-11-10.json')
self.assertEqual(receipt['delivery'], 'confirmed')
store.delivery.assert_called_once_with('synthetic/2026-11-10.json')

store.reserve.return_value = (False, {**receipt, 'delivery': 'receipt_unverified'})
code, result = execute_scheduled(slot, configuration, store, send, now=lambda: NOW)
self.assertEqual((code, result['delivery'], result['deadline_receipt']),
(200, 'receipt_unverified', 'already_recorded'))
store.finish.assert_not_called()
send.assert_not_called()

def test_deadline_receipt_storage_failure_retries_check_without_sending(self):
store, send = Mock(), Mock()
store.delivery.return_value = None
store.reserve.side_effect = core.StoreUnavailable()
configuration = Mock(recipient_scope='synthetic', cutover_date='2026-11-10', enabled=True)
slot = datetime.fromisoformat('2026-11-09T23:30:00Z')
code, result = execute_scheduled(slot, configuration, store, send, now=lambda: NOW)
self.assertEqual((code, result['delivery'], result['deadline_receipt']),
(503, 'receipt_unverified', 'unverified'))
send.assert_not_called()
Loading