@cratis/pi 7.56.3 → 7.58.0
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/package/corpus/harnesses/copilot/instructions/elixir.instructions.md +2 -2
- package/package/corpus/product-sync.json +11 -9
- package/package/corpus/rules/elixir.md +2 -2
- package/package/corpus/skills/cratis-chronicle-client-elixir/SKILL.md +64 -87
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/append-api.md +10 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/captures.md +43 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/client-options.md +6 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/connections.md +1 -1
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/constraints.md +63 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/decision-reads.md +65 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/discovery-and-generations.md +56 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/event-sources.md +1 -1
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/idioms-and-inspection.md +2 -2
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/observability.md +37 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/patterns.md +22 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/pitfalls.md +9 -1
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/projections.md +28 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/read-models.md +41 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/testing.md +63 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/upgrading.md +18 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/verification.json +17 -4
- package/package/corpus/skills/cratis-screenplay-automations-and-translations/references/translate-example.md +18 -16
- package/package/corpus/skills/cratis-screenplay-model-authoring/SKILL.md +1 -1
- package/package/corpus/skills/cratis-screenplay-model-authoring/references/screen-authoring-workflow.md +25 -0
- package/package/corpus/skills/cratis-screenplay-read-surface/SKILL.md +1 -1
- package/package/corpus/skills/cratis-screenplay-scenario-coverage/references/invoicing-dues-design.md +7 -4
- package/package/corpus/skills/cratis-screenplay-scenario-coverage/references/invoicing-dues-example.md +10 -7
- package/package/corpus/skills/cratis-screenplay-toolchain/references/screenplay-contract.json +7460 -1159
- package/package/corpus/skills/cratis-screenplay-toolchain/references/screenplay-contract.version +1 -1
- package/package/corpus/skills/cratis-screenplay-ui-composition/SKILL.md +34 -2
- package/package/corpus/skills/cratis-screenplay-ui-composition/references/application-shell.md +301 -0
- package/package/corpus/skills/cratis-screenplay-ui-composition/verification.json +109 -6
- package/package.json +1 -1
package/package/corpus/skills/cratis-chronicle-client-elixir/references/discovery-and-generations.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<!-- Copyright (c) Cratis. All rights reserved. -->
|
|
2
|
+
<!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->
|
|
3
|
+
|
|
4
|
+
# Discovery, artifact identity and event generations
|
|
5
|
+
|
|
6
|
+
Source: `Cratis/Chronicle.Elixir`: `Source/chronicle/lib/chronicle/discovery/`, `artifacts.ex`, `client.ex`, `events/event_type.ex`, `events/event_types.ex`, `reactors/reactor.ex`, `reducers/reducer.ex`, `lib/mix/tasks/chronicle.discover.ex`, and `Documentation/discovery.md`, `events.md`, `reactors.md`, `reducers.md`.
|
|
7
|
+
|
|
8
|
+
Requires `cratis_chronicle` 3.13.0.
|
|
9
|
+
|
|
10
|
+
## The discovery report
|
|
11
|
+
|
|
12
|
+
`Chronicle.Client` builds a `%Chronicle.Discovery.Report{}` at start, logs a one-line summary at `:info` and the whole report at `:debug`. Read it with `Chronicle.Client.discovery_report()` and print it with `Chronicle.Discovery.Report.format/1`. `Chronicle.Artifacts.report(:my_app)` builds the same report without starting a client; `Chronicle.Artifacts.discover/1` still returns its plain map.
|
|
13
|
+
|
|
14
|
+
Run it from the shell with `mix chronicle.discover --otp-app my_app`; add `--connection-string "chronicle://..."` to compare against a kernel (event store `"default"`, namespace `"Default"`) and `--fail-on-drift` to exit `1` on a warning-severity finding. Exit codes: `0` clean, `1` drift found, `2` could not run (no `--otp-app`, or kernel unreachable). `--fail-on-drift` has no effect without `--connection-string`.
|
|
15
|
+
|
|
16
|
+
## Pin ids that default to a module name
|
|
17
|
+
|
|
18
|
+
A reactor, reducer or declarative projection with no `:id` defaults it from its module name (reactors and reducers: the full module name; projections: the last segment). **Renaming or moving the module then creates a different artifact and silently resets its observation position.** Pin it:
|
|
19
|
+
|
|
20
|
+
```elixir
|
|
21
|
+
use Chronicle.Reactors.Reactor, id: "notification-reactor"
|
|
22
|
+
use Chronicle.Projections.Projection, id: "account-summary", model: MyApp.ReadModels.AccountSummary
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Discovery logs a warning once per module per BEAM node for a defaulted id. `warn_defaulted_ids: false` on `Chronicle.Client` suppresses it; prefer pinning. In-memory test clients skip it.
|
|
26
|
+
|
|
27
|
+
## Drift policy
|
|
28
|
+
|
|
29
|
+
Before registering, the client compares what it would register with the kernel. The `:drift_policy` client option:
|
|
30
|
+
|
|
31
|
+
| Value | Behavior |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| `:warn` (default) | Log findings and register anyway |
|
|
34
|
+
| `:fail` | Log findings and refuse to register on a `:warning`-severity finding |
|
|
35
|
+
| `:ignore` | Skip the check and its kernel calls |
|
|
36
|
+
|
|
37
|
+
Findings: `:schema_changed_without_generation` (an event type generation's coarse shape changed: added, removed or retyped field), `:observer_missing_from_code` (always `:info`, never blocks: other clients may own the observer), `:observer_event_types_changed` (a reactor's handled types differ; `:warning`). An indeterminate check (kernel unreachable, exception, 5 s deadline) continues under `:warn` and stops under `:fail`. Read the current findings with `Chronicle.Client.identity_drift()`. The kernel rejects a schema change without a bumped generation itself, regardless of policy.
|
|
38
|
+
|
|
39
|
+
## Event type generations
|
|
40
|
+
|
|
41
|
+
A module for an **older** generation uses `generation_for:` instead of its own `:id`, so the ids cannot drift apart:
|
|
42
|
+
|
|
43
|
+
```elixir
|
|
44
|
+
defmodule MyApp.Events.CustomerRegisteredV1 do
|
|
45
|
+
use Chronicle.Events.EventType, generation_for: MyApp.Events.CustomerRegistered, generation: 1
|
|
46
|
+
defstruct customer_id: "", owner_name: ""
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`generation_for:` requires an explicit `generation:` and cannot be combined with `:id`. Bridge the generations with `Chronicle.Events.Migration` (exactly one migrator per pair).
|
|
51
|
+
|
|
52
|
+
Mistakes such as two modules for one id and generation, two migrators for a pair, or `generation_for:` naming a module that is not an event type are logged and the client continues. Set `strict_validation: true` on `Chronicle.Client` to raise instead; `Chronicle.Events.EventTypes.validate/2` returns `:ok | {:error, exception}` and `validate!/2` raises.
|
|
53
|
+
|
|
54
|
+
## Pinned generation delivery
|
|
55
|
+
|
|
56
|
+
By default (`:compatibility`) every event is upgraded to the generation of the module a reactor or reducer handles. For a handler written against an **older** generation, `use Chronicle.Reactors.Reactor, generation_delivery: :pinned` (or the same on a reducer) delivers the event as appended. `context.appended_generation` reports the appended generation (`0` means unknown). The client-wide default is `event_generation_delivery: :pinned`; an artifact's own option wins. A pinned observer that handles two generations of one event type raises `Chronicle.Observers.MultipleGenerationsOfEventTypeInObserver` on start.
|
|
@@ -63,4 +63,4 @@ Without an explicit `:concurrency_scope`, the client reads the tail narrowed by
|
|
|
63
63
|
|
|
64
64
|
The wire carries one scope per event source id. Guarded entries of that id must agree on dimensions and selected source type, stream type and stream id. Unguarded entries never suppress a later guarded entry. Incompatible derived guards fail before any tail read or append; use separate batches, or deliberately supply one explicit scope on an entry for that id.
|
|
65
65
|
|
|
66
|
-
Reactor and reducer contexts expose `:event_source` as the registered name, or `nil` for events appended without a definition.
|
|
66
|
+
Reactor and reducer contexts expose `:event_source` as the registered name, or `nil` for events appended without a definition. Only reactor contexts carry `:delivery_id`.
|
package/package/corpus/skills/cratis-chronicle-client-elixir/references/idioms-and-inspection.md
CHANGED
|
@@ -25,8 +25,8 @@ client-side. Observer types include `:reactor`, `:projection`, `:reducer`,
|
|
|
25
25
|
- **Discovery is compile-time reflection.** The `use` macros generate
|
|
26
26
|
`__chronicle_*__/1` functions via `__before_compile__`, and discovery is
|
|
27
27
|
`function_exported?/3` over the application's modules — not runtime scanning.
|
|
28
|
-
- **
|
|
29
|
-
|
|
28
|
+
- **Telemetry is built in.** The client emits OpenTelemetry spans and `:telemetry`
|
|
29
|
+
events; read [observability.md](observability.md). Logging is `Logger`.
|
|
30
30
|
- **No formatter export.** `.formatter.exs` declares no `locals_without_parens`
|
|
31
31
|
for `from`, `join`, `removed_with`, `from_every`, `pii`, `subject`, `unique`, or
|
|
32
32
|
`unique_event_type`, and there is no `import_deps: [:cratis_chronicle]` to
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<!-- Copyright (c) Cratis. All rights reserved. -->
|
|
2
|
+
<!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->
|
|
3
|
+
|
|
4
|
+
# Observability: OpenTelemetry and `:telemetry`
|
|
5
|
+
|
|
6
|
+
Source: `Cratis/Chronicle.Elixir`: `Source/chronicle/lib/chronicle/diagnostics/tracing.ex`, `connections/trace_propagation_interceptor.ex`, and `Documentation/observability.md`.
|
|
7
|
+
|
|
8
|
+
Requires `cratis_chronicle` 3.13.0.
|
|
9
|
+
|
|
10
|
+
The client is always instrumented and depends only on `opentelemetry_api`. With no OpenTelemetry SDK started in the release, every span and propagation call is a no-op and nothing leaves the process; there is no Chronicle-specific enable switch. To export, add `opentelemetry` and `opentelemetry_exporter` to the application and configure them the standard way (`OTEL_*` variables or `config :opentelemetry`).
|
|
11
|
+
|
|
12
|
+
All spans use the instrumentation scope `Cratis.Chronicle.Client`, shared with the other Chronicle clients.
|
|
13
|
+
|
|
14
|
+
| Span | Kind | Where |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| `cratis.chronicle.client.event_sequence.append` | Client | `append/3`, `append_with_result/3` |
|
|
17
|
+
| `cratis.chronicle.client.event_sequence.append_many` | Client | `append_many/3` variants, `append_many_for_event_sources/2`, unit-of-work commit |
|
|
18
|
+
| `cratis.chronicle.client.reactor.handle` | Consumer | a reactor delivery |
|
|
19
|
+
| `cratis.chronicle.client.reducer.reduce` | Consumer | a reducer delivery |
|
|
20
|
+
|
|
21
|
+
Attributes: `cratis.event_store.name`, `cratis.event_store.namespace`, `cratis.event_sequence.id`, `cratis.event_type.id` (append spans), `cratis.event.count` (batches), `cratis.observer.id`, `cratis.correlation_id`. **`cratis.event_source.id` is off by default** because an event source id can be sensitive; enable it with `config :cratis_chronicle, trace_event_source_id: true`.
|
|
22
|
+
|
|
23
|
+
A span is marked error when the operation returns `{:error, _}`, and a raised exception is recorded before re-raising. The status description is a fixed classification; messages, reasons and stacktraces are never put on a span because they can carry payloads.
|
|
24
|
+
|
|
25
|
+
## `:telemetry` events
|
|
26
|
+
|
|
27
|
+
For metrics built on `:telemetry`, the same operations emit spans of `:start`, `:stop` and `:exception` events:
|
|
28
|
+
|
|
29
|
+
- `[:cratis, :chronicle, :client, :append, ...]`
|
|
30
|
+
- `[:cratis, :chronicle, :client, :reactor, :handle, ...]`
|
|
31
|
+
- `[:cratis, :chronicle, :client, :reducer, :reduce, ...]`
|
|
32
|
+
|
|
33
|
+
The `:stop` metadata carries `error: true` when the operation returned `{:error, _}`. Observer lag is a separate event, `[:chronicle, :observer, :lag]`.
|
|
34
|
+
|
|
35
|
+
## Trace propagation
|
|
36
|
+
|
|
37
|
+
Every outbound gRPC call carries W3C `traceparent`/`tracestate` for the span active in the **calling process**, injected by `Chronicle.Connections.TracePropagationInterceptor` (always installed). A trace continues into the kernel only when the append or query runs inside the active span (for example inside `OpenTelemetry.Tracer.with_span/2`); a `Task` is a different process. The kernel does not store trace context on events, so an observer's span is not linked to the append that caused it. OpenTelemetry metrics instruments are not emitted, only `:telemetry` events.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!-- Copyright (c) Cratis. All rights reserved. -->
|
|
2
|
+
<!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->
|
|
3
|
+
|
|
4
|
+
# Behavior patterns
|
|
5
|
+
|
|
6
|
+
Source: `Cratis/Chronicle.Elixir`: `Source/chronicle/lib/chronicle/patterns.ex` and `Documentation/patterns.md`.
|
|
7
|
+
|
|
8
|
+
Requires `cratis_chronicle` 3.13.0.
|
|
9
|
+
|
|
10
|
+
`Chronicle.Patterns` queries the recurring behavior (combinations of facets such as `Day`, `TimeBucket`, `CommandType`, `AggregateType`) that Chronicle mined from an event store's history for a scope, for example one user. **An empty result is an answer, not a failure**: the scope has no established behavior for that context. Treat it as "do not claim to know".
|
|
11
|
+
|
|
12
|
+
```elixir
|
|
13
|
+
{:ok, patterns} = Chronicle.Patterns.get_patterns_at("user-42")
|
|
14
|
+
{:ok, patterns} = Chronicle.Patterns.get_patterns_at("user-42", moment, also_constraining: %{Chronicle.Patterns.FacetName.aggregate_type() => "Invoice"})
|
|
15
|
+
action = pattern.facets[Chronicle.Patterns.FacetName.command_type()]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- Default to `get_patterns_at/3` ("what does this scope usually do now"). It derives `Day` and `TimeBucket` from the moment's **own offset**, not UTC, with the rule the kernel mined with; the moment defaults to now.
|
|
19
|
+
- Use `get_patterns/3` and `get_usual_actions/3` only for a hand-built facet map; `get_usual_actions/3` returns at most one answer per action.
|
|
20
|
+
- `get_patterns_for_scope/2` and `get_scopes/1` inspect what has been mined.
|
|
21
|
+
- `Chronicle.Patterns.time_bucket/1` exposes the kernel's part-of-day rule (`:early_morning`, `:morning`, `:midday`, `:afternoon`, `:evening`, `:night`). **Never write a copy**: a slot the mining never used looks exactly like "nothing established".
|
|
22
|
+
- Options on every call: `:client`, `:namespace`, `:minimum_confidence` (0.0 to 1.0) and `:max_results`. The all-patterns query is not exposed.
|
|
@@ -16,10 +16,18 @@ Source: `Cratis/Chronicle.Elixir`.
|
|
|
16
16
|
| Assuming the namespace default is `"default"` | It is `"Default"`; the moduledoc is wrong |
|
|
17
17
|
| `:ok = Chronicle.append(...)` in a server | A constraint violation returns `{:error, _}` and raises `MatchError` |
|
|
18
18
|
| Waiting for `:connected` | Observers attach at `:registered` |
|
|
19
|
-
| Reading `:correlation_id` from
|
|
19
|
+
| Reading `:correlation_id` or `:caused_by` from the context of an older client | It was a bare six-key map before the `EventContext` struct; see the upgrading reference |
|
|
20
20
|
| Two read models with the same final module segment | The default id is the last segment only |
|
|
21
21
|
| Appending from a `Task` and expecting the correlation id | Ambient context is per process |
|
|
22
22
|
| Treating quiet observers as "no events" | Keepalive eviction can silence observers while appends still succeed |
|
|
23
23
|
| Shipping the default TLS behavior | `skip_tls_validation` defaults to `true` |
|
|
24
24
|
| Forgetting `global_handlers:` or `:otp_app` discovery | Shared mappings are not merged into variants unless registered |
|
|
25
25
|
| A shared handler mapping a field one variant lacks | `GlobalHandlerPropertyNotOnVariant` at registration, not a silently skipped mapping |
|
|
26
|
+
| `DecisionReads.get/2` outside `Chronicle.decide/2` | Returns `{:error, :requires_unit_of_work}`; it guards nothing |
|
|
27
|
+
| Unset expected sequence number as "expect no event" | It disables the check; use `ConcurrencyScope.expecting_no_matching_event/1` |
|
|
28
|
+
| Passing no `client: client` in a `Chronicle.Testing.Case` spec | The call addresses the default client, not the test's |
|
|
29
|
+
| Trusting a spec that passed on the in-memory kernel | Queries, projections and `closes_stream` return `{:error, {:not_supported_in_memory, rpc}}`; use `Testing.Integration` |
|
|
30
|
+
| Renaming a reactor, reducer or projection module without a pinned `:id` | Its observation position silently resets |
|
|
31
|
+
| Capture event type ids with hyphens | `append/2,3` needs an uppercase-first, letters-digits-underscore id |
|
|
32
|
+
| Nested `children/3` ids reused across read model instances | Nested events can land on the wrong instance; use UUIDs |
|
|
33
|
+
| Reading a read model right after an append | `{:ok, nil}` means absent or not projected yet; use `Projections.wait_for/3` |
|
|
@@ -50,3 +50,31 @@ from_every(increment: [event_counts: {:event_context, :type}])
|
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
`decrement:` supports the corresponding dictionary operation. Use typed defaults for numeric and boolean fields on every model: schemas are generated from those defaults, not static Elixir types.
|
|
53
|
+
|
|
54
|
+
## Child collections
|
|
55
|
+
|
|
56
|
+
Requires `cratis_chronicle` 3.13.0 for the one-line form, `removed_with_join/2`, `clear_with/2` and the compile-time checks below; `children/2,3` itself requires 3.11.0.
|
|
57
|
+
|
|
58
|
+
`children/2,3` declares a list on the parent that grows, changes and shrinks with events. It works in `use Chronicle.ReadModels.ReadModel` and `use Chronicle.Projections.Projection`; declare the list field's element type with `types: [items: {:list, MyApp.ReadModels.BasketItem}]`.
|
|
59
|
+
|
|
60
|
+
```elixir
|
|
61
|
+
children :items, identified_by: :item_id do
|
|
62
|
+
from MyApp.Events.ItemAdded,
|
|
63
|
+
key: :item_id,
|
|
64
|
+
parent_key: :event_source_id,
|
|
65
|
+
set: [item_id: :item_id, name: :name]
|
|
66
|
+
|
|
67
|
+
removed_with MyApp.Events.ItemRemoved, key: :item_id, parent_key: :event_source_id
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A collection that only grows from one event needs no block: `children :entries, from: MyApp.Events.EntryAdded, identified_by: :entry_id, key: :entry_id, set: [entry_id: :entry_id, name: :name]`.
|
|
72
|
+
|
|
73
|
+
- Inside the block `from/2`, `join/2`, `removed_with/2`, `removed_with_join/2`, `clear_with/2`, `from_every/1` and a nested `children/3` take the options they take on the read model. `key` selects the child an event applies to; `parent_key` selects its parent.
|
|
74
|
+
- A `join` in a children block defaults `:on` to `identified_by`; an explicit disagreeing `:on` compiles with a warning. A root `join/2` still requires `:on`.
|
|
75
|
+
- `removed_with_join event, key: :archived_entry_id` removes a child when an event from a **different** event source matches it by property instead of the collection key.
|
|
76
|
+
- `clear_with event, [:field]` (or `clear: [:field]` in a mapping) sets a scalar back to no value. The field must default to `nil`; a field defaulting to `""` is not nullable to the kernel, so clearing writes the type default.
|
|
77
|
+
- **Give nested child collections globally unique identifiers (UUIDs).** In a nested `children/3`, `parent_key` names a child of the collection one level up, and the kernel finds it across every instance of the read model; two instances that both hold `"e-1"` can receive each other's nested events.
|
|
78
|
+
- Compile-time errors cover a `children` without `:identified_by`, a root `join/2` without `:on`, one target property assigned twice by one event, and a `from`/`join` target or `children` name that is not a struct field.
|
|
79
|
+
|
|
80
|
+
A read model document is limited to 4 MiB; give a child collection that can grow without bound its own read model.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<!-- Copyright (c) Cratis. All rights reserved. -->
|
|
2
|
+
<!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->
|
|
3
|
+
|
|
4
|
+
# Querying, observing and previewing read models
|
|
5
|
+
|
|
6
|
+
Source: `Cratis/Chronicle.Elixir`: `Source/chronicle/lib/chronicle/read_models.ex`, `read_models/watchers.ex`, `read_models/reactor.ex`, `projections.ex`, and `Documentation/read-models.md`, `projections.md`.
|
|
7
|
+
|
|
8
|
+
Requires `cratis_chronicle` 3.13.0 for everything below except `watch/2`.
|
|
9
|
+
|
|
10
|
+
A read model is eventually consistent: `Chronicle.read_model/3` returns `{:ok, nil}` both for an absent key and for one not projected yet. To read your own write, append with `append_and_wait_for_completion/3` or call `Chronicle.Projections.wait_for/3`.
|
|
11
|
+
|
|
12
|
+
## Wait for a projection
|
|
13
|
+
|
|
14
|
+
```elixir
|
|
15
|
+
{:ok, %{sequence_number: sequence}} = Chronicle.EventSequences.EventLog.append_with_result(id, event)
|
|
16
|
+
:ok = Chronicle.Projections.wait_for(MyApp.ReadModels.Account, sequence)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`wait_for/3` polls until the projection has handled that sequence number or returns `{:error, :timeout}`.
|
|
20
|
+
|
|
21
|
+
## Filter and sort
|
|
22
|
+
|
|
23
|
+
`Chronicle.ReadModels.query/2` accepts `:filter` (all conditions must match; a value means equality, or `{:gt, v}`, `{:gte, v}`, `{:lt, v}`, `{:lte, v}`, `{:ne, v}`, `{:in, [v]}`, `{:contains, v}`) and `:sort` (`{:amount, :desc}`). An unknown field returns `{:error, {:unknown_field, field}}`.
|
|
24
|
+
|
|
25
|
+
**The client applies the filter** by paging through the whole read model, because the kernel contract cannot filter. A read model with more than `:max_scan` instances (default `10_000`) returns `{:error, {:scan_limit_exceeded, max_scan}}` rather than a partial answer. For large data, give the question its own projection.
|
|
26
|
+
|
|
27
|
+
## Live changes: pick by what you need
|
|
28
|
+
|
|
29
|
+
| Need | Function | Messages the process receives |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| Changes to a read model, one stream per caller | `watch/2` / `unwatch/1` | `{:chronicle_read_model_changed, module, %Changeset{}}`, `{:chronicle_read_model_watch_error, module, reason}` (the watch has ended; call again) |
|
|
32
|
+
| Same, shared across callers (default) | `subscribe/2` / `unsubscribe/1` | the two above plus `{:chronicle_read_model_watch_ended, module, reason}` and `{:chronicle_read_model_watch_resumed, module}` |
|
|
33
|
+
| The current page of instances, re-sent on change | `observe/2` / `unobserve/1`, or `observe_stream/2` as an Elixir `Stream` of `%QueryResult{}` | `{:chronicle_read_model_instances, module, %{instances: _}}`, `{:chronicle_read_model_observe_error, module, reason}` (observation ended) |
|
|
34
|
+
|
|
35
|
+
`subscribe/2` shares one kernel stream per read model, client, namespace and event sequence and reconnects with backoff (500 ms doubling to 30 s). **Changes made while the stream was down are not replayed**; query again after `:chronicle_read_model_watch_resumed`. `observe/2` accepts `:page_size`, `:filter` and `:sort`; with a filter, `total_count` counts matches within the delivered page only.
|
|
36
|
+
|
|
37
|
+
A `use Chronicle.ReadModels.Reactor, model: M` module reacts to read model changes through optional `on_added/2`, `on_modified/2` and `on_removed/2`, and is registered with the client's `:read_model_reactors` option. It subscribes through `subscribe/2` and re-subscribes after a reconnect.
|
|
38
|
+
|
|
39
|
+
## Ad-hoc queries and previews
|
|
40
|
+
|
|
41
|
+
`Chronicle.Projections.query/2` sends a Projection Declaration Language string to the kernel and returns the projected entries without registering a projection or defining a module; entries are plain maps unless `:read_model` is given. Bad syntax returns `{:error, {:invalid_declaration, [%{message:, line:, column:}]}}`. `Chronicle.Projections.preview/2` runs an **already registered** projection's declaration over the stored event sequence; previewing over events you supply is not possible.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<!-- Copyright (c) Cratis. All rights reserved. -->
|
|
2
|
+
<!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->
|
|
3
|
+
|
|
4
|
+
# Testing against an in-memory kernel
|
|
5
|
+
|
|
6
|
+
Source: `Cratis/Chronicle.Elixir`: `Source/chronicle/lib/chronicle/testing.ex`, `testing/case.ex`, `testing/assertions.ex`, `testing/in_memory_kernel.ex`, `testing/integration.ex`, `testing/projections.ex`, and `Documentation/testing.md`.
|
|
7
|
+
|
|
8
|
+
Requires `cratis_chronicle` 3.13.0. Earlier clients ship no `Chronicle.Testing` modules.
|
|
9
|
+
|
|
10
|
+
## Default: `Chronicle.Testing.Case`
|
|
11
|
+
|
|
12
|
+
Use the case template for ExUnit specs. It starts a real `Chronicle.Client` against `Chronicle.Testing.InMemoryKernel`, puts the client name in the test context as `:chronicle`, and stops it on exit. Every test gets a unique event store, so it is safe under `async: true`.
|
|
13
|
+
|
|
14
|
+
```elixir
|
|
15
|
+
# Copyright (c) Cratis. All rights reserved.
|
|
16
|
+
# Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
17
|
+
|
|
18
|
+
defmodule MyApp.AccountTest do
|
|
19
|
+
use Chronicle.Testing.Case,
|
|
20
|
+
event_types: [MyApp.Events.AccountOpened],
|
|
21
|
+
reducers: [MyApp.Reducers.AccountReducer]
|
|
22
|
+
|
|
23
|
+
import Chronicle.Testing.Assertions
|
|
24
|
+
|
|
25
|
+
test "opening an account appends AccountOpened", %{chronicle: client} do
|
|
26
|
+
:ok =
|
|
27
|
+
Chronicle.EventSequences.EventLog.append(
|
|
28
|
+
"account-1",
|
|
29
|
+
%MyApp.Events.AccountOpened{owner_name: "Alice"},
|
|
30
|
+
client: client
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
assert_appended(client, MyApp.Events.AccountOpened, event_source_id: "account-1")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Pass `client: client` on every call.** Without it the call addresses the default `Chronicle.Client` name, not the test's client.
|
|
39
|
+
|
|
40
|
+
`start_supervised_client/1` (outside ExUnit templates; pair with `stop_supervised_client/1`) accepts `:event_types`, `:event_sources`, `:constraints` (standalone constraint modules), `:reactors`, `:reducers`, `:projections`, `:name`, `:event_store` and `:namespace`.
|
|
41
|
+
|
|
42
|
+
## Waiting and reading
|
|
43
|
+
|
|
44
|
+
An append returns once the event is committed; reactors and reducers run afterward in `Chronicle.Testing.Dispatcher`, synchronously after each committed append. Call `Chronicle.Testing.wait_for_observers(client)` before asserting on their effects; it is a sleep-free barrier that returns `{:error, failures}` when a reactor or reducer raised.
|
|
45
|
+
|
|
46
|
+
- `Chronicle.Testing.read_model(client, ReducerModule, key)` reads the state a **reducer** built, keyed by event source id.
|
|
47
|
+
- `Chronicle.Testing.appended_events(client, event_source_id)` returns decoded `Chronicle.Events.AppendedEvent` structs.
|
|
48
|
+
|
|
49
|
+
## Assertions
|
|
50
|
+
|
|
51
|
+
`assert_appended/3` and `refute_appended/3` take a matcher: an exact event struct (`==`), an event type module, or a one-arity predicate; options include `:event_source_id` and `:count`. `assert_constraint_violation(result, constraint_name \\ nil)` and `assert_concurrency_violation(result)` match the `{:error, {:constraint_violations, _}}` and `{:error, {:concurrency_violations, _}}` append results.
|
|
52
|
+
|
|
53
|
+
## What the in-memory kernel does not model
|
|
54
|
+
|
|
55
|
+
Appends, reads, the tail lookups, concurrency scopes (including `expecting_no_matching_event/1`), and `unique/2` and `unique_event_type/1` constraints are modeled. **Everything else returns `{:error, {:not_supported_in_memory, rpc}}`**: `closes_stream` constraints, `ReadModels` queries and `Projections` queries. A spec that passes against it proves client logic, not kernel behavior.
|
|
56
|
+
|
|
57
|
+
`Chronicle.Testing.Projections.apply_event(ReadModel, instance_or_nil, event)` folds one event through the supported subset of a read model's declarations (`from/2` with `:set`, `:add`, `:subtract`, `:count`, `:increment`, `:decrement`, `:clear`, AutoMap, and `removed_with/2`). `join/2`, `children/2`, `from_every/1` and `removed_with_join/2` raise `Chronicle.Testing.UnsupportedProjectionOperation`; test those against a real kernel.
|
|
58
|
+
|
|
59
|
+
## Real kernel: `Chronicle.Testing.Integration`
|
|
60
|
+
|
|
61
|
+
Tag these specs `@moduletag :integration`. `Chronicle.Testing.Integration.setup_isolated_client(opts, client \\ Chronicle.Client)` starts a real client with a unique event store and namespace, reading `CHRONICLE_INTEGRATION_CONNECTION_STRING` unless `:connection_string` is given, and waits for registration. Use `append_with_result/3` then `Integration.wait_for_observers(result.sequence_number, client: client)`; do not sleep. The stores it creates are not deleted afterward because the kernel has no delete call.
|
|
62
|
+
|
|
63
|
+
`Chronicle` adopts `Chronicle.Behaviour`, so code that only needs to be observed calling the facade can use a Mox mock (`Mox.defmock(Mock, for: Chronicle.Behaviour)`); Mox is not a dependency of the client.
|
|
@@ -32,3 +32,21 @@ Rejected appends (including concurrency/constraint failures) return `{:error, re
|
|
|
32
32
|
Declared `unique(..., message: ...)` violation messages are preserved on single, batch and transactional appends. Retain the constraint name and diagnostics when handling violations rather than assuming every message came from the kernel.
|
|
33
33
|
|
|
34
34
|
Requires `cratis_chronicle` 3.10.0 and Chronicle kernel 19.30.0 for named event source definitions.
|
|
35
|
+
|
|
36
|
+
## Behavior changes to review when raising the requirement
|
|
37
|
+
|
|
38
|
+
Requires `cratis_chronicle` 3.13.0 for the behaviors below; each is a difference an older application can observe.
|
|
39
|
+
|
|
40
|
+
| Behavior | Review |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| Reactor and reducer `context` is a `Chronicle.Events.EventContext` struct | Pattern matches on a bare map keep working; `correlation_id`, `caused_by`, `causation`, `tags`, `subject`, `appended_generation` and `replaying?` are now present |
|
|
43
|
+
| `drift_policy` defaults to `:warn` | Start-up makes extra kernel calls and logs findings; set `:ignore` to skip them, `:fail` to block on a drift warning |
|
|
44
|
+
| Discovery warns once per module for a reactor, reducer or projection with no pinned `:id` | Pin ids before renaming or moving modules; see [discovery-and-generations.md](discovery-and-generations.md) |
|
|
45
|
+
| Appending a struct that is not an event type returns `{:error, {:unknown_event_type, module}}` | Handle it as an ordinary append error |
|
|
46
|
+
| Spans and `:telemetry` events are always emitted | No action without an SDK; see [observability.md](observability.md) |
|
|
47
|
+
| The client installs a Logger filter for Mint response crash reports | Keep your own production filter for the remaining crash shapes |
|
|
48
|
+
| `children/2,3` and projection mistakes are rejected at compile time | Fix declarations that assigned one target twice or named a missing field |
|
|
49
|
+
|
|
50
|
+
Requires `cratis_chronicle` 3.11.0 for the context struct; it was a bare map before.
|
|
51
|
+
|
|
52
|
+
Capabilities covered by these references: decision reads ([decision-reads.md](decision-reads.md)), `Chronicle.Testing` ([testing.md](testing.md)), captures ([captures.md](captures.md)), standalone constraints ([constraints.md](constraints.md)), `Chronicle.Patterns` ([patterns.md](patterns.md)) and read model subscriptions ([read-models.md](read-models.md)). A git tag is not a Hex release: confirm the version is on hex.pm before depending on it.
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
"fixtures": [],
|
|
5
5
|
"assertions": [
|
|
6
6
|
{ "kind": "skill-contains", "value": "`Cratis/Chronicle.Elixir`" },
|
|
7
|
-
{ "kind": "skill-contains", "value": "{:cratis_chronicle, \"~> 3.
|
|
7
|
+
{ "kind": "skill-contains", "value": "{:cratis_chronicle, \"~> 3.13\"}" },
|
|
8
8
|
{ "kind": "skill-contains", "value": "ConnectionString.development()" },
|
|
9
9
|
{ "kind": "skill-contains", "value": "`~> 1.18`" },
|
|
10
10
|
{ "kind": "skill-contains", "value": "`nil` registers as string" },
|
|
11
|
-
{ "kind": "skill-contains", "value": "**The
|
|
12
|
-
{ "kind": "skill-contains", "value": "`
|
|
11
|
+
{ "kind": "skill-contains", "value": "**The context is a `Chronicle.Events.EventContext` struct**" },
|
|
12
|
+
{ "kind": "skill-contains", "value": "`delivery_id` (stable across replay and failed-partition" },
|
|
13
13
|
{ "kind": "skill-contains", "value": "**Page 1 is the second page**" },
|
|
14
14
|
{ "kind": "skill-contains", "value": "`:retry_attempts` is accepted but ignored", "file": "references/connections.md" },
|
|
15
15
|
{ "kind": "skill-contains", "value": "Client certificates cannot be combined with `disableTls=true`", "file": "references/connections.md" },
|
|
@@ -18,6 +18,19 @@
|
|
|
18
18
|
{ "kind": "skill-contains", "value": "`Chronicle.Events.ConcurrencyScope.none()`", "file": "references/event-sources.md" },
|
|
19
19
|
{ "kind": "skill-contains", "value": "**no many-event variant**", "file": "references/append-api.md" },
|
|
20
20
|
{ "kind": "skill-contains", "value": "`subject/1` is not imported by `EventType`", "file": "references/compliance.md" },
|
|
21
|
-
{ "kind": "skill-contains", "value": "calendar year, not the ISO week-year", "file": "references/projections.md" }
|
|
21
|
+
{ "kind": "skill-contains", "value": "calendar year, not the ISO week-year", "file": "references/projections.md" },
|
|
22
|
+
{ "kind": "skill-contains", "value": "Outside one it returns `{:error, :requires_unit_of_work}`", "file": "references/decision-reads.md" },
|
|
23
|
+
{ "kind": "skill-contains", "value": "**Leaving the expected sequence number unset turns the check off.**", "file": "references/decision-reads.md" },
|
|
24
|
+
{ "kind": "skill-contains", "value": "**Everything else returns `{:error, {:not_supported_in_memory, rpc}}`**", "file": "references/testing.md" },
|
|
25
|
+
{ "kind": "skill-contains", "value": "**Pass `client: client` on every call.**", "file": "references/testing.md" },
|
|
26
|
+
{ "kind": "skill-contains", "value": "`name:` is **required** on a standalone declaration.", "file": "references/constraints.md" },
|
|
27
|
+
{ "kind": "skill-contains", "value": "**Renaming or moving the module then creates a different artifact", "file": "references/discovery-and-generations.md" },
|
|
28
|
+
{ "kind": "skill-contains", "value": "`mix chronicle.discover --otp-app my_app`", "file": "references/discovery-and-generations.md" },
|
|
29
|
+
{ "kind": "skill-contains", "value": "**An appended event type's id must start with an uppercase letter", "file": "references/captures.md" },
|
|
30
|
+
{ "kind": "skill-contains", "value": "**Give nested child collections globally unique identifiers (UUIDs).**", "file": "references/projections.md" },
|
|
31
|
+
{ "kind": "skill-contains", "value": "**The client applies the filter**", "file": "references/read-models.md" },
|
|
32
|
+
{ "kind": "skill-contains", "value": "**`cratis.event_source.id` is off by default**", "file": "references/observability.md" },
|
|
33
|
+
{ "kind": "skill-contains", "value": "**An empty result is an answer, not a failure**", "file": "references/patterns.md" },
|
|
34
|
+
{ "kind": "skill-contains", "value": "`{:error, {:unknown_event_type, module}}`", "file": "references/append-api.md" }
|
|
22
35
|
]
|
|
23
36
|
}
|
|
@@ -57,7 +57,7 @@ module Exams
|
|
|
57
57
|
then denied
|
|
58
58
|
|
|
59
59
|
slice Translate ImportProctorResults
|
|
60
|
-
description "Per external field: reference -> correlation key (our attempt id, echoed); outcome -> translated to our enum; score -> kept; examinerNote -> ignored (free text, may hold personal data, no consumer). Repeated delivery:
|
|
60
|
+
description "Per external field: reference -> correlation key (our attempt id, echoed); outcome -> translated to our enum; score -> kept; examinerNote -> ignored (free text, may hold personal data, no consumer). Repeated delivery: the first record of a key appends once (`when added`, Chronicle's first-observation rule); a later delivery of the same key appends nothing, even if the outcome or score changed, so it does not implement corrections. OneResultPerAttempt refuses a second local result. Changed outcomes and score-only corrections need an explicit correction policy (open decision, recorded in STATE.md, not an overwrite) before this is a complete integration."
|
|
61
61
|
capture ProctorResults
|
|
62
62
|
source webhook
|
|
63
63
|
path /webhooks/proctoring/results
|
|
@@ -67,21 +67,9 @@ module Exams
|
|
|
67
67
|
"P" => passed
|
|
68
68
|
"F" => failed
|
|
69
69
|
append ProctorResultReceived
|
|
70
|
-
when
|
|
70
|
+
when added
|
|
71
71
|
outcome = $.outcome
|
|
72
72
|
score = $.score
|
|
73
|
-
reaction PassTranslator
|
|
74
|
-
when ProctorResultReceived
|
|
75
|
-
outcome
|
|
76
|
-
score
|
|
77
|
-
produces ExamPassed
|
|
78
|
-
score = score
|
|
79
|
-
where outcome == "passed"
|
|
80
|
-
reaction FailTranslator
|
|
81
|
-
when ProctorResultReceived
|
|
82
|
-
outcome
|
|
83
|
-
produces ExamFailed
|
|
84
|
-
where outcome == "failed"
|
|
85
73
|
event ProctorResultReceived
|
|
86
74
|
outcome ProctorOutcome
|
|
87
75
|
score Int
|
|
@@ -112,12 +100,26 @@ module Exams
|
|
|
112
100
|
outcome = "failed"
|
|
113
101
|
score = 40
|
|
114
102
|
then error "Constraint 'OneResultPerAttempt' is violated: the event source already has the constrained event."
|
|
103
|
+
|
|
104
|
+
slice Automation TranslateProctorResults
|
|
105
|
+
description "Our facts from the provider's: the translators react to the external fact the Translate slice records"
|
|
106
|
+
reaction PassTranslator
|
|
107
|
+
when ProctorResultReceived
|
|
108
|
+
outcome
|
|
109
|
+
score
|
|
110
|
+
produces ExamPassed
|
|
111
|
+
score = score
|
|
112
|
+
where outcome == "passed"
|
|
113
|
+
reaction FailTranslator
|
|
114
|
+
when ProctorResultReceived
|
|
115
|
+
outcome
|
|
116
|
+
produces ExamFailed
|
|
117
|
+
where outcome == "failed"
|
|
115
118
|
```
|
|
116
119
|
|
|
117
120
|
Notes for the reader:
|
|
118
121
|
|
|
119
|
-
- A capture
|
|
120
|
-
slice. `then` lists every new fact, so `TranslatingAPass` names both the external fact and ours.
|
|
122
|
+
- A capture and the external fact it appends stay in the `Translate` slice; the translator reactions that answer that fact sit in an `Automation` slice beside it (a reaction in the capture's own directionless `Translate` slice is `PLAY0694`). `then` lists every new fact, so `TranslatingAPass` names both the external fact and ours.
|
|
121
123
|
- The unauthenticated-booking spec needs an explicit `given caller` (empty means no caller);
|
|
122
124
|
an authorized command exercised without one is PLAY0389 at binding.
|
|
123
125
|
- Authenticity of the webhook, tenant ownership and a score-correction policy are not in the
|
|
@@ -244,7 +244,7 @@ Never invent commands, parameters, syntax nodes or downstream capabilities.
|
|
|
244
244
|
| Commands, events, validation and concurrency | `cratis-screenplay-command-surface` |
|
|
245
245
|
| Projections and reducers | `cratis-screenplay-projections` |
|
|
246
246
|
| Read models, queries and screens | `cratis-screenplay-read-surface` |
|
|
247
|
-
| Layouts, forms, contributions, component bindings, toolbars, outlets, themes and localization | `cratis-screenplay-ui-composition` |
|
|
247
|
+
| Layouts, forms, contributions, navigation regions, slice buttons, template references, sample data, component bindings, toolbars, outlets, themes and localization | `cratis-screenplay-ui-composition` |
|
|
248
248
|
| Captures, reactions and triggers | `cratis-screenplay-captures-and-reactions` |
|
|
249
249
|
| Behavioral examples and assertions | `cratis-screenplay-specifications` |
|
|
250
250
|
| Rendering/running an admitted model | `cratis-stage-rendering-and-sandbox` |
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
- 3. Review and apply with revision checks
|
|
12
12
|
- 4. Validate authoring and executable readiness
|
|
13
13
|
- 5. Run/render using the same configuration
|
|
14
|
+
- Edit the application shell
|
|
14
15
|
- 6. Use Screenplay-owned fixtures
|
|
15
16
|
- Replay the workflow in CI without a Screenplay checkout
|
|
16
17
|
- Edit, render, run and see it in the browser
|
|
@@ -148,6 +149,30 @@ represented in the generated plan or runtime diagnostics. A consumer that cannot
|
|
|
148
149
|
implement one reports a blocking diagnostic. Silent fallback to the default screen
|
|
149
150
|
composition is a failure.
|
|
150
151
|
|
|
152
|
+
## Edit the application shell
|
|
153
|
+
|
|
154
|
+
Navigation regions, toolbar buttons, blueprint template references and sample data are typed nodes and follow the same
|
|
155
|
+
open, read, propose, review, apply, reopen loop; the syntax and rules are in
|
|
156
|
+
`cratis-screenplay-ui-composition` (`references/application-shell.md`). The vectors to replay against are the
|
|
157
|
+
`screen-shell-navigation-positive` source case and its rejection vectors in `Cratis.Screenplay.CanonicalCorpus`.
|
|
158
|
+
|
|
159
|
+
1. `read-ast` with `kind` `NavigationRegionSyntax`, `NavigationItemSyntax`, `ToolbarButtonSyntax`,
|
|
160
|
+
`TemplateAssignmentSyntax`, `ScreenTemplateReferenceSyntax` or `SampleDataPackSyntax`, `includeContent: true`.
|
|
161
|
+
Select the occurrence by its parent: a region belongs to the `LayoutSyntax` or a `ModuleSyntax`, and the same item id
|
|
162
|
+
can appear in several regions.
|
|
163
|
+
2. `propose-ast` with `replace` (copy the returned node, change `mode`, `items`, `slice`, `version` or a row value),
|
|
164
|
+
`add` to a member the node kind declares (`navigation`, `items`, `children`, `buttons`) or `remove`. A member that
|
|
165
|
+
holds nothing is not in the node JSON; `add` still accepts it.
|
|
166
|
+
3. Read the proposal's diagnostics before applying. A refusal such as `PLAY0750` (a module item targeting another
|
|
167
|
+
module), `PLAY0762` (a button on a State View slice), `PLAY0722` (a blueprint no profile lists) or `PLAY0714` (an
|
|
168
|
+
unknown active pack) is the answer; change the proposal, not the rule.
|
|
169
|
+
4. `apply` with both revisions. A `StaleRevision` means the model moved: reopen, re-read the nodes and propose again.
|
|
170
|
+
5. Switching a module from inherit to override copies the application's items; retarget each copy to the module's own
|
|
171
|
+
features first or the proposal is refused.
|
|
172
|
+
|
|
173
|
+
Requires Screenplay 4.188.0 or later (`screenplay --version`) to `add` the first region, button or sub-item to a member
|
|
174
|
+
the node JSON omits while empty; an older MCP server answers "Unknown typed syntax member".
|
|
175
|
+
|
|
151
176
|
## 6. Use Screenplay-owned fixtures
|
|
152
177
|
|
|
153
178
|
The canonical multi-file application belongs to the Screenplay/conformance corpus,
|
|
@@ -317,6 +317,6 @@ Read [screen patterns](references/screen-patterns.md) when deciding whether a sc
|
|
|
317
317
|
Attribution and licences: `references/provenance.md`.
|
|
318
318
|
|
|
319
319
|
- How events fill the read model: `cratis-screenplay-projections`.
|
|
320
|
-
- Layouts, templates, forms, component bindings, toolbars, outlets, contributions, packages and themes: `cratis-screenplay-ui-composition`.
|
|
320
|
+
- Layouts, templates, forms, component bindings, toolbars and slice buttons, outlets, contributions, navigation regions, sample data, packages and themes: `cratis-screenplay-ui-composition`.
|
|
321
321
|
- Asserting query results: `cratis-screenplay-specifications`.
|
|
322
322
|
- Deciding which read models exist: `cratis-screenplay-event-modeling`.
|
|
@@ -93,15 +93,18 @@ module Invoicing
|
|
|
93
93
|
when outcome
|
|
94
94
|
outcome = $.outcome
|
|
95
95
|
reason = $.reason
|
|
96
|
+
event PaymentGatewayResultReceived
|
|
97
|
+
outcome GatewayOutcome
|
|
98
|
+
reason String
|
|
99
|
+
event PaymentReceived
|
|
100
|
+
|
|
101
|
+
slice Automation TranslatePaymentGatewayResults
|
|
102
|
+
description "Gives PaymentReceived its producer: the translator reacts to the provider's external fact"
|
|
96
103
|
reaction ReceivedTranslator
|
|
97
104
|
when PaymentGatewayResultReceived
|
|
98
105
|
outcome
|
|
99
106
|
produces PaymentReceived
|
|
100
107
|
where outcome == "captured"
|
|
101
|
-
event PaymentGatewayResultReceived
|
|
102
|
-
outcome GatewayOutcome
|
|
103
|
-
reason String
|
|
104
|
-
event PaymentReceived
|
|
105
108
|
|
|
106
109
|
slice StateChange SettleOrVoidInvoice
|
|
107
110
|
description "An invoice ends either settled or voided, never both (SettleOrVoid, enforced at append). NOT enforced in the model today: that the invoice exists and is still open (settling) and that only an open, unpaid invoice may be voided. Stated as `reads OpenInvoice` + `require` (PLAY0271/PLAY0268 at binding; Screenplay#129). Target: Arc [ProtectedDecision] with DecisionRead, or a Chronicle DCB. Requirements to test in the target: settling an unknown invoice is rejected; voiding a paid invoice is rejected; voiding an open invoice succeeds."
|
|
@@ -205,9 +205,18 @@ module Invoicing
|
|
|
205
205
|
"C" => captured
|
|
206
206
|
"D" => declined
|
|
207
207
|
append PaymentGatewayResultReceived
|
|
208
|
-
when
|
|
208
|
+
when added
|
|
209
209
|
outcome = $.outcome
|
|
210
210
|
reason = $.reason
|
|
211
|
+
event PaymentGatewayResultReceived
|
|
212
|
+
outcome GatewayOutcome
|
|
213
|
+
reason String
|
|
214
|
+
event PaymentReceived
|
|
215
|
+
event PaymentDeclined
|
|
216
|
+
reason String
|
|
217
|
+
|
|
218
|
+
slice Automation TranslatePaymentGatewayResults
|
|
219
|
+
description "Our facts from the provider's: the translators react to the external fact the Translate slice records"
|
|
211
220
|
reaction ReceivedTranslator
|
|
212
221
|
when PaymentGatewayResultReceived
|
|
213
222
|
outcome
|
|
@@ -220,12 +229,6 @@ module Invoicing
|
|
|
220
229
|
produces PaymentDeclined
|
|
221
230
|
reason = reason
|
|
222
231
|
where outcome == "declined"
|
|
223
|
-
event PaymentGatewayResultReceived
|
|
224
|
-
outcome GatewayOutcome
|
|
225
|
-
reason String
|
|
226
|
-
event PaymentReceived
|
|
227
|
-
event PaymentDeclined
|
|
228
|
-
reason String
|
|
229
232
|
|
|
230
233
|
specification TranslatingADeclinedCardResult
|
|
231
234
|
when capture PaymentGatewayResults
|