@reventlessdev/online-shop-hybrid-ordering 1.0.0-alpha.274 → 1.0.0-alpha.275

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +10 -10
  3. package/src/Notification/Automation/MarketingDigest.res +32 -0
  4. package/src/Notification/Automation/MarketingDigest.res.mjs +62 -0
  5. package/src/Notification/Automation/MarketingDigest_Automation.res +97 -0
  6. package/src/Notification/Automation/MarketingDigest_Automation.res.mjs +211 -0
  7. package/src/Notification/Automation/NotificationIntake.res.mjs +3 -0
  8. package/src/Notification/Automation/NotificationIntake_Automation.res.mjs +10 -3
  9. package/src/Notification/StateChange/NotificationPreferences.res +9 -0
  10. package/src/Notification/StateChange/NotificationPreferences.res.mjs +18 -1
  11. package/src/Notification/StateChange/NotificationPreferences_Behavior.res +11 -0
  12. package/src/Notification/StateChange/NotificationPreferences_Behavior.res.mjs +26 -0
  13. package/src/Order/Automation/AutoShipOrder.res.mjs +3 -0
  14. package/src/Order/Automation/AutoShipOrder_Automation.res.mjs +10 -3
  15. package/src/Plugin.res +15 -2
  16. package/src/Plugin.res.mjs +28 -2
  17. package/tests/Flow/OrderingFlow_GWT.res.mjs +4 -2
  18. package/tests/Notification/Automation/MarketingDigest_GWT.res +84 -0
  19. package/tests/Notification/Automation/MarketingDigest_GWT.res.mjs +235 -0
  20. package/tests/Notification/Automation/NotificationIntake_GWT.res.mjs +20 -1
  21. package/tests/Notification/StateChange/NotificationPreferences_GWT.res +27 -0
  22. package/tests/Notification/StateChange/NotificationPreferences_GWT.res.mjs +29 -2
  23. package/tests/Order/Automation/AutoShipOrder_GWT.res.mjs +20 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,23 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 1.0.0-alpha.275 (2026-10-16)
7
+
8
+ * feat!(automation): schedules set by events ([53bc436](https://github.com/ReventlessDev/reventless-core/commit/53bc4363dffce81ef36475728bf0dae704716021))
9
+ * feat!(automation): declared schedules tick an automation ([420acd2](https://github.com/ReventlessDev/reventless-core/commit/420acd2233aadf700141a31d9d3e0fdc8074bef7))
10
+ ### Features
11
+
12
+ * **examples:** a marketing digest at the time each customer chooses ([3521b10](https://github.com/ReventlessDev/reventless-core/commit/3521b10c4f3ff64f3e4561e94e051892cdfe5d5f))
13
+
14
+ ### BREAKING CHANGES
15
+
16
+ * a hand-written AutomationSlice.Mapping.Make the PPX does
17
+ not see must define `let reschedule = _ => []`.
18
+ * a hand-written AutomationSlice spec the PPX does not see must
19
+ declare `let schedule = None`.
20
+
21
+
22
+
6
23
  # 1.0.0-alpha.274 (2026-10-15)
7
24
 
8
25
  * feat!: an automation's process takes ~now, and collect the event's recorded time ([8c8f3f5](https://github.com/ReventlessDev/reventless-core/commit/8c8f3f570474b1658b605b726077debc7d6a1dc7))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/online-shop-hybrid-ordering",
3
- "version": "1.0.0-alpha.274",
3
+ "version": "1.0.0-alpha.275",
4
4
  "description": "Online Shop Hybrid example — Ordering plugin (Customer aggregate + Order/CatalogProduct DCB)",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -21,20 +21,20 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "sury": "11.0.0",
24
- "@reventlessdev/online-shop-hybrid-catalog-spec": "1.0.0-alpha.161",
25
- "@reventlessdev/reventless-infra": "3.0.0-alpha.192",
24
+ "@reventlessdev/online-shop-hybrid-catalog-spec": "1.0.0-alpha.162",
25
+ "@reventlessdev/online-shop-hybrid-ordering-spec": "1.0.0-alpha.161",
26
26
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.23",
27
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.243",
28
- "@reventlessdev/online-shop-hybrid-ordering-spec": "1.0.0-alpha.160",
29
- "@reventlessdev/reventless-local": "3.0.0-alpha.286",
30
- "@reventlessdev/reventless-spec": "3.0.0-alpha.164",
31
- "@reventlessdev/trait-address-geocoding": "1.0.0-alpha.45",
32
- "@reventlessdev/trait-notification": "1.0.0-alpha.45"
27
+ "@reventlessdev/reventless-gwt": "1.0.0-alpha.244",
28
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.193",
29
+ "@reventlessdev/reventless-local": "3.0.0-alpha.287",
30
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.165",
31
+ "@reventlessdev/trait-notification": "1.0.0-alpha.46",
32
+ "@reventlessdev/trait-address-geocoding": "1.0.0-alpha.46"
33
33
  },
34
34
  "devDependencies": {
35
35
  "rescript": "12.3.1",
36
36
  "sury-ppx": "11.0.0",
37
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.100"
37
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.101"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "rescript": "^12.3.1"
@@ -0,0 +1,32 @@
1
+ // MarketingDigest AutomationSlice.
2
+ //
3
+ // A digest of the shop's offers, sent to each recipient at the time of day they
4
+ // chose. The slice declares no `schedule`: every time comes from an event, one per
5
+ // recipient, keyed by their id, so the tick names whose time came.
6
+ //
7
+ // Not the notification trait's `Digest` delivery, which gathers a rule's
8
+ // occurrences into one message per window. This one has no occurrences to gather;
9
+ // it is a message the shop sends on the recipient's clock.
10
+
11
+ @@reventless.spec
12
+
13
+ @schema
14
+ type todoItem = {recipientId: string}
15
+
16
+ @schema
17
+ type command =
18
+ | RequestNotification({
19
+ recipientId: string,
20
+ category: NotificationPreferences.category,
21
+ reference: string,
22
+ subjectType: string,
23
+ subjectRef: string,
24
+ subject: string,
25
+ body: string,
26
+ sourceId: string,
27
+ origin: NotificationPreferences.origin,
28
+ })
29
+
30
+ let maxRetries = 3
31
+ let heartbeatInterval = 60
32
+ let targetName = "NotificationPreferences"
@@ -0,0 +1,62 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Sury from "sury";
4
+ import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
5
+ import * as NotificationPreferences$OrderingPlugin from "../StateChange/NotificationPreferences.res.mjs";
6
+
7
+ let todoItemSchema = Sury.$schema(s => ({
8
+ recipientId: s.m(Sury.string)
9
+ }));
10
+
11
+ let commandSchema = Sury.$schema(s => ({
12
+ TAG: "RequestNotification",
13
+ recipientId: s.m(DcbTag$Reventless.string),
14
+ category: s.m(NotificationPreferences$OrderingPlugin.categorySchema),
15
+ reference: s.m(Sury.string),
16
+ subjectType: s.m(Sury.string),
17
+ subjectRef: s.m(Sury.string),
18
+ subject: s.m(Sury.string),
19
+ body: s.m(Sury.string),
20
+ sourceId: s.m(DcbTag$Reventless.string),
21
+ origin: s.m(NotificationPreferences$OrderingPlugin.originSchema)
22
+ }));
23
+
24
+ function authorizationOf(param) {
25
+ return "AllowAuthenticated";
26
+ }
27
+
28
+ function commandTransition(param) {
29
+ return "Undeclared";
30
+ }
31
+
32
+ let traits = [];
33
+
34
+ let name = "MarketingDigest";
35
+
36
+ let Id;
37
+
38
+ let maxRetries = 3;
39
+
40
+ let heartbeatInterval = 60;
41
+
42
+ let targetName = "NotificationPreferences";
43
+
44
+ let moduleUrl = "@reventlessdev/online-shop-hybrid-ordering/src/Notification/Automation/MarketingDigest.res.mjs";
45
+
46
+ let schedule;
47
+
48
+ export {
49
+ name,
50
+ Id,
51
+ todoItemSchema,
52
+ commandSchema,
53
+ maxRetries,
54
+ heartbeatInterval,
55
+ targetName,
56
+ moduleUrl,
57
+ authorizationOf,
58
+ commandTransition,
59
+ traits,
60
+ schedule,
61
+ }
62
+ /* todoItemSchema Not a pure module */
@@ -0,0 +1,97 @@
1
+ @@reventless.automation
2
+
3
+ // The recipient's choice and their unsubscribing set and clear their schedule;
4
+ // every outcome of a request finishes the day's digest, as in `NotificationIntake`.
5
+ module OrderingDcbSource = {
6
+ let name = "OrderingDcbEventLog"
7
+
8
+ @schema
9
+ type event =
10
+ | DigestTimeChosen({recipientId: string, hour: int, minute: int, zone: string})
11
+ | NotificationUnsubscribed({
12
+ recipientId: string,
13
+ category: NotificationPreferences.category,
14
+ channel: NotificationPreferences.channel,
15
+ })
16
+ | NotificationRequested({reference: string})
17
+ | NotificationSuppressed({reference: string})
18
+ | NotificationUndeliverable({reference: string})
19
+ | NotificationDeferred({reference: string})
20
+ }
21
+
22
+ /** The recipient's time of day, on their own clock. */
23
+ let dailyAt = (~hour, ~minute, ~zone) => Reventless.Schedule.Calendar({
24
+ calendar: Daily({hour, minute}),
25
+ zone: ?(zone == "" ? None : Some(zone)),
26
+ })
27
+
28
+ // The key is the recipient whose time came.
29
+ module FromClock = Mapping.Make(
30
+ Reventless.Clock,
31
+ MarketingDigest,
32
+ {
33
+ open Reventless.Clock
34
+ let collect = (Tick({key, at}), ~sourceId as _, _ctx) => [
35
+ (itemId(key, at), ({recipientId: key}: MarketingDigest.todoItem)),
36
+ ]
37
+ let resolve = _ => None
38
+ },
39
+ )
40
+
41
+ module FromOrderingDcb = Mapping.Make(
42
+ OrderingDcbSource,
43
+ MarketingDigest,
44
+ {
45
+ open OrderingDcbSource
46
+
47
+ let collect = (_, ~sourceId as _, _ctx) => []
48
+
49
+ let resolve = event =>
50
+ switch event {
51
+ | NotificationRequested({reference})
52
+ | NotificationSuppressed({reference})
53
+ | NotificationUndeliverable({reference})
54
+ | NotificationDeferred({reference}) =>
55
+ Some(reference)
56
+ | DigestTimeChosen(_)
57
+ | NotificationUnsubscribed(_) =>
58
+ None
59
+ }
60
+
61
+ // Unsubscribing from marketing on any channel stops the digest: the request
62
+ // would be suppressed anyway, and a schedule that only ever sends nothing is a
63
+ // schedule nobody should pay for.
64
+ let reschedule = event =>
65
+ switch event {
66
+ | DigestTimeChosen({recipientId, hour, minute, zone}) => [
67
+ Set({key: recipientId, time: dailyAt(~hour, ~minute, ~zone)}),
68
+ ]
69
+ | NotificationUnsubscribed({recipientId, category: Marketing}) => [Clear({key: recipientId})]
70
+ | _ => []
71
+ }
72
+ },
73
+ )
74
+
75
+ let mappings: array<module(Mapping)> = [module(FromClock), module(FromOrderingDcb)]
76
+
77
+ // The item id travels as the request's reference and comes back on its outcome.
78
+ // The wording is fixed: what the offers are is the catalog's, and this shop has
79
+ // no campaign component yet to say.
80
+ let process = (id, item: MarketingDigest.todoItem) => Some((
81
+ item.recipientId,
82
+ MarketingDigest.RequestNotification({
83
+ recipientId: item.recipientId,
84
+ category: Marketing,
85
+ reference: id,
86
+ subjectType: "",
87
+ subjectRef: "",
88
+ subject: "This week's offers",
89
+ body: "A look at what is new in the shop this week.",
90
+ sourceId: `${OrderingDcbSource.name}:DigestTimeChosen`,
91
+ origin: Default,
92
+ }),
93
+ ))
94
+
95
+ // A day's digest that never got an answer is not worth announcing: tomorrow's
96
+ // comes regardless.
97
+ let onExhausted = (_id, _item) => None
@@ -0,0 +1,211 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Sury from "sury";
4
+ import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
5
+ import * as Clock$Reventless from "@reventlessdev/reventless-spec/src/components/Clock.res.mjs";
6
+ import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
7
+ import * as AutomationSlice$Reventless from "@reventlessdev/reventless-spec/src/components/AutomationSlice.res.mjs";
8
+ import * as MarketingDigest$OrderingPlugin from "./MarketingDigest.res.mjs";
9
+ import * as NotificationPreferences$OrderingPlugin from "../StateChange/NotificationPreferences.res.mjs";
10
+
11
+ let M = AutomationSlice$Reventless.Mappings.Make({
12
+ name: MarketingDigest$OrderingPlugin.name,
13
+ moduleUrl: MarketingDigest$OrderingPlugin.moduleUrl,
14
+ todoItemSchema: MarketingDigest$OrderingPlugin.todoItemSchema,
15
+ commandSchema: MarketingDigest$OrderingPlugin.commandSchema,
16
+ maxRetries: MarketingDigest$OrderingPlugin.maxRetries,
17
+ heartbeatInterval: MarketingDigest$OrderingPlugin.heartbeatInterval,
18
+ targetName: MarketingDigest$OrderingPlugin.targetName,
19
+ schedule: undefined
20
+ });
21
+
22
+ let name = "OrderingDcbEventLog";
23
+
24
+ let eventSchema = Sury.union([
25
+ Sury.$schema(s => ({
26
+ TAG: "DigestTimeChosen",
27
+ recipientId: s.m(DcbTag$Reventless.string),
28
+ hour: s.m(Sury.int32),
29
+ minute: s.m(Sury.int32),
30
+ zone: s.m(Sury.string)
31
+ })),
32
+ Sury.$schema(s => ({
33
+ TAG: "NotificationUnsubscribed",
34
+ recipientId: s.m(DcbTag$Reventless.string),
35
+ category: s.m(NotificationPreferences$OrderingPlugin.categorySchema),
36
+ channel: s.m(NotificationPreferences$OrderingPlugin.channelSchema)
37
+ })),
38
+ Sury.$schema(s => ({
39
+ TAG: "NotificationRequested",
40
+ reference: s.m(Sury.string)
41
+ })),
42
+ Sury.$schema(s => ({
43
+ TAG: "NotificationSuppressed",
44
+ reference: s.m(Sury.string)
45
+ })),
46
+ Sury.$schema(s => ({
47
+ TAG: "NotificationUndeliverable",
48
+ reference: s.m(Sury.string)
49
+ })),
50
+ Sury.$schema(s => ({
51
+ TAG: "NotificationDeferred",
52
+ reference: s.m(Sury.string)
53
+ }))
54
+ ]);
55
+
56
+ let OrderingDcbSource = {
57
+ Id: undefined,
58
+ name: name,
59
+ eventSchema: eventSchema
60
+ };
61
+
62
+ function dailyAt(hour, minute, zone) {
63
+ return {
64
+ TAG: "Calendar",
65
+ calendar: {
66
+ TAG: "Daily",
67
+ hour: hour,
68
+ minute: minute
69
+ },
70
+ zone: zone === "" ? undefined : zone
71
+ };
72
+ }
73
+
74
+ function collect(param, param$1, _ctx) {
75
+ let key = param.key;
76
+ return [[
77
+ Clock$Reventless.itemId(key, param.at),
78
+ {
79
+ recipientId: key
80
+ }
81
+ ]];
82
+ }
83
+
84
+ function resolve(param) {
85
+
86
+ }
87
+
88
+ function reschedule(param) {
89
+ return [];
90
+ }
91
+
92
+ let FromClock = AutomationSlice$Reventless.Mapping.Make({
93
+ Id: Id$Reventless.$$String,
94
+ name: Clock$Reventless.name,
95
+ eventSchema: Clock$Reventless.eventSchema
96
+ })({
97
+ name: MarketingDigest$OrderingPlugin.name,
98
+ moduleUrl: MarketingDigest$OrderingPlugin.moduleUrl,
99
+ todoItemSchema: MarketingDigest$OrderingPlugin.todoItemSchema,
100
+ commandSchema: MarketingDigest$OrderingPlugin.commandSchema,
101
+ maxRetries: MarketingDigest$OrderingPlugin.maxRetries,
102
+ heartbeatInterval: MarketingDigest$OrderingPlugin.heartbeatInterval,
103
+ targetName: MarketingDigest$OrderingPlugin.targetName,
104
+ schedule: undefined
105
+ })({
106
+ collect: collect,
107
+ resolve: resolve,
108
+ reschedule: reschedule
109
+ });
110
+
111
+ function collect$1(param, param$1, _ctx) {
112
+ return [];
113
+ }
114
+
115
+ function resolve$1(event) {
116
+ switch (event.TAG) {
117
+ case "DigestTimeChosen" :
118
+ case "NotificationUnsubscribed" :
119
+ return;
120
+ default:
121
+ return event.reference;
122
+ }
123
+ }
124
+
125
+ function reschedule$1(event) {
126
+ switch (event.TAG) {
127
+ case "DigestTimeChosen" :
128
+ return [{
129
+ TAG: "Set",
130
+ key: event.recipientId,
131
+ time: dailyAt(event.hour, event.minute, event.zone)
132
+ }];
133
+ case "NotificationUnsubscribed" :
134
+ switch (event.category) {
135
+ case "OrderConfirmation" :
136
+ case "ShippingUpdate" :
137
+ return [];
138
+ case "Marketing" :
139
+ return [{
140
+ TAG: "Clear",
141
+ key: event.recipientId
142
+ }];
143
+ }
144
+ default:
145
+ return [];
146
+ }
147
+ }
148
+
149
+ let FromOrderingDcb = AutomationSlice$Reventless.Mapping.Make({
150
+ Id: Id$Reventless.$$String,
151
+ name: name,
152
+ eventSchema: eventSchema
153
+ })({
154
+ name: MarketingDigest$OrderingPlugin.name,
155
+ moduleUrl: MarketingDigest$OrderingPlugin.moduleUrl,
156
+ todoItemSchema: MarketingDigest$OrderingPlugin.todoItemSchema,
157
+ commandSchema: MarketingDigest$OrderingPlugin.commandSchema,
158
+ maxRetries: MarketingDigest$OrderingPlugin.maxRetries,
159
+ heartbeatInterval: MarketingDigest$OrderingPlugin.heartbeatInterval,
160
+ targetName: MarketingDigest$OrderingPlugin.targetName,
161
+ schedule: undefined
162
+ })({
163
+ collect: collect$1,
164
+ resolve: resolve$1,
165
+ reschedule: reschedule$1
166
+ });
167
+
168
+ let mappings = [
169
+ FromClock,
170
+ FromOrderingDcb
171
+ ];
172
+
173
+ function process(id, item, param) {
174
+ return [
175
+ item.recipientId,
176
+ {
177
+ TAG: "RequestNotification",
178
+ recipientId: item.recipientId,
179
+ category: "Marketing",
180
+ reference: id,
181
+ subjectType: "",
182
+ subjectRef: "",
183
+ subject: "This week's offers",
184
+ body: "A look at what is new in the shop this week.",
185
+ sourceId: name + `:DigestTimeChosen`,
186
+ origin: "Default"
187
+ }
188
+ ];
189
+ }
190
+
191
+ function onExhausted(_id, _item) {
192
+
193
+ }
194
+
195
+ let Spec;
196
+
197
+ let moduleUrl = "@reventlessdev/online-shop-hybrid-ordering/src/Notification/Automation/MarketingDigest_Automation.res.mjs";
198
+
199
+ export {
200
+ Spec,
201
+ M,
202
+ OrderingDcbSource,
203
+ dailyAt,
204
+ FromClock,
205
+ FromOrderingDcb,
206
+ mappings,
207
+ process,
208
+ onExhausted,
209
+ moduleUrl,
210
+ }
211
+ /* M Not a pure module */
@@ -46,6 +46,8 @@ let targetName = "NotificationPreferences";
46
46
 
47
47
  let moduleUrl = "@reventlessdev/online-shop-hybrid-ordering/src/Notification/Automation/NotificationIntake.res.mjs";
48
48
 
49
+ let schedule;
50
+
49
51
  export {
50
52
  name,
51
53
  Id,
@@ -58,5 +60,6 @@ export {
58
60
  authorizationOf,
59
61
  commandTransition,
60
62
  traits,
63
+ schedule,
61
64
  }
62
65
  /* todoItemSchema Not a pure module */
@@ -19,7 +19,8 @@ let M = AutomationSlice$Reventless.Mappings.Make({
19
19
  commandSchema: NotificationIntake$OrderingPlugin.commandSchema,
20
20
  maxRetries: NotificationIntake$OrderingPlugin.maxRetries,
21
21
  heartbeatInterval: NotificationIntake$OrderingPlugin.heartbeatInterval,
22
- targetName: NotificationIntake$OrderingPlugin.targetName
22
+ targetName: NotificationIntake$OrderingPlugin.targetName,
23
+ schedule: undefined
23
24
  });
24
25
 
25
26
  let name = "OrderingDcbEventLog";
@@ -132,6 +133,10 @@ function resolve(event) {
132
133
  }
133
134
  }
134
135
 
136
+ function reschedule(param) {
137
+ return [];
138
+ }
139
+
135
140
  let FromOrderingDcb = AutomationSlice$Reventless.Mapping.Make({
136
141
  Id: Id$Reventless.$$String,
137
142
  name: name,
@@ -143,10 +148,12 @@ let FromOrderingDcb = AutomationSlice$Reventless.Mapping.Make({
143
148
  commandSchema: NotificationIntake$OrderingPlugin.commandSchema,
144
149
  maxRetries: NotificationIntake$OrderingPlugin.maxRetries,
145
150
  heartbeatInterval: NotificationIntake$OrderingPlugin.heartbeatInterval,
146
- targetName: NotificationIntake$OrderingPlugin.targetName
151
+ targetName: NotificationIntake$OrderingPlugin.targetName,
152
+ schedule: undefined
147
153
  })({
148
154
  collect: collect,
149
- resolve: resolve
155
+ resolve: resolve,
156
+ reschedule: reschedule
150
157
  });
151
158
 
152
159
  let mappings = [FromOrderingDcb];
@@ -68,6 +68,12 @@ type command =
68
68
  // the field with the authenticated caller before the command is published.
69
69
  | Subscribe({@owner recipientId: string, category: category, channel: channel})
70
70
  | Unsubscribe({@owner recipientId: string, category: category, channel: channel})
71
+ // When in the day the recipient wants the marketing digest, on their own clock:
72
+ // an hour and minute, read in `zone` (an IANA name such as "Europe/Berlin"; empty
73
+ // is UTC). Plain fields rather than a `Schedule.time`, because a person picks a
74
+ // time of day, and an input cannot carry a tagged union. `MarketingDigest` turns
75
+ // it into the recipient's schedule.
76
+ | ChooseDigestTime({@owner recipientId: string, hour: int, minute: int, zone: string})
71
77
  // Also relayed: a notifiable thing happened. `reference` is the requester's own
72
78
  // key for it, echoed back on whichever outcome follows, so the automation that
73
79
  // asked can tell its work is finished. Opaque here on purpose — this slice must
@@ -112,12 +118,15 @@ type error =
112
118
  // refusal, unlike the relayed commands, which record a fact instead: a person
113
119
  // is at the other end of this one and can be told.
114
120
  | RecipientUnknown
121
+ // An hour outside 0 to 23 or a minute outside 0 to 59.
122
+ | DigestTimeInvalid
115
123
 
116
124
  @schema
117
125
  type event =
118
126
  | RecipientAnnounced({recipientId: string, email: string})
119
127
  | NotificationSubscribed({recipientId: string, category: category, channel: channel})
120
128
  | NotificationUnsubscribed({recipientId: string, category: category, channel: channel})
129
+ | DigestTimeChosen({recipientId: string, hour: int, minute: int, zone: string})
121
130
  // The addressed message. `address` is the snapshot the send slice delivers to,
122
131
  // and the reason this event exists rather than the send slice reading state.
123
132
  | NotificationRequested({
@@ -74,6 +74,13 @@ let commandSchema = Sury.union([
74
74
  category: s.m(categorySchema),
75
75
  channel: s.m(channelSchema)
76
76
  })),
77
+ Sury.$schema(s => ({
78
+ TAG: "ChooseDigestTime",
79
+ recipientId: s.m(Owner$Reventless.mark(DcbTag$Reventless.string)),
80
+ hour: s.m(Sury.int32),
81
+ minute: s.m(Sury.int32),
82
+ zone: s.m(Sury.string)
83
+ })),
77
84
  Sury.$schema(s => ({
78
85
  TAG: "RequestNotification",
79
86
  recipientId: s.m(DcbTag$Reventless.string),
@@ -100,7 +107,10 @@ let commandSchema = Sury.union([
100
107
  }))
101
108
  ]);
102
109
 
103
- let errorSchema = Sury.literal("RecipientUnknown");
110
+ let errorSchema = Sury.union([
111
+ Sury.literal("RecipientUnknown"),
112
+ Sury.literal("DigestTimeInvalid")
113
+ ]);
104
114
 
105
115
  let eventSchema = Sury.union([
106
116
  Sury.$schema(s => ({
@@ -120,6 +130,13 @@ let eventSchema = Sury.union([
120
130
  category: s.m(categorySchema),
121
131
  channel: s.m(channelSchema)
122
132
  })),
133
+ Sury.$schema(s => ({
134
+ TAG: "DigestTimeChosen",
135
+ recipientId: s.m(DcbTag$Reventless.string),
136
+ hour: s.m(Sury.int32),
137
+ minute: s.m(Sury.int32),
138
+ zone: s.m(Sury.string)
139
+ })),
123
140
  Sury.$schema(s => ({
124
141
  TAG: "NotificationRequested",
125
142
  recipientId: s.m(DcbTag$Reventless.string),
@@ -215,6 +215,17 @@ let decide = (state, command) =>
215
215
  )
216
216
  )
217
217
 
218
+ // Not the trait's: when a digest comes is this shop's offer, and the trait holds
219
+ // no time. Only a recipient the directory knows may choose one.
220
+ | ChooseDigestTime({recipientId, hour, minute, zone}) =>
221
+ if state.contacts->Array.length == 0 {
222
+ Error(RecipientUnknown)
223
+ } else if hour < 0 || hour > 23 || minute < 0 || minute > 59 {
224
+ Error(DigestTimeInvalid)
225
+ } else {
226
+ Ok([DigestTimeChosen({recipientId, hour, minute, zone})])
227
+ }
228
+
218
229
  // Reported by the send slice once the provider has settled. No rule to state —
219
230
  // the outcome is whatever the provider said — so these stay here rather than
220
231
  // being pushed through a trait that would only pass them along.
@@ -209,6 +209,32 @@ function decide(state, command) {
209
209
  category: categoryKey(command.category),
210
210
  channel: channelKey(command.channel)
211
211
  });
212
+ case "ChooseDigestTime" :
213
+ if (state.contacts.length === 0) {
214
+ return {
215
+ TAG: "Error",
216
+ _0: "RecipientUnknown"
217
+ };
218
+ }
219
+ let minute = command.minute;
220
+ let hour = command.hour;
221
+ if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
222
+ return {
223
+ TAG: "Error",
224
+ _0: "DigestTimeInvalid"
225
+ };
226
+ } else {
227
+ return {
228
+ TAG: "Ok",
229
+ _0: [{
230
+ TAG: "DigestTimeChosen",
231
+ recipientId: command.recipientId,
232
+ hour: hour,
233
+ minute: minute,
234
+ zone: command.zone
235
+ }]
236
+ };
237
+ }
212
238
  case "RequestNotification" :
213
239
  let origin = command.origin;
214
240
  let body = command.body;
@@ -35,6 +35,8 @@ let targetName = "ShipOrder";
35
35
 
36
36
  let moduleUrl = "@reventlessdev/online-shop-hybrid-ordering/src/Order/Automation/AutoShipOrder.res.mjs";
37
37
 
38
+ let schedule;
39
+
38
40
  export {
39
41
  name,
40
42
  Id,
@@ -47,5 +49,6 @@ export {
47
49
  authorizationOf,
48
50
  commandTransition,
49
51
  traits,
52
+ schedule,
50
53
  }
51
54
  /* todoItemSchema Not a pure module */