@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.
Files changed (29) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +8 -8
  3. package/src/Customer/Aggregate/Customer.res +39 -2
  4. package/src/Customer/Aggregate/Customer.res.mjs +30 -2
  5. package/src/Customer/Aggregate/Customer_Behavior.res +71 -8
  6. package/src/Customer/Aggregate/Customer_Behavior.res.mjs +157 -56
  7. package/src/Customer/OutboundTranslationSlice/GeocodeCustomerAddress.res +40 -0
  8. package/src/Customer/OutboundTranslationSlice/GeocodeCustomerAddress.res.mjs +65 -0
  9. package/src/Customer/OutboundTranslationSlice/GeocodeCustomerAddress_Translation.res +53 -0
  10. package/src/Customer/OutboundTranslationSlice/GeocodeCustomerAddress_Translation.res.mjs +89 -0
  11. package/src/Customer/ReadModelStream/Customers.res +16 -0
  12. package/src/Customer/ReadModelStream/Customers.res.mjs +32 -2
  13. package/src/Customer/ReadModelStream/Customers_Projections.res +53 -4
  14. package/src/Customer/ReadModelStream/Customers_Projections.res.mjs +37 -0
  15. package/src/Order/OutboundTranslationSlice/SendOrderConfirmation.res +2 -0
  16. package/src/Order/OutboundTranslationSlice/SendOrderConfirmation.res.mjs +3 -0
  17. package/src/Order/OutboundTranslationSlice/SendOrderConfirmation_Translation.res +2 -1
  18. package/src/Order/OutboundTranslationSlice/SendOrderConfirmation_Translation.res.mjs +1 -1
  19. package/src/Plugin.res.mjs +1 -0
  20. package/src/Service/GeocodingService.res +91 -0
  21. package/src/Service/GeocodingService.res.mjs +123 -0
  22. package/tests/Customer/Aggregate/Customer_GWT.res +79 -3
  23. package/tests/Customer/Aggregate/Customer_GWT.res.mjs +156 -1
  24. package/tests/Customer/OutboundTranslationSlice/GeocodeCustomerAddress_GWT.res +90 -0
  25. package/tests/Customer/OutboundTranslationSlice/GeocodeCustomerAddress_GWT.res.mjs +150 -0
  26. package/tests/Customer/ReadModelStream/Customers_GWT.res +29 -3
  27. package/tests/Customer/ReadModelStream/Customers_GWT.res.mjs +16 -1
  28. package/tests/Flow/OrderingFlow_GWT.res.mjs +1 -0
  29. 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.178",
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-catalog-spec": "1.0.0-alpha.91",
25
- "@reventlessdev/online-shop-hybrid-ordering-spec": "1.0.0-alpha.91",
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-infra": "3.0.0-alpha.121",
28
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.151",
29
- "@reventlessdev/reventless-local": "3.0.0-alpha.192",
30
- "@reventlessdev/reventless-spec": "3.0.0-alpha.96"
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.60"
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
- | SetLocation({location: Reventless.GeoPoint.t})
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
- | LocationSet({location: Reventless.GeoPoint.t})
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: "SetLocation",
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({email: string, address: string})
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})) => Active({email, address})
17
- | (Active(_), Registered({email, address})) => Active({email, address})
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
- | (Active(s), AddressUpdated({address})) => Active({...s, address})
20
- | (Active(_), LocationSet(_)) => state
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
- | (Active(_), SetLocation({location})) => Ok([LocationSet({location: location})])
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
- | (NotCreated, SetLocation(_)) => Error(CustomerNotFound)
97
+
40
98
  | (Deactivated, Register(_)) => Error(CustomerAlreadyDeactivated)
41
99
  | (Deactivated, UpdateEmail(_)) => Error(CustomerAlreadyDeactivated)
42
100
  | (Deactivated, UpdateAddress(_)) => Error(CustomerAlreadyDeactivated)
43
- | (Deactivated, SetLocation(_)) => Error(CustomerAlreadyDeactivated)
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 state;
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
- } else if (typeof command !== "object") {
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
- if (typeof command !== "object") {
85
- return {
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
- } else {
121
+ default:
120
122
  return {
121
- TAG: "Ok",
122
- _0: [{
123
- TAG: "AddressUpdated",
124
- address: address
125
- }]
123
+ TAG: "Error",
124
+ _0: "CustomerAlreadyDeactivated"
126
125
  };
127
- }
128
- case "SetLocation" :
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 */