@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.
- package/CHANGELOG.md +24 -0
- package/package.json +10 -10
- package/src/Notification/Automation/MarketingDigest.res +32 -0
- package/src/Notification/Automation/MarketingDigest.res.mjs +62 -0
- package/src/Notification/Automation/MarketingDigest_Automation.res +99 -0
- package/src/Notification/Automation/MarketingDigest_Automation.res.mjs +217 -0
- package/src/Notification/Automation/NotificationIntake.res.mjs +3 -0
- package/src/Notification/Automation/NotificationIntake_Automation.res.mjs +10 -3
- package/src/Notification/StateChange/NotificationPreferences.res +13 -0
- package/src/Notification/StateChange/NotificationPreferences.res.mjs +19 -1
- package/src/Notification/StateChange/NotificationPreferences_Behavior.res +14 -0
- package/src/Notification/StateChange/NotificationPreferences_Behavior.res.mjs +31 -0
- package/src/Order/Automation/AutoShipOrder.res.mjs +3 -0
- package/src/Order/Automation/AutoShipOrder_Automation.res.mjs +10 -3
- package/src/Order/Automation/CancelUnshippedOrder.res +19 -0
- package/src/Order/Automation/CancelUnshippedOrder.res.mjs +55 -0
- package/src/Order/Automation/CancelUnshippedOrder_Automation.res +60 -0
- package/src/Order/Automation/CancelUnshippedOrder_Automation.res.mjs +148 -0
- package/src/Plugin.res +22 -2
- package/src/Plugin.res.mjs +52 -2
- package/tests/Flow/OrderingFlow_GWT.res.mjs +4 -2
- package/tests/Notification/Automation/MarketingDigest_GWT.res +89 -0
- package/tests/Notification/Automation/MarketingDigest_GWT.res.mjs +241 -0
- package/tests/Notification/Automation/NotificationIntake_GWT.res.mjs +20 -1
- package/tests/Notification/StateChange/NotificationPreferences_GWT.res +44 -0
- package/tests/Notification/StateChange/NotificationPreferences_GWT.res.mjs +46 -2
- package/tests/Order/Automation/AutoShipOrder_GWT.res.mjs +20 -1
- package/tests/Order/Automation/CancelUnshippedOrder_GWT.res +49 -0
- 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.
|
|
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/
|
|
28
|
-
"@reventlessdev/online-shop-hybrid-ordering-spec": "1.0.0-alpha.
|
|
29
|
-
"@reventlessdev/reventless-
|
|
30
|
-
"@reventlessdev/reventless-
|
|
31
|
-
"@reventlessdev/
|
|
32
|
-
"@reventlessdev/
|
|
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.
|
|
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.
|
|
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 */
|