@reventlessdev/online-shop-hybrid-ordering 1.0.0-alpha.178 → 1.0.0-alpha.180
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 +16 -0
- package/package.json +8 -8
- package/src/Customer/Aggregate/Customer.res +39 -2
- package/src/Customer/Aggregate/Customer.res.mjs +30 -2
- package/src/Customer/Aggregate/Customer_Behavior.res +71 -8
- package/src/Customer/Aggregate/Customer_Behavior.res.mjs +157 -56
- package/src/Customer/OutboundTranslationSlice/GeocodeCustomerAddress.res +40 -0
- package/src/Customer/OutboundTranslationSlice/GeocodeCustomerAddress.res.mjs +65 -0
- package/src/Customer/OutboundTranslationSlice/GeocodeCustomerAddress_Translation.res +53 -0
- package/src/Customer/OutboundTranslationSlice/GeocodeCustomerAddress_Translation.res.mjs +89 -0
- package/src/Customer/ReadModelStream/Customers.res +16 -0
- package/src/Customer/ReadModelStream/Customers.res.mjs +32 -2
- package/src/Customer/ReadModelStream/Customers_Projections.res +53 -4
- package/src/Customer/ReadModelStream/Customers_Projections.res.mjs +37 -0
- package/src/Order/OutboundTranslationSlice/SendOrderConfirmation.res +2 -0
- package/src/Order/OutboundTranslationSlice/SendOrderConfirmation.res.mjs +3 -0
- package/src/Order/OutboundTranslationSlice/SendOrderConfirmation_Translation.res +2 -1
- package/src/Order/OutboundTranslationSlice/SendOrderConfirmation_Translation.res.mjs +1 -1
- package/src/Plugin.res.mjs +1 -0
- package/src/Service/GeocodingService.res +91 -0
- package/src/Service/GeocodingService.res.mjs +123 -0
- package/tests/Customer/Aggregate/Customer_GWT.res +79 -3
- package/tests/Customer/Aggregate/Customer_GWT.res.mjs +156 -1
- package/tests/Customer/OutboundTranslationSlice/GeocodeCustomerAddress_GWT.res +90 -0
- package/tests/Customer/OutboundTranslationSlice/GeocodeCustomerAddress_GWT.res.mjs +150 -0
- package/tests/Customer/ReadModelStream/Customers_GWT.res +29 -3
- package/tests/Customer/ReadModelStream/Customers_GWT.res.mjs +16 -1
- package/tests/Flow/OrderingFlow_GWT.res.mjs +1 -0
- package/tests/Order/OutboundTranslationSlice/SendOrderConfirmation_GWT.res.mjs +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
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.180 (2026-08-03)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **aws:** let a deployment hand its plugins a geocoder ([143bf41](https://github.com/ReventlessDev/reventless-core/commit/143bf4137765362ce07dd42db0f4a57057da9f13))
|
|
11
|
+
* **outbound:** let an outbound slice read an aggregate, and geocode addresses with it ([867e63e](https://github.com/ReventlessDev/reventless-core/commit/867e63e774ebc8b78b2b19c78645c8a12a8d06f6))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# 1.0.0-alpha.179 (2026-08-02)
|
|
15
|
+
|
|
16
|
+
**Note:** Version bump only for package @reventlessdev/online-shop-hybrid-ordering
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
# 1.0.0-alpha.178 (2026-08-02)
|
|
7
23
|
|
|
8
24
|
**Note:** Version bump only for package @reventlessdev/online-shop-hybrid-ordering
|
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.180",
|
|
4
4
|
"description": "Online Shop Hybrid example — Ordering plugin (Customer aggregate + Order/CatalogProduct DCB)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -21,18 +21,18 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"sury": "^11.0.0-alpha.4",
|
|
24
|
-
"@reventlessdev/online-shop-hybrid-
|
|
25
|
-
"@reventlessdev/online-shop-hybrid-
|
|
24
|
+
"@reventlessdev/online-shop-hybrid-ordering-spec": "1.0.0-alpha.93",
|
|
25
|
+
"@reventlessdev/online-shop-hybrid-catalog-spec": "1.0.0-alpha.93",
|
|
26
26
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
27
|
-
"@reventlessdev/reventless-
|
|
28
|
-
"@reventlessdev/reventless-
|
|
29
|
-
"@reventlessdev/reventless-local": "3.0.0-alpha.
|
|
30
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
27
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.153",
|
|
28
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.123",
|
|
29
|
+
"@reventlessdev/reventless-local": "3.0.0-alpha.194",
|
|
30
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.98"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"rescript": "12.3.0",
|
|
34
34
|
"sury-ppx": "11.0.0-alpha.2",
|
|
35
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
35
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.62"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"rescript": "12.3.0"
|
|
@@ -8,12 +8,35 @@
|
|
|
8
8
|
// already stored decodes unchanged — and the declaration buys the range checks
|
|
9
9
|
// at the boundary and the marker the command form and the map view read, instead
|
|
10
10
|
// of both being inferred from the field names.
|
|
11
|
+
// A human enters an address and nothing else. `GeocodeCustomerAddress` — an
|
|
12
|
+
// OutboundTranslationSlice subscribed to this aggregate — turns that address
|
|
13
|
+
// into a point and reports the outcome back through the two `@noApi` commands
|
|
14
|
+
// below.
|
|
15
|
+
//
|
|
16
|
+
// The rule that shapes this API: **whoever supplies a point supplies the address
|
|
17
|
+
// it belongs to.** So a caller never sends a bare coordinate — `SetLocation` is
|
|
18
|
+
// the slice's shape, not a client's, and stays internal. A UI corrects a row
|
|
19
|
+
// through `SetAddressLocation`, which covers the pin-only fix too (same address,
|
|
20
|
+
// new point).
|
|
21
|
+
//
|
|
22
|
+
// The two are not interchangeable even though their payloads nearly are. The
|
|
23
|
+
// slice's `SetLocation` is a *report about a past address* and loses to a
|
|
24
|
+
// subsequent change; `SetAddressLocation` is an *assertion about the present* and
|
|
25
|
+
// wins. That is the whole reason `resolvedFrom` exists.
|
|
11
26
|
@schema
|
|
12
27
|
type command =
|
|
13
28
|
| Register({email: string, address: string})
|
|
14
29
|
| UpdateEmail({email: string})
|
|
30
|
+
// Change the address and let the geocoder find the point.
|
|
15
31
|
| UpdateAddress({address: string})
|
|
16
|
-
|
|
32
|
+
// Change the address *and* say where it is — a client that already has a point
|
|
33
|
+
// (it geocoded, or a human dragged the pin). Suppresses the geocoder for this
|
|
34
|
+
// address, because there is nothing left to look up.
|
|
35
|
+
| SetAddressLocation({address: string, location: Reventless.GeoPoint.t})
|
|
36
|
+
// `resolvedFrom` is a staleness token as much as provenance: an answer for an
|
|
37
|
+
// address that has since changed is dropped rather than applied.
|
|
38
|
+
| @noApi SetLocation({location: Reventless.GeoPoint.t, resolvedFrom: string})
|
|
39
|
+
| @noApi MarkAddressUnresolvable({address: string, reason: string})
|
|
17
40
|
| Deactivate
|
|
18
41
|
|
|
19
42
|
@schema
|
|
@@ -21,7 +44,21 @@ type event =
|
|
|
21
44
|
| Registered({email: string, address: string})
|
|
22
45
|
| EmailUpdated({email: string})
|
|
23
46
|
| AddressUpdated({address: string})
|
|
24
|
-
|
|
47
|
+
// `resolvedFrom` is the address this point was derived from — provenance, not
|
|
48
|
+
// the address of record. Keeping it separate is what makes "is the pin still
|
|
49
|
+
// current?" a decidable question instead of an assumption, and it is what the
|
|
50
|
+
// slice reads to know whether an address still needs geocoding.
|
|
51
|
+
| LocationSet({location: Reventless.GeoPoint.t, resolvedFrom: string})
|
|
52
|
+
// A client supplied both halves at once. Deliberately *not* `AddressUpdated` +
|
|
53
|
+
// `LocationSet`: the geocoding slice collects `AddressUpdated`, so emitting it
|
|
54
|
+
// here would spend a request re-geocoding an address a human just pinned by
|
|
55
|
+
// hand, then race the machine's answer against theirs. This event is not in
|
|
56
|
+
// the slice's consumed set, so the slice stands down.
|
|
57
|
+
| AddressLocated({address: string, location: Reventless.GeoPoint.t})
|
|
58
|
+
// The geocoder answered and had nothing usable. A fact rather than an absence:
|
|
59
|
+
// `location: None` already means "not looked up yet", and one state meaning
|
|
60
|
+
// two things is one nobody can act on.
|
|
61
|
+
| AddressUnresolvable({address: string, reason: string})
|
|
25
62
|
| Deactivated
|
|
26
63
|
|
|
27
64
|
@schema
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Api$ReventlessInfra from "@reventlessdev/reventless-infra/src/components/Api.res.mjs";
|
|
4
5
|
import * as GeoPoint$Reventless from "@reventlessdev/reventless-spec/src/semantic/GeoPoint.res.mjs";
|
|
5
6
|
|
|
6
7
|
let commandSchema = S.union([
|
|
@@ -18,9 +19,20 @@ let commandSchema = S.union([
|
|
|
18
19
|
address: s.m(S.string)
|
|
19
20
|
})),
|
|
20
21
|
S.schema(s => ({
|
|
21
|
-
TAG: "
|
|
22
|
+
TAG: "SetAddressLocation",
|
|
23
|
+
address: s.m(S.string),
|
|
22
24
|
location: s.m(GeoPoint$Reventless.schema)
|
|
23
25
|
})),
|
|
26
|
+
S.schema(s => ({
|
|
27
|
+
TAG: "SetLocation",
|
|
28
|
+
location: s.m(GeoPoint$Reventless.schema),
|
|
29
|
+
resolvedFrom: s.m(S.string)
|
|
30
|
+
})),
|
|
31
|
+
S.schema(s => ({
|
|
32
|
+
TAG: "MarkAddressUnresolvable",
|
|
33
|
+
address: s.m(S.string),
|
|
34
|
+
reason: s.m(S.string)
|
|
35
|
+
})),
|
|
24
36
|
S.literal("Deactivate")
|
|
25
37
|
]);
|
|
26
38
|
|
|
@@ -40,8 +52,19 @@ let eventSchema = S.union([
|
|
|
40
52
|
})),
|
|
41
53
|
S.schema(s => ({
|
|
42
54
|
TAG: "LocationSet",
|
|
55
|
+
location: s.m(GeoPoint$Reventless.schema),
|
|
56
|
+
resolvedFrom: s.m(S.string)
|
|
57
|
+
})),
|
|
58
|
+
S.schema(s => ({
|
|
59
|
+
TAG: "AddressLocated",
|
|
60
|
+
address: s.m(S.string),
|
|
43
61
|
location: s.m(GeoPoint$Reventless.schema)
|
|
44
62
|
})),
|
|
63
|
+
S.schema(s => ({
|
|
64
|
+
TAG: "AddressUnresolvable",
|
|
65
|
+
address: s.m(S.string),
|
|
66
|
+
reason: s.m(S.string)
|
|
67
|
+
})),
|
|
45
68
|
S.literal("Deactivated")
|
|
46
69
|
]);
|
|
47
70
|
|
|
@@ -51,6 +74,11 @@ let errorSchema = S.union([
|
|
|
51
74
|
S.literal("CustomerAlreadyDeactivated")
|
|
52
75
|
]);
|
|
53
76
|
|
|
77
|
+
let commandSchema$1 = Api$ReventlessInfra.markNoApiVariants(commandSchema, [
|
|
78
|
+
"SetLocation",
|
|
79
|
+
"MarkAddressUnresolvable"
|
|
80
|
+
]);
|
|
81
|
+
|
|
54
82
|
function commandAuthorization(param) {
|
|
55
83
|
return "AllowAuthenticated";
|
|
56
84
|
}
|
|
@@ -64,9 +92,9 @@ let moduleUrl = "@reventlessdev/online-shop-hybrid-ordering/src/Customer/Aggrega
|
|
|
64
92
|
export {
|
|
65
93
|
name,
|
|
66
94
|
Id,
|
|
67
|
-
commandSchema,
|
|
68
95
|
eventSchema,
|
|
69
96
|
errorSchema,
|
|
97
|
+
commandSchema$1 as commandSchema,
|
|
70
98
|
moduleUrl,
|
|
71
99
|
commandAuthorization,
|
|
72
100
|
}
|
|
@@ -3,21 +3,47 @@
|
|
|
3
3
|
|
|
4
4
|
@@reventless.behavior
|
|
5
5
|
|
|
6
|
+
// `location` and `locationResolvedFrom` are separate fields rather than one,
|
|
7
|
+
// because there are three states and a lone `option` can only express two: no
|
|
8
|
+
// attempt yet, a point found, and an address *tried and found wanting* (which
|
|
9
|
+
// has a resolved-from but no point). The read model surfaces the same three.
|
|
10
|
+
//
|
|
11
|
+
// The invariant they maintain: `locationResolvedFrom` is either `None` or equal
|
|
12
|
+
// to `address`. Every arm below preserves it, and it is what makes "is this pin
|
|
13
|
+
// still current?" a decidable question rather than an assumption.
|
|
6
14
|
@schema
|
|
7
15
|
type state =
|
|
8
16
|
| NotCreated
|
|
9
|
-
| Active({
|
|
17
|
+
| Active({
|
|
18
|
+
email: string,
|
|
19
|
+
address: string,
|
|
20
|
+
location: option<Reventless.GeoPoint.t>,
|
|
21
|
+
locationResolvedFrom: option<string>,
|
|
22
|
+
})
|
|
10
23
|
| Deactivated
|
|
11
24
|
|
|
12
25
|
let initialState = NotCreated
|
|
13
26
|
|
|
14
27
|
let evolve = (state, event) =>
|
|
15
28
|
switch (state, event) {
|
|
16
|
-
| (NotCreated, Registered({email, address})) =>
|
|
17
|
-
|
|
29
|
+
| (NotCreated, Registered({email, address})) =>
|
|
30
|
+
Active({email, address, location: None, locationResolvedFrom: None})
|
|
31
|
+
| (Active(s), Registered({email, address})) => Active({...s, email, address})
|
|
18
32
|
| (Active(s), EmailUpdated({email})) => Active({...s, email})
|
|
19
|
-
|
|
20
|
-
|
|
33
|
+
// A new address invalidates whatever was known about the old one — dropping
|
|
34
|
+
// both is what puts the row back in front of the geocoding slice.
|
|
35
|
+
| (Active(s), AddressUpdated({address})) =>
|
|
36
|
+
Active({...s, address, location: None, locationResolvedFrom: None})
|
|
37
|
+
| (Active(s), LocationSet({location, resolvedFrom})) =>
|
|
38
|
+
Active({...s, location: Some(location), locationResolvedFrom: Some(resolvedFrom)})
|
|
39
|
+
// Both halves at once, from a client. `locationResolvedFrom` is the address
|
|
40
|
+
// itself: the caller supplied the pair, so there is nothing left to resolve.
|
|
41
|
+
| (Active(s), AddressLocated({address, location})) =>
|
|
42
|
+
Active({...s, address, location: Some(location), locationResolvedFrom: Some(address)})
|
|
43
|
+
// Tried, and no usable answer. Recording the address keeps the slice from
|
|
44
|
+
// handing it back for another round.
|
|
45
|
+
| (Active(s), AddressUnresolvable({address})) =>
|
|
46
|
+
Active({...s, location: None, locationResolvedFrom: Some(address)})
|
|
21
47
|
| (Active(_), Customer.Deactivated) => Deactivated
|
|
22
48
|
| (Deactivated, _) => state
|
|
23
49
|
| (NotCreated, _) => state
|
|
@@ -28,18 +54,55 @@ let decide = (state, command) =>
|
|
|
28
54
|
| (NotCreated, Register({email, address})) => Ok([Registered({email, address})])
|
|
29
55
|
| (NotCreated, UpdateEmail(_)) => Error(CustomerNotFound)
|
|
30
56
|
| (NotCreated, UpdateAddress(_)) => Error(CustomerNotFound)
|
|
57
|
+
| (NotCreated, SetAddressLocation(_)) => Error(CustomerNotFound)
|
|
58
|
+
| (NotCreated, SetLocation(_)) => Error(CustomerNotFound)
|
|
59
|
+
| (NotCreated, MarkAddressUnresolvable(_)) => Error(CustomerNotFound)
|
|
31
60
|
| (NotCreated, Deactivate) => Error(CustomerNotFound)
|
|
61
|
+
|
|
32
62
|
| (Active(_), Register(_)) => Error(CustomerAlreadyRegistered)
|
|
33
63
|
| (Active(s), UpdateEmail({email})) if email == s.email => Ok([])
|
|
34
64
|
| (Active(_), UpdateEmail({email})) => Ok([EmailUpdated({email: email})])
|
|
35
65
|
| (Active(s), UpdateAddress({address})) if address == s.address => Ok([])
|
|
36
66
|
| (Active(_), UpdateAddress({address})) => Ok([AddressUpdated({address: address})])
|
|
37
|
-
|
|
67
|
+
|
|
68
|
+
// Client-supplied pair. Idempotent only when *both* halves already match —
|
|
69
|
+
// same address with a different point is a pin correction, and swallowing it
|
|
70
|
+
// would lose the one edit a human came here to make.
|
|
71
|
+
| (Active(s), SetAddressLocation({address, location}))
|
|
72
|
+
if address == s.address && s.location == Some(location) => Ok([])
|
|
73
|
+
| (Active(_), SetAddressLocation({address, location})) =>
|
|
74
|
+
Ok([AddressLocated({address, location})])
|
|
75
|
+
|
|
76
|
+
// Stale: the slice is reporting on an address this customer has since moved
|
|
77
|
+
// off. Applying it would pin the row's new address at the old one's point,
|
|
78
|
+
// with nothing to say the two disagree.
|
|
79
|
+
| (Active(s), SetLocation({resolvedFrom})) if resolvedFrom != s.address => Ok([])
|
|
80
|
+
// Redelivery. Commands arrive at-least-once and the slice re-publishes on
|
|
81
|
+
// every heartbeat sweep until its TODO row clears, so without this an
|
|
82
|
+
// unchanged answer appends a duplicate event on every pass.
|
|
83
|
+
| (Active(s), SetLocation({location, resolvedFrom}))
|
|
84
|
+
if s.location == Some(location) && s.locationResolvedFrom == Some(resolvedFrom) => Ok([])
|
|
85
|
+
| (Active(_), SetLocation({location, resolvedFrom})) =>
|
|
86
|
+
Ok([LocationSet({location, resolvedFrom})])
|
|
87
|
+
|
|
88
|
+
// Same two guards, for the verdict: stale reports are dropped, and a repeated
|
|
89
|
+
// verdict on the same address is a no-op.
|
|
90
|
+
| (Active(s), MarkAddressUnresolvable({address})) if address != s.address => Ok([])
|
|
91
|
+
| (Active(s), MarkAddressUnresolvable({address})) if s.locationResolvedFrom == Some(address) =>
|
|
92
|
+
Ok([])
|
|
93
|
+
| (Active(_), MarkAddressUnresolvable({address, reason})) =>
|
|
94
|
+
Ok([AddressUnresolvable({address, reason})])
|
|
95
|
+
|
|
38
96
|
| (Active(_), Deactivate) => Ok([Customer.Deactivated])
|
|
39
|
-
|
|
97
|
+
|
|
40
98
|
| (Deactivated, Register(_)) => Error(CustomerAlreadyDeactivated)
|
|
41
99
|
| (Deactivated, UpdateEmail(_)) => Error(CustomerAlreadyDeactivated)
|
|
42
100
|
| (Deactivated, UpdateAddress(_)) => Error(CustomerAlreadyDeactivated)
|
|
43
|
-
| (Deactivated,
|
|
101
|
+
| (Deactivated, SetAddressLocation(_)) => Error(CustomerAlreadyDeactivated)
|
|
102
|
+
// A deactivated customer has no location work owing — swallow rather than
|
|
103
|
+
// error, so an in-flight geocode landing after deactivation does not park a
|
|
104
|
+
// TODO row in Failed forever.
|
|
105
|
+
| (Deactivated, SetLocation(_)) => Ok([])
|
|
106
|
+
| (Deactivated, MarkAddressUnresolvable(_)) => Ok([])
|
|
44
107
|
| (Deactivated, Deactivate) => Ok([]) // idempotent
|
|
45
108
|
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
5
|
+
import * as GeoPoint$Reventless from "@reventlessdev/reventless-spec/src/semantic/GeoPoint.res.mjs";
|
|
4
6
|
|
|
5
7
|
let stateSchema = S.union([
|
|
6
8
|
S.literal("NotCreated"),
|
|
7
9
|
S.schema(s => ({
|
|
8
10
|
TAG: "Active",
|
|
9
11
|
email: s.m(S.string),
|
|
10
|
-
address: s.m(S.string)
|
|
12
|
+
address: s.m(S.string),
|
|
13
|
+
location: s.m(S.option(GeoPoint$Reventless.schema)),
|
|
14
|
+
locationResolvedFrom: s.m(S.option(S.string))
|
|
11
15
|
})),
|
|
12
16
|
S.literal("Deactivated")
|
|
13
17
|
]);
|
|
@@ -18,7 +22,9 @@ function evolve(state, event) {
|
|
|
18
22
|
return {
|
|
19
23
|
TAG: "Active",
|
|
20
24
|
email: event.email,
|
|
21
|
-
address: event.address
|
|
25
|
+
address: event.address,
|
|
26
|
+
location: undefined,
|
|
27
|
+
locationResolvedFrom: undefined
|
|
22
28
|
};
|
|
23
29
|
} else {
|
|
24
30
|
return state;
|
|
@@ -32,22 +38,51 @@ function evolve(state, event) {
|
|
|
32
38
|
return {
|
|
33
39
|
TAG: "Active",
|
|
34
40
|
email: event.email,
|
|
35
|
-
address: event.address
|
|
41
|
+
address: event.address,
|
|
42
|
+
location: state.location,
|
|
43
|
+
locationResolvedFrom: state.locationResolvedFrom
|
|
36
44
|
};
|
|
37
45
|
case "EmailUpdated" :
|
|
38
46
|
return {
|
|
39
47
|
TAG: "Active",
|
|
40
48
|
email: event.email,
|
|
41
|
-
address: state.address
|
|
49
|
+
address: state.address,
|
|
50
|
+
location: state.location,
|
|
51
|
+
locationResolvedFrom: state.locationResolvedFrom
|
|
42
52
|
};
|
|
43
53
|
case "AddressUpdated" :
|
|
44
54
|
return {
|
|
45
55
|
TAG: "Active",
|
|
46
56
|
email: state.email,
|
|
47
|
-
address: event.address
|
|
57
|
+
address: event.address,
|
|
58
|
+
location: undefined,
|
|
59
|
+
locationResolvedFrom: undefined
|
|
48
60
|
};
|
|
49
61
|
case "LocationSet" :
|
|
50
|
-
return
|
|
62
|
+
return {
|
|
63
|
+
TAG: "Active",
|
|
64
|
+
email: state.email,
|
|
65
|
+
address: state.address,
|
|
66
|
+
location: event.location,
|
|
67
|
+
locationResolvedFrom: event.resolvedFrom
|
|
68
|
+
};
|
|
69
|
+
case "AddressLocated" :
|
|
70
|
+
let address = event.address;
|
|
71
|
+
return {
|
|
72
|
+
TAG: "Active",
|
|
73
|
+
email: state.email,
|
|
74
|
+
address: address,
|
|
75
|
+
location: event.location,
|
|
76
|
+
locationResolvedFrom: address
|
|
77
|
+
};
|
|
78
|
+
case "AddressUnresolvable" :
|
|
79
|
+
return {
|
|
80
|
+
TAG: "Active",
|
|
81
|
+
email: state.email,
|
|
82
|
+
address: state.address,
|
|
83
|
+
location: undefined,
|
|
84
|
+
locationResolvedFrom: event.address
|
|
85
|
+
};
|
|
51
86
|
}
|
|
52
87
|
}
|
|
53
88
|
|
|
@@ -69,70 +104,136 @@ function decide(state, command) {
|
|
|
69
104
|
}]
|
|
70
105
|
};
|
|
71
106
|
}
|
|
72
|
-
}
|
|
107
|
+
}
|
|
108
|
+
if (typeof command !== "object") {
|
|
73
109
|
return {
|
|
74
110
|
TAG: "Ok",
|
|
75
111
|
_0: []
|
|
76
112
|
};
|
|
77
|
-
} else {
|
|
78
|
-
return {
|
|
79
|
-
TAG: "Error",
|
|
80
|
-
_0: "CustomerAlreadyDeactivated"
|
|
81
|
-
};
|
|
82
113
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
TAG: "Ok",
|
|
87
|
-
_0: ["Deactivated"]
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
switch (command.TAG) {
|
|
91
|
-
case "Register" :
|
|
92
|
-
return {
|
|
93
|
-
TAG: "Error",
|
|
94
|
-
_0: "CustomerAlreadyRegistered"
|
|
95
|
-
};
|
|
96
|
-
case "UpdateEmail" :
|
|
97
|
-
let email = command.email;
|
|
98
|
-
if (email === state.email) {
|
|
99
|
-
return {
|
|
100
|
-
TAG: "Ok",
|
|
101
|
-
_0: []
|
|
102
|
-
};
|
|
103
|
-
} else {
|
|
104
|
-
return {
|
|
105
|
-
TAG: "Ok",
|
|
106
|
-
_0: [{
|
|
107
|
-
TAG: "EmailUpdated",
|
|
108
|
-
email: email
|
|
109
|
-
}]
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
case "UpdateAddress" :
|
|
113
|
-
let address = command.address;
|
|
114
|
-
if (address === state.address) {
|
|
114
|
+
switch (command.TAG) {
|
|
115
|
+
case "SetLocation" :
|
|
116
|
+
case "MarkAddressUnresolvable" :
|
|
115
117
|
return {
|
|
116
118
|
TAG: "Ok",
|
|
117
119
|
_0: []
|
|
118
120
|
};
|
|
119
|
-
|
|
121
|
+
default:
|
|
120
122
|
return {
|
|
121
|
-
TAG: "
|
|
122
|
-
_0:
|
|
123
|
-
TAG: "AddressUpdated",
|
|
124
|
-
address: address
|
|
125
|
-
}]
|
|
123
|
+
TAG: "Error",
|
|
124
|
+
_0: "CustomerAlreadyDeactivated"
|
|
126
125
|
};
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
if (typeof command !== "object") {
|
|
129
129
|
return {
|
|
130
130
|
TAG: "Ok",
|
|
131
|
-
_0: [
|
|
132
|
-
TAG: "LocationSet",
|
|
133
|
-
location: command.location
|
|
134
|
-
}]
|
|
131
|
+
_0: ["Deactivated"]
|
|
135
132
|
};
|
|
133
|
+
}
|
|
134
|
+
switch (command.TAG) {
|
|
135
|
+
case "Register" :
|
|
136
|
+
return {
|
|
137
|
+
TAG: "Error",
|
|
138
|
+
_0: "CustomerAlreadyRegistered"
|
|
139
|
+
};
|
|
140
|
+
case "UpdateEmail" :
|
|
141
|
+
let email = command.email;
|
|
142
|
+
if (email === state.email) {
|
|
143
|
+
return {
|
|
144
|
+
TAG: "Ok",
|
|
145
|
+
_0: []
|
|
146
|
+
};
|
|
147
|
+
} else {
|
|
148
|
+
return {
|
|
149
|
+
TAG: "Ok",
|
|
150
|
+
_0: [{
|
|
151
|
+
TAG: "EmailUpdated",
|
|
152
|
+
email: email
|
|
153
|
+
}]
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
case "UpdateAddress" :
|
|
157
|
+
let address = command.address;
|
|
158
|
+
if (address === state.address) {
|
|
159
|
+
return {
|
|
160
|
+
TAG: "Ok",
|
|
161
|
+
_0: []
|
|
162
|
+
};
|
|
163
|
+
} else {
|
|
164
|
+
return {
|
|
165
|
+
TAG: "Ok",
|
|
166
|
+
_0: [{
|
|
167
|
+
TAG: "AddressUpdated",
|
|
168
|
+
address: address
|
|
169
|
+
}]
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
case "SetAddressLocation" :
|
|
173
|
+
let location = command.location;
|
|
174
|
+
let address$1 = command.address;
|
|
175
|
+
if (address$1 === state.address && Primitive_object.equal(state.location, location)) {
|
|
176
|
+
return {
|
|
177
|
+
TAG: "Ok",
|
|
178
|
+
_0: []
|
|
179
|
+
};
|
|
180
|
+
} else {
|
|
181
|
+
return {
|
|
182
|
+
TAG: "Ok",
|
|
183
|
+
_0: [{
|
|
184
|
+
TAG: "AddressLocated",
|
|
185
|
+
address: address$1,
|
|
186
|
+
location: location
|
|
187
|
+
}]
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
case "SetLocation" :
|
|
191
|
+
let resolvedFrom = command.resolvedFrom;
|
|
192
|
+
if (resolvedFrom !== state.address) {
|
|
193
|
+
return {
|
|
194
|
+
TAG: "Ok",
|
|
195
|
+
_0: []
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
let location$1 = command.location;
|
|
199
|
+
if (Primitive_object.equal(state.location, location$1) && Primitive_object.equal(state.locationResolvedFrom, resolvedFrom)) {
|
|
200
|
+
return {
|
|
201
|
+
TAG: "Ok",
|
|
202
|
+
_0: []
|
|
203
|
+
};
|
|
204
|
+
} else {
|
|
205
|
+
return {
|
|
206
|
+
TAG: "Ok",
|
|
207
|
+
_0: [{
|
|
208
|
+
TAG: "LocationSet",
|
|
209
|
+
location: location$1,
|
|
210
|
+
resolvedFrom: resolvedFrom
|
|
211
|
+
}]
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
case "MarkAddressUnresolvable" :
|
|
215
|
+
let address$2 = command.address;
|
|
216
|
+
if (address$2 !== state.address) {
|
|
217
|
+
return {
|
|
218
|
+
TAG: "Ok",
|
|
219
|
+
_0: []
|
|
220
|
+
};
|
|
221
|
+
} else if (Primitive_object.equal(state.locationResolvedFrom, address$2)) {
|
|
222
|
+
return {
|
|
223
|
+
TAG: "Ok",
|
|
224
|
+
_0: []
|
|
225
|
+
};
|
|
226
|
+
} else {
|
|
227
|
+
return {
|
|
228
|
+
TAG: "Ok",
|
|
229
|
+
_0: [{
|
|
230
|
+
TAG: "AddressUnresolvable",
|
|
231
|
+
address: address$2,
|
|
232
|
+
reason: command.reason
|
|
233
|
+
}]
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
136
237
|
}
|
|
137
238
|
}
|
|
138
239
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// GeocodeCustomerAddress OutboundTranslationSlice.
|
|
2
|
+
//
|
|
3
|
+
// A human enters an address and nothing else. This slice turns that address into
|
|
4
|
+
// a point by asking Amazon Location, and reports the outcome back to the Customer
|
|
5
|
+
// aggregate — `SetLocation` when it is sure, `MarkAddressUnresolvable` when it is
|
|
6
|
+
// not. Neither command is callable from the API; see `Customer.res`.
|
|
7
|
+
//
|
|
8
|
+
// This is the *unattended* path. A client that can geocode for itself would send
|
|
9
|
+
// the point with the command and never reach here — `collect` is where that
|
|
10
|
+
// future arm goes, and it is the only place this slice would need to change.
|
|
11
|
+
|
|
12
|
+
@@reventless.spec
|
|
13
|
+
|
|
14
|
+
@schema
|
|
15
|
+
type consumedEvent =
|
|
16
|
+
| Registered({email: string, address: string})
|
|
17
|
+
| AddressUpdated({address: string})
|
|
18
|
+
|
|
19
|
+
@schema
|
|
20
|
+
type outboundItem = {customerId: string, address: string}
|
|
21
|
+
|
|
22
|
+
@schema
|
|
23
|
+
type inboundCommand =
|
|
24
|
+
| SetLocation({location: Reventless.GeoPoint.t, resolvedFrom: string})
|
|
25
|
+
| MarkAddressUnresolvable({address: string, reason: string})
|
|
26
|
+
|
|
27
|
+
// Retries here are for a geocoder that is *down*, not for one that has answered.
|
|
28
|
+
// An address the service has no match for is resolved by publishing
|
|
29
|
+
// `MarkAddressUnresolvable`, not by trying again three times.
|
|
30
|
+
let maxRetries = 3
|
|
31
|
+
let heartbeatInterval = 60
|
|
32
|
+
let targetName = Some("Customer")
|
|
33
|
+
|
|
34
|
+
// The Customer aggregate, by its Spec.name — the capability that made this slice
|
|
35
|
+
// possible at all. Before it, an outbound slice could only read its plugin's DCB
|
|
36
|
+
// event log, and a customer's lifecycle is an aggregate.
|
|
37
|
+
let sourceNames = ["Customer"]
|
|
38
|
+
|
|
39
|
+
// Drawn as an external box outside the Ordering plugin in the Event Graph.
|
|
40
|
+
let externalSystem = Some("AwsLocation")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as GeoPoint$Reventless from "@reventlessdev/reventless-spec/src/semantic/GeoPoint.res.mjs";
|
|
5
|
+
|
|
6
|
+
let consumedEventSchema = S.union([
|
|
7
|
+
S.schema(s => ({
|
|
8
|
+
TAG: "Registered",
|
|
9
|
+
email: s.m(S.string),
|
|
10
|
+
address: s.m(S.string)
|
|
11
|
+
})),
|
|
12
|
+
S.schema(s => ({
|
|
13
|
+
TAG: "AddressUpdated",
|
|
14
|
+
address: s.m(S.string)
|
|
15
|
+
}))
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
let outboundItemSchema = S.schema(s => ({
|
|
19
|
+
customerId: s.m(S.string),
|
|
20
|
+
address: s.m(S.string)
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
let inboundCommandSchema = S.union([
|
|
24
|
+
S.schema(s => ({
|
|
25
|
+
TAG: "SetLocation",
|
|
26
|
+
location: s.m(GeoPoint$Reventless.schema),
|
|
27
|
+
resolvedFrom: s.m(S.string)
|
|
28
|
+
})),
|
|
29
|
+
S.schema(s => ({
|
|
30
|
+
TAG: "MarkAddressUnresolvable",
|
|
31
|
+
address: s.m(S.string),
|
|
32
|
+
reason: s.m(S.string)
|
|
33
|
+
}))
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
let sourceNames = ["Customer"];
|
|
37
|
+
|
|
38
|
+
let name = "GeocodeCustomerAddress";
|
|
39
|
+
|
|
40
|
+
let Id;
|
|
41
|
+
|
|
42
|
+
let maxRetries = 3;
|
|
43
|
+
|
|
44
|
+
let heartbeatInterval = 60;
|
|
45
|
+
|
|
46
|
+
let targetName = "Customer";
|
|
47
|
+
|
|
48
|
+
let externalSystem = "AwsLocation";
|
|
49
|
+
|
|
50
|
+
let moduleUrl = "@reventlessdev/online-shop-hybrid-ordering/src/Customer/OutboundTranslationSlice/GeocodeCustomerAddress.res.mjs";
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
name,
|
|
54
|
+
Id,
|
|
55
|
+
consumedEventSchema,
|
|
56
|
+
outboundItemSchema,
|
|
57
|
+
inboundCommandSchema,
|
|
58
|
+
maxRetries,
|
|
59
|
+
heartbeatInterval,
|
|
60
|
+
targetName,
|
|
61
|
+
sourceNames,
|
|
62
|
+
externalSystem,
|
|
63
|
+
moduleUrl,
|
|
64
|
+
}
|
|
65
|
+
/* consumedEventSchema Not a pure module */
|