Skip to content

Commit c11b2eb

Browse files
authored
feat(events): ADR-041 delivery seam for sibling-app outbound deliveries (#1810)
* feat(events): ADR-041 delivery seam — typed request/concluded events over the CloudEvents pipeline * style(events): spec tags on the seam contract, justify the flat provenance ctor, fix the attempt-shape assertions
1 parent e6e3f57 commit c11b2eb

12 files changed

Lines changed: 1462 additions & 2 deletions

File tree

lib/AppInfo/Application.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@
4242
use OCA\Integriq\Capabilities;
4343
use OCA\Integriq\Controller\HealthController;
4444
use OCA\Integriq\Controller\MetricsController;
45+
use OCA\Integriq\Event\DeliveryRequestedEvent;
4546
use OCA\Integriq\EventListener\CloudEventListener;
47+
use OCA\Integriq\EventListener\DeliveryRequestedListener;
4648
use OCA\Integriq\EventListener\EndpointCacheInvalidationListener;
4749
use OCA\Integriq\EventListener\NextcloudCalendarEventListener;
4850
use OCA\Integriq\EventListener\NextcloudFileEventListener;
@@ -199,6 +201,11 @@ function ($c) {
199201
$dispatcher->addServiceListener(eventName: ObjectCreatedEvent::class, className: CloudEventListener::class);
200202
$dispatcher->addServiceListener(eventName: ObjectUpdatedEvent::class, className: CloudEventListener::class);
201203
$dispatcher->addServiceListener(eventName: ObjectDeletedEvent::class, className: CloudEventListener::class);
204+
// ADR-041 cross-app delivery seam: a sibling app (dossiq, ...) raises
205+
// a typed DeliveryRequestedEvent; this listener ingests it into the
206+
// same CloudEvents pipeline (subscription routing, retry, dead-letter,
207+
// replay) and writes the synchronous result slot back on the event.
208+
$dispatcher->addServiceListener(eventName: DeliveryRequestedEvent::class, className: DeliveryRequestedListener::class);
202209
// Nextcloud-core-event triggers (nextcloud-event-hub). Each family
203210
// normalizes its NC event into the SAME `event` CloudEvents envelope
204211
// shape the OR-object pipeline above already uses, then hands off to
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
<?php
2+
3+
/**
4+
* Integriq DeliveryConcluded Event.
5+
*
6+
* The asynchronous half of the ADR-041 delivery seam: dispatched when a
7+
* delivery that entered through {@see DeliveryRequestedEvent} reaches a
8+
* terminal state — delivered, or abandoned after the retry budget is spent.
9+
* Consumers MUST filter on `getSourceApp()` matching their own app id, keep
10+
* the projection local and idempotent, and never advance state on a
11+
* non-terminal outcome. It is dispatched only for provenance-carrying
12+
* requests; ordinary CloudEvent traffic never produces one.
13+
*
14+
* @category Event
15+
* @package OCA\Integriq\Event
16+
*
17+
* @author Conduction Development Team <info@conduction.nl>
18+
* @copyright 2026 Conduction B.V.
19+
* @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
20+
*
21+
* SPDX-License-Identifier: EUPL-1.2
22+
* SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
23+
*
24+
* @version GIT: <git_id>
25+
*
26+
* @link https://conduction.nl
27+
*
28+
* @spec openspec/changes/absorb-dossiq-deliveries/specs/delivery-intake/spec.md
29+
*/
30+
31+
declare(strict_types=1);
32+
33+
namespace OCA\Integriq\Event;
34+
35+
use OCP\EventDispatcher\Event;
36+
37+
/**
38+
* Terminal outcome of a cross-app delivery request.
39+
*
40+
* @spec openspec/changes/absorb-dossiq-deliveries/specs/delivery-intake/spec.md
41+
*
42+
* @SuppressWarnings(PHPMD.ExcessiveParameterList) -- the ADR-041 event contract is a flat
43+
* readonly provenance envelope (sourceApp, subject coordinates, kind/channel, correlation);
44+
* folding fields into an array would untype the contract the consumer stubs must mirror
45+
* verbatim. Mirrors the decidiq DecisionRequestedEvent precedent.
46+
*/
47+
class DeliveryConcludedEvent extends Event {
48+
/**
49+
* Terminal status: the delivery succeeded.
50+
*/
51+
public const STATUS_DELIVERED = 'delivered';
52+
53+
/**
54+
* Terminal status: the retry budget is spent, no further attempts.
55+
*/
56+
public const STATUS_ABANDONED = 'abandoned';
57+
58+
/**
59+
* Constructor.
60+
*
61+
* @param string $sourceApp The app that raised the original request.
62+
* @param string $correlationId The caller's correlation id, echoed verbatim.
63+
* @param string $subjectId The subject object id from the original request.
64+
* @param string $channel The delivery channel from the original request.
65+
* @param string $status Terminal status: {@see self::STATUS_DELIVERED} or {@see self::STATUS_ABANDONED}.
66+
* @param string $eventId Uuid of the CloudEvent `event` object.
67+
* @param string $messageId Uuid of the `event_message` delivery record.
68+
* @param int $attempts How many delivery attempts were made.
69+
* @param string|null $error The last delivery error, or null on success.
70+
* @param string $concludedAt ISO 8601 timestamp of the terminal transition.
71+
*
72+
* @return void
73+
*/
74+
public function __construct(
75+
private readonly string $sourceApp,
76+
private readonly string $correlationId,
77+
private readonly string $subjectId,
78+
private readonly string $channel,
79+
private readonly string $status,
80+
private readonly string $eventId,
81+
private readonly string $messageId,
82+
private readonly int $attempts,
83+
private readonly ?string $error,
84+
private readonly string $concludedAt,
85+
) {
86+
parent::__construct();
87+
}//end __construct()
88+
89+
/**
90+
* The app that raised the original request.
91+
*
92+
* @return string The source app id.
93+
*
94+
* @spec openspec/changes/absorb-dossiq-deliveries/specs/delivery-intake/spec.md
95+
*/
96+
public function getSourceApp(): string {
97+
return $this->sourceApp;
98+
}//end getSourceApp()
99+
100+
/**
101+
* The caller's correlation id.
102+
*
103+
* @return string The correlation id.
104+
*
105+
* @spec openspec/changes/absorb-dossiq-deliveries/specs/delivery-intake/spec.md
106+
*/
107+
public function getCorrelationId(): string {
108+
return $this->correlationId;
109+
}//end getCorrelationId()
110+
111+
/**
112+
* The subject object id from the original request.
113+
*
114+
* @return string The subject id.
115+
*
116+
* @spec openspec/changes/absorb-dossiq-deliveries/specs/delivery-intake/spec.md
117+
*/
118+
public function getSubjectId(): string {
119+
return $this->subjectId;
120+
}//end getSubjectId()
121+
122+
/**
123+
* The delivery channel from the original request.
124+
*
125+
* @return string The channel.
126+
*
127+
* @spec openspec/changes/absorb-dossiq-deliveries/specs/delivery-intake/spec.md
128+
*/
129+
public function getChannel(): string {
130+
return $this->channel;
131+
}//end getChannel()
132+
133+
/**
134+
* Terminal status of the delivery.
135+
*
136+
* @return string One of the STATUS_* constants.
137+
*
138+
* @spec openspec/changes/absorb-dossiq-deliveries/specs/delivery-intake/spec.md
139+
*/
140+
public function getStatus(): string {
141+
return $this->status;
142+
}//end getStatus()
143+
144+
/**
145+
* Uuid of the CloudEvent `event` object.
146+
*
147+
* @return string The event uuid.
148+
*
149+
* @spec openspec/changes/absorb-dossiq-deliveries/specs/delivery-intake/spec.md
150+
*/
151+
public function getEventId(): string {
152+
return $this->eventId;
153+
}//end getEventId()
154+
155+
/**
156+
* Uuid of the `event_message` delivery record.
157+
*
158+
* @return string The message uuid.
159+
*
160+
* @spec openspec/changes/absorb-dossiq-deliveries/specs/delivery-intake/spec.md
161+
*/
162+
public function getMessageId(): string {
163+
return $this->messageId;
164+
}//end getMessageId()
165+
166+
/**
167+
* How many delivery attempts were made.
168+
*
169+
* @return int The attempt count.
170+
*
171+
* @spec openspec/changes/absorb-dossiq-deliveries/specs/delivery-intake/spec.md
172+
*/
173+
public function getAttempts(): int {
174+
return $this->attempts;
175+
}//end getAttempts()
176+
177+
/**
178+
* The last delivery error.
179+
*
180+
* @return string|null The error, or null on success.
181+
*
182+
* @spec openspec/changes/absorb-dossiq-deliveries/specs/delivery-intake/spec.md
183+
*/
184+
public function getError(): ?string {
185+
return $this->error;
186+
}//end getError()
187+
188+
/**
189+
* When the delivery reached its terminal state.
190+
*
191+
* @return string ISO 8601 timestamp.
192+
*
193+
* @spec openspec/changes/absorb-dossiq-deliveries/specs/delivery-intake/spec.md
194+
*/
195+
public function getConcludedAt(): string {
196+
return $this->concludedAt;
197+
}//end getConcludedAt()
198+
}//end class

0 commit comments

Comments
 (0)