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

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 (29) hide show
  1. package/CHANGELOG.md +24 -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 +99 -0
  6. package/src/Notification/Automation/MarketingDigest_Automation.res.mjs +217 -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 +13 -0
  10. package/src/Notification/StateChange/NotificationPreferences.res.mjs +19 -1
  11. package/src/Notification/StateChange/NotificationPreferences_Behavior.res +14 -0
  12. package/src/Notification/StateChange/NotificationPreferences_Behavior.res.mjs +31 -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/Order/Automation/CancelUnshippedOrder.res +19 -0
  16. package/src/Order/Automation/CancelUnshippedOrder.res.mjs +55 -0
  17. package/src/Order/Automation/CancelUnshippedOrder_Automation.res +60 -0
  18. package/src/Order/Automation/CancelUnshippedOrder_Automation.res.mjs +148 -0
  19. package/src/Plugin.res +22 -2
  20. package/src/Plugin.res.mjs +52 -2
  21. package/tests/Flow/OrderingFlow_GWT.res.mjs +4 -2
  22. package/tests/Notification/Automation/MarketingDigest_GWT.res +89 -0
  23. package/tests/Notification/Automation/MarketingDigest_GWT.res.mjs +241 -0
  24. package/tests/Notification/Automation/NotificationIntake_GWT.res.mjs +20 -1
  25. package/tests/Notification/StateChange/NotificationPreferences_GWT.res +44 -0
  26. package/tests/Notification/StateChange/NotificationPreferences_GWT.res.mjs +46 -2
  27. package/tests/Order/Automation/AutoShipOrder_GWT.res.mjs +20 -1
  28. package/tests/Order/Automation/CancelUnshippedOrder_GWT.res +49 -0
  29. package/tests/Order/Automation/CancelUnshippedOrder_GWT.res.mjs +160 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,30 @@
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.276 (2026-10-16)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **examples:** a digest time needs marketing by email ([8fd0baa](https://github.com/ReventlessDev/reventless-core/commit/8fd0baa78310d2c001ecf8c04546b789f5e19bb1))
11
+
12
+
13
+ # 1.0.0-alpha.275 (2026-10-16)
14
+
15
+ * feat!(automation): schedules set by events ([53bc436](https://github.com/ReventlessDev/reventless-core/commit/53bc4363dffce81ef36475728bf0dae704716021))
16
+ * feat!(automation): declared schedules tick an automation ([420acd2](https://github.com/ReventlessDev/reventless-core/commit/420acd2233aadf700141a31d9d3e0fdc8074bef7))
17
+ ### Features
18
+
19
+ * **examples:** a marketing digest at the time each customer chooses ([3521b10](https://github.com/ReventlessDev/reventless-core/commit/3521b10c4f3ff64f3e4561e94e051892cdfe5d5f))
20
+
21
+ ### BREAKING CHANGES
22
+
23
+ * a hand-written AutomationSlice.Mapping.Make the PPX does
24
+ not see must define `let reschedule = _ => []`.
25
+ * a hand-written AutomationSlice spec the PPX does not see must
26
+ declare `let schedule = None`.
27
+
28
+
29
+
6
30
  # 1.0.0-alpha.274 (2026-10-15)
7
31
 
8
32
  * 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.276",
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",
26
24
  "@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"
25
+ "@reventlessdev/online-shop-hybrid-catalog-spec": "1.0.0-alpha.162",
26
+ "@reventlessdev/online-shop-hybrid-ordering-spec": "1.0.0-alpha.161",
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-address-geocoding": "1.0.0-alpha.46",
32
+ "@reventlessdev/trait-notification": "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.102"
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,99 @@
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 by email stops the digest: a time can only be
62
+ // chosen while it is on, and a schedule that only ever sends nothing is one
63
+ // 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, channel: Email}) => [
70
+ Clear({key: recipientId}),
71
+ ]
72
+ | _ => []
73
+ }
74
+ },
75
+ )
76
+
77
+ let mappings: array<module(Mapping)> = [module(FromClock), module(FromOrderingDcb)]
78
+
79
+ // The item id travels as the request's reference and comes back on its outcome.
80
+ // The wording is fixed: what the offers are is the catalog's, and this shop has
81
+ // no campaign component yet to say.
82
+ let process = (id, item: MarketingDigest.todoItem) => Some((
83
+ item.recipientId,
84
+ MarketingDigest.RequestNotification({
85
+ recipientId: item.recipientId,
86
+ category: Marketing,
87
+ reference: id,
88
+ subjectType: "",
89
+ subjectRef: "",
90
+ subject: "This week's offers",
91
+ body: "A look at what is new in the shop this week.",
92
+ sourceId: `${OrderingDcbSource.name}:DigestTimeChosen`,
93
+ origin: Default,
94
+ }),
95
+ ))
96
+
97
+ // A day's digest that never got an answer is not worth announcing: tomorrow's
98
+ // comes regardless.
99
+ let onExhausted = (_id, _item) => None
@@ -0,0 +1,217 @@
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
+ switch (event.channel) {
140
+ case "Email" :
141
+ return [{
142
+ TAG: "Clear",
143
+ key: event.recipientId
144
+ }];
145
+ case "Sms" :
146
+ case "Push" :
147
+ return [];
148
+ }
149
+ }
150
+ default:
151
+ return [];
152
+ }
153
+ }
154
+
155
+ let FromOrderingDcb = AutomationSlice$Reventless.Mapping.Make({
156
+ Id: Id$Reventless.$$String,
157
+ name: name,
158
+ eventSchema: eventSchema
159
+ })({
160
+ name: MarketingDigest$OrderingPlugin.name,
161
+ moduleUrl: MarketingDigest$OrderingPlugin.moduleUrl,
162
+ todoItemSchema: MarketingDigest$OrderingPlugin.todoItemSchema,
163
+ commandSchema: MarketingDigest$OrderingPlugin.commandSchema,
164
+ maxRetries: MarketingDigest$OrderingPlugin.maxRetries,
165
+ heartbeatInterval: MarketingDigest$OrderingPlugin.heartbeatInterval,
166
+ targetName: MarketingDigest$OrderingPlugin.targetName,
167
+ schedule: undefined
168
+ })({
169
+ collect: collect$1,
170
+ resolve: resolve$1,
171
+ reschedule: reschedule$1
172
+ });
173
+
174
+ let mappings = [
175
+ FromClock,
176
+ FromOrderingDcb
177
+ ];
178
+
179
+ function process(id, item, param) {
180
+ return [
181
+ item.recipientId,
182
+ {
183
+ TAG: "RequestNotification",
184
+ recipientId: item.recipientId,
185
+ category: "Marketing",
186
+ reference: id,
187
+ subjectType: "",
188
+ subjectRef: "",
189
+ subject: "This week's offers",
190
+ body: "A look at what is new in the shop this week.",
191
+ sourceId: name + `:DigestTimeChosen`,
192
+ origin: "Default"
193
+ }
194
+ ];
195
+ }
196
+
197
+ function onExhausted(_id, _item) {
198
+
199
+ }
200
+
201
+ let Spec;
202
+
203
+ let moduleUrl = "@reventlessdev/online-shop-hybrid-ordering/src/Notification/Automation/MarketingDigest_Automation.res.mjs";
204
+
205
+ export {
206
+ Spec,
207
+ M,
208
+ OrderingDcbSource,
209
+ dailyAt,
210
+ FromClock,
211
+ FromOrderingDcb,
212
+ mappings,
213
+ process,
214
+ onExhausted,
215
+ moduleUrl,
216
+ }
217
+ /* 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,19 @@ 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
123
+ // A digest time for a recipient who does not take marketing by email: every
124
+ // digest would be suppressed, and their unsubscribing, which changes nothing,
125
+ // could never stop the schedule.
126
+ | MarketingNotSubscribed
115
127
 
116
128
  @schema
117
129
  type event =
118
130
  | RecipientAnnounced({recipientId: string, email: string})
119
131
  | NotificationSubscribed({recipientId: string, category: category, channel: channel})
120
132
  | NotificationUnsubscribed({recipientId: string, category: category, channel: channel})
133
+ | DigestTimeChosen({recipientId: string, hour: int, minute: int, zone: string})
121
134
  // The addressed message. `address` is the snapshot the send slice delivers to,
122
135
  // and the reason this event exists rather than the send slice reading state.
123
136
  | 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,11 @@ 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
+ Sury.literal("MarketingNotSubscribed")
114
+ ]);
104
115
 
105
116
  let eventSchema = Sury.union([
106
117
  Sury.$schema(s => ({
@@ -120,6 +131,13 @@ let eventSchema = Sury.union([
120
131
  category: s.m(categorySchema),
121
132
  channel: s.m(channelSchema)
122
133
  })),
134
+ Sury.$schema(s => ({
135
+ TAG: "DigestTimeChosen",
136
+ recipientId: s.m(DcbTag$Reventless.string),
137
+ hour: s.m(Sury.int32),
138
+ minute: s.m(Sury.int32),
139
+ zone: s.m(Sury.string)
140
+ })),
123
141
  Sury.$schema(s => ({
124
142
  TAG: "NotificationRequested",
125
143
  recipientId: s.m(DcbTag$Reventless.string),
@@ -215,6 +215,20 @@ 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, who takes marketing by email,
220
+ // may choose one; unsubscribing from it is then what stops the digest.
221
+ | ChooseDigestTime({recipientId, hour, minute, zone}) =>
222
+ if state.contacts->Array.length == 0 {
223
+ Error(RecipientUnknown)
224
+ } else if hour < 0 || hour > 23 || minute < 0 || minute > 59 {
225
+ Error(DigestTimeInvalid)
226
+ } else if !(state->Rules.enabled(~posture, "Marketing", Email)) {
227
+ Error(MarketingNotSubscribed)
228
+ } else {
229
+ Ok([DigestTimeChosen({recipientId, hour, minute, zone})])
230
+ }
231
+
218
232
  // Reported by the send slice once the provider has settled. No rule to state —
219
233
  // the outcome is whatever the provider said — so these stay here rather than
220
234
  // being pushed through a trait that would only pass them along.
@@ -209,6 +209,37 @@ 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 if (Notification_Rules$TraitNotification.enabled(state, posture, "Marketing", "Email")) {
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
+ } else {
238
+ return {
239
+ TAG: "Error",
240
+ _0: "MarketingNotSubscribed"
241
+ };
242
+ }
212
243
  case "RequestNotification" :
213
244
  let origin = command.origin;
214
245
  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 */