@shortlink-org/portolan 0.1.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/LICENSE +21 -0
- package/README.md +387 -0
- package/catalog/enum_test.go +46 -0
- package/catalog/model.go +932 -0
- package/catalog/roundtrip_test.go +185 -0
- package/catalog/via_test.go +38 -0
- package/cli/portolan.mjs +368 -0
- package/go.mod +14 -0
- package/go.sum +14 -0
- package/index.html +13 -0
- package/internal/commands/cargo.go +154 -0
- package/internal/commands/commands.go +143 -0
- package/internal/commands/commands_test.go +323 -0
- package/internal/commands/gradle.go +77 -0
- package/internal/commands/justfile.go +82 -0
- package/internal/commands/makefile.go +106 -0
- package/internal/commands/maven.go +93 -0
- package/internal/commands/packagejson.go +159 -0
- package/internal/commands/pyproject.go +169 -0
- package/internal/commands/taskfile.go +101 -0
- package/internal/commands/testdata/estate/.cargo/config.toml +7 -0
- package/internal/commands/testdata/estate/Makefile +33 -0
- package/internal/commands/testdata/estate/Taskfile.yml +21 -0
- package/internal/commands/testdata/estate/build.gradle.kts +21 -0
- package/internal/commands/testdata/estate/justfile +20 -0
- package/internal/commands/testdata/estate/package.json +13 -0
- package/internal/commands/testdata/estate/pom.xml +29 -0
- package/internal/commands/testdata/estate/pyproject.toml +27 -0
- package/internal/commands/testdata/estate/xtask/src/main.rs +27 -0
- package/internal/commands/testdata/golden/commands.json +305 -0
- package/internal/gohttp/analyze.go +2500 -0
- package/internal/gohttp/endpoints.go +1067 -0
- package/internal/gohttp/roots.go +320 -0
- package/internal/gohttp/typed.go +143 -0
- package/internal/goscan/constants.go +85 -0
- package/internal/goscan/goscan_test.go +227 -0
- package/internal/goscan/names.go +52 -0
- package/internal/goscan/parse_test.go +11 -0
- package/internal/goscan/source.go +36 -0
- package/internal/goscan/tree.go +155 -0
- package/internal/goscan/types.go +99 -0
- package/internal/wsdl/ids.go +127 -0
- package/internal/wsdl/ids_test.go +21 -0
- package/internal/wsdl/model.go +62 -0
- package/internal/wsdl/parse.go +920 -0
- package/internal/wsdl/parse_test.go +133 -0
- package/package.json +117 -0
- package/plugin/describe.go +107 -0
- package/plugin/describe_test.go +114 -0
- package/plugin/protocol.go +117 -0
- package/plugin/schematest/schematest.go +126 -0
- package/plugins/README.md +1160 -0
- package/plugins/extract-adr/describe.go +19 -0
- package/plugins/extract-adr/describe_test.go +11 -0
- package/plugins/extract-adr/extract.go +153 -0
- package/plugins/extract-adr/extract_test.go +350 -0
- package/plugins/extract-adr/history.go +99 -0
- package/plugins/extract-adr/main.go +65 -0
- package/plugins/extract-adr/options.schema.json +26 -0
- package/plugins/extract-adr/parse.go +642 -0
- package/plugins/extract-adr/parse_test.go +405 -0
- package/plugins/extract-asyncapi/describe.go +19 -0
- package/plugins/extract-asyncapi/describe_test.go +11 -0
- package/plugins/extract-asyncapi/extract.go +315 -0
- package/plugins/extract-asyncapi/extract_test.go +197 -0
- package/plugins/extract-asyncapi/main.go +48 -0
- package/plugins/extract-asyncapi/options.schema.json +26 -0
- package/plugins/extract-asyncapi/spec.go +151 -0
- package/plugins/extract-celery/README.md +169 -0
- package/plugins/extract-celery/extract.py +346 -0
- package/plugins/extract-celery/extract_test.py +192 -0
- package/plugins/extract-celery/main.py +66 -0
- package/plugins/extract-celery/options.py +31 -0
- package/plugins/extract-celery/options.schema.json +31 -0
- package/plugins/extract-celery/producers.py +156 -0
- package/plugins/extract-celery/routes_test.py +106 -0
- package/plugins/extract-commands/describe.go +19 -0
- package/plugins/extract-commands/describe_test.go +11 -0
- package/plugins/extract-commands/extract.go +79 -0
- package/plugins/extract-commands/extract_test.go +92 -0
- package/plugins/extract-commands/main.go +55 -0
- package/plugins/extract-commands/options.schema.json +20 -0
- package/plugins/extract-csr/avro.go +258 -0
- package/plugins/extract-csr/describe.go +19 -0
- package/plugins/extract-csr/describe_test.go +11 -0
- package/plugins/extract-csr/extract.go +338 -0
- package/plugins/extract-csr/extract_test.go +374 -0
- package/plugins/extract-csr/jsonschema.go +343 -0
- package/plugins/extract-csr/lock.go +25 -0
- package/plugins/extract-csr/main.go +84 -0
- package/plugins/extract-csr/options.schema.json +43 -0
- package/plugins/extract-csr/subject.go +102 -0
- package/plugins/extract-django/README.md +386 -0
- package/plugins/extract-django/apps.py +100 -0
- package/plugins/extract-django/auth.py +302 -0
- package/plugins/extract-django/auth_test.py +188 -0
- package/plugins/extract-django/clients.py +121 -0
- package/plugins/extract-django/contracts.py +458 -0
- package/plugins/extract-django/database.py +119 -0
- package/plugins/extract-django/domain.py +318 -0
- package/plugins/extract-django/events.py +86 -0
- package/plugins/extract-django/extract.py +601 -0
- package/plugins/extract-django/extract_test.py +397 -0
- package/plugins/extract-django/flows.py +1278 -0
- package/plugins/extract-django/flows_test.py +338 -0
- package/plugins/extract-django/ids.py +42 -0
- package/plugins/extract-django/ids_test.py +30 -0
- package/plugins/extract-django/lifecycle.py +222 -0
- package/plugins/extract-django/lifecycle_test.py +76 -0
- package/plugins/extract-django/main.py +66 -0
- package/plugins/extract-django/openapi.py +229 -0
- package/plugins/extract-django/openapi_test.py +60 -0
- package/plugins/extract-django/operations.py +96 -0
- package/plugins/extract-django/options.py +61 -0
- package/plugins/extract-django/options.schema.json +126 -0
- package/plugins/extract-django/routing.py +214 -0
- package/plugins/extract-django/routing_test.py +198 -0
- package/plugins/extract-django/serializers.py +558 -0
- package/plugins/extract-django/store.py +434 -0
- package/plugins/extract-django/transport.py +242 -0
- package/plugins/extract-flows/describe.go +19 -0
- package/plugins/extract-flows/describe_test.go +11 -0
- package/plugins/extract-flows/extract.go +91 -0
- package/plugins/extract-flows/main.go +45 -0
- package/plugins/extract-flows/options.schema.json +16 -0
- package/plugins/extract-flows/parse.go +593 -0
- package/plugins/extract-flows/parse_test.go +204 -0
- package/plugins/extract-glossary/describe.go +19 -0
- package/plugins/extract-glossary/describe_test.go +11 -0
- package/plugins/extract-glossary/extract.go +115 -0
- package/plugins/extract-glossary/extract_test.go +220 -0
- package/plugins/extract-glossary/main.go +59 -0
- package/plugins/extract-glossary/options.schema.json +21 -0
- package/plugins/extract-glossary/parse.go +214 -0
- package/plugins/extract-glossary/parse_test.go +203 -0
- package/plugins/extract-go/aggregate.go +214 -0
- package/plugins/extract-go/client.go +409 -0
- package/plugins/extract-go/client_test.go +266 -0
- package/plugins/extract-go/describe.go +19 -0
- package/plugins/extract-go/describe_test.go +11 -0
- package/plugins/extract-go/enum.go +195 -0
- package/plugins/extract-go/enum_test.go +82 -0
- package/plugins/extract-go/event.go +99 -0
- package/plugins/extract-go/extract.go +191 -0
- package/plugins/extract-go/extract_test.go +261 -0
- package/plugins/extract-go/flow.go +1441 -0
- package/plugins/extract-go/flow_test.go +609 -0
- package/plugins/extract-go/httpclient.go +174 -0
- package/plugins/extract-go/httpclient_test.go +296 -0
- package/plugins/extract-go/ids.go +92 -0
- package/plugins/extract-go/layout.go +225 -0
- package/plugins/extract-go/layout_test.go +125 -0
- package/plugins/extract-go/lifecycle.go +324 -0
- package/plugins/extract-go/lifecycle_test.go +108 -0
- package/plugins/extract-go/main.go +79 -0
- package/plugins/extract-go/operation.go +157 -0
- package/plugins/extract-go/options.schema.json +66 -0
- package/plugins/extract-go/source.go +316 -0
- package/plugins/extract-go/transport.go +321 -0
- package/plugins/extract-go/transport_test.go +145 -0
- package/plugins/extract-go/wiring.go +434 -0
- package/plugins/extract-go-nats/describe.go +19 -0
- package/plugins/extract-go-nats/describe_test.go +11 -0
- package/plugins/extract-go-nats/extract.go +177 -0
- package/plugins/extract-go-nats/extract_test.go +389 -0
- package/plugins/extract-go-nats/index.go +379 -0
- package/plugins/extract-go-nats/main.go +42 -0
- package/plugins/extract-go-nats/options.schema.json +21 -0
- package/plugins/extract-go-nats/resolve.go +161 -0
- package/plugins/extract-go-nats/sites.go +224 -0
- package/plugins/extract-graphql/describe.go +19 -0
- package/plugins/extract-graphql/describe_test.go +11 -0
- package/plugins/extract-graphql/extract.go +433 -0
- package/plugins/extract-graphql/extract_test.go +256 -0
- package/plugins/extract-graphql/ids.go +49 -0
- package/plugins/extract-graphql/lex.go +237 -0
- package/plugins/extract-graphql/main.go +51 -0
- package/plugins/extract-graphql/options.schema.json +31 -0
- package/plugins/extract-graphql/parse.go +621 -0
- package/plugins/extract-graphql/parse_test.go +122 -0
- package/plugins/extract-http-clients/describe.go +19 -0
- package/plugins/extract-http-clients/describe_test.go +11 -0
- package/plugins/extract-http-clients/extract.go +705 -0
- package/plugins/extract-http-clients/extract_test.go +1263 -0
- package/plugins/extract-http-clients/main.go +42 -0
- package/plugins/extract-http-clients/options.schema.json +33 -0
- package/plugins/extract-java/README.md +190 -0
- package/plugins/extract-java/build/org/portolan/extract/Catalog.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$2.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Call.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Client.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients$Route.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Clients.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Domain$Aggregate.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Domain.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Events$Found.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Events.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Binding.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Draft.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Frame.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows$Options.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Flows.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Ids.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Json$Reader.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Json.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle$Move.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Lifecycle.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi$Operation.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi$Spec.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/OpenApi.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations$1.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations$UseCase.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Operations.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto$Rpc.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto$Service.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Proto.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source$Project.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source$Unit.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Source.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Transport$Endpoint.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Transport.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml$Line.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml$Reader.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Yaml.class +0 -0
- package/plugins/extract-java/options.schema.json +70 -0
- package/plugins/extract-openapi/describe.go +19 -0
- package/plugins/extract-openapi/describe_test.go +11 -0
- package/plugins/extract-openapi/discover.go +243 -0
- package/plugins/extract-openapi/extract.go +526 -0
- package/plugins/extract-openapi/extract_test.go +545 -0
- package/plugins/extract-openapi/main.go +75 -0
- package/plugins/extract-openapi/options.schema.json +62 -0
- package/plugins/extract-openapi/spec.go +350 -0
- package/plugins/extract-project/README.md +22 -0
- package/plugins/extract-project/describe.go +19 -0
- package/plugins/extract-project/describe_test.go +11 -0
- package/plugins/extract-project/extract.go +221 -0
- package/plugins/extract-project/extract_test.go +109 -0
- package/plugins/extract-project/main.go +41 -0
- package/plugins/extract-project/options.schema.json +48 -0
- package/plugins/extract-proto/ast.go +125 -0
- package/plugins/extract-proto/consumes.go +77 -0
- package/plugins/extract-proto/describe.go +19 -0
- package/plugins/extract-proto/extract.go +293 -0
- package/plugins/extract-proto/extract_test.go +459 -0
- package/plugins/extract-proto/ids.go +89 -0
- package/plugins/extract-proto/ids_test.go +57 -0
- package/plugins/extract-proto/lex.go +285 -0
- package/plugins/extract-proto/main.go +100 -0
- package/plugins/extract-proto/module.go +120 -0
- package/plugins/extract-proto/options.schema.json +51 -0
- package/plugins/extract-proto/parse.go +720 -0
- package/plugins/extract-proto/parse_test.go +307 -0
- package/plugins/extract-proto/provides.go +236 -0
- package/plugins/extract-proto/resolve.go +222 -0
- package/plugins/extract-proto/resolve_test.go +119 -0
- package/plugins/extract-python-kafka/README.md +36 -0
- package/plugins/extract-python-kafka/extract.py +211 -0
- package/plugins/extract-python-kafka/extract_test.py +68 -0
- package/plugins/extract-python-kafka/main.py +59 -0
- package/plugins/extract-python-kafka/options.py +29 -0
- package/plugins/extract-python-kafka/options.schema.json +31 -0
- package/plugins/extract-redis/README.md +11 -0
- package/plugins/extract-redis/describe.go +19 -0
- package/plugins/extract-redis/describe_test.go +11 -0
- package/plugins/extract-redis/extract.go +183 -0
- package/plugins/extract-redis/extract_test.go +168 -0
- package/plugins/extract-redis/keyspaces.go +469 -0
- package/plugins/extract-redis/main.go +44 -0
- package/plugins/extract-redis/options.schema.json +30 -0
- package/plugins/extract-river/describe.go +19 -0
- package/plugins/extract-river/describe_test.go +11 -0
- package/plugins/extract-river/extract.go +507 -0
- package/plugins/extract-river/extract_test.go +132 -0
- package/plugins/extract-river/main.go +42 -0
- package/plugins/extract-river/options.schema.json +21 -0
- package/plugins/extract-rust/Cargo.lock +133 -0
- package/plugins/extract-rust/Cargo.toml +23 -0
- package/plugins/extract-rust/README.md +204 -0
- package/plugins/extract-rust/options.schema.json +65 -0
- package/plugins/extract-rust/rustfmt.toml +1 -0
- package/plugins/extract-sql/ddl.go +893 -0
- package/plugins/extract-sql/ddl_test.go +401 -0
- package/plugins/extract-sql/describe.go +19 -0
- package/plugins/extract-sql/describe_test.go +11 -0
- package/plugins/extract-sql/layout.go +221 -0
- package/plugins/extract-sql/layout_test.go +55 -0
- package/plugins/extract-sql/lineage.go +107 -0
- package/plugins/extract-sql/main.go +141 -0
- package/plugins/extract-sql/maps.go +564 -0
- package/plugins/extract-sql/maps_java.go +117 -0
- package/plugins/extract-sql/maps_rust.go +333 -0
- package/plugins/extract-sql/maps_rust_test.go +70 -0
- package/plugins/extract-sql/maps_test.go +204 -0
- package/plugins/extract-sql/maps_ts.go +398 -0
- package/plugins/extract-sql/maps_ts_test.go +136 -0
- package/plugins/extract-sql/options.schema.json +53 -0
- package/plugins/extract-sql/projection.go +70 -0
- package/plugins/extract-sql/projection_test.go +50 -0
- package/plugins/extract-sql/store.go +420 -0
- package/plugins/extract-sql/store_test.go +233 -0
- package/plugins/extract-sql/view.go +295 -0
- package/plugins/extract-ts/README.md +306 -0
- package/plugins/extract-ts/ast.test.ts +178 -0
- package/plugins/extract-ts/ast.ts +648 -0
- package/plugins/extract-ts/clients.ts +185 -0
- package/plugins/extract-ts/domain.ts +164 -0
- package/plugins/extract-ts/extract.test.ts +208 -0
- package/plugins/extract-ts/extract.ts +200 -0
- package/plugins/extract-ts/flows.ts +783 -0
- package/plugins/extract-ts/graphql.test.ts +73 -0
- package/plugins/extract-ts/graphql.ts +181 -0
- package/plugins/extract-ts/ids.test.ts +21 -0
- package/plugins/extract-ts/ids.ts +52 -0
- package/plugins/extract-ts/lifecycle.ts +138 -0
- package/plugins/extract-ts/main.ts +48 -0
- package/plugins/extract-ts/openapi.test.ts +64 -0
- package/plugins/extract-ts/openapi.ts +99 -0
- package/plugins/extract-ts/operations.ts +93 -0
- package/plugins/extract-ts/options.schema.json +96 -0
- package/plugins/extract-ts/source.test.ts +174 -0
- package/plugins/extract-ts/source.ts +465 -0
- package/plugins/extract-ts/transport.ts +147 -0
- package/plugins/extract-ts/tsconfig.json +13 -0
- package/plugins/extract-ts/wiring.ts +190 -0
- package/plugins/extract-watermill/describe.go +19 -0
- package/plugins/extract-watermill/describe_test.go +11 -0
- package/plugins/extract-watermill/extract.go +1228 -0
- package/plugins/extract-watermill/extract_test.go +234 -0
- package/plugins/extract-watermill/main.go +41 -0
- package/plugins/extract-watermill/options.schema.json +21 -0
- package/plugins/extract-wsdl/describe.go +19 -0
- package/plugins/extract-wsdl/describe_test.go +11 -0
- package/plugins/extract-wsdl/extract.go +148 -0
- package/plugins/extract-wsdl/extract_test.go +45 -0
- package/plugins/extract-wsdl/main.go +53 -0
- package/plugins/extract-wsdl/options.schema.json +59 -0
- package/plugins/fetch-bsr/auth.go +114 -0
- package/plugins/fetch-bsr/bsr.go +240 -0
- package/plugins/fetch-bsr/cache.go +66 -0
- package/plugins/fetch-bsr/describe.go +19 -0
- package/plugins/fetch-bsr/fetch.go +192 -0
- package/plugins/fetch-bsr/fetch_test.go +465 -0
- package/plugins/fetch-bsr/lock.go +71 -0
- package/plugins/fetch-bsr/main.go +117 -0
- package/plugins/fetch-bsr/options.schema.json +42 -0
- package/plugins/fetch-csr/auth.go +95 -0
- package/plugins/fetch-csr/cache.go +69 -0
- package/plugins/fetch-csr/describe.go +19 -0
- package/plugins/fetch-csr/describe_test.go +11 -0
- package/plugins/fetch-csr/fetch.go +254 -0
- package/plugins/fetch-csr/fetch_test.go +494 -0
- package/plugins/fetch-csr/lock.go +95 -0
- package/plugins/fetch-csr/main.go +122 -0
- package/plugins/fetch-csr/options.schema.json +42 -0
- package/plugins/fetch-csr/registry.go +209 -0
- package/plugins/fetch-git/cache.go +65 -0
- package/plugins/fetch-git/describe.go +19 -0
- package/plugins/fetch-git/describe_test.go +11 -0
- package/plugins/fetch-git/fetch.go +181 -0
- package/plugins/fetch-git/fetch_test.go +332 -0
- package/plugins/fetch-git/git.go +169 -0
- package/plugins/fetch-git/lock.go +72 -0
- package/plugins/fetch-git/main.go +127 -0
- package/plugins/fetch-git/offline.go +39 -0
- package/plugins/fetch-git/options.schema.json +42 -0
- package/plugins/fetch-git/pin.go +90 -0
- package/plugins/fetch-git/pin_test.go +74 -0
- package/plugins/gen-backstage/describe.go +17 -0
- package/plugins/gen-backstage/main.go +22 -0
- package/plugins/gen-backstage/options.schema.json +30 -0
- package/plugins/gen-backstage/plugin.go +473 -0
- package/plugins/gen-backstage/plugin_test.go +145 -0
- package/plugins/gen-backstage.wasm +0 -0
- package/plugins/gen-markdown/adr.go +162 -0
- package/plugins/gen-markdown/aggregate.go +362 -0
- package/plugins/gen-markdown/canonical.go +93 -0
- package/plugins/gen-markdown/context.go +90 -0
- package/plugins/gen-markdown/coverage_test.go +89 -0
- package/plugins/gen-markdown/describe.go +19 -0
- package/plugins/gen-markdown/describe_test.go +11 -0
- package/plugins/gen-markdown/external.go +71 -0
- package/plugins/gen-markdown/flow.go +277 -0
- package/plugins/gen-markdown/glossary.go +76 -0
- package/plugins/gen-markdown/glossary_test.go +148 -0
- package/plugins/gen-markdown/llms.go +302 -0
- package/plugins/gen-markdown/main.go +22 -0
- package/plugins/gen-markdown/markdown.go +254 -0
- package/plugins/gen-markdown/markdown_test.go +100 -0
- package/plugins/gen-markdown/module.go +107 -0
- package/plugins/gen-markdown/options.schema.json +16 -0
- package/plugins/gen-markdown/plugin.go +39 -0
- package/plugins/gen-markdown/quality_test.go +176 -0
- package/plugins/gen-markdown/redis_test.go +38 -0
- package/plugins/gen-markdown/render.go +344 -0
- package/plugins/gen-markdown/render_test.go +186 -0
- package/plugins/gen-markdown/service.go +456 -0
- package/plugins/gen-markdown/source.go +177 -0
- package/plugins/gen-markdown/store.go +201 -0
- package/plugins/gen-markdown.wasm +0 -0
- package/plugins/gen-mermaid/describe.go +17 -0
- package/plugins/gen-mermaid/main.go +22 -0
- package/plugins/gen-mermaid/options.schema.json +10 -0
- package/plugins/gen-mermaid/plugin.go +104 -0
- package/plugins/gen-mermaid/plugin_test.go +33 -0
- package/plugins/gen-mermaid.wasm +0 -0
- package/plugins/openapi/ids.go +261 -0
- package/plugins/openapi/ids_test.go +98 -0
- package/plugins/pyplugin/catalog.py +209 -0
- package/plugins/pyplugin/celery_conf.py +436 -0
- package/plugins/pyplugin/celery_tasks.py +76 -0
- package/plugins/pyplugin/kafka.py +570 -0
- package/plugins/pyplugin/names.py +38 -0
- package/plugins/pyplugin/protocol.py +79 -0
- package/plugins/pyplugin/source.py +285 -0
- package/plugins/verify-codeowners/describe.go +19 -0
- package/plugins/verify-codeowners/describe_test.go +11 -0
- package/plugins/verify-codeowners/main.go +75 -0
- package/plugins/verify-codeowners/match.go +85 -0
- package/plugins/verify-codeowners/match_test.go +47 -0
- package/plugins/verify-codeowners/options.schema.json +15 -0
- package/plugins/verify-codeowners/owners.go +164 -0
- package/plugins/verify-codeowners/owners_test.go +225 -0
- package/plugins/verify-codeowners/parse.go +90 -0
- package/plugins/verify-codeowners/parse_test.go +62 -0
- package/plugins/verify-otel/describe.go +19 -0
- package/plugins/verify-otel/describe_test.go +11 -0
- package/plugins/verify-otel/main.go +53 -0
- package/plugins/verify-otel/match.go +336 -0
- package/plugins/verify-otel/options.schema.json +30 -0
- package/plugins/verify-otel/otlp.go +200 -0
- package/plugins/verify-otel/verify.go +734 -0
- package/plugins/verify-otel/verify_test.go +460 -0
- package/portolan.json +1073 -0
- package/public/favicon.svg +12 -0
- package/schema/portolan.schema.json +2558 -0
- package/scripts/README.md +53 -0
- package/scripts/build-report.mjs +60 -0
- package/scripts/build-report.test.mjs +44 -0
- package/scripts/builtin-plugins.mjs +49 -0
- package/scripts/catalog-sources.mjs +67 -0
- package/scripts/diff.mjs +315 -0
- package/scripts/diff.test.mjs +151 -0
- package/scripts/forge-comment.mjs +144 -0
- package/scripts/forge-comment.test.mjs +151 -0
- package/scripts/forge-release.mjs +138 -0
- package/scripts/forge-release.test.mjs +106 -0
- package/scripts/forge.mjs +61 -0
- package/scripts/gen-likec4.mjs +854 -0
- package/scripts/gen-likec4.test.mjs +258 -0
- package/scripts/gen.mjs +603 -0
- package/scripts/generated-docs.test.mjs +66 -0
- package/scripts/local-api.mjs +1192 -0
- package/scripts/local-api.test.mjs +387 -0
- package/scripts/manifest.mjs +214 -0
- package/scripts/manifest.test.mjs +148 -0
- package/scripts/output-path.mjs +79 -0
- package/scripts/output-path.test.mjs +45 -0
- package/scripts/package-smoke.mjs +37 -0
- package/scripts/plugin-host.mjs +419 -0
- package/scripts/plugin-host.test.mjs +109 -0
- package/scripts/plugin-wasm-worker.mjs +34 -0
- package/scripts/run-builtin.mjs +81 -0
- package/scripts/schema.mjs +357 -0
- package/scripts/site-docs.mjs +295 -0
- package/scripts/site-docs.test.mjs +181 -0
- package/scripts/vendor-lock.mjs +58 -0
- package/scripts/vendor-lock.test.mjs +69 -0
- package/src/README.md +38 -0
- package/src/app/App.tsx +448 -0
- package/src/app/BranchPicker.test.ts +46 -0
- package/src/app/BranchPicker.tsx +153 -0
- package/src/app/Breadcrumbs.test.ts +38 -0
- package/src/app/Breadcrumbs.tsx +223 -0
- package/src/app/BuildStamp.tsx +146 -0
- package/src/app/CommandPalette.tsx +292 -0
- package/src/app/HashScroll.tsx +45 -0
- package/src/app/Sidebar.tsx +1742 -0
- package/src/app/TopBar.tsx +393 -0
- package/src/app/density.tsx +64 -0
- package/src/app/forge-access.test.ts +27 -0
- package/src/app/forge-access.tsx +80 -0
- package/src/app/panels.tsx +141 -0
- package/src/app/pins.tsx +242 -0
- package/src/app/query-client.ts +24 -0
- package/src/app/responsive.ts +55 -0
- package/src/app/shortcuts.test.ts +39 -0
- package/src/app/shortcuts.tsx +282 -0
- package/src/app/theme.tsx +51 -0
- package/src/app/title.test.ts +14 -0
- package/src/app/title.ts +30 -0
- package/src/app/toast.tsx +59 -0
- package/src/app/tree-keys.test.ts +80 -0
- package/src/app/tree-keys.ts +87 -0
- package/src/app/ui-store.ts +52 -0
- package/src/catalog-error.test.ts +105 -0
- package/src/catalog-profile.test.ts +59 -0
- package/src/catalog-profile.ts +101 -0
- package/src/catalog-stores.test.ts +431 -0
- package/src/catalog.test.ts +1215 -0
- package/src/catalog.ts +3125 -0
- package/src/chat/Cards.tsx +408 -0
- package/src/chat/ChatPanel.tsx +79 -0
- package/src/chat/ChatSettings.tsx +219 -0
- package/src/chat/Composer.tsx +99 -0
- package/src/chat/Conversation.tsx +281 -0
- package/src/chat/Header.tsx +46 -0
- package/src/chat/Message.tsx +49 -0
- package/src/chat/Notice.tsx +195 -0
- package/src/chat/Starter.tsx +67 -0
- package/src/chat/Steps.tsx +45 -0
- package/src/chat/Waiting.tsx +87 -0
- package/src/chat/flags.test.ts +59 -0
- package/src/chat/flags.ts +155 -0
- package/src/chat/linkify.test.ts +30 -0
- package/src/chat/linkify.ts +45 -0
- package/src/chat/prefs.ts +56 -0
- package/src/chat/prompt.test.ts +43 -0
- package/src/chat/prompt.ts +157 -0
- package/src/chat/store.ts +35 -0
- package/src/chat/tools.ts +58 -0
- package/src/chat/transport.ts +70 -0
- package/src/components/AdrRow.tsx +29 -0
- package/src/components/AnchorLink.tsx +67 -0
- package/src/components/ApiReference.tsx +134 -0
- package/src/components/AsyncApiReference.tsx +120 -0
- package/src/components/CatalogStamp.tsx +155 -0
- package/src/components/ChannelRows.tsx +132 -0
- package/src/components/CommandRows.tsx +139 -0
- package/src/components/DiagramSkeleton.tsx +71 -0
- package/src/components/EditorLink.tsx +85 -0
- package/src/components/FieldTree.tsx +305 -0
- package/src/components/FlowTrigger.tsx +34 -0
- package/src/components/Ident.tsx +89 -0
- package/src/components/Integrations.tsx +243 -0
- package/src/components/LifecycleDiagram.tsx +110 -0
- package/src/components/MachineDocs.tsx +42 -0
- package/src/components/Markdown.tsx +173 -0
- package/src/components/MarkdownTable.tsx +206 -0
- package/src/components/Mermaid.tsx +211 -0
- package/src/components/MethodRows.tsx +309 -0
- package/src/components/Overlay.tsx +125 -0
- package/src/components/PageHeader.tsx +189 -0
- package/src/components/RowActions.tsx +97 -0
- package/src/components/SchemaDocument.tsx +89 -0
- package/src/components/Select.tsx +126 -0
- package/src/components/ShapeRows.tsx +118 -0
- package/src/components/SourceDoc.tsx +82 -0
- package/src/components/SourcePreview.tsx +441 -0
- package/src/components/TabRow.tsx +182 -0
- package/src/components/TechIcon.tsx +34 -0
- package/src/components/Toc.tsx +132 -0
- package/src/components/WhatLinksHere.tsx +273 -0
- package/src/components/WsdlReference.tsx +84 -0
- package/src/components/ddd-icons.tsx +107 -0
- package/src/components/kind.tsx +157 -0
- package/src/components/logo.tsx +61 -0
- package/src/components/primitives.tsx +264 -0
- package/src/data.test.ts +18 -0
- package/src/data.ts +202 -0
- package/src/enrich.test.ts +1150 -0
- package/src/enrich.ts +672 -0
- package/src/er/ErCanvas.tsx +533 -0
- package/src/er/RedisSchema.tsx +74 -0
- package/src/er/RelationCard.tsx +267 -0
- package/src/er/StoreHeader.tsx +155 -0
- package/src/er/TableNode.tsx +84 -0
- package/src/er/ViewNode.tsx +83 -0
- package/src/er/layout.test.ts +115 -0
- package/src/er/layout.ts +77 -0
- package/src/er/lineage.test.ts +140 -0
- package/src/er/lineage.ts +107 -0
- package/src/er/markers.tsx +79 -0
- package/src/er/spec.test.ts +344 -0
- package/src/er/spec.ts +426 -0
- package/src/flow/ChainList.tsx +162 -0
- package/src/flow/FlowTable.tsx +123 -0
- package/src/flow/FlowToolbar.tsx +372 -0
- package/src/flow/StepDetail.tsx +380 -0
- package/src/flow/StepRail.tsx +428 -0
- package/src/flow/answers.test.ts +50 -0
- package/src/flow/answers.ts +58 -0
- package/src/flow/chain.test.ts +383 -0
- package/src/flow/chain.ts +353 -0
- package/src/flow/chapters.test.ts +185 -0
- package/src/flow/chapters.ts +225 -0
- package/src/flow/continues.test.ts +102 -0
- package/src/flow/continues.ts +65 -0
- package/src/flow/cross-context.test.ts +97 -0
- package/src/flow/cross-context.ts +35 -0
- package/src/flow/mermaid.test.ts +22 -0
- package/src/flow/mermaid.ts +141 -0
- package/src/flow/outline.test.ts +212 -0
- package/src/flow/outline.ts +170 -0
- package/src/flow/paths.test.ts +129 -0
- package/src/flow/paths.ts +147 -0
- package/src/flow/prefs.test.ts +80 -0
- package/src/flow/prefs.ts +104 -0
- package/src/graph/DependencyGraph.tsx +283 -0
- package/src/graph/DependencyNodes.tsx +162 -0
- package/src/graph/EdgeLabel.tsx +61 -0
- package/src/graph/FocusedEventGraph.tsx +211 -0
- package/src/graph/GraphToolbar.tsx +187 -0
- package/src/graph/RoutedEdge.tsx +80 -0
- package/src/graph/dependency-layout.test.ts +360 -0
- package/src/graph/dependency-layout.ts +444 -0
- package/src/graph/elk.test.ts +70 -0
- package/src/graph/elk.ts +287 -0
- package/src/graph/nodes.tsx +97 -0
- package/src/graph/theme.ts +81 -0
- package/src/graph/useElkFlow.ts +125 -0
- package/src/index.css +1547 -0
- package/src/language/InLanguage.tsx +40 -0
- package/src/language/TermCard.tsx +132 -0
- package/src/language/cards.test.ts +114 -0
- package/src/language/cards.ts +111 -0
- package/src/lib/admonition.test.ts +49 -0
- package/src/lib/admonition.ts +48 -0
- package/src/lib/adr.test.ts +131 -0
- package/src/lib/adr.ts +68 -0
- package/src/lib/api.test.ts +122 -0
- package/src/lib/api.ts +135 -0
- package/src/lib/backlinks.test.ts +330 -0
- package/src/lib/backlinks.ts +567 -0
- package/src/lib/branch-compare.ts +16 -0
- package/src/lib/build-info.test.ts +76 -0
- package/src/lib/build-info.ts +80 -0
- package/src/lib/catalog-diff.test.ts +275 -0
- package/src/lib/catalog-diff.ts +655 -0
- package/src/lib/clipboard.ts +43 -0
- package/src/lib/comparison-memory.test.ts +40 -0
- package/src/lib/comparison-memory.ts +39 -0
- package/src/lib/context-color.ts +27 -0
- package/src/lib/context-map.test.ts +148 -0
- package/src/lib/context-map.ts +399 -0
- package/src/lib/data-model.test.ts +245 -0
- package/src/lib/data-model.ts +399 -0
- package/src/lib/data-problems.test.ts +178 -0
- package/src/lib/data-problems.ts +314 -0
- package/src/lib/derive.test.ts +251 -0
- package/src/lib/derive.ts +515 -0
- package/src/lib/editor-link.test.ts +125 -0
- package/src/lib/editor-link.ts +96 -0
- package/src/lib/editor-prefs.ts +27 -0
- package/src/lib/event-graph.test.ts +237 -0
- package/src/lib/event-graph.ts +327 -0
- package/src/lib/export-canvas.ts +37 -0
- package/src/lib/export-file.test.ts +30 -0
- package/src/lib/export-file.ts +20 -0
- package/src/lib/flow-tree.test.ts +177 -0
- package/src/lib/flow-tree.ts +160 -0
- package/src/lib/forge-refs.test.ts +50 -0
- package/src/lib/forge-refs.ts +60 -0
- package/src/lib/format.test.ts +35 -0
- package/src/lib/format.ts +64 -0
- package/src/lib/github-catalog.test.ts +301 -0
- package/src/lib/github-catalog.ts +610 -0
- package/src/lib/integrations.test.ts +119 -0
- package/src/lib/integrations.ts +133 -0
- package/src/lib/kinds.test.ts +91 -0
- package/src/lib/kinds.ts +204 -0
- package/src/lib/lifecycle.test.ts +93 -0
- package/src/lib/lifecycle.ts +165 -0
- package/src/lib/local-api.ts +194 -0
- package/src/lib/motion-features.ts +11 -0
- package/src/lib/motion.test.ts +120 -0
- package/src/lib/motion.tsx +269 -0
- package/src/lib/palette-recent.test.ts +80 -0
- package/src/lib/palette-recent.ts +90 -0
- package/src/lib/palette.test.ts +365 -0
- package/src/lib/palette.ts +593 -0
- package/src/lib/pins.test.ts +139 -0
- package/src/lib/pins.ts +130 -0
- package/src/lib/problems.test.ts +112 -0
- package/src/lib/project-preview.ts +5 -0
- package/src/lib/proto-problems.test.ts +121 -0
- package/src/lib/proto-problems.ts +65 -0
- package/src/lib/queries.test.ts +113 -0
- package/src/lib/queries.ts +131 -0
- package/src/lib/registry.test.ts +219 -0
- package/src/lib/registry.ts +208 -0
- package/src/lib/scenarios.ts +547 -0
- package/src/lib/setup-info.test.ts +199 -0
- package/src/lib/setup-info.ts +415 -0
- package/src/lib/shape.test.ts +289 -0
- package/src/lib/shape.ts +454 -0
- package/src/lib/sidebar-prefs.test.ts +32 -0
- package/src/lib/sidebar-prefs.ts +75 -0
- package/src/lib/source-code.test.ts +159 -0
- package/src/lib/source-code.ts +286 -0
- package/src/lib/source-doc.test.ts +131 -0
- package/src/lib/source-doc.ts +103 -0
- package/src/lib/source-highlight.test.ts +29 -0
- package/src/lib/source-highlight.ts +24 -0
- package/src/lib/source-link.test.ts +153 -0
- package/src/lib/source-link.ts +201 -0
- package/src/lib/spec-files.test.ts +24 -0
- package/src/lib/spec-files.ts +23 -0
- package/src/lib/table-directive.test.ts +139 -0
- package/src/lib/table-directive.ts +106 -0
- package/src/lib/tech.test.ts +120 -0
- package/src/lib/tech.ts +421 -0
- package/src/lib/terms.test.ts +67 -0
- package/src/lib/terms.ts +179 -0
- package/src/lib/wire-problems.test.ts +263 -0
- package/src/lib/wire-problems.ts +281 -0
- package/src/likec4/C4View.tsx +55 -0
- package/src/likec4/CanvasBridge.tsx +187 -0
- package/src/likec4/FlowView.tsx +235 -0
- package/src/likec4/InteractiveView.tsx +144 -0
- package/src/likec4/canvas-viewport.test.ts +149 -0
- package/src/likec4/canvas-viewport.ts +135 -0
- package/src/likec4/flow-edges.test.ts +51 -0
- package/src/likec4/flow-edges.ts +89 -0
- package/src/likec4/frame-theme.test.ts +76 -0
- package/src/likec4/frame-theme.ts +101 -0
- package/src/likec4/highlight-css.test.ts +49 -0
- package/src/likec4/highlight-css.ts +52 -0
- package/src/likec4/ids.test.ts +151 -0
- package/src/likec4/ids.ts +115 -0
- package/src/likec4/levels.tsx +70 -0
- package/src/likec4/mapping.test.ts +66 -0
- package/src/likec4/mapping.ts +54 -0
- package/src/likec4/reserved.json +141 -0
- package/src/likec4/view-index.ts +78 -0
- package/src/main.tsx +13 -0
- package/src/map/ContextMapGraph.tsx +274 -0
- package/src/map/ContextNode.tsx +70 -0
- package/src/map/FloatingEdge.tsx +86 -0
- package/src/merge.test.ts +1038 -0
- package/src/merge.ts +1004 -0
- package/src/pages/AdrDetail.tsx +242 -0
- package/src/pages/AdrIndex.tsx +114 -0
- package/src/pages/AggregatePage.tsx +727 -0
- package/src/pages/BlockPage.tsx +478 -0
- package/src/pages/CatalogFailure.tsx +111 -0
- package/src/pages/Changes.tsx +313 -0
- package/src/pages/ContextMap.tsx +534 -0
- package/src/pages/ContextPage.tsx +346 -0
- package/src/pages/EnumPage.tsx +282 -0
- package/src/pages/EventPage.tsx +686 -0
- package/src/pages/ExternalPage.tsx +204 -0
- package/src/pages/FlowDetail.tsx +663 -0
- package/src/pages/FlowIndex.tsx +303 -0
- package/src/pages/GraphPage.tsx +217 -0
- package/src/pages/Language.tsx +257 -0
- package/src/pages/ModulePage.tsx +366 -0
- package/src/pages/NotFound.tsx +54 -0
- package/src/pages/Overview.tsx +344 -0
- package/src/pages/Problems.tsx +360 -0
- package/src/pages/RegistryIndex.tsx +118 -0
- package/src/pages/ServicePage.tsx +746 -0
- package/src/pages/Settings.tsx +975 -0
- package/src/pages/StorePage.tsx +114 -0
- package/src/peek/PeekCard.test.ts +35 -0
- package/src/peek/PeekCard.tsx +118 -0
- package/src/peek/PeekLayer.tsx +147 -0
- package/src/peek/model.test.ts +156 -0
- package/src/peek/model.ts +253 -0
- package/src/routes.test.ts +222 -0
- package/src/routes.ts +449 -0
- package/src/selection/DetailPanel.tsx +1407 -0
- package/src/selection/hash.test.ts +73 -0
- package/src/selection/hash.ts +58 -0
- package/src/selection/model.test.ts +199 -0
- package/src/selection/model.ts +473 -0
- package/src/selection/pages.test.ts +115 -0
- package/src/selection/pages.ts +268 -0
- package/src/selection/store.test.ts +62 -0
- package/src/selection/store.ts +46 -0
- package/src/selection/sync.tsx +99 -0
- package/src/table/ColumnsMenu.tsx +72 -0
- package/src/table/DataTable.tsx +780 -0
- package/src/table/Facets.tsx +54 -0
- package/src/table/GroupMenu.tsx +69 -0
- package/src/table/Toolbar.tsx +190 -0
- package/src/table/cells.tsx +117 -0
- package/src/table/compare.test.ts +117 -0
- package/src/table/compare.ts +196 -0
- package/src/table/export.test.ts +88 -0
- package/src/table/export.ts +66 -0
- package/src/table/facet-groups.test.ts +74 -0
- package/src/table/facet-groups.ts +56 -0
- package/src/table/grouping.test.ts +84 -0
- package/src/table/grouping.ts +82 -0
- package/src/table/infer.test.ts +57 -0
- package/src/table/infer.ts +50 -0
- package/src/table/persist.test.ts +76 -0
- package/src/table/persist.ts +112 -0
- package/src/table/sort-url.test.ts +75 -0
- package/src/table/sort-url.ts +52 -0
- package/src/table/types.ts +80 -0
- package/src/table/useTableState.ts +220 -0
- package/src/test-catalog.ts +7 -0
- package/src/testing/estate/data__catalog.json +2847 -0
- package/src/testing/estate/data__flows.json +1724 -0
- package/src/testing/estate/examples__auth__portolan__api.json +212 -0
- package/src/testing/estate/examples__auth__portolan__domain.json +1204 -0
- package/src/testing/estate/examples__auth__portolan__glossary.json +226 -0
- package/src/testing/estate/examples__auth__portolan__observed.json +757 -0
- package/src/testing/estate/examples__auth__portolan__stores.json +195 -0
- package/src/testing/estate/examples__shop__cart__portolan__api.json +234 -0
- package/src/testing/estate/examples__shop__cart__portolan__commands.json +86 -0
- package/src/testing/estate/examples__shop__cart__portolan__domain.json +966 -0
- package/src/testing/estate/examples__shop__cart__portolan__observed.json +498 -0
- package/src/testing/estate/examples__shop__cart__portolan__stores.json +208 -0
- package/src/testing/estate/examples__shop__oms__portolan__glossary.json +98 -0
- package/src/testing/estate/examples__shop__oms__portolan__proto.json +186 -0
- package/src/testing/estate.ts +49 -0
- package/src/trail/Trail.tsx +81 -0
- package/src/trail/model.test.ts +157 -0
- package/src/trail/model.ts +189 -0
- package/src/trail/record.tsx +32 -0
- package/src/trail/store.test.ts +66 -0
- package/src/trail/store.ts +102 -0
- package/tsconfig.json +35 -0
- package/vite.config.ts +171 -0
|
@@ -0,0 +1,2847 @@
|
|
|
1
|
+
{
|
|
2
|
+
"defs": {
|
|
3
|
+
"Money": {
|
|
4
|
+
"fields": [
|
|
5
|
+
{
|
|
6
|
+
"name": "amountMinor",
|
|
7
|
+
"type": "int64",
|
|
8
|
+
"doc": "Amount in the minor unit of the currency."
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"name": "currency",
|
|
12
|
+
"type": "string",
|
|
13
|
+
"doc": "ISO 4217 code, upper case."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"Address": {
|
|
18
|
+
"fields": [
|
|
19
|
+
{
|
|
20
|
+
"name": "line1",
|
|
21
|
+
"type": "string",
|
|
22
|
+
"doc": "Street and number."
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "line2",
|
|
26
|
+
"type": "string",
|
|
27
|
+
"doc": "Optional second line."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "city",
|
|
31
|
+
"type": "string",
|
|
32
|
+
"doc": "City or locality."
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "postcode",
|
|
36
|
+
"type": "string",
|
|
37
|
+
"doc": "Postal code, unvalidated."
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "country",
|
|
41
|
+
"type": "string",
|
|
42
|
+
"doc": "ISO 3166-1 alpha-2."
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"LineItem": {
|
|
47
|
+
"fields": [
|
|
48
|
+
{
|
|
49
|
+
"name": "sku",
|
|
50
|
+
"type": "string",
|
|
51
|
+
"doc": "Catalog SKU at the time of ordering."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "quantity",
|
|
55
|
+
"type": "int32",
|
|
56
|
+
"doc": "Units ordered; always positive."
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "unitPrice",
|
|
60
|
+
"type": "Money",
|
|
61
|
+
"doc": "Price per unit, net of tax.",
|
|
62
|
+
"ref": "Money"
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
"CustomerRef": {
|
|
67
|
+
"fields": [
|
|
68
|
+
{
|
|
69
|
+
"name": "id",
|
|
70
|
+
"type": "string",
|
|
71
|
+
"doc": "Stable customer identifier."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "segment",
|
|
75
|
+
"type": "string",
|
|
76
|
+
"doc": "Pricing segment at time of capture."
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
"FxRate": {
|
|
81
|
+
"fields": [
|
|
82
|
+
{
|
|
83
|
+
"name": "base",
|
|
84
|
+
"type": "string",
|
|
85
|
+
"doc": "ISO 4217 code the rate converts from — the currency the customer was quoted in."
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "quote",
|
|
89
|
+
"type": "string",
|
|
90
|
+
"doc": "ISO 4217 code the rate converts to — the currency the acquirer settles in."
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "rateMicros",
|
|
94
|
+
"type": "int64",
|
|
95
|
+
"doc": "Rate scaled by 1e6. An integer on purpose: a float rate cannot be reproduced exactly from a stored posting."
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "quotedAt",
|
|
99
|
+
"type": "time.Time",
|
|
100
|
+
"doc": "When the rate was taken. A rate older than 60 seconds is refused rather than used."
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "source",
|
|
104
|
+
"type": "string",
|
|
105
|
+
"doc": "`acquirer` for anything that moves money, `ecb-daily` for reporting only. The two disagree by a spread and must never be mixed."
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
"GatewayRef": {
|
|
110
|
+
"fields": [
|
|
111
|
+
{
|
|
112
|
+
"name": "provider",
|
|
113
|
+
"type": "string",
|
|
114
|
+
"doc": "Which gateway owns the id. Only `psp` today; the column exists because a second acquirer is already scheduled."
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"name": "chargeId",
|
|
118
|
+
"type": "string",
|
|
119
|
+
"doc": "The gateway's own id for the charge. The one handle that survives a restore of our database."
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"name": "eventId",
|
|
123
|
+
"type": "string",
|
|
124
|
+
"doc": "Id of the webhook event that last touched this charge. This is what the dedup table is keyed on."
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"contexts": [
|
|
130
|
+
{
|
|
131
|
+
"id": "shop",
|
|
132
|
+
"slug": "shop",
|
|
133
|
+
"name": "Shop",
|
|
134
|
+
"summary": "Everything the customer touches before money moves: baskets, orders and prices.",
|
|
135
|
+
"classification": "core",
|
|
136
|
+
"services": [
|
|
137
|
+
{
|
|
138
|
+
"id": "shop.oms",
|
|
139
|
+
"slug": "oms",
|
|
140
|
+
"name": "Order Management",
|
|
141
|
+
"repo": "github.com/acme/shop",
|
|
142
|
+
"path": "services/oms",
|
|
143
|
+
"readme": "# Order Management Service\n\n`shop.oms` owns the lifecycle of a customer order from the moment a basket is\nchecked out until the order is either fulfilled or cancelled. It is the write\nside of the `shop` bounded context and the only service permitted to mutate\norder state.\n\n## Responsibilities\n\n- Accept `PlaceOrder` and turn a basket snapshot into an immutable order.\n- Coordinate quoting, authorisation and dispatch without owning any of them.\n- Publish domain events describing what happened, never what should happen next.\n\n## Non-responsibilities\n\nPricing rules live in `shop.pricing`. Money movement lives in\n`payments.ledger`. Physical fulfilment lives in `delivery.core`. The OMS\nholds no schedule, no price list and no ledger balance.\n\n## Order lifecycle\n\n```mermaid\nstateDiagram-v2\n [*] --> Draft\n Draft --> Quoted: BasketCheckedOut\n Quoted --> Placed: OrderPlaced\n Placed --> Confirmed: PaymentCaptured\n Placed --> Cancelled: risk rejected\n Confirmed --> Fulfilled: ShipmentDelivered\n Cancelled --> [*]\n Fulfilled --> [*]\n```\n\n## Aggregates\n\n| Aggregate | Root entity | Events | Notes |\n| --------- | ------------- | ------ | --------------------------------------- |\n| `order` | `Order` | 3 | Transactional boundary for one order. |\n| `basket` | `Basket` | 1 | Short lived; expires after 24h. |\n\n## Consistency\n\nEvery command runs inside a single database transaction that also appends to the\noutbox table. The relay publishes to the bus at least once, so all consumers must\nbe idempotent on `event_id`.\n\n## Operational notes\n\n- Read replicas serve `GetOrder`; expect up to 400ms of replication lag.\n- The fraud scoring call is best-effort and fails open after 250ms.\n",
|
|
144
|
+
"provides": [
|
|
145
|
+
{
|
|
146
|
+
"id": "shop.v1.Orders",
|
|
147
|
+
"methods": [
|
|
148
|
+
{
|
|
149
|
+
"name": "PlaceOrder"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"name": "GetOrder"
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"name": "CancelOrder"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"name": "ConfirmOrder"
|
|
159
|
+
}
|
|
160
|
+
],
|
|
161
|
+
"source": "proto/shop/v1/orders.proto:12",
|
|
162
|
+
"messages": [
|
|
163
|
+
{
|
|
164
|
+
"name": "PlaceOrderRequest",
|
|
165
|
+
"fields": [
|
|
166
|
+
{
|
|
167
|
+
"name": "customer",
|
|
168
|
+
"type": "CustomerRef",
|
|
169
|
+
"doc": "Who is placing the order.",
|
|
170
|
+
"ref": "CustomerRef"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"name": "items",
|
|
174
|
+
"type": "[]LineItem",
|
|
175
|
+
"doc": "Basket contents at checkout.",
|
|
176
|
+
"ref": "LineItem"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"name": "shipTo",
|
|
180
|
+
"type": "Address",
|
|
181
|
+
"doc": "Delivery address.",
|
|
182
|
+
"ref": "Address"
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"name": "PlaceOrderResponse",
|
|
188
|
+
"fields": [
|
|
189
|
+
{
|
|
190
|
+
"name": "orderId",
|
|
191
|
+
"type": "string",
|
|
192
|
+
"doc": "Id of the order just created."
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"name": "total",
|
|
196
|
+
"type": "Money",
|
|
197
|
+
"doc": "Total charged.",
|
|
198
|
+
"ref": "Money"
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
],
|
|
205
|
+
"consumes": [
|
|
206
|
+
{
|
|
207
|
+
"id": "shop.v1.Pricing/GetQuote",
|
|
208
|
+
"peer": "shop.pricing",
|
|
209
|
+
"status": "verified",
|
|
210
|
+
"source": "internal/oms/client/pricing.go:34"
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"id": "payments.v1.Payments/Authorize",
|
|
214
|
+
"peer": "payments.ledger",
|
|
215
|
+
"status": "verified",
|
|
216
|
+
"source": "internal/oms/client/payments.go:51",
|
|
217
|
+
"note": "Covered end to end by services/oms/test/integration/order_accepted_test.go."
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"id": "payments.v1.Payments/Capture",
|
|
221
|
+
"peer": "payments.ledger",
|
|
222
|
+
"status": "declared",
|
|
223
|
+
"source": "internal/oms/client/payments.go:78"
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"id": "payments.v1.Payments/Refund",
|
|
227
|
+
"peer": "payments.ledger",
|
|
228
|
+
"status": "declared",
|
|
229
|
+
"source": "internal/oms/client/payments.go:104"
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"id": "fraud.v2.Scoring/Score",
|
|
233
|
+
"peer": "fraud-scoring",
|
|
234
|
+
"status": "unresolved",
|
|
235
|
+
"source": "internal/oms/client/fraud.go:22",
|
|
236
|
+
"note": "No service in the catalog provides fraud.v2.Scoring. The target is configured per environment via FRAUD_ADDR."
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
"id": "delivery.v1.Delivery/GetShipment",
|
|
240
|
+
"peer": "delivery.core",
|
|
241
|
+
"status": "declared",
|
|
242
|
+
"source": "internal/oms/client/delivery.go:41"
|
|
243
|
+
}
|
|
244
|
+
],
|
|
245
|
+
"aggregates": [
|
|
246
|
+
{
|
|
247
|
+
"id": "shop.oms.order",
|
|
248
|
+
"slug": "order",
|
|
249
|
+
"name": "Order",
|
|
250
|
+
"readme": "# Order\n\nThe transactional boundary for a single customer order. One order, one\naggregate instance, one row lock.\n\n## Invariants\n\n- An order always has at least one line item.\n- The total is recomputed from line items on every mutation; it is never set\n directly by a caller.\n- A cancelled order is terminal. There is no un-cancel.\n- Every line, and the total, is in the order's own currency. Mixing two\n currencies in one order is refused at placement; nothing converts.\n- An order carries the risk decision that let it through, including a\n `review` decision reached only because the scorer timed out.\n\n## Commands\n\n| Command | Precondition | Emits |\n| -------------- | ----------------------- | ---------------- |\n| `PlaceOrder` | quote is unexpired | `OrderPlaced` |\n| `ConfirmOrder` | payment captured | `OrderConfirmed` |\n| `CancelOrder` | not already fulfilled | `OrderCancelled` |\n\n## Queries\n\n`GetOrder` and `ListOrdersForCustomer` are served from a read replica and are\neventually consistent with the write side.\n\n## Concurrency\n\nOptimistic, on a `version` column. A conflicting write returns\n`ErrConcurrentModification` and the caller is expected to reload and retry.",
|
|
251
|
+
"root": "Order",
|
|
252
|
+
"entities": [
|
|
253
|
+
{
|
|
254
|
+
"id": "shop.oms.order.order",
|
|
255
|
+
"slug": "order",
|
|
256
|
+
"name": "Order",
|
|
257
|
+
"doc": "The order itself. Identity is the order id, which is minted at placement and never reused.",
|
|
258
|
+
"fields": [
|
|
259
|
+
{
|
|
260
|
+
"name": "id",
|
|
261
|
+
"type": "string",
|
|
262
|
+
"doc": "Order id, minted at placement."
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"name": "status",
|
|
266
|
+
"type": "Status",
|
|
267
|
+
"doc": "placed | awaiting-review | confirmed | cancelled. `awaiting-review` is where a fail-open risk check leaves the order: accepted, but not yet trusted."
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"name": "customer",
|
|
271
|
+
"type": "CustomerRef",
|
|
272
|
+
"doc": "Who placed it.",
|
|
273
|
+
"ref": "CustomerRef"
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"name": "lines",
|
|
277
|
+
"type": "[]OrderLine",
|
|
278
|
+
"doc": "One line per SKU ordered."
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
"name": "total",
|
|
282
|
+
"type": "Money",
|
|
283
|
+
"doc": "Order total, net of shipping.",
|
|
284
|
+
"ref": "Money"
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
"name": "shipTo",
|
|
288
|
+
"type": "Address",
|
|
289
|
+
"doc": "Delivery address as entered.",
|
|
290
|
+
"ref": "Address"
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"name": "currency",
|
|
294
|
+
"type": "string",
|
|
295
|
+
"doc": "Presentment currency, frozen at placement. Every Money on the order is in it; a basket that would mix two is refused rather than converted."
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
"name": "risk",
|
|
299
|
+
"type": "RiskDecision",
|
|
300
|
+
"doc": "How the risk check answered — including the case where it did not answer at all."
|
|
301
|
+
}
|
|
302
|
+
]
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
"id": "shop.oms.order.order-line",
|
|
306
|
+
"slug": "order-line",
|
|
307
|
+
"name": "OrderLine",
|
|
308
|
+
"doc": "A single ordered SKU. Has identity inside the order so a line can be amended without rewriting the rest.",
|
|
309
|
+
"fields": [
|
|
310
|
+
{
|
|
311
|
+
"name": "lineNo",
|
|
312
|
+
"type": "int32",
|
|
313
|
+
"doc": "Position in the order, 1-based."
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"name": "sku",
|
|
317
|
+
"type": "string",
|
|
318
|
+
"doc": "Catalog SKU at the time of ordering."
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
"name": "quantity",
|
|
322
|
+
"type": "int32",
|
|
323
|
+
"doc": "Units ordered; always positive."
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
"name": "unitPrice",
|
|
327
|
+
"type": "Money",
|
|
328
|
+
"doc": "Price per unit, net of tax.",
|
|
329
|
+
"ref": "Money"
|
|
330
|
+
}
|
|
331
|
+
]
|
|
332
|
+
}
|
|
333
|
+
],
|
|
334
|
+
"valueObjects": [
|
|
335
|
+
{
|
|
336
|
+
"id": "shop.oms.order.money",
|
|
337
|
+
"slug": "money",
|
|
338
|
+
"name": "Money",
|
|
339
|
+
"doc": "An amount in a single currency. Equal amounts in the same currency are the same value.",
|
|
340
|
+
"ref": "Money"
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
"id": "shop.oms.order.address",
|
|
344
|
+
"slug": "address",
|
|
345
|
+
"name": "Address",
|
|
346
|
+
"doc": "A postal address, unvalidated. Frozen onto the order at placement.",
|
|
347
|
+
"ref": "Address"
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
"id": "shop.oms.order.customer-ref",
|
|
351
|
+
"slug": "customer-ref",
|
|
352
|
+
"name": "CustomerRef",
|
|
353
|
+
"doc": "A pointer at the customer, plus the pricing segment as it stood when the order was taken.",
|
|
354
|
+
"ref": "CustomerRef"
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
"id": "shop.oms.order.risk-decision",
|
|
358
|
+
"slug": "risk-decision",
|
|
359
|
+
"name": "RiskDecision",
|
|
360
|
+
"doc": "What the scorer said about this order, and what was done when it said nothing.",
|
|
361
|
+
"fields": [
|
|
362
|
+
{
|
|
363
|
+
"name": "score",
|
|
364
|
+
"type": "int32",
|
|
365
|
+
"doc": "0–100 as the model returned it. Absent when the decision was `review`."
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
"name": "decision",
|
|
369
|
+
"type": "string",
|
|
370
|
+
"doc": "accept | reject | review. `review` is what a timed-out scorer produces, not an answer the model gave."
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
"name": "decidedAt",
|
|
374
|
+
"type": "time.Time",
|
|
375
|
+
"doc": "When the decision was reached, which for `review` is when the deadline expired."
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
"name": "source",
|
|
379
|
+
"type": "string",
|
|
380
|
+
"doc": "`fraud.v2.Scoring` when the model answered, `fail-open` when it did not."
|
|
381
|
+
}
|
|
382
|
+
]
|
|
383
|
+
}
|
|
384
|
+
],
|
|
385
|
+
"operations": [
|
|
386
|
+
{
|
|
387
|
+
"id": "PlaceOrder",
|
|
388
|
+
"kind": "command",
|
|
389
|
+
"doc": "Turns a checked-out basket into an order. Requires an unexpired quote and one currency across every line; writes the order and its OrderPlaced outbox row in the same transaction."
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
"id": "ConfirmOrder",
|
|
393
|
+
"kind": "command",
|
|
394
|
+
"doc": "Commits the order to fulfilment once the payment is authorized. Emits OrderConfirmed. Refuses an order still `awaiting-review`."
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
"id": "CancelOrder",
|
|
398
|
+
"kind": "command",
|
|
399
|
+
"doc": "Ends the order. Allowed only while the shipment is `held` or `planned`, and that state is read from delivery rather than assumed. Emits OrderCancelled."
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
"id": "GetOrder",
|
|
403
|
+
"kind": "query",
|
|
404
|
+
"doc": "One order with its lines and its risk decision. Served from a replica, so a read straight after a write can still show the previous status."
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
"id": "ListOrdersForCustomer",
|
|
408
|
+
"kind": "query",
|
|
409
|
+
"doc": "A customer's orders, newest first, paginated on the placement timestamp. Never joins payments — the caller resolves those itself."
|
|
410
|
+
}
|
|
411
|
+
],
|
|
412
|
+
"events": [
|
|
413
|
+
{
|
|
414
|
+
"id": "shop.oms.order.OrderPlaced",
|
|
415
|
+
"slug": "order-placed",
|
|
416
|
+
"name": "OrderPlaced",
|
|
417
|
+
"versions": [
|
|
418
|
+
{
|
|
419
|
+
"version": "v1",
|
|
420
|
+
"doc": "Emitted once a basket has been converted into an immutable order.",
|
|
421
|
+
"source": "internal/oms/domain/order/events.go:41",
|
|
422
|
+
"fields": [
|
|
423
|
+
{
|
|
424
|
+
"name": "orderId",
|
|
425
|
+
"type": "string",
|
|
426
|
+
"doc": "Identifier of the newly placed order."
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
"name": "customer",
|
|
430
|
+
"type": "CustomerRef",
|
|
431
|
+
"doc": "Who placed the order.",
|
|
432
|
+
"ref": "CustomerRef"
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
"name": "items",
|
|
436
|
+
"type": "[]LineItem",
|
|
437
|
+
"doc": "Frozen basket contents.",
|
|
438
|
+
"ref": "LineItem"
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
"name": "total",
|
|
442
|
+
"type": "Money",
|
|
443
|
+
"doc": "Order total, net of shipping.",
|
|
444
|
+
"ref": "Money"
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
"name": "shipTo",
|
|
448
|
+
"type": "Address",
|
|
449
|
+
"doc": "Delivery address as entered by the customer.",
|
|
450
|
+
"ref": "Address"
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
"name": "placedAt",
|
|
454
|
+
"type": "time.Time",
|
|
455
|
+
"doc": "Server time the order was accepted."
|
|
456
|
+
}
|
|
457
|
+
]
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
"version": "v2",
|
|
461
|
+
"doc": "Adds the acquisition channel so pricing and delivery can differ per channel.",
|
|
462
|
+
"source": "internal/oms/domain/order/events.go:78",
|
|
463
|
+
"fields": [
|
|
464
|
+
{
|
|
465
|
+
"name": "orderId",
|
|
466
|
+
"type": "string",
|
|
467
|
+
"doc": "Identifier of the newly placed order."
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
"name": "customer",
|
|
471
|
+
"type": "CustomerRef",
|
|
472
|
+
"doc": "Who placed the order.",
|
|
473
|
+
"ref": "CustomerRef"
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
"name": "items",
|
|
477
|
+
"type": "[]LineItem",
|
|
478
|
+
"doc": "Frozen basket contents.",
|
|
479
|
+
"ref": "LineItem"
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
"name": "total",
|
|
483
|
+
"type": "Money",
|
|
484
|
+
"doc": "Order total, net of shipping.",
|
|
485
|
+
"ref": "Money"
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
"name": "shipTo",
|
|
489
|
+
"type": "Address",
|
|
490
|
+
"doc": "Delivery address as entered by the customer.",
|
|
491
|
+
"ref": "Address"
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
"name": "placedAt",
|
|
495
|
+
"type": "time.Time",
|
|
496
|
+
"doc": "Server time the order was accepted."
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
"name": "channel",
|
|
500
|
+
"type": "string",
|
|
501
|
+
"doc": "New in v2. One of web, ios, android, pos."
|
|
502
|
+
}
|
|
503
|
+
]
|
|
504
|
+
}
|
|
505
|
+
],
|
|
506
|
+
"consumers": [
|
|
507
|
+
{
|
|
508
|
+
"service": "payments.ledger",
|
|
509
|
+
"status": "verified"
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
"service": "shop.pricing",
|
|
513
|
+
"status": "declared"
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
"service": "analytics-sink",
|
|
517
|
+
"status": "unresolved",
|
|
518
|
+
"note": "Seen on the bus in traces; no consumer registration found in any repo."
|
|
519
|
+
}
|
|
520
|
+
]
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
"id": "shop.oms.order.OrderConfirmed",
|
|
524
|
+
"slug": "order-confirmed",
|
|
525
|
+
"name": "OrderConfirmed",
|
|
526
|
+
"versions": [
|
|
527
|
+
{
|
|
528
|
+
"version": "v1",
|
|
529
|
+
"doc": "Payment captured and the order is committed to fulfilment.",
|
|
530
|
+
"source": "internal/oms/domain/order/events.go:150",
|
|
531
|
+
"fields": [
|
|
532
|
+
{
|
|
533
|
+
"name": "orderId",
|
|
534
|
+
"type": "string",
|
|
535
|
+
"doc": "Order that was confirmed."
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
"name": "paymentId",
|
|
539
|
+
"type": "string",
|
|
540
|
+
"doc": "Ledger payment that backs this confirmation."
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
"name": "confirmedAt",
|
|
544
|
+
"type": "time.Time",
|
|
545
|
+
"doc": "Server time of confirmation."
|
|
546
|
+
}
|
|
547
|
+
]
|
|
548
|
+
}
|
|
549
|
+
],
|
|
550
|
+
"consumers": [
|
|
551
|
+
{
|
|
552
|
+
"service": "delivery.core",
|
|
553
|
+
"status": "verified"
|
|
554
|
+
}
|
|
555
|
+
]
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
"id": "shop.oms.order.OrderCancelled",
|
|
559
|
+
"slug": "order-cancelled",
|
|
560
|
+
"name": "OrderCancelled",
|
|
561
|
+
"versions": [
|
|
562
|
+
{
|
|
563
|
+
"version": "v1",
|
|
564
|
+
"doc": "The order was cancelled before fulfilment, by the customer or by risk.",
|
|
565
|
+
"source": "internal/oms/domain/order/events.go:112",
|
|
566
|
+
"fields": [
|
|
567
|
+
{
|
|
568
|
+
"name": "orderId",
|
|
569
|
+
"type": "string",
|
|
570
|
+
"doc": "Order that was cancelled."
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
"name": "reason",
|
|
574
|
+
"type": "string",
|
|
575
|
+
"doc": "One of customer_request, risk_rejected, payment_failed."
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
"name": "refundDue",
|
|
579
|
+
"type": "Money",
|
|
580
|
+
"doc": "Amount to be refunded, zero if nothing was captured.",
|
|
581
|
+
"ref": "Money"
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
"name": "cancelledAt",
|
|
585
|
+
"type": "time.Time",
|
|
586
|
+
"doc": "Server time of cancellation."
|
|
587
|
+
}
|
|
588
|
+
]
|
|
589
|
+
}
|
|
590
|
+
],
|
|
591
|
+
"consumers": [
|
|
592
|
+
{
|
|
593
|
+
"service": "payments.ledger",
|
|
594
|
+
"status": "verified"
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
"service": "delivery.core",
|
|
598
|
+
"status": "declared"
|
|
599
|
+
}
|
|
600
|
+
]
|
|
601
|
+
}
|
|
602
|
+
],
|
|
603
|
+
"enums": [
|
|
604
|
+
{
|
|
605
|
+
"id": "shop.oms.order.status",
|
|
606
|
+
"slug": "status",
|
|
607
|
+
"name": "Status",
|
|
608
|
+
"doc": "Where an order is in its life. The moves between these are the lifecycle.",
|
|
609
|
+
"values": [
|
|
610
|
+
{
|
|
611
|
+
"name": "placed",
|
|
612
|
+
"doc": "Came in from a checked-out basket; nothing is paid yet."
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
"name": "confirmed",
|
|
616
|
+
"doc": "The total is held with payments."
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
"name": "cancelled",
|
|
620
|
+
"doc": "Nobody will be charged; a hold, if any, is given back."
|
|
621
|
+
}
|
|
622
|
+
]
|
|
623
|
+
}
|
|
624
|
+
]
|
|
625
|
+
}
|
|
626
|
+
],
|
|
627
|
+
"stores": [
|
|
628
|
+
"shop.oms.pg"
|
|
629
|
+
]
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
"id": "shop.pricing",
|
|
633
|
+
"slug": "pricing",
|
|
634
|
+
"name": "Pricing",
|
|
635
|
+
"repo": "github.com/acme/shop",
|
|
636
|
+
"path": "services/pricing",
|
|
637
|
+
"readme": "# Pricing Service\n\n`shop.pricing` answers one question: what should this basket cost for this\ncustomer, right now. It is a pure read model over price lists, promotions and\ncustomer agreements. It never writes order state.\n\n## Design\n\nQuotes are immutable and time-boxed. A quote is issued with an explicit\nexpiry; the OMS must re-quote rather than reuse an expired one. This keeps\npricing decisions auditable long after the price list has changed.\n\n```mermaid\nflowchart LR\n A[GetQuote] --> B{customer agreement?}\n B -- yes --> C[Contract price list]\n B -- no --> D[Public price list]\n C --> E[Apply promotions]\n D --> E\n E --> F[QuoteIssued]\n```\n\n## Price list precedence\n\n| Rank | Source | Scope | Overrides |\n| ---- | ------------------- | ----------------- | -------------------- |\n| 1 | Contract price list | One customer | Everything below |\n| 2 | Regional list | One country | Public list |\n| 3 | Public list | Everyone | Nothing |\n\nPromotions are applied after precedence is resolved and are never stacked more\nthan two deep.\n\n## Aggregates\n\n- `quote` — an issued quote and its expiry. Emits `QuoteIssued` and\n `QuoteExpired`.\n- `price-list` — the price list itself. It is edited through an internal admin\n tool that writes directly to the store, so this aggregate currently publishes\n no domain events. That is a known gap, not a design choice.\n\n## Caching\n\nQuotes are cached for 60 seconds keyed by basket hash and customer segment. The\ncache is deliberately short: a stale quote is worse than a slow one.\n",
|
|
638
|
+
"provides": [
|
|
639
|
+
{
|
|
640
|
+
"id": "shop.v1.Pricing",
|
|
641
|
+
"methods": [
|
|
642
|
+
{
|
|
643
|
+
"name": "GetQuote"
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
"name": "ListPriceLists"
|
|
647
|
+
}
|
|
648
|
+
],
|
|
649
|
+
"source": "proto/shop/v1/pricing.proto:9",
|
|
650
|
+
"messages": [
|
|
651
|
+
{
|
|
652
|
+
"name": "GetQuoteRequest",
|
|
653
|
+
"fields": [
|
|
654
|
+
{
|
|
655
|
+
"name": "items",
|
|
656
|
+
"type": "[]LineItem",
|
|
657
|
+
"doc": "Lines to price.",
|
|
658
|
+
"ref": "LineItem"
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
"name": "customer",
|
|
662
|
+
"type": "CustomerRef",
|
|
663
|
+
"doc": "Segment drives the discount.",
|
|
664
|
+
"ref": "CustomerRef"
|
|
665
|
+
}
|
|
666
|
+
]
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
"name": "GetQuoteResponse",
|
|
670
|
+
"fields": [
|
|
671
|
+
{
|
|
672
|
+
"name": "quoteId",
|
|
673
|
+
"type": "string",
|
|
674
|
+
"doc": "Quote id, to be echoed at checkout."
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
"name": "total",
|
|
678
|
+
"type": "Money",
|
|
679
|
+
"doc": "Quoted total.",
|
|
680
|
+
"ref": "Money"
|
|
681
|
+
}
|
|
682
|
+
]
|
|
683
|
+
}
|
|
684
|
+
]
|
|
685
|
+
}
|
|
686
|
+
],
|
|
687
|
+
"consumes": [
|
|
688
|
+
{
|
|
689
|
+
"id": "shop.v1.Orders/GetOrder",
|
|
690
|
+
"peer": "shop.oms",
|
|
691
|
+
"status": "verified",
|
|
692
|
+
"source": "internal/pricing/client/orders.go:19"
|
|
693
|
+
}
|
|
694
|
+
],
|
|
695
|
+
"aggregates": [
|
|
696
|
+
{
|
|
697
|
+
"id": "shop.pricing.quote",
|
|
698
|
+
"slug": "quote",
|
|
699
|
+
"name": "Quote",
|
|
700
|
+
"readme": "# Quote\n\nAn immutable, time-boxed price for one basket and one customer.\n\n## Currency\n\nQuoting never converts. The customer's currency selects the price list, and a\ncurrency no active list covers is a refusal — an FX rate belongs to the\nmoment money moves, not to the moment a price is shown.\n\n## Why immutable\n\nA quote is a promise. Re-deriving it later would give a different answer once\nprice lists change, which makes disputes unwinnable. So the quote is stored\nwhole, including which price list won and which promotions applied.\n\n## Commands\n\n| Command | Notes |\n| ------------ | ------------------------------------------- |\n| `IssueQuote` | Computes precedence and applies promotions |\n| `ExpireQuote`| Called by the sweeper, never by a customer |\n\n## Queries\n\n`GetQuote` returns the stored quote verbatim, including expired ones, so\nsupport can explain what a customer was shown.",
|
|
701
|
+
"root": "Quote",
|
|
702
|
+
"entities": [
|
|
703
|
+
{
|
|
704
|
+
"id": "shop.pricing.quote.quote",
|
|
705
|
+
"slug": "quote",
|
|
706
|
+
"name": "Quote",
|
|
707
|
+
"doc": "A price offered for a basket, valid until it expires.",
|
|
708
|
+
"fields": [
|
|
709
|
+
{
|
|
710
|
+
"name": "id",
|
|
711
|
+
"type": "string",
|
|
712
|
+
"doc": "Quote id."
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
"name": "basketId",
|
|
716
|
+
"type": "string",
|
|
717
|
+
"doc": "Basket the quote was calculated for."
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
"name": "lines",
|
|
721
|
+
"type": "[]QuotedLine",
|
|
722
|
+
"doc": "Per-SKU pricing."
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
"name": "total",
|
|
726
|
+
"type": "Money",
|
|
727
|
+
"doc": "Quoted total.",
|
|
728
|
+
"ref": "Money"
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
"name": "expiresAt",
|
|
732
|
+
"type": "time.Time",
|
|
733
|
+
"doc": "After this, the quote is void."
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
"name": "currency",
|
|
737
|
+
"type": "string",
|
|
738
|
+
"doc": "ISO 4217, taken from the price list that won. A basket in a currency no active list covers is refused, not converted."
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
"name": "priceListId",
|
|
742
|
+
"type": "string",
|
|
743
|
+
"doc": "Which list produced these prices. Stored so a disputed quote can be re-read against the list as it was."
|
|
744
|
+
}
|
|
745
|
+
]
|
|
746
|
+
}
|
|
747
|
+
],
|
|
748
|
+
"valueObjects": [
|
|
749
|
+
{
|
|
750
|
+
"id": "shop.pricing.quote.money",
|
|
751
|
+
"slug": "money",
|
|
752
|
+
"name": "Money",
|
|
753
|
+
"doc": "An amount in a single currency.",
|
|
754
|
+
"ref": "Money"
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
"id": "shop.pricing.quote.quoted-line",
|
|
758
|
+
"slug": "quoted-line",
|
|
759
|
+
"name": "QuotedLine",
|
|
760
|
+
"doc": "The price arrived at for one SKU, with the discount that produced it. Local to pricing; nothing outside quotes uses this shape.",
|
|
761
|
+
"fields": [
|
|
762
|
+
{
|
|
763
|
+
"name": "sku",
|
|
764
|
+
"type": "string",
|
|
765
|
+
"doc": "SKU priced."
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
"name": "unitPrice",
|
|
769
|
+
"type": "Money",
|
|
770
|
+
"doc": "Price per unit after discount.",
|
|
771
|
+
"ref": "Money"
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
"name": "discountMinor",
|
|
775
|
+
"type": "int64",
|
|
776
|
+
"doc": "Discount applied, in the minor unit."
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
"name": "rule",
|
|
780
|
+
"type": "string",
|
|
781
|
+
"doc": "Id of the pricing rule that fired."
|
|
782
|
+
}
|
|
783
|
+
]
|
|
784
|
+
}
|
|
785
|
+
],
|
|
786
|
+
"operations": [
|
|
787
|
+
{
|
|
788
|
+
"id": "IssueQuote",
|
|
789
|
+
"kind": "command",
|
|
790
|
+
"doc": "Prices a basket against the list matching the customer's segment and currency. A basket in a currency no active list covers is refused rather than converted."
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
"id": "ExpireQuote",
|
|
794
|
+
"kind": "command",
|
|
795
|
+
"doc": "Marks a quote past its validity. Runs on a sweep rather than on read, so an expired quote is briefly still readable."
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
"id": "GetQuote",
|
|
799
|
+
"kind": "query",
|
|
800
|
+
"doc": "One quote, whole: the lines, which price list won, and which rule produced each discount."
|
|
801
|
+
}
|
|
802
|
+
],
|
|
803
|
+
"events": [
|
|
804
|
+
{
|
|
805
|
+
"id": "shop.pricing.quote.QuoteIssued",
|
|
806
|
+
"slug": "quote-issued",
|
|
807
|
+
"name": "QuoteIssued",
|
|
808
|
+
"versions": [
|
|
809
|
+
{
|
|
810
|
+
"version": "v1",
|
|
811
|
+
"doc": "A priced, time-boxed answer for one basket and one customer.",
|
|
812
|
+
"source": "internal/pricing/domain/quote/events.go:27",
|
|
813
|
+
"fields": [
|
|
814
|
+
{
|
|
815
|
+
"name": "quoteId",
|
|
816
|
+
"type": "string",
|
|
817
|
+
"doc": "Identifier of the issued quote."
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
"name": "basketId",
|
|
821
|
+
"type": "string",
|
|
822
|
+
"doc": "Basket the quote was computed for."
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
"name": "total",
|
|
826
|
+
"type": "Money",
|
|
827
|
+
"doc": "Quoted total including promotions.",
|
|
828
|
+
"ref": "Money"
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
"name": "priceListId",
|
|
832
|
+
"type": "string",
|
|
833
|
+
"doc": "Price list that won precedence."
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
"name": "expiresAt",
|
|
837
|
+
"type": "time.Time",
|
|
838
|
+
"doc": "After this instant the quote must not be used."
|
|
839
|
+
}
|
|
840
|
+
]
|
|
841
|
+
}
|
|
842
|
+
],
|
|
843
|
+
"consumers": [
|
|
844
|
+
{
|
|
845
|
+
"service": "shop.oms",
|
|
846
|
+
"status": "verified"
|
|
847
|
+
}
|
|
848
|
+
]
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
"id": "shop.pricing.quote.QuoteExpired",
|
|
852
|
+
"slug": "quote-expired",
|
|
853
|
+
"name": "QuoteExpired",
|
|
854
|
+
"versions": [
|
|
855
|
+
{
|
|
856
|
+
"version": "v1",
|
|
857
|
+
"doc": "A quote passed its expiry without being consumed.",
|
|
858
|
+
"source": "internal/pricing/domain/quote/events.go:61",
|
|
859
|
+
"fields": [
|
|
860
|
+
{
|
|
861
|
+
"name": "quoteId",
|
|
862
|
+
"type": "string",
|
|
863
|
+
"doc": "Quote that expired."
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
"name": "expiredAt",
|
|
867
|
+
"type": "time.Time",
|
|
868
|
+
"doc": "Instant the sweeper observed the expiry."
|
|
869
|
+
}
|
|
870
|
+
]
|
|
871
|
+
}
|
|
872
|
+
],
|
|
873
|
+
"consumers": [
|
|
874
|
+
{
|
|
875
|
+
"service": "shop.oms",
|
|
876
|
+
"status": "declared",
|
|
877
|
+
"note": "Handler exists but is registered behind a disabled feature flag."
|
|
878
|
+
}
|
|
879
|
+
]
|
|
880
|
+
}
|
|
881
|
+
]
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
"id": "shop.pricing.price-list",
|
|
885
|
+
"slug": "price-list",
|
|
886
|
+
"name": "PriceList",
|
|
887
|
+
"readme": "# PriceList\n\nThe set of prices that quoting reads from. Contract, regional and public lists\nshare this aggregate and differ only by scope.\n\n## No events\n\nThis aggregate publishes no domain events. Price lists are edited through an\ninternal admin tool that writes to the store directly, bypassing the domain\nlayer entirely.\n\nThat is a gap, not a decision. Anything that wants to react to a price change\ncurrently polls. Emitting `PriceListPublished` is tracked as work, and until it\nexists this page will keep showing an empty event list.\n\n## Commands\n\n| Command | Notes |\n| ------------------- | ---------------------------------------- |\n| `ImportPriceList` | Bulk CSV import, validated then swapped |\n| `ArchivePriceList` | Soft delete; quoting stops selecting it |\n\n## Queries\n\n`ListPriceLists` is the only read path and is heavily cached.",
|
|
888
|
+
"root": "PriceList",
|
|
889
|
+
"entities": [
|
|
890
|
+
{
|
|
891
|
+
"id": "shop.pricing.price-list.price-list",
|
|
892
|
+
"slug": "price-list",
|
|
893
|
+
"name": "PriceList",
|
|
894
|
+
"doc": "A dated set of prices, imported wholesale from the merchandising system.",
|
|
895
|
+
"fields": [
|
|
896
|
+
{
|
|
897
|
+
"name": "id",
|
|
898
|
+
"type": "string",
|
|
899
|
+
"doc": "Price list id."
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
"name": "name",
|
|
903
|
+
"type": "string",
|
|
904
|
+
"doc": "Human name, as given by merchandising."
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
"name": "validFrom",
|
|
908
|
+
"type": "time.Time",
|
|
909
|
+
"doc": "First day the list applies."
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
"name": "rows",
|
|
913
|
+
"type": "[]PriceRow",
|
|
914
|
+
"doc": "One row per SKU."
|
|
915
|
+
},
|
|
916
|
+
{
|
|
917
|
+
"name": "currency",
|
|
918
|
+
"type": "string",
|
|
919
|
+
"doc": "ISO 4217. A list prices in exactly one currency; a second currency is a second list, not a conversion."
|
|
920
|
+
}
|
|
921
|
+
]
|
|
922
|
+
},
|
|
923
|
+
{
|
|
924
|
+
"id": "shop.pricing.price-list.price-row",
|
|
925
|
+
"slug": "price-row",
|
|
926
|
+
"name": "PriceRow",
|
|
927
|
+
"doc": "One SKU's price inside a list. Identified by SKU within its list.",
|
|
928
|
+
"fields": [
|
|
929
|
+
{
|
|
930
|
+
"name": "sku",
|
|
931
|
+
"type": "string",
|
|
932
|
+
"doc": "SKU priced."
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
"name": "price",
|
|
936
|
+
"type": "Money",
|
|
937
|
+
"doc": "List price, net of tax.",
|
|
938
|
+
"ref": "Money"
|
|
939
|
+
}
|
|
940
|
+
]
|
|
941
|
+
}
|
|
942
|
+
],
|
|
943
|
+
"valueObjects": [
|
|
944
|
+
{
|
|
945
|
+
"id": "shop.pricing.price-list.money",
|
|
946
|
+
"slug": "money",
|
|
947
|
+
"name": "Money",
|
|
948
|
+
"doc": "An amount in a single currency.",
|
|
949
|
+
"ref": "Money"
|
|
950
|
+
}
|
|
951
|
+
],
|
|
952
|
+
"operations": [
|
|
953
|
+
{
|
|
954
|
+
"id": "ImportPriceList",
|
|
955
|
+
"kind": "command",
|
|
956
|
+
"doc": "Replaces a list wholesale from the merchandising export. A partial import is rejected outright — half a price list is worse than a stale one."
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
"id": "ArchivePriceList",
|
|
960
|
+
"kind": "command",
|
|
961
|
+
"doc": "Takes a list out of quoting without deleting it. Quotes already issued from it stay valid until they expire."
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
"id": "ListPriceLists",
|
|
965
|
+
"kind": "query",
|
|
966
|
+
"doc": "Active lists with their currency and validity window. The only pricing query the storefront may reach directly."
|
|
967
|
+
}
|
|
968
|
+
],
|
|
969
|
+
"events": []
|
|
970
|
+
}
|
|
971
|
+
],
|
|
972
|
+
"stores": [
|
|
973
|
+
"shop.pricing.cache",
|
|
974
|
+
"shop.oms.pg"
|
|
975
|
+
]
|
|
976
|
+
}
|
|
977
|
+
]
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
"id": "payments",
|
|
981
|
+
"slug": "payments",
|
|
982
|
+
"name": "Payments",
|
|
983
|
+
"summary": "A double-entry ledger and the PSP integrations that feed it. Nothing here knows what an order is for.",
|
|
984
|
+
"classification": "core",
|
|
985
|
+
"services": [
|
|
986
|
+
{
|
|
987
|
+
"id": "payments.ledger",
|
|
988
|
+
"slug": "ledger",
|
|
989
|
+
"name": "Ledger",
|
|
990
|
+
"repo": "github.com/acme/payments",
|
|
991
|
+
"path": "services/ledger",
|
|
992
|
+
"readme": "# Payments Ledger\n\n`payments.ledger` is a double-entry ledger. Every movement of money is two\nbalanced postings, and nothing is ever deleted. Corrections are compensating\nentries.\n\n## Model\n\nAn authorisation reserves funds without moving them. A capture moves the\nreserved funds. A refund moves them back. Each of these is a separate posting\npair against the customer, merchant and settlement accounts.\n\n```mermaid\nsequenceDiagram\n participant OMS as shop.oms\n participant L as payments.ledger\n participant PSP as psp (external)\n OMS->>L: Authorize\n L->>PSP: reserve funds\n PSP-->>L: auth code\n L-->>OMS: PaymentAuthorized\n OMS->>L: Capture\n L->>PSP: settle\n L-->>OMS: PaymentCaptured\n```\n\n## Accounts\n\n| Account | Type | Increases on | Decreases on |\n| -------------- | --------- | ------------------- | ------------------ |\n| `customer` | liability | refund | capture |\n| `merchant` | asset | capture | refund |\n| `settlement` | asset | payout received | payout disbursed |\n| `fees` | expense | capture | refund |\n\n## Guarantees\n\n- Postings are append only. There is no `UPDATE` on the postings table.\n- Every write is idempotent on `idempotency_key`, retained for 30 days.\n- The sum of all postings for a transaction is always zero; a nightly job\n asserts this and pages on failure.\n\n## Aggregates\n\n- `payment` — authorisation, capture and decline.\n- `refund` — full and partial refunds against a captured payment.\n",
|
|
993
|
+
"provides": [
|
|
994
|
+
{
|
|
995
|
+
"id": "payments.v1.Payments",
|
|
996
|
+
"methods": [
|
|
997
|
+
{
|
|
998
|
+
"name": "Authorize"
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
"name": "Capture"
|
|
1002
|
+
},
|
|
1003
|
+
{
|
|
1004
|
+
"name": "Refund"
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
"name": "GetPayment"
|
|
1008
|
+
}
|
|
1009
|
+
],
|
|
1010
|
+
"source": "proto/payments/v1/payments.proto:14",
|
|
1011
|
+
"messages": [
|
|
1012
|
+
{
|
|
1013
|
+
"name": "AuthorizeRequest",
|
|
1014
|
+
"fields": [
|
|
1015
|
+
{
|
|
1016
|
+
"name": "orderId",
|
|
1017
|
+
"type": "string",
|
|
1018
|
+
"doc": "Order to authorize against."
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
"name": "amount",
|
|
1022
|
+
"type": "Money",
|
|
1023
|
+
"doc": "Amount to hold.",
|
|
1024
|
+
"ref": "Money"
|
|
1025
|
+
}
|
|
1026
|
+
]
|
|
1027
|
+
}
|
|
1028
|
+
]
|
|
1029
|
+
}
|
|
1030
|
+
],
|
|
1031
|
+
"consumes": [
|
|
1032
|
+
{
|
|
1033
|
+
"id": "shop.v1.Orders/GetOrder",
|
|
1034
|
+
"peer": "shop.oms",
|
|
1035
|
+
"status": "verified",
|
|
1036
|
+
"source": "internal/ledger/client/orders.go:27"
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
"id": "psp.v2.Charges/Create",
|
|
1040
|
+
"peer": "psp-gateway",
|
|
1041
|
+
"status": "unresolved",
|
|
1042
|
+
"source": "internal/ledger/adapter/psp/client.go:64",
|
|
1043
|
+
"note": "Authorization. The peer is a third party; no service in the estate provides psp.v2.Charges, so the call cannot be resolved to anything the catalog knows."
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
"id": "psp.v2.Charges/Capture",
|
|
1047
|
+
"peer": "psp-gateway",
|
|
1048
|
+
"status": "unresolved",
|
|
1049
|
+
"source": "internal/ledger/adapter/psp/client.go:102",
|
|
1050
|
+
"note": "Settlement of an existing authorization. Same unresolvable peer as the rest of psp.v2.Charges."
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
"id": "psp.v2.Charges/Refund",
|
|
1054
|
+
"peer": "psp-gateway",
|
|
1055
|
+
"status": "unresolved",
|
|
1056
|
+
"source": "internal/ledger/adapter/psp/client.go:138",
|
|
1057
|
+
"note": "Returns money on a settled charge. Same unresolvable peer as the rest of psp.v2.Charges."
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
"id": "psp.v2.Charges/Void",
|
|
1061
|
+
"peer": "psp-gateway",
|
|
1062
|
+
"status": "unresolved",
|
|
1063
|
+
"source": "internal/ledger/adapter/psp/client.go:171",
|
|
1064
|
+
"note": "Cancels an authorization that was never captured. Same unresolvable peer as the rest of psp.v2.Charges."
|
|
1065
|
+
}
|
|
1066
|
+
],
|
|
1067
|
+
"aggregates": [
|
|
1068
|
+
{
|
|
1069
|
+
"id": "payments.ledger.payment",
|
|
1070
|
+
"slug": "payment",
|
|
1071
|
+
"name": "Payment",
|
|
1072
|
+
"readme": "# Payment\n\nOne authorisation and the captures made against it.\n\n## Postings\n\nEvery state change writes a balanced pair of postings. Nothing is updated in\nplace; a correction is a new pair with the opposite sign.\n\n| Transition | Debit | Credit |\n| ------------ | ------------ | ------------ |\n| authorise | (none) | (none) |\n| capture | `merchant` | `customer` |\n| fee | `fees` | `merchant` |\n| decline | (none) | (none) |\n\nAuthorisations and declines write no postings: no money has moved.\n\n## Currency\n\nThe customer is charged in the currency they were quoted in. When that is not\nthe currency the acquirer settles in, the rate is taken from the acquirer at\nauthorisation and stored on the payment — the posting has to be reproducible\nyears later, and a rate re-fetched then would be a different number. A\ncurrency with no acquirer behind it is a `currency_unsupported` decline, not a\nconversion.\n\n## Failure\n\n| Outcome | Postings | Retryable |\n| ---------- | -------- | -------------------------------- |\n| `soft` | none | yes, on another instrument |\n| `hard` | none | no |\n| `system` | none | yes, same instrument, with backoff |\n| `timeout` | none yet | the webhook decides, not us |\n\nA timeout is the only outcome that is not yet an outcome: the charge may well\nhave succeeded at the gateway, and until the webhook arrives nothing here may\nassume either way.\n\n## Commands\n\n`Authorize`, `Capture` and `Decline` are all idempotent on\n`idempotency_key`, which is retained for 30 days.\n\n## Queries\n\n`GetPayment` returns the payment and its full posting history.",
|
|
1073
|
+
"root": "Payment",
|
|
1074
|
+
"entities": [
|
|
1075
|
+
{
|
|
1076
|
+
"id": "payments.ledger.payment.payment",
|
|
1077
|
+
"slug": "payment",
|
|
1078
|
+
"name": "Payment",
|
|
1079
|
+
"doc": "One attempt to take money for an order, from authorization through to capture or decline.",
|
|
1080
|
+
"fields": [
|
|
1081
|
+
{
|
|
1082
|
+
"name": "id",
|
|
1083
|
+
"type": "string",
|
|
1084
|
+
"doc": "Payment id."
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
"name": "orderId",
|
|
1088
|
+
"type": "string",
|
|
1089
|
+
"doc": "Order being paid for."
|
|
1090
|
+
},
|
|
1091
|
+
{
|
|
1092
|
+
"name": "amount",
|
|
1093
|
+
"type": "Money",
|
|
1094
|
+
"doc": "Amount authorized.",
|
|
1095
|
+
"ref": "Money"
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
"name": "state",
|
|
1099
|
+
"type": "string",
|
|
1100
|
+
"doc": "authorized | captured | declined | voided | settled. `voided` is a cancellation before capture and costs nothing; `declined` is a refusal."
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
"name": "card",
|
|
1104
|
+
"type": "CardRef",
|
|
1105
|
+
"doc": "Instrument used, tokenized."
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
"name": "settlement",
|
|
1109
|
+
"type": "Money",
|
|
1110
|
+
"doc": "What the acquirer will actually settle, after any conversion. Equal to `amount` whenever the two currencies match.",
|
|
1111
|
+
"ref": "Money"
|
|
1112
|
+
},
|
|
1113
|
+
{
|
|
1114
|
+
"name": "fx",
|
|
1115
|
+
"type": "FxRate",
|
|
1116
|
+
"doc": "The rate that produced `settlement`. Absent when no conversion happened, which is the common case.",
|
|
1117
|
+
"ref": "FxRate"
|
|
1118
|
+
},
|
|
1119
|
+
{
|
|
1120
|
+
"name": "attempts",
|
|
1121
|
+
"type": "[]CaptureAttempt",
|
|
1122
|
+
"doc": "Every capture attempt, in order. The retry loop appends; nothing is ever overwritten."
|
|
1123
|
+
},
|
|
1124
|
+
{
|
|
1125
|
+
"name": "gateway",
|
|
1126
|
+
"type": "GatewayRef",
|
|
1127
|
+
"doc": "The charge as the gateway knows it.",
|
|
1128
|
+
"ref": "GatewayRef"
|
|
1129
|
+
}
|
|
1130
|
+
]
|
|
1131
|
+
},
|
|
1132
|
+
{
|
|
1133
|
+
"id": "payments.ledger.payment.capture-attempt",
|
|
1134
|
+
"slug": "capture-attempt",
|
|
1135
|
+
"name": "CaptureAttempt",
|
|
1136
|
+
"doc": "One try at settling an authorization. Has identity inside the payment because the attempt number is what makes a retry safe.",
|
|
1137
|
+
"fields": [
|
|
1138
|
+
{
|
|
1139
|
+
"name": "attempt",
|
|
1140
|
+
"type": "int32",
|
|
1141
|
+
"doc": "1-based. Together with the order id this is the key ADR payments.0004 makes the journal idempotent on."
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
"name": "requestedAt",
|
|
1145
|
+
"type": "time.Time",
|
|
1146
|
+
"doc": "When the attempt left for the gateway, not when it came back."
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
"name": "outcome",
|
|
1150
|
+
"type": "string",
|
|
1151
|
+
"doc": "pending | captured | declined | timeout. A `timeout` is not a failure: only the gateway's webhook can say which it was."
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
"name": "gateway",
|
|
1155
|
+
"type": "GatewayRef",
|
|
1156
|
+
"doc": "The gateway's ids for this attempt. A retry gets a new event id and keeps the charge id.",
|
|
1157
|
+
"ref": "GatewayRef"
|
|
1158
|
+
}
|
|
1159
|
+
]
|
|
1160
|
+
}
|
|
1161
|
+
],
|
|
1162
|
+
"valueObjects": [
|
|
1163
|
+
{
|
|
1164
|
+
"id": "payments.ledger.payment.money",
|
|
1165
|
+
"slug": "money",
|
|
1166
|
+
"name": "Money",
|
|
1167
|
+
"doc": "An amount in a single currency.",
|
|
1168
|
+
"ref": "Money"
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
"id": "payments.ledger.payment.card-ref",
|
|
1172
|
+
"slug": "card-ref",
|
|
1173
|
+
"name": "CardRef",
|
|
1174
|
+
"doc": "A tokenized instrument. Never holds a PAN; the token is the identity as far as the ledger is concerned.",
|
|
1175
|
+
"fields": [
|
|
1176
|
+
{
|
|
1177
|
+
"name": "brand",
|
|
1178
|
+
"type": "string",
|
|
1179
|
+
"doc": "visa | mastercard | amex."
|
|
1180
|
+
},
|
|
1181
|
+
{
|
|
1182
|
+
"name": "last4",
|
|
1183
|
+
"type": "string",
|
|
1184
|
+
"doc": "Last four digits, for display only."
|
|
1185
|
+
},
|
|
1186
|
+
{
|
|
1187
|
+
"name": "token",
|
|
1188
|
+
"type": "string",
|
|
1189
|
+
"doc": "Vault token; the only thing the ledger stores."
|
|
1190
|
+
}
|
|
1191
|
+
]
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
"id": "payments.ledger.payment.gateway-ref",
|
|
1195
|
+
"slug": "gateway-ref",
|
|
1196
|
+
"name": "GatewayRef",
|
|
1197
|
+
"doc": "How the gateway refers to a charge. Ours to store, never ours to mint.",
|
|
1198
|
+
"ref": "GatewayRef"
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
"id": "payments.ledger.payment.fx-rate",
|
|
1202
|
+
"slug": "fx-rate",
|
|
1203
|
+
"name": "FxRate",
|
|
1204
|
+
"doc": "The rate applied when the customer's currency is not the one we settle in.",
|
|
1205
|
+
"ref": "FxRate"
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
"id": "payments.ledger.payment.decline-reason",
|
|
1209
|
+
"slug": "decline-reason",
|
|
1210
|
+
"name": "DeclineReason",
|
|
1211
|
+
"doc": "Why a payment was refused, in enough detail to decide whether trying again is worth anything.",
|
|
1212
|
+
"fields": [
|
|
1213
|
+
{
|
|
1214
|
+
"name": "code",
|
|
1215
|
+
"type": "string",
|
|
1216
|
+
"doc": "The gateway's code, verbatim: `insufficient_funds`, `do_not_honor`, `currency_unsupported`, `expired_card`."
|
|
1217
|
+
},
|
|
1218
|
+
{
|
|
1219
|
+
"name": "category",
|
|
1220
|
+
"type": "string",
|
|
1221
|
+
"doc": "soft | hard | system. `soft` may be retried on another instrument, `hard` may not, `system` is ours to fix."
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
"name": "retryable",
|
|
1225
|
+
"type": "bool",
|
|
1226
|
+
"doc": "Derived from the category, stored anyway so a consumer does not have to know the mapping."
|
|
1227
|
+
},
|
|
1228
|
+
{
|
|
1229
|
+
"name": "gatewayMessage",
|
|
1230
|
+
"type": "string",
|
|
1231
|
+
"doc": "The gateway's own words. Kept for support and never shown to a customer."
|
|
1232
|
+
}
|
|
1233
|
+
]
|
|
1234
|
+
}
|
|
1235
|
+
],
|
|
1236
|
+
"operations": [
|
|
1237
|
+
{
|
|
1238
|
+
"id": "Authorize",
|
|
1239
|
+
"kind": "command",
|
|
1240
|
+
"doc": "Reserves funds at the gateway and fixes the settlement currency. Idempotent on the caller's key for 30 days: a repeat returns the first answer instead of reserving twice."
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
"id": "Capture",
|
|
1244
|
+
"kind": "command",
|
|
1245
|
+
"doc": "Settles a held authorization, wholly or in part. Appends a CaptureAttempt and posts the journal pair; the attempt number is what makes a retry safe."
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
"id": "Decline",
|
|
1249
|
+
"kind": "command",
|
|
1250
|
+
"doc": "Records a refusal, the gateway's or ours, with its code and category. Writes no postings — no money moved."
|
|
1251
|
+
},
|
|
1252
|
+
{
|
|
1253
|
+
"id": "GetPayment",
|
|
1254
|
+
"kind": "query",
|
|
1255
|
+
"doc": "The payment with its capture attempts, its FX rate if there was one, and the full posting history."
|
|
1256
|
+
}
|
|
1257
|
+
],
|
|
1258
|
+
"events": [
|
|
1259
|
+
{
|
|
1260
|
+
"id": "payments.ledger.payment.PaymentAuthorized",
|
|
1261
|
+
"slug": "payment-authorized",
|
|
1262
|
+
"name": "PaymentAuthorized",
|
|
1263
|
+
"versions": [
|
|
1264
|
+
{
|
|
1265
|
+
"version": "v1",
|
|
1266
|
+
"doc": "Funds reserved with the PSP. No money has moved yet.",
|
|
1267
|
+
"source": "internal/ledger/domain/payment/events.go:44",
|
|
1268
|
+
"fields": [
|
|
1269
|
+
{
|
|
1270
|
+
"name": "paymentId",
|
|
1271
|
+
"type": "string",
|
|
1272
|
+
"doc": "Ledger identifier for this payment."
|
|
1273
|
+
},
|
|
1274
|
+
{
|
|
1275
|
+
"name": "orderId",
|
|
1276
|
+
"type": "string",
|
|
1277
|
+
"doc": "Order the authorisation belongs to."
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
"name": "amount",
|
|
1281
|
+
"type": "Money",
|
|
1282
|
+
"doc": "Amount reserved.",
|
|
1283
|
+
"ref": "Money"
|
|
1284
|
+
},
|
|
1285
|
+
{
|
|
1286
|
+
"name": "authCode",
|
|
1287
|
+
"type": "string",
|
|
1288
|
+
"doc": "Opaque PSP authorisation code."
|
|
1289
|
+
},
|
|
1290
|
+
{
|
|
1291
|
+
"name": "expiresAt",
|
|
1292
|
+
"type": "time.Time",
|
|
1293
|
+
"doc": "Authorisations lapse after seven days."
|
|
1294
|
+
}
|
|
1295
|
+
]
|
|
1296
|
+
}
|
|
1297
|
+
],
|
|
1298
|
+
"consumers": [
|
|
1299
|
+
{
|
|
1300
|
+
"service": "shop.oms",
|
|
1301
|
+
"status": "verified"
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
"service": "delivery.core",
|
|
1305
|
+
"status": "declared",
|
|
1306
|
+
"note": "Used only to pre-warm route planning."
|
|
1307
|
+
}
|
|
1308
|
+
]
|
|
1309
|
+
},
|
|
1310
|
+
{
|
|
1311
|
+
"id": "payments.ledger.payment.PaymentCaptured",
|
|
1312
|
+
"slug": "payment-captured",
|
|
1313
|
+
"name": "PaymentCaptured",
|
|
1314
|
+
"versions": [
|
|
1315
|
+
{
|
|
1316
|
+
"version": "v1",
|
|
1317
|
+
"doc": "Reserved funds settled. This is the event fulfilment reacts to.",
|
|
1318
|
+
"source": "internal/ledger/domain/payment/events.go:88",
|
|
1319
|
+
"fields": [
|
|
1320
|
+
{
|
|
1321
|
+
"name": "paymentId",
|
|
1322
|
+
"type": "string",
|
|
1323
|
+
"doc": "Ledger identifier for this payment."
|
|
1324
|
+
},
|
|
1325
|
+
{
|
|
1326
|
+
"name": "orderId",
|
|
1327
|
+
"type": "string",
|
|
1328
|
+
"doc": "Order the capture belongs to."
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
"name": "amount",
|
|
1332
|
+
"type": "Money",
|
|
1333
|
+
"doc": "Amount actually captured; may be less than authorised.",
|
|
1334
|
+
"ref": "Money"
|
|
1335
|
+
},
|
|
1336
|
+
{
|
|
1337
|
+
"name": "capturedAt",
|
|
1338
|
+
"type": "time.Time",
|
|
1339
|
+
"doc": "Instant the PSP confirmed settlement."
|
|
1340
|
+
}
|
|
1341
|
+
]
|
|
1342
|
+
}
|
|
1343
|
+
],
|
|
1344
|
+
"consumers": [
|
|
1345
|
+
{
|
|
1346
|
+
"service": "delivery.core",
|
|
1347
|
+
"status": "verified"
|
|
1348
|
+
},
|
|
1349
|
+
{
|
|
1350
|
+
"service": "shop.oms",
|
|
1351
|
+
"status": "verified"
|
|
1352
|
+
}
|
|
1353
|
+
]
|
|
1354
|
+
},
|
|
1355
|
+
{
|
|
1356
|
+
"id": "payments.ledger.payment.PaymentDeclined",
|
|
1357
|
+
"slug": "payment-declined",
|
|
1358
|
+
"name": "PaymentDeclined",
|
|
1359
|
+
"versions": [
|
|
1360
|
+
{
|
|
1361
|
+
"version": "v1",
|
|
1362
|
+
"doc": "The PSP refused the authorisation or the capture.",
|
|
1363
|
+
"source": "internal/ledger/domain/payment/events.go:126",
|
|
1364
|
+
"fields": [
|
|
1365
|
+
{
|
|
1366
|
+
"name": "paymentId",
|
|
1367
|
+
"type": "string",
|
|
1368
|
+
"doc": "Ledger identifier for the attempted payment."
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
"name": "orderId",
|
|
1372
|
+
"type": "string",
|
|
1373
|
+
"doc": "Order that failed to pay."
|
|
1374
|
+
},
|
|
1375
|
+
{
|
|
1376
|
+
"name": "code",
|
|
1377
|
+
"type": "DeclineCode",
|
|
1378
|
+
"doc": "Normalised decline code, not the raw PSP string."
|
|
1379
|
+
},
|
|
1380
|
+
{
|
|
1381
|
+
"name": "retryable",
|
|
1382
|
+
"type": "bool",
|
|
1383
|
+
"doc": "Whether another attempt is worth making."
|
|
1384
|
+
}
|
|
1385
|
+
]
|
|
1386
|
+
}
|
|
1387
|
+
],
|
|
1388
|
+
"consumers": [
|
|
1389
|
+
{
|
|
1390
|
+
"service": "shop.oms",
|
|
1391
|
+
"status": "verified"
|
|
1392
|
+
}
|
|
1393
|
+
]
|
|
1394
|
+
}
|
|
1395
|
+
],
|
|
1396
|
+
"enums": [
|
|
1397
|
+
{
|
|
1398
|
+
"id": "payments.ledger.payment.decline-code",
|
|
1399
|
+
"slug": "decline-code",
|
|
1400
|
+
"name": "DeclineCode",
|
|
1401
|
+
"doc": "Why the money was not held, as a closed set a consumer switches on.",
|
|
1402
|
+
"values": [
|
|
1403
|
+
{
|
|
1404
|
+
"name": "CARD_REFUSED",
|
|
1405
|
+
"doc": "The network would not hold the amount on this instrument."
|
|
1406
|
+
},
|
|
1407
|
+
{
|
|
1408
|
+
"name": "ORDER_CANCELLED",
|
|
1409
|
+
"doc": "The order was already cancelled when the hold was asked for; nothing was asked of the network."
|
|
1410
|
+
},
|
|
1411
|
+
{
|
|
1412
|
+
"name": "GATEWAY_TIMEOUT",
|
|
1413
|
+
"doc": "The gateway did not answer in time; retired, since an outage now records nothing.",
|
|
1414
|
+
"deprecated": true
|
|
1415
|
+
}
|
|
1416
|
+
]
|
|
1417
|
+
}
|
|
1418
|
+
]
|
|
1419
|
+
},
|
|
1420
|
+
{
|
|
1421
|
+
"id": "payments.ledger.refund",
|
|
1422
|
+
"slug": "refund",
|
|
1423
|
+
"name": "Refund",
|
|
1424
|
+
"readme": "# Refund\n\nA compensating movement against a captured payment.\n\n## Rules\n\n- The sum of refunds against a payment may never exceed the captured amount.\n- A refund against an uncaptured payment is a void, handled by `Payment`, not\n here.\n- Partial refunds are unlimited in count but each needs a distinct reason.\n\n## Settling\n\nA refund is not done when it is issued. It leaves here in `pending` and the\ngateway confirms it by webhook, usually in hours and sometimes in days. The\nhourly poll exists because that webhook is not guaranteed. A refund the\ngateway rejects is stored with its reason and published nowhere — the one\noutcome in this aggregate that no consumer can see.\n\n## Commands\n\n| Command | Precondition |\n| -------------- | ------------------------------- |\n| `IssueRefund` | Payment captured, headroom left |\n\n## Queries\n\n`ListRefunds` returns refunds for one payment, newest first.",
|
|
1425
|
+
"root": "Refund",
|
|
1426
|
+
"entities": [
|
|
1427
|
+
{
|
|
1428
|
+
"id": "payments.ledger.refund.refund",
|
|
1429
|
+
"slug": "refund",
|
|
1430
|
+
"name": "Refund",
|
|
1431
|
+
"doc": "Money returned against a captured payment.",
|
|
1432
|
+
"fields": [
|
|
1433
|
+
{
|
|
1434
|
+
"name": "id",
|
|
1435
|
+
"type": "string",
|
|
1436
|
+
"doc": "Refund id."
|
|
1437
|
+
},
|
|
1438
|
+
{
|
|
1439
|
+
"name": "paymentId",
|
|
1440
|
+
"type": "string",
|
|
1441
|
+
"doc": "Payment being refunded."
|
|
1442
|
+
},
|
|
1443
|
+
{
|
|
1444
|
+
"name": "amount",
|
|
1445
|
+
"type": "Money",
|
|
1446
|
+
"doc": "Amount returned; never exceeds the capture.",
|
|
1447
|
+
"ref": "Money"
|
|
1448
|
+
},
|
|
1449
|
+
{
|
|
1450
|
+
"name": "reason",
|
|
1451
|
+
"type": "string",
|
|
1452
|
+
"doc": "Free text, taken from the support tool."
|
|
1453
|
+
},
|
|
1454
|
+
{
|
|
1455
|
+
"name": "state",
|
|
1456
|
+
"type": "string",
|
|
1457
|
+
"doc": "requested | pending | settled | rejected. `pending` belongs to the gateway, not to us, and it can sit there for days."
|
|
1458
|
+
},
|
|
1459
|
+
{
|
|
1460
|
+
"name": "settlement",
|
|
1461
|
+
"type": "Settlement",
|
|
1462
|
+
"doc": "Where the gateway has got to with it."
|
|
1463
|
+
},
|
|
1464
|
+
{
|
|
1465
|
+
"name": "gateway",
|
|
1466
|
+
"type": "GatewayRef",
|
|
1467
|
+
"doc": "The charge the refund is written against.",
|
|
1468
|
+
"ref": "GatewayRef"
|
|
1469
|
+
}
|
|
1470
|
+
]
|
|
1471
|
+
}
|
|
1472
|
+
],
|
|
1473
|
+
"valueObjects": [
|
|
1474
|
+
{
|
|
1475
|
+
"id": "payments.ledger.refund.money",
|
|
1476
|
+
"slug": "money",
|
|
1477
|
+
"name": "Money",
|
|
1478
|
+
"doc": "An amount in a single currency.",
|
|
1479
|
+
"ref": "Money"
|
|
1480
|
+
},
|
|
1481
|
+
{
|
|
1482
|
+
"id": "payments.ledger.refund.settlement",
|
|
1483
|
+
"slug": "settlement",
|
|
1484
|
+
"name": "Settlement",
|
|
1485
|
+
"doc": "The gateway's progress on a refund. Ours to record, not to decide.",
|
|
1486
|
+
"fields": [
|
|
1487
|
+
{
|
|
1488
|
+
"name": "state",
|
|
1489
|
+
"type": "string",
|
|
1490
|
+
"doc": "pending | settled | rejected, as last reported."
|
|
1491
|
+
},
|
|
1492
|
+
{
|
|
1493
|
+
"name": "requestedAt",
|
|
1494
|
+
"type": "time.Time",
|
|
1495
|
+
"doc": "When the refund was sent to the gateway."
|
|
1496
|
+
},
|
|
1497
|
+
{
|
|
1498
|
+
"name": "settledAt",
|
|
1499
|
+
"type": "time.Time",
|
|
1500
|
+
"doc": "When the gateway confirmed. Absent while pending, and absent forever if rejected."
|
|
1501
|
+
},
|
|
1502
|
+
{
|
|
1503
|
+
"name": "polls",
|
|
1504
|
+
"type": "int32",
|
|
1505
|
+
"doc": "How many times we asked. The poll is the fallback for a webhook that never came."
|
|
1506
|
+
}
|
|
1507
|
+
]
|
|
1508
|
+
},
|
|
1509
|
+
{
|
|
1510
|
+
"id": "payments.ledger.refund.gateway-ref",
|
|
1511
|
+
"slug": "gateway-ref",
|
|
1512
|
+
"name": "GatewayRef",
|
|
1513
|
+
"doc": "The same shared reference the payment carries — a refund is written against the gateway's charge, not against ours.",
|
|
1514
|
+
"ref": "GatewayRef"
|
|
1515
|
+
}
|
|
1516
|
+
],
|
|
1517
|
+
"operations": [
|
|
1518
|
+
{
|
|
1519
|
+
"id": "IssueRefund",
|
|
1520
|
+
"kind": "command",
|
|
1521
|
+
"doc": "Returns money against a captured payment, up to the headroom left on it. Lands in `pending` and stays there until the gateway settles or refuses it."
|
|
1522
|
+
},
|
|
1523
|
+
{
|
|
1524
|
+
"id": "ListRefunds",
|
|
1525
|
+
"kind": "query",
|
|
1526
|
+
"doc": "Refunds against one payment, newest first, including the ones the gateway rejected."
|
|
1527
|
+
}
|
|
1528
|
+
],
|
|
1529
|
+
"events": [
|
|
1530
|
+
{
|
|
1531
|
+
"id": "payments.ledger.refund.RefundIssued",
|
|
1532
|
+
"slug": "refund-issued",
|
|
1533
|
+
"name": "RefundIssued",
|
|
1534
|
+
"versions": [
|
|
1535
|
+
{
|
|
1536
|
+
"version": "v1",
|
|
1537
|
+
"doc": "A compensating posting pair against a captured payment.",
|
|
1538
|
+
"source": "internal/ledger/domain/refund/events.go:30",
|
|
1539
|
+
"fields": [
|
|
1540
|
+
{
|
|
1541
|
+
"name": "refundId",
|
|
1542
|
+
"type": "string",
|
|
1543
|
+
"doc": "Identifier of the refund."
|
|
1544
|
+
},
|
|
1545
|
+
{
|
|
1546
|
+
"name": "paymentId",
|
|
1547
|
+
"type": "string",
|
|
1548
|
+
"doc": "Payment being refunded."
|
|
1549
|
+
},
|
|
1550
|
+
{
|
|
1551
|
+
"name": "amount",
|
|
1552
|
+
"type": "Money",
|
|
1553
|
+
"doc": "Amount refunded; may be partial.",
|
|
1554
|
+
"ref": "Money"
|
|
1555
|
+
},
|
|
1556
|
+
{
|
|
1557
|
+
"name": "reason",
|
|
1558
|
+
"type": "string",
|
|
1559
|
+
"doc": "Free text, surfaced to support tooling."
|
|
1560
|
+
}
|
|
1561
|
+
]
|
|
1562
|
+
}
|
|
1563
|
+
],
|
|
1564
|
+
"consumers": [
|
|
1565
|
+
{
|
|
1566
|
+
"service": "shop.oms",
|
|
1567
|
+
"status": "declared"
|
|
1568
|
+
},
|
|
1569
|
+
{
|
|
1570
|
+
"service": "delivery.core",
|
|
1571
|
+
"status": "declared"
|
|
1572
|
+
}
|
|
1573
|
+
]
|
|
1574
|
+
}
|
|
1575
|
+
]
|
|
1576
|
+
}
|
|
1577
|
+
],
|
|
1578
|
+
"stores": [
|
|
1579
|
+
"payments.ledger.pg"
|
|
1580
|
+
]
|
|
1581
|
+
}
|
|
1582
|
+
]
|
|
1583
|
+
},
|
|
1584
|
+
{
|
|
1585
|
+
"id": "delivery",
|
|
1586
|
+
"slug": "delivery",
|
|
1587
|
+
"name": "Delivery",
|
|
1588
|
+
"summary": "Routes, parcels and proof of delivery. Reacts to captured payments, never to placed orders.",
|
|
1589
|
+
"classification": "supporting",
|
|
1590
|
+
"services": [
|
|
1591
|
+
{
|
|
1592
|
+
"id": "delivery.core",
|
|
1593
|
+
"slug": "core",
|
|
1594
|
+
"name": "Delivery Core",
|
|
1595
|
+
"repo": "github.com/acme/delivery",
|
|
1596
|
+
"path": "services/core",
|
|
1597
|
+
"readme": "# Delivery Core\n\n`delivery.core` turns confirmed orders into physical movement: routes, parcels\nand proof of delivery. It is the only service in the `delivery` context and it\nowns every carrier integration.\n\n## Boundaries\n\nDelivery reacts to `PaymentCaptured` rather than to `OrderPlaced`. An order\nthat is placed but never paid must never reach a van. This is the single most\nimportant rule in the context.\n\n```mermaid\nflowchart TD\n P[PaymentCaptured] --> R[Plan route]\n R --> S[ShipmentDispatched]\n S --> T{scan events}\n T -- delivered --> D[ShipmentDelivered]\n T -- failed --> F[Retry next window]\n F --> R\n```\n\n## Carriers\n\n| Carrier | Coverage | Tracking | Cutoff |\n| --------- | ------------- | ------------- | ------- |\n| `inhouse` | Metro only | Live GPS | 18:00 |\n| `natpost` | National | Scan events | 15:30 |\n| `express` | International | Scan events | 12:00 |\n\nCarrier choice is decided at route planning time and is not part of the order.\n\n## Aggregates\n\n- `shipment` — one parcel from dispatch to proof of delivery.\n- `route` — a planned sequence of stops for one vehicle and one window.\n\n## Retries\n\nA failed delivery attempt does not fail the shipment. The shipment stays open\nand is re-routed into the next available window, up to three attempts, after\nwhich it is returned to the depot and the order is flagged for support.\n\n## Observability\n\nEvery scan event carries the trace id of the originating `PaymentCaptured`\nmessage, which is how the shipment tracking flow was derived from traces.\n",
|
|
1598
|
+
"provides": [
|
|
1599
|
+
{
|
|
1600
|
+
"id": "delivery.v1.Delivery",
|
|
1601
|
+
"methods": [
|
|
1602
|
+
{
|
|
1603
|
+
"name": "PlanRoute"
|
|
1604
|
+
},
|
|
1605
|
+
{
|
|
1606
|
+
"name": "TrackShipment"
|
|
1607
|
+
},
|
|
1608
|
+
{
|
|
1609
|
+
"name": "GetShipment"
|
|
1610
|
+
}
|
|
1611
|
+
],
|
|
1612
|
+
"source": "proto/delivery/v1/delivery.proto:11"
|
|
1613
|
+
}
|
|
1614
|
+
],
|
|
1615
|
+
"consumes": [
|
|
1616
|
+
{
|
|
1617
|
+
"id": "shop.v1.Orders/GetOrder",
|
|
1618
|
+
"peer": "shop.oms",
|
|
1619
|
+
"status": "verified",
|
|
1620
|
+
"source": "internal/delivery/client/orders.go:31"
|
|
1621
|
+
},
|
|
1622
|
+
{
|
|
1623
|
+
"id": "payments.v1.Payments/GetPayment",
|
|
1624
|
+
"peer": "payments.ledger",
|
|
1625
|
+
"status": "declared",
|
|
1626
|
+
"source": "internal/delivery/client/payments.go:18"
|
|
1627
|
+
}
|
|
1628
|
+
],
|
|
1629
|
+
"aggregates": [
|
|
1630
|
+
{
|
|
1631
|
+
"id": "delivery.core.shipment",
|
|
1632
|
+
"slug": "shipment",
|
|
1633
|
+
"name": "Shipment",
|
|
1634
|
+
"readme": "# Shipment\n\nOne parcel, from dispatch to proof of delivery.\n\n## Attempts\n\nA failed attempt does not fail the shipment. The shipment stays open and is\nre-routed into the next window, up to three attempts.\n\n| Attempt | Outcome | Next |\n| ------- | ------------------ | -------------------------- |\n| 1 | no answer | re-route, next window |\n| 2 | no answer | re-route, next window |\n| 3 | no answer | return to depot, flag order |\n\n## Exceptions\n\nAn `EXCEPTION` scan stops the shipment advancing and opens a delivery\nexception. What reads that queue is not in this catalog and emits no spans, so\nthe shipment-tracking flow ends there too — the gap is real, not a modelling\nomission.\n\n## Commands\n\n`Dispatch`, `RecordScan` and `RecordDelivery` are the only writes. Scans\narrive from carrier webhooks and are deduplicated on the carrier scan id.\n\n## Queries\n\n`TrackShipment` is public-facing and rate limited per tracking reference.",
|
|
1635
|
+
"root": "Shipment",
|
|
1636
|
+
"entities": [
|
|
1637
|
+
{
|
|
1638
|
+
"id": "delivery.core.shipment.shipment",
|
|
1639
|
+
"slug": "shipment",
|
|
1640
|
+
"name": "Shipment",
|
|
1641
|
+
"doc": "Goods on their way to one address. The unit carriers scan.",
|
|
1642
|
+
"fields": [
|
|
1643
|
+
{
|
|
1644
|
+
"name": "id",
|
|
1645
|
+
"type": "string",
|
|
1646
|
+
"doc": "Shipment id."
|
|
1647
|
+
},
|
|
1648
|
+
{
|
|
1649
|
+
"name": "orderId",
|
|
1650
|
+
"type": "string",
|
|
1651
|
+
"doc": "Order the goods belong to."
|
|
1652
|
+
},
|
|
1653
|
+
{
|
|
1654
|
+
"name": "parcels",
|
|
1655
|
+
"type": "[]Parcel",
|
|
1656
|
+
"doc": "Physical parcels in this shipment."
|
|
1657
|
+
},
|
|
1658
|
+
{
|
|
1659
|
+
"name": "shipTo",
|
|
1660
|
+
"type": "Address",
|
|
1661
|
+
"doc": "Destination.",
|
|
1662
|
+
"ref": "Address"
|
|
1663
|
+
},
|
|
1664
|
+
{
|
|
1665
|
+
"name": "state",
|
|
1666
|
+
"type": "string",
|
|
1667
|
+
"doc": "held | planned | dispatched | delivered | exception. A shipment sits in `held` from order confirmation until the money is actually captured."
|
|
1668
|
+
},
|
|
1669
|
+
{
|
|
1670
|
+
"name": "scans",
|
|
1671
|
+
"type": "[]Scan",
|
|
1672
|
+
"doc": "Every carrier scan, oldest first. Append-only: a corrected scan is a new one."
|
|
1673
|
+
}
|
|
1674
|
+
]
|
|
1675
|
+
},
|
|
1676
|
+
{
|
|
1677
|
+
"id": "delivery.core.shipment.parcel",
|
|
1678
|
+
"slug": "parcel",
|
|
1679
|
+
"name": "Parcel",
|
|
1680
|
+
"doc": "One box. Has its own tracking code and is scanned independently of its shipment.",
|
|
1681
|
+
"fields": [
|
|
1682
|
+
{
|
|
1683
|
+
"name": "id",
|
|
1684
|
+
"type": "string",
|
|
1685
|
+
"doc": "Parcel id."
|
|
1686
|
+
},
|
|
1687
|
+
{
|
|
1688
|
+
"name": "weightGrams",
|
|
1689
|
+
"type": "int32",
|
|
1690
|
+
"doc": "Gross weight at dispatch."
|
|
1691
|
+
},
|
|
1692
|
+
{
|
|
1693
|
+
"name": "tracking",
|
|
1694
|
+
"type": "TrackingCode",
|
|
1695
|
+
"doc": "Carrier tracking code."
|
|
1696
|
+
}
|
|
1697
|
+
]
|
|
1698
|
+
},
|
|
1699
|
+
{
|
|
1700
|
+
"id": "delivery.core.shipment.scan",
|
|
1701
|
+
"slug": "scan",
|
|
1702
|
+
"name": "Scan",
|
|
1703
|
+
"doc": "One reading of a parcel by a carrier. Has identity because the carrier's scan id is what a retried webhook is deduplicated on.",
|
|
1704
|
+
"fields": [
|
|
1705
|
+
{
|
|
1706
|
+
"name": "carrierEventId",
|
|
1707
|
+
"type": "string",
|
|
1708
|
+
"doc": "The carrier's id for the scan. Deduplication is on this alone; a retried webhook carries the same one."
|
|
1709
|
+
},
|
|
1710
|
+
{
|
|
1711
|
+
"name": "code",
|
|
1712
|
+
"type": "string",
|
|
1713
|
+
"doc": "IN_TRANSIT | DELIVERED | EXCEPTION. The carrier's vocabulary, stored unmapped so a new code is visible rather than swallowed."
|
|
1714
|
+
},
|
|
1715
|
+
{
|
|
1716
|
+
"name": "scannedAt",
|
|
1717
|
+
"type": "time.Time",
|
|
1718
|
+
"doc": "When the carrier says it happened, which is not when we heard about it."
|
|
1719
|
+
},
|
|
1720
|
+
{
|
|
1721
|
+
"name": "location",
|
|
1722
|
+
"type": "string",
|
|
1723
|
+
"doc": "Depot or delivery point as the carrier names it. Free text; no two carriers agree."
|
|
1724
|
+
}
|
|
1725
|
+
]
|
|
1726
|
+
}
|
|
1727
|
+
],
|
|
1728
|
+
"valueObjects": [
|
|
1729
|
+
{
|
|
1730
|
+
"id": "delivery.core.shipment.address",
|
|
1731
|
+
"slug": "address",
|
|
1732
|
+
"name": "Address",
|
|
1733
|
+
"doc": "A postal address, unvalidated.",
|
|
1734
|
+
"ref": "Address"
|
|
1735
|
+
},
|
|
1736
|
+
{
|
|
1737
|
+
"id": "delivery.core.shipment.tracking-code",
|
|
1738
|
+
"slug": "tracking-code",
|
|
1739
|
+
"name": "TrackingCode",
|
|
1740
|
+
"doc": "A carrier and the code it issued. Meaningless without the carrier, so the two travel together.",
|
|
1741
|
+
"fields": [
|
|
1742
|
+
{
|
|
1743
|
+
"name": "carrier",
|
|
1744
|
+
"type": "string",
|
|
1745
|
+
"doc": "Carrier code, e.g. DHL."
|
|
1746
|
+
},
|
|
1747
|
+
{
|
|
1748
|
+
"name": "code",
|
|
1749
|
+
"type": "string",
|
|
1750
|
+
"doc": "Tracking code as issued by the carrier."
|
|
1751
|
+
}
|
|
1752
|
+
]
|
|
1753
|
+
}
|
|
1754
|
+
],
|
|
1755
|
+
"operations": [
|
|
1756
|
+
{
|
|
1757
|
+
"id": "Dispatch",
|
|
1758
|
+
"kind": "command",
|
|
1759
|
+
"doc": "Releases a held shipment to a carrier and mints its tracking codes. Refused unless the payment is captured."
|
|
1760
|
+
},
|
|
1761
|
+
{
|
|
1762
|
+
"id": "RecordScan",
|
|
1763
|
+
"kind": "command",
|
|
1764
|
+
"doc": "Appends a carrier scan, deduplicated on the carrier's scan id. An EXCEPTION scan stops the shipment advancing and opens a delivery exception."
|
|
1765
|
+
},
|
|
1766
|
+
{
|
|
1767
|
+
"id": "RecordDelivery",
|
|
1768
|
+
"kind": "command",
|
|
1769
|
+
"doc": "Records proof of delivery and closes the shipment. Terminal: a scan arriving afterwards is stored and changes nothing."
|
|
1770
|
+
},
|
|
1771
|
+
{
|
|
1772
|
+
"id": "TrackShipment",
|
|
1773
|
+
"kind": "query",
|
|
1774
|
+
"doc": "The public tracking view for one reference. Rate limited per reference, and returns scans rather than internal state."
|
|
1775
|
+
},
|
|
1776
|
+
{
|
|
1777
|
+
"id": "GetShipment",
|
|
1778
|
+
"kind": "query",
|
|
1779
|
+
"doc": "The whole shipment, for callers inside the estate. This is what OMS reads before it will allow a cancellation."
|
|
1780
|
+
}
|
|
1781
|
+
],
|
|
1782
|
+
"events": [
|
|
1783
|
+
{
|
|
1784
|
+
"id": "delivery.core.shipment.ShipmentDispatched",
|
|
1785
|
+
"slug": "shipment-dispatched",
|
|
1786
|
+
"name": "ShipmentDispatched",
|
|
1787
|
+
"versions": [
|
|
1788
|
+
{
|
|
1789
|
+
"version": "v1",
|
|
1790
|
+
"doc": "A parcel physically left the depot.",
|
|
1791
|
+
"source": "internal/delivery/domain/shipment/events.go:52",
|
|
1792
|
+
"fields": [
|
|
1793
|
+
{
|
|
1794
|
+
"name": "shipmentId",
|
|
1795
|
+
"type": "string",
|
|
1796
|
+
"doc": "Identifier of the shipment."
|
|
1797
|
+
},
|
|
1798
|
+
{
|
|
1799
|
+
"name": "orderId",
|
|
1800
|
+
"type": "string",
|
|
1801
|
+
"doc": "Order being fulfilled."
|
|
1802
|
+
},
|
|
1803
|
+
{
|
|
1804
|
+
"name": "carrier",
|
|
1805
|
+
"type": "string",
|
|
1806
|
+
"doc": "One of inhouse, natpost, express."
|
|
1807
|
+
},
|
|
1808
|
+
{
|
|
1809
|
+
"name": "trackingRef",
|
|
1810
|
+
"type": "string",
|
|
1811
|
+
"doc": "Carrier tracking reference."
|
|
1812
|
+
},
|
|
1813
|
+
{
|
|
1814
|
+
"name": "shipTo",
|
|
1815
|
+
"type": "Address",
|
|
1816
|
+
"doc": "Address the parcel is routed to.",
|
|
1817
|
+
"ref": "Address"
|
|
1818
|
+
}
|
|
1819
|
+
]
|
|
1820
|
+
}
|
|
1821
|
+
],
|
|
1822
|
+
"consumers": [
|
|
1823
|
+
{
|
|
1824
|
+
"service": "shop.oms",
|
|
1825
|
+
"status": "verified"
|
|
1826
|
+
},
|
|
1827
|
+
{
|
|
1828
|
+
"service": "analytics-sink",
|
|
1829
|
+
"status": "declared",
|
|
1830
|
+
"note": "Registered in the traces exporter, not in any Go repo."
|
|
1831
|
+
}
|
|
1832
|
+
]
|
|
1833
|
+
},
|
|
1834
|
+
{
|
|
1835
|
+
"id": "delivery.core.shipment.ShipmentDelivered",
|
|
1836
|
+
"slug": "shipment-delivered",
|
|
1837
|
+
"name": "ShipmentDelivered",
|
|
1838
|
+
"versions": [
|
|
1839
|
+
{
|
|
1840
|
+
"version": "v1",
|
|
1841
|
+
"doc": "Proof of delivery recorded. Terminal for the shipment.",
|
|
1842
|
+
"source": "internal/delivery/domain/shipment/events.go:97",
|
|
1843
|
+
"fields": [
|
|
1844
|
+
{
|
|
1845
|
+
"name": "shipmentId",
|
|
1846
|
+
"type": "string",
|
|
1847
|
+
"doc": "Identifier of the shipment."
|
|
1848
|
+
},
|
|
1849
|
+
{
|
|
1850
|
+
"name": "orderId",
|
|
1851
|
+
"type": "string",
|
|
1852
|
+
"doc": "Order that is now fulfilled."
|
|
1853
|
+
},
|
|
1854
|
+
{
|
|
1855
|
+
"name": "signedBy",
|
|
1856
|
+
"type": "string",
|
|
1857
|
+
"doc": "Name captured at the door, empty for contactless."
|
|
1858
|
+
},
|
|
1859
|
+
{
|
|
1860
|
+
"name": "deliveredAt",
|
|
1861
|
+
"type": "time.Time",
|
|
1862
|
+
"doc": "Instant of the delivery scan."
|
|
1863
|
+
}
|
|
1864
|
+
]
|
|
1865
|
+
}
|
|
1866
|
+
],
|
|
1867
|
+
"consumers": [
|
|
1868
|
+
{
|
|
1869
|
+
"service": "shop.oms",
|
|
1870
|
+
"status": "verified"
|
|
1871
|
+
},
|
|
1872
|
+
{
|
|
1873
|
+
"service": "analytics-sink",
|
|
1874
|
+
"status": "unresolved",
|
|
1875
|
+
"note": "Observed downstream of the bus in OTel traces; owner unknown."
|
|
1876
|
+
}
|
|
1877
|
+
]
|
|
1878
|
+
}
|
|
1879
|
+
]
|
|
1880
|
+
},
|
|
1881
|
+
{
|
|
1882
|
+
"id": "delivery.core.route",
|
|
1883
|
+
"slug": "route",
|
|
1884
|
+
"name": "Route",
|
|
1885
|
+
"readme": "# Route\n\nA planned sequence of stops for one vehicle and one delivery window.\n\n## Planning\n\nRoutes are planned once per window, ninety minutes before the window opens.\nShipments that miss the cut-off wait for the next window rather than forcing a\nre-plan; re-planning a live route is not supported.\n\n## Commands\n\n| Command | Notes |\n| ------------- | --------------------------------------- |\n| `PlanRoute` | Assigns vehicle and stop order |\n| `CloseRoute` | Terminal; unfinished stops are re-queued |\n\n## Queries\n\n`GetRoute` returns stops in planned order, not in scan order.",
|
|
1886
|
+
"root": "Route",
|
|
1887
|
+
"entities": [
|
|
1888
|
+
{
|
|
1889
|
+
"id": "delivery.core.route.route",
|
|
1890
|
+
"slug": "route",
|
|
1891
|
+
"name": "Route",
|
|
1892
|
+
"doc": "A day's driving for one vehicle, as planned the night before.",
|
|
1893
|
+
"fields": [
|
|
1894
|
+
{
|
|
1895
|
+
"name": "id",
|
|
1896
|
+
"type": "string",
|
|
1897
|
+
"doc": "Route id."
|
|
1898
|
+
},
|
|
1899
|
+
{
|
|
1900
|
+
"name": "date",
|
|
1901
|
+
"type": "string",
|
|
1902
|
+
"doc": "Delivery date, ISO."
|
|
1903
|
+
},
|
|
1904
|
+
{
|
|
1905
|
+
"name": "vehicleId",
|
|
1906
|
+
"type": "string",
|
|
1907
|
+
"doc": "Vehicle assigned."
|
|
1908
|
+
},
|
|
1909
|
+
{
|
|
1910
|
+
"name": "stops",
|
|
1911
|
+
"type": "[]Stop",
|
|
1912
|
+
"doc": "Stops in driving order."
|
|
1913
|
+
}
|
|
1914
|
+
]
|
|
1915
|
+
},
|
|
1916
|
+
{
|
|
1917
|
+
"id": "delivery.core.route.stop",
|
|
1918
|
+
"slug": "stop",
|
|
1919
|
+
"name": "Stop",
|
|
1920
|
+
"doc": "One address on a route. Identified by its position, which is what the driver's app shows.",
|
|
1921
|
+
"fields": [
|
|
1922
|
+
{
|
|
1923
|
+
"name": "sequence",
|
|
1924
|
+
"type": "int32",
|
|
1925
|
+
"doc": "Position on the route, 1-based."
|
|
1926
|
+
},
|
|
1927
|
+
{
|
|
1928
|
+
"name": "address",
|
|
1929
|
+
"type": "Address",
|
|
1930
|
+
"doc": "Where to stop.",
|
|
1931
|
+
"ref": "Address"
|
|
1932
|
+
},
|
|
1933
|
+
{
|
|
1934
|
+
"name": "shipmentId",
|
|
1935
|
+
"type": "string",
|
|
1936
|
+
"doc": "Shipment dropped here."
|
|
1937
|
+
}
|
|
1938
|
+
]
|
|
1939
|
+
}
|
|
1940
|
+
],
|
|
1941
|
+
"valueObjects": [
|
|
1942
|
+
{
|
|
1943
|
+
"id": "delivery.core.route.address",
|
|
1944
|
+
"slug": "address",
|
|
1945
|
+
"name": "Address",
|
|
1946
|
+
"doc": "A postal address, unvalidated.",
|
|
1947
|
+
"ref": "Address"
|
|
1948
|
+
}
|
|
1949
|
+
],
|
|
1950
|
+
"operations": [
|
|
1951
|
+
{
|
|
1952
|
+
"id": "PlanRoute",
|
|
1953
|
+
"kind": "command",
|
|
1954
|
+
"doc": "Sequences one window's shipments for one vehicle. Runs 90 minutes before the window opens; anything missing the cut-off waits for the next one."
|
|
1955
|
+
},
|
|
1956
|
+
{
|
|
1957
|
+
"id": "CloseRoute",
|
|
1958
|
+
"kind": "command",
|
|
1959
|
+
"doc": "Ends a route once every stop is recorded. A route with unrecorded stops cannot be closed."
|
|
1960
|
+
},
|
|
1961
|
+
{
|
|
1962
|
+
"id": "GetRoute",
|
|
1963
|
+
"kind": "query",
|
|
1964
|
+
"doc": "One route with its stops in driving order."
|
|
1965
|
+
}
|
|
1966
|
+
],
|
|
1967
|
+
"events": [
|
|
1968
|
+
{
|
|
1969
|
+
"id": "delivery.core.route.RoutePlanned",
|
|
1970
|
+
"slug": "route-planned",
|
|
1971
|
+
"name": "RoutePlanned",
|
|
1972
|
+
"versions": [
|
|
1973
|
+
{
|
|
1974
|
+
"version": "v1",
|
|
1975
|
+
"doc": "A vehicle and window were assigned a stop sequence.",
|
|
1976
|
+
"source": "internal/delivery/domain/route/events.go:38",
|
|
1977
|
+
"fields": [
|
|
1978
|
+
{
|
|
1979
|
+
"name": "routeId",
|
|
1980
|
+
"type": "string",
|
|
1981
|
+
"doc": "Identifier of the route."
|
|
1982
|
+
},
|
|
1983
|
+
{
|
|
1984
|
+
"name": "vehicleId",
|
|
1985
|
+
"type": "string",
|
|
1986
|
+
"doc": "Vehicle assigned to the route."
|
|
1987
|
+
},
|
|
1988
|
+
{
|
|
1989
|
+
"name": "stopCount",
|
|
1990
|
+
"type": "int32",
|
|
1991
|
+
"doc": "Number of stops on the route."
|
|
1992
|
+
},
|
|
1993
|
+
{
|
|
1994
|
+
"name": "windowStart",
|
|
1995
|
+
"type": "time.Time",
|
|
1996
|
+
"doc": "Start of the delivery window."
|
|
1997
|
+
}
|
|
1998
|
+
]
|
|
1999
|
+
}
|
|
2000
|
+
],
|
|
2001
|
+
"consumers": [
|
|
2002
|
+
{
|
|
2003
|
+
"service": "shop.oms",
|
|
2004
|
+
"status": "declared"
|
|
2005
|
+
}
|
|
2006
|
+
]
|
|
2007
|
+
}
|
|
2008
|
+
]
|
|
2009
|
+
}
|
|
2010
|
+
],
|
|
2011
|
+
"stores": [
|
|
2012
|
+
"delivery.core.pg",
|
|
2013
|
+
"shop.oms.pg"
|
|
2014
|
+
]
|
|
2015
|
+
}
|
|
2016
|
+
]
|
|
2017
|
+
}
|
|
2018
|
+
],
|
|
2019
|
+
"flows": [],
|
|
2020
|
+
"adrs": [
|
|
2021
|
+
{
|
|
2022
|
+
"id": "shop.oms.0007",
|
|
2023
|
+
"slug": "shop-oms-0007-cart-reads-via-repository",
|
|
2024
|
+
"number": 7,
|
|
2025
|
+
"title": "Cart reads go through CartRepository, not Temporal Queries",
|
|
2026
|
+
"status": "accepted",
|
|
2027
|
+
"date": "2026-04-23",
|
|
2028
|
+
"scope": {
|
|
2029
|
+
"kind": "service",
|
|
2030
|
+
"service": "shop.oms"
|
|
2031
|
+
},
|
|
2032
|
+
"supersedes": [
|
|
2033
|
+
"shop.oms.0003"
|
|
2034
|
+
],
|
|
2035
|
+
"relates": {
|
|
2036
|
+
"services": [
|
|
2037
|
+
"shop.oms"
|
|
2038
|
+
],
|
|
2039
|
+
"flows": [
|
|
2040
|
+
"checkout"
|
|
2041
|
+
]
|
|
2042
|
+
},
|
|
2043
|
+
"source": "docs/adr/0007-cart-reads-via-repository.md",
|
|
2044
|
+
"body": "# Cart reads go through CartRepository, not Temporal Queries\n\n## Context and Problem Statement\n\nA cart is owned by a long-running Temporal workflow for the whole of a checkout\nattempt. Reads of cart state come from three places: the storefront polling the\ncart page, the OMS API answering `GetCart`, and support tooling.\n\nADR-0003 chose to serve those reads with `QueryWorkflow` against the\nrunning workflow. That decision was never fully implemented, and the gap only\nbecame visible when the read path started failing in staging:\n\n- the cart workflow **registers no query handler at all** — `SetQueryHandler`\n is absent from `cart_workflow.go`, so every `QueryWorkflow` call fails with\n `unknown queryType`;\n- `EVENT_GET` is **reserved but unwired** in `proto/shop/v1/cart.proto`. The\n enum value exists so the wire numbering stays stable, and nothing dispatches\n on it;\n- reads therefore fall back to whatever the caller has locally, which for the\n storefront meant a stale session copy.\n\nWe need one read path that is actually wired up, and we need it to survive a\ncompleted or archived workflow, which a query cannot.\n\n## Decision Drivers\n\n- A read must work after the workflow has closed. Carts are read for days\n afterwards by support and by the returns flow.\n- Read load must not be answered by the workflow worker fleet.\n- The read path must be testable without a Temporal dev server.\n- Whatever we choose has to be the *only* path, so there is no second answer to\n \"what is in this cart\".\n\n## Considered Options\n\n1. **`CartRepository` over the `cart_projection` table** — the workflow\n publishes events, a projector writes rows, reads hit Postgres.\n2. **`QueryWorkflow` against the running workflow** — the option ADR-0003 took.\n3. **Both, with the query as a fast path** — query first, fall back to the\n projection.\n\n## Decision Outcome\n\nChosen option: **`CartRepository` over the projection**.\n\nOption 2 cannot answer reads once the workflow closes, and pins read latency to\nworker availability. Option 3 gives two sources for one fact, which is how the\nstaging drift started; a cart read that sometimes comes from the workflow and\nsometimes from the projection is a cart read nobody can reason about.\n\nThe read path is now, in full:\n\n```mermaid\nsequenceDiagram\n autonumber\n participant SF as storefront\n participant API as shop.oms API\n participant Repo as CartRepository\n participant PG as postgres · cart_projection\n participant WF as cart workflow\n\n SF->>API: GetCart(cart_id)\n API->>Repo: Load(ctx, cart_id)\n Repo->>PG: SELECT * FROM cart_projection WHERE cart_id = $1\n PG-->>Repo: row (or none)\n Repo-->>API: Cart\n API-->>SF: CartResponse\n\n Note over WF,PG: writes only - the workflow never answers a read\n WF-)PG: projector applies cart events\n```\n\n### Consequences\n\n- Good: reads keep working after the workflow closes, and can be served from a\n read replica.\n- Good: `CartRepository` is an interface; unit tests use an in-memory\n implementation and never start a Temporal test environment.\n- Bad: the projection is eventually consistent. A read issued in the same\n millisecond as a write can miss it, so the checkout path reads its own writes\n from the command result rather than re-reading the cart.\n- Bad: one more thing to run — the projector is now on the critical path for\n correctness of reads, and needs its own lag alert.\n\n## More Information\n\n`EVENT_GET` stays reserved in `proto/shop/v1/cart.proto`. Removing it would\nrecycle the field number onto a different meaning for any client still holding\nan old descriptor. It is dead on purpose; do not wire it.\n"
|
|
2045
|
+
},
|
|
2046
|
+
{
|
|
2047
|
+
"id": "shop.oms.0003",
|
|
2048
|
+
"slug": "shop-oms-0003-cart-state-via-temporal-query",
|
|
2049
|
+
"number": 3,
|
|
2050
|
+
"title": "Read cart state via Temporal QueryWorkflow",
|
|
2051
|
+
"status": "superseded",
|
|
2052
|
+
"date": "2025-06-18",
|
|
2053
|
+
"scope": {
|
|
2054
|
+
"kind": "service",
|
|
2055
|
+
"service": "shop.oms"
|
|
2056
|
+
},
|
|
2057
|
+
"supersededBy": "shop.oms.0007",
|
|
2058
|
+
"relates": {
|
|
2059
|
+
"services": [
|
|
2060
|
+
"shop.oms"
|
|
2061
|
+
],
|
|
2062
|
+
"flows": [
|
|
2063
|
+
"checkout"
|
|
2064
|
+
]
|
|
2065
|
+
},
|
|
2066
|
+
"source": "docs/adr/0003-cart-state-via-temporal-query.md",
|
|
2067
|
+
"body": "# Read cart state via Temporal QueryWorkflow\n\n## Context and Problem Statement\n\nThe cart is being moved out of the session store and into a Temporal workflow,\nso that an abandoned checkout can be resumed and so that the cart has one\nowner. The workflow holds the authoritative cart in memory. The storefront\nstill needs to render the cart on every page load.\n\nWhere should that read come from?\n\n## Decision Drivers\n\n- The workflow already holds the exact state; anything else is a copy.\n- We do not want to build and operate a projection for a value that exists in\n memory a few milliseconds away.\n- Temporal queries are strongly consistent against the workflow's own history.\n\n## Considered Options\n\n1. **`QueryWorkflow` against the cart workflow.**\n2. A read-model projection in Postgres, fed by cart events.\n3. Keep the session copy and accept the drift.\n\n## Decision Outcome\n\nChosen option: **`QueryWorkflow`**. It gives a strongly consistent read with no\nnew storage, no projector to run and no lag to alert on. Option 2 is a lot of\nmachinery for a value we already have; option 3 is the status quo we are trying\nto leave.\n\n```mermaid\nsequenceDiagram\n autonumber\n participant SF as storefront\n participant API as shop.oms API\n participant TC as temporal frontend\n participant WF as cart workflow\n\n SF->>API: GetCart(cart_id)\n API->>TC: QueryWorkflow(cart_id, \"cart.get\")\n TC->>WF: dispatch query handler\n WF-->>TC: Cart (in-memory state)\n TC-->>API: Cart\n API-->>SF: CartResponse\n```\n\n### Consequences\n\n- Good: no projection, no projector, no lag.\n- Good: the read is as fresh as the workflow's own state.\n- Bad: a read fails once the workflow completes or is archived. Support tooling\n will need a different path for historical carts.\n- Bad: read load lands on the workflow worker fleet.\n\n## More Information\n\n`EVENT_GET` is added to `proto/shop/v1/cart.proto` for the query dispatch.\n"
|
|
2068
|
+
},
|
|
2069
|
+
{
|
|
2070
|
+
"id": "org.0001",
|
|
2071
|
+
"slug": "org-0001-client-protos-in-consumer-infrastructure",
|
|
2072
|
+
"number": 1,
|
|
2073
|
+
"title": "Client proto copies live in the consumer's infrastructure layer",
|
|
2074
|
+
"status": "accepted",
|
|
2075
|
+
"date": "2025-03-11",
|
|
2076
|
+
"scope": {
|
|
2077
|
+
"kind": "org"
|
|
2078
|
+
},
|
|
2079
|
+
"relates": {
|
|
2080
|
+
"services": [
|
|
2081
|
+
"shop.oms",
|
|
2082
|
+
"delivery.core"
|
|
2083
|
+
]
|
|
2084
|
+
},
|
|
2085
|
+
"source": "docs/adr/0001-client-protos-in-consumer-infrastructure.md",
|
|
2086
|
+
"body": "# Client proto copies live in the consumer's infrastructure layer\n\n## Context and Problem Statement\n\nWhen `shop.oms` calls `payments.v1.Payments/Authorize`, it needs the payments\n`.proto`. Where does that file come from at build time?\n\nA shared schema repository consumed by every service is the obvious answer, and\nit is the one that couples every release to every other release: a change to a\nmessage nobody in `shop` uses still forces `shop` to bump, regenerate and\nretest.\n\n## Decision Drivers\n\n- A team must be able to release without waiting on another team's schema bump.\n- The boundary between contexts should be explicit in the code, not implicit in\n a shared package.\n- Drift between a producer's schema and a consumer's copy must be **detectable**,\n not merely hoped against.\n\n## Considered Options\n\n1. **A vendored copy of the producer's `.proto` under the consumer's\n `internal/infrastructure/<peer>/`**, an anti-corruption layer in the DDD\n sense: the copy is the consumer's translation boundary.\n2. **A shared schema repo** published as one versioned artifact for everyone.\n3. **Runtime reflection** — resolve descriptors from the server at startup.\n\n## Decision Outcome\n\nChosen option: **vendored copies in the consumer's infrastructure layer**.\n\nThis is the ACL trade-off taken deliberately:\n\n| | compile-time independence | drift risk |\n|---|---|---|\n| vendored copy | full — a consumer builds and ships alone | real — the copy can fall behind |\n| shared repo | none — every bump is everyone's bump | none |\n| reflection | full | moved to runtime, where it fails in production instead of in CI |\n\nWe take the drift risk because we can *measure* it, and we cannot measure the\ncost of coordinated releases until it has already been paid.\n\nThe mitigation is mechanical: **the catalog merge compares descriptors.** Each\nproducer publishes the descriptor set for the services it provides; each\nconsumer's vendored copy is parsed at catalog build time; a field, method or\nenum value that differs between the two is reported against the consuming\nservice and shows on its page as an unresolved call rather than silently\ncompiling.\n\n### Consequences\n\n- Good: `shop` and `delivery` release on their own cadence.\n- Good: the copy is a real boundary — a consumer may narrow a message to the\n fields it actually uses, and that narrowing is visible in review.\n- Bad: the same `.proto` exists in several repositories. Someone reading two of\n them may reasonably wonder which is authoritative. The producer's is; the\n header comment on every vendored copy says so.\n- Bad: drift is caught at catalog build, which is after merge. It is not caught\n at compile time, and it never will be.\n"
|
|
2087
|
+
},
|
|
2088
|
+
{
|
|
2089
|
+
"id": "org.0002",
|
|
2090
|
+
"slug": "org-0002-event-version-in-package-path",
|
|
2091
|
+
"number": 2,
|
|
2092
|
+
"title": "Domain event schema version is encoded in the package path (events/v1)",
|
|
2093
|
+
"status": "accepted",
|
|
2094
|
+
"date": "2025-05-02",
|
|
2095
|
+
"scope": {
|
|
2096
|
+
"kind": "org"
|
|
2097
|
+
},
|
|
2098
|
+
"relates": {
|
|
2099
|
+
"events": [
|
|
2100
|
+
"shop.cart.basket.BasketCheckedOut"
|
|
2101
|
+
]
|
|
2102
|
+
},
|
|
2103
|
+
"source": "docs/adr/0002-event-version-in-package-path.md",
|
|
2104
|
+
"body": "# Domain event schema version is encoded in the package path (events/v1)\n\n## Context and Problem Statement\n\nA domain event outlives the code that published it. Consumers deserialize\nmessages written months earlier, and a consumer that is down for a day comes\nback to a backlog of the *old* shape. We need a versioning scheme that a\nconsumer can act on before it parses a single byte.\n\n## Decision Drivers\n\n- A consumer must be able to decide \"can I read this?\" from routing information\n alone, without a registry lookup.\n- Two versions of one event must be publishable at the same time during a\n migration.\n- The version must be visible in the source tree, in the topic name and in the\n type URL — not only in a field.\n\n## Considered Options\n\n1. **Version in the package path**: `shop.events.v1.BasketCheckedOut`, file at\n `proto/shop/events/v1/basket.proto`.\n2. **A `version` field inside the message.**\n3. **A schema registry** with subject-level compatibility rules.\n\n## Decision Outcome\n\nChosen option: **version in the package path**.\n\nThe version becomes part of the fully-qualified name, so it is part of the type\nURL, part of the generated Go package, and part of the topic name\n(`shop.events.v1.basket-checked-out`). A consumer subscribes to the versions it\nunderstands and is structurally incapable of receiving one it does not.\n\nOption 2 puts the version *inside* the payload, which means parsing an unknown\nshape to discover that it is unknown. Option 3 is the right answer at a\ndifferent scale; it adds an operational dependency on the publish path that we\nare not willing to take yet.\n\n### Consequences\n\n- Good: an unreadable version is never delivered — it is a different topic.\n- Good: v1 and v2 publish side by side during a migration; the producer emits\n both until the v1 subscription drains.\n- Bad: a breaking change means a new package, a new topic and a period of dual\n publishing. This is deliberate friction, and it is the point.\n- Bad: additive changes still land in v1. \"Is this additive?\" stays a judgement\n call at review time, and the catalog's field diff per version is what makes\n that call reviewable.\n"
|
|
2105
|
+
},
|
|
2106
|
+
{
|
|
2107
|
+
"id": "payments.0004",
|
|
2108
|
+
"slug": "payments-0004-idempotent-journal-entries",
|
|
2109
|
+
"number": 4,
|
|
2110
|
+
"title": "Journal entries are idempotent by (order_id, attempt)",
|
|
2111
|
+
"status": "proposed",
|
|
2112
|
+
"date": "2026-02-09",
|
|
2113
|
+
"scope": {
|
|
2114
|
+
"kind": "context",
|
|
2115
|
+
"context": "payments"
|
|
2116
|
+
},
|
|
2117
|
+
"relates": {
|
|
2118
|
+
"flows": [
|
|
2119
|
+
"checkout"
|
|
2120
|
+
],
|
|
2121
|
+
"events": [
|
|
2122
|
+
"payments.ledger.payment.PaymentCaptured"
|
|
2123
|
+
]
|
|
2124
|
+
},
|
|
2125
|
+
"source": "docs/adr/0004-idempotent-journal-entries.md",
|
|
2126
|
+
"body": "# Journal entries are idempotent by (order_id, attempt)\n\n## Context and Problem Statement\n\nThe ledger writes a journal entry when a capture succeeds. The capture path is\nat-least-once end to end: the PSP retries its webhook, our own consumer retries\non redelivery, and an operator can replay a partition. Today a replayed capture\nwrites a second journal entry, and the ledger has to be repaired by hand.\n\nThe natural key is not `order_id` alone: a declined authorization is followed by\na second, legitimate attempt on the same order, and that attempt must produce\nits own entry.\n\n## Decision Drivers\n\n- Replaying the capture consumer must be safe, always, with no operator\n ceremony.\n- A genuine retry by the customer is a distinct financial fact and must not be\n collapsed into the first one.\n- The guarantee should be enforced by the database, not by application code that\n can be bypassed by the next caller.\n\n## Considered Options\n\n1. **A unique constraint on `(order_id, attempt)`**, with `attempt` carried on\n the capture command and echoed on the event.\n2. **A dedup table keyed by the message id** of the inbound event.\n3. **Application-level \"does an entry exist?\" check** before insert.\n\n## Decision Outcome\n\nProposed: **unique constraint on `(order_id, attempt)`**, with the insert\nwritten as `INSERT ... ON CONFLICT (order_id, attempt) DO NOTHING`.\n\nOption 2 dedups the *transport*, not the *fact*: the same capture arriving via a\nwebhook and via the event bus has two message ids and would write twice. Option\n3 is a read-then-write race that fails exactly under the concurrency it is meant\nto protect against.\n\n### Consequences\n\n- Good: replay is a no-op at the storage layer, whatever the caller does.\n- Good: the key is a domain fact, so it holds across transports.\n- Bad: `attempt` has to be threaded through the capture command and onto\n `PaymentCaptured`; a producer that omits it cannot be made idempotent.\n- Bad: existing rows have no `attempt`. The backfill assigns `attempt = 1` and\n is only correct if no order in history was captured twice — which has to be\n checked before this is accepted, not after.\n\n## Open Questions\n\n- Does the PSP guarantee a stable attempt identifier across its own retries, or\n do we mint it ourselves at authorization time?\n- What is the correct behaviour when a capture arrives for an attempt that was\n never authorized? Currently it would insert; it should probably reject.\n"
|
|
2127
|
+
}
|
|
2128
|
+
],
|
|
2129
|
+
"stores": [
|
|
2130
|
+
{
|
|
2131
|
+
"id": "shop.oms.pg",
|
|
2132
|
+
"slug": "pg",
|
|
2133
|
+
"name": "Order management database",
|
|
2134
|
+
"kind": "postgres",
|
|
2135
|
+
"owner": "shop.oms",
|
|
2136
|
+
"source": "shop/oms/db/migrations",
|
|
2137
|
+
"tables": [
|
|
2138
|
+
{
|
|
2139
|
+
"id": "shop.oms.pg.orders",
|
|
2140
|
+
"name": "orders",
|
|
2141
|
+
"doc": "One row per order. The row lock taken here is the aggregate boundary.",
|
|
2142
|
+
"role": "aggregate-root",
|
|
2143
|
+
"persists": {
|
|
2144
|
+
"aggregate": "shop.oms.order"
|
|
2145
|
+
},
|
|
2146
|
+
"columns": [
|
|
2147
|
+
{
|
|
2148
|
+
"name": "id",
|
|
2149
|
+
"type": "uuid",
|
|
2150
|
+
"nullable": false,
|
|
2151
|
+
"pk": true,
|
|
2152
|
+
"maps": "Order.id",
|
|
2153
|
+
"doc": "Order identity, generated on placement."
|
|
2154
|
+
},
|
|
2155
|
+
{
|
|
2156
|
+
"name": "status",
|
|
2157
|
+
"type": "text",
|
|
2158
|
+
"nullable": false,
|
|
2159
|
+
"maps": "Order.status"
|
|
2160
|
+
},
|
|
2161
|
+
{
|
|
2162
|
+
"name": "customer_id",
|
|
2163
|
+
"type": "uuid",
|
|
2164
|
+
"nullable": false,
|
|
2165
|
+
"maps": "Order.customer"
|
|
2166
|
+
},
|
|
2167
|
+
{
|
|
2168
|
+
"name": "total_minor",
|
|
2169
|
+
"type": "bigint",
|
|
2170
|
+
"nullable": false,
|
|
2171
|
+
"maps": "Order.total"
|
|
2172
|
+
},
|
|
2173
|
+
{
|
|
2174
|
+
"name": "currency",
|
|
2175
|
+
"type": "char(3)",
|
|
2176
|
+
"nullable": false,
|
|
2177
|
+
"maps": "Order.currency"
|
|
2178
|
+
},
|
|
2179
|
+
{
|
|
2180
|
+
"name": "ship_to",
|
|
2181
|
+
"type": "jsonb",
|
|
2182
|
+
"nullable": false,
|
|
2183
|
+
"maps": "Order.shipTo"
|
|
2184
|
+
},
|
|
2185
|
+
{
|
|
2186
|
+
"name": "risk",
|
|
2187
|
+
"type": "jsonb",
|
|
2188
|
+
"nullable": true,
|
|
2189
|
+
"maps": "Order.risk"
|
|
2190
|
+
},
|
|
2191
|
+
{
|
|
2192
|
+
"name": "placed_at",
|
|
2193
|
+
"type": "timestamptz",
|
|
2194
|
+
"nullable": false
|
|
2195
|
+
},
|
|
2196
|
+
{
|
|
2197
|
+
"name": "updated_at",
|
|
2198
|
+
"type": "timestamptz",
|
|
2199
|
+
"nullable": false
|
|
2200
|
+
}
|
|
2201
|
+
],
|
|
2202
|
+
"indexes": [
|
|
2203
|
+
{
|
|
2204
|
+
"name": "orders_status_placed_at_idx",
|
|
2205
|
+
"columns": [
|
|
2206
|
+
"status",
|
|
2207
|
+
"placed_at"
|
|
2208
|
+
],
|
|
2209
|
+
"unique": false
|
|
2210
|
+
},
|
|
2211
|
+
{
|
|
2212
|
+
"name": "orders_customer_idx",
|
|
2213
|
+
"columns": [
|
|
2214
|
+
"customer_id"
|
|
2215
|
+
],
|
|
2216
|
+
"unique": false
|
|
2217
|
+
}
|
|
2218
|
+
]
|
|
2219
|
+
},
|
|
2220
|
+
{
|
|
2221
|
+
"id": "shop.oms.pg.order_items",
|
|
2222
|
+
"name": "order_items",
|
|
2223
|
+
"doc": "Lines of an order, keyed inside it. Nothing outside the aggregate points here.",
|
|
2224
|
+
"role": "child",
|
|
2225
|
+
"persists": {
|
|
2226
|
+
"aggregate": "shop.oms.order",
|
|
2227
|
+
"block": "shop.oms.order.order-line"
|
|
2228
|
+
},
|
|
2229
|
+
"columns": [
|
|
2230
|
+
{
|
|
2231
|
+
"name": "order_id",
|
|
2232
|
+
"type": "uuid",
|
|
2233
|
+
"nullable": false,
|
|
2234
|
+
"pk": true,
|
|
2235
|
+
"fk": {
|
|
2236
|
+
"table": "shop.oms.pg.orders",
|
|
2237
|
+
"column": "id",
|
|
2238
|
+
"onDelete": "cascade"
|
|
2239
|
+
}
|
|
2240
|
+
},
|
|
2241
|
+
{
|
|
2242
|
+
"name": "line_no",
|
|
2243
|
+
"type": "integer",
|
|
2244
|
+
"nullable": false,
|
|
2245
|
+
"pk": true,
|
|
2246
|
+
"maps": "OrderLine.lineNo"
|
|
2247
|
+
},
|
|
2248
|
+
{
|
|
2249
|
+
"name": "sku",
|
|
2250
|
+
"type": "text",
|
|
2251
|
+
"nullable": false,
|
|
2252
|
+
"maps": "OrderLine.sku"
|
|
2253
|
+
},
|
|
2254
|
+
{
|
|
2255
|
+
"name": "quantity",
|
|
2256
|
+
"type": "bigint",
|
|
2257
|
+
"nullable": false,
|
|
2258
|
+
"maps": "OrderLine.quantity"
|
|
2259
|
+
},
|
|
2260
|
+
{
|
|
2261
|
+
"name": "unit_price_minor",
|
|
2262
|
+
"type": "bigint",
|
|
2263
|
+
"nullable": false,
|
|
2264
|
+
"maps": "OrderLine.unitPrice"
|
|
2265
|
+
}
|
|
2266
|
+
]
|
|
2267
|
+
},
|
|
2268
|
+
{
|
|
2269
|
+
"id": "shop.oms.pg.outbox",
|
|
2270
|
+
"name": "outbox",
|
|
2271
|
+
"doc": "Events committed with the state change that produced them.",
|
|
2272
|
+
"role": "outbox",
|
|
2273
|
+
"columns": [
|
|
2274
|
+
{
|
|
2275
|
+
"name": "id",
|
|
2276
|
+
"type": "bigserial",
|
|
2277
|
+
"nullable": false,
|
|
2278
|
+
"pk": true
|
|
2279
|
+
},
|
|
2280
|
+
{
|
|
2281
|
+
"name": "aggregate_id",
|
|
2282
|
+
"type": "uuid",
|
|
2283
|
+
"nullable": false,
|
|
2284
|
+
"from": [
|
|
2285
|
+
"shop.oms.pg.orders.id"
|
|
2286
|
+
],
|
|
2287
|
+
"doc": "The order the event is about. Copied from orders.id in the same transaction."
|
|
2288
|
+
},
|
|
2289
|
+
{
|
|
2290
|
+
"name": "event_type",
|
|
2291
|
+
"type": "text",
|
|
2292
|
+
"nullable": false,
|
|
2293
|
+
"doc": "The event name, as published."
|
|
2294
|
+
},
|
|
2295
|
+
{
|
|
2296
|
+
"name": "payload",
|
|
2297
|
+
"type": "jsonb",
|
|
2298
|
+
"nullable": false,
|
|
2299
|
+
"doc": "The event body, exactly as published."
|
|
2300
|
+
},
|
|
2301
|
+
{
|
|
2302
|
+
"name": "created_at",
|
|
2303
|
+
"type": "timestamptz",
|
|
2304
|
+
"nullable": false
|
|
2305
|
+
},
|
|
2306
|
+
{
|
|
2307
|
+
"name": "published_at",
|
|
2308
|
+
"type": "timestamptz",
|
|
2309
|
+
"nullable": true
|
|
2310
|
+
}
|
|
2311
|
+
],
|
|
2312
|
+
"indexes": [
|
|
2313
|
+
{
|
|
2314
|
+
"name": "outbox_unpublished_idx",
|
|
2315
|
+
"columns": [
|
|
2316
|
+
"published_at"
|
|
2317
|
+
],
|
|
2318
|
+
"unique": false
|
|
2319
|
+
}
|
|
2320
|
+
]
|
|
2321
|
+
},
|
|
2322
|
+
{
|
|
2323
|
+
"id": "shop.oms.pg.price_snapshots",
|
|
2324
|
+
"name": "price_snapshots",
|
|
2325
|
+
"doc": "Quotes as pricing left them. Written by shop.pricing, not by this service.",
|
|
2326
|
+
"persists": {
|
|
2327
|
+
"aggregate": "shop.pricing.quote"
|
|
2328
|
+
},
|
|
2329
|
+
"columns": [
|
|
2330
|
+
{
|
|
2331
|
+
"name": "quote_id",
|
|
2332
|
+
"type": "text",
|
|
2333
|
+
"nullable": false,
|
|
2334
|
+
"pk": true,
|
|
2335
|
+
"maps": "Quote.id"
|
|
2336
|
+
},
|
|
2337
|
+
{
|
|
2338
|
+
"name": "basket_id",
|
|
2339
|
+
"type": "text",
|
|
2340
|
+
"nullable": false,
|
|
2341
|
+
"maps": "Quote.basketId"
|
|
2342
|
+
},
|
|
2343
|
+
{
|
|
2344
|
+
"name": "total_minor",
|
|
2345
|
+
"type": "bigint",
|
|
2346
|
+
"nullable": false,
|
|
2347
|
+
"maps": "Quote.total"
|
|
2348
|
+
},
|
|
2349
|
+
{
|
|
2350
|
+
"name": "captured_at",
|
|
2351
|
+
"type": "timestamptz",
|
|
2352
|
+
"nullable": false
|
|
2353
|
+
}
|
|
2354
|
+
]
|
|
2355
|
+
}
|
|
2356
|
+
],
|
|
2357
|
+
"views": [
|
|
2358
|
+
{
|
|
2359
|
+
"id": "shop.oms.pg.v_open_orders",
|
|
2360
|
+
"name": "v_open_orders",
|
|
2361
|
+
"doc": "Orders not yet delivered, with their line count. What the ops console reads.",
|
|
2362
|
+
"persists": {
|
|
2363
|
+
"aggregate": "shop.oms.order"
|
|
2364
|
+
},
|
|
2365
|
+
"reads": [
|
|
2366
|
+
"shop.oms.pg.orders",
|
|
2367
|
+
"shop.oms.pg.order_items"
|
|
2368
|
+
],
|
|
2369
|
+
"source": "shop/oms/db/migrations/0007_open_orders.sql",
|
|
2370
|
+
"definition": "CREATE VIEW v_open_orders AS\nSELECT o.id AS order_id, o.status, o.customer_id, o.total_minor,\n o.placed_at, count(i.line_no) AS line_count\n FROM orders o\n LEFT JOIN order_items i ON i.order_id = o.id\n WHERE o.status <> 'delivered'\n GROUP BY o.id;",
|
|
2371
|
+
"columns": [
|
|
2372
|
+
{
|
|
2373
|
+
"name": "order_id",
|
|
2374
|
+
"type": "uuid",
|
|
2375
|
+
"nullable": false,
|
|
2376
|
+
"from": [
|
|
2377
|
+
"shop.oms.pg.orders.id"
|
|
2378
|
+
],
|
|
2379
|
+
"maps": "Order.id"
|
|
2380
|
+
},
|
|
2381
|
+
{
|
|
2382
|
+
"name": "status",
|
|
2383
|
+
"type": "text",
|
|
2384
|
+
"nullable": false,
|
|
2385
|
+
"from": [
|
|
2386
|
+
"shop.oms.pg.orders.status"
|
|
2387
|
+
],
|
|
2388
|
+
"maps": "Order.status"
|
|
2389
|
+
},
|
|
2390
|
+
{
|
|
2391
|
+
"name": "customer_id",
|
|
2392
|
+
"type": "uuid",
|
|
2393
|
+
"nullable": false,
|
|
2394
|
+
"from": [
|
|
2395
|
+
"shop.oms.pg.orders.customer_id"
|
|
2396
|
+
],
|
|
2397
|
+
"maps": "Order.customer"
|
|
2398
|
+
},
|
|
2399
|
+
{
|
|
2400
|
+
"name": "total_minor",
|
|
2401
|
+
"type": "bigint",
|
|
2402
|
+
"nullable": false,
|
|
2403
|
+
"from": [
|
|
2404
|
+
"shop.oms.pg.orders.total_minor"
|
|
2405
|
+
],
|
|
2406
|
+
"maps": "Order.total"
|
|
2407
|
+
},
|
|
2408
|
+
{
|
|
2409
|
+
"name": "placed_at",
|
|
2410
|
+
"type": "timestamptz",
|
|
2411
|
+
"nullable": false,
|
|
2412
|
+
"from": [
|
|
2413
|
+
"shop.oms.pg.orders.placed_at"
|
|
2414
|
+
]
|
|
2415
|
+
},
|
|
2416
|
+
{
|
|
2417
|
+
"name": "line_count",
|
|
2418
|
+
"type": "bigint",
|
|
2419
|
+
"nullable": false,
|
|
2420
|
+
"from": [
|
|
2421
|
+
"shop.oms.pg.order_items.line_no"
|
|
2422
|
+
],
|
|
2423
|
+
"doc": "count(*) over the order's lines, so it is a number where the source is a key."
|
|
2424
|
+
}
|
|
2425
|
+
]
|
|
2426
|
+
},
|
|
2427
|
+
{
|
|
2428
|
+
"id": "shop.oms.pg.mv_orders_daily",
|
|
2429
|
+
"name": "mv_orders_daily",
|
|
2430
|
+
"materialized": true,
|
|
2431
|
+
"doc": "Yesterday's order volume, refreshed nightly. Stale by design between refreshes.",
|
|
2432
|
+
"reads": [
|
|
2433
|
+
"shop.oms.pg.v_open_orders"
|
|
2434
|
+
],
|
|
2435
|
+
"source": "shop/oms/db/migrations/0009_orders_daily.sql",
|
|
2436
|
+
"definition": "CREATE MATERIALIZED VIEW mv_orders_daily AS\nSELECT date_trunc('day', placed_at) AS day,\n count(*) AS orders, sum(total_minor) AS gross_minor\n FROM v_open_orders\n GROUP BY 1;",
|
|
2437
|
+
"columns": [
|
|
2438
|
+
{
|
|
2439
|
+
"name": "day",
|
|
2440
|
+
"type": "timestamptz",
|
|
2441
|
+
"nullable": false,
|
|
2442
|
+
"from": [
|
|
2443
|
+
"shop.oms.pg.v_open_orders.placed_at"
|
|
2444
|
+
]
|
|
2445
|
+
},
|
|
2446
|
+
{
|
|
2447
|
+
"name": "orders",
|
|
2448
|
+
"type": "bigint",
|
|
2449
|
+
"nullable": false,
|
|
2450
|
+
"from": [
|
|
2451
|
+
"shop.oms.pg.v_open_orders.order_id"
|
|
2452
|
+
]
|
|
2453
|
+
},
|
|
2454
|
+
{
|
|
2455
|
+
"name": "gross_minor",
|
|
2456
|
+
"type": "bigint",
|
|
2457
|
+
"nullable": true,
|
|
2458
|
+
"from": [
|
|
2459
|
+
"shop.oms.pg.v_open_orders.total_minor"
|
|
2460
|
+
],
|
|
2461
|
+
"doc": "Two hops from orders.total_minor, which is the column a discrepancy is chased back to."
|
|
2462
|
+
}
|
|
2463
|
+
]
|
|
2464
|
+
}
|
|
2465
|
+
]
|
|
2466
|
+
},
|
|
2467
|
+
{
|
|
2468
|
+
"id": "shop.pricing.cache",
|
|
2469
|
+
"slug": "cache",
|
|
2470
|
+
"name": "Price list cache",
|
|
2471
|
+
"kind": "redis",
|
|
2472
|
+
"owner": "shop.pricing",
|
|
2473
|
+
"source": "shop/pricing/internal/cache",
|
|
2474
|
+
"tables": []
|
|
2475
|
+
},
|
|
2476
|
+
{
|
|
2477
|
+
"id": "payments.ledger.pg",
|
|
2478
|
+
"slug": "pg",
|
|
2479
|
+
"name": "Ledger database",
|
|
2480
|
+
"kind": "postgres",
|
|
2481
|
+
"owner": "payments.ledger",
|
|
2482
|
+
"source": "payments/ledger/db/migrations",
|
|
2483
|
+
"tables": [
|
|
2484
|
+
{
|
|
2485
|
+
"id": "payments.ledger.pg.journal_entries",
|
|
2486
|
+
"name": "journal_entries",
|
|
2487
|
+
"doc": "Append-only. One row per capture attempt, unique on (order_id, attempt) per ADR payments.0004.",
|
|
2488
|
+
"role": "aggregate-root",
|
|
2489
|
+
"persists": {
|
|
2490
|
+
"aggregate": "payments.ledger.payment"
|
|
2491
|
+
},
|
|
2492
|
+
"columns": [
|
|
2493
|
+
{
|
|
2494
|
+
"name": "id",
|
|
2495
|
+
"type": "text",
|
|
2496
|
+
"nullable": false,
|
|
2497
|
+
"pk": true,
|
|
2498
|
+
"maps": "Payment.id"
|
|
2499
|
+
},
|
|
2500
|
+
{
|
|
2501
|
+
"name": "order_id",
|
|
2502
|
+
"type": "text",
|
|
2503
|
+
"nullable": false,
|
|
2504
|
+
"maps": "Payment.orderId"
|
|
2505
|
+
},
|
|
2506
|
+
{
|
|
2507
|
+
"name": "attempt",
|
|
2508
|
+
"type": "integer",
|
|
2509
|
+
"nullable": false,
|
|
2510
|
+
"maps": "CaptureAttempt.attempt"
|
|
2511
|
+
},
|
|
2512
|
+
{
|
|
2513
|
+
"name": "amount_minor",
|
|
2514
|
+
"type": "bigint",
|
|
2515
|
+
"nullable": false,
|
|
2516
|
+
"maps": "Payment.amount"
|
|
2517
|
+
},
|
|
2518
|
+
{
|
|
2519
|
+
"name": "currency",
|
|
2520
|
+
"type": "char(3)",
|
|
2521
|
+
"nullable": false
|
|
2522
|
+
},
|
|
2523
|
+
{
|
|
2524
|
+
"name": "state",
|
|
2525
|
+
"type": "text",
|
|
2526
|
+
"nullable": false,
|
|
2527
|
+
"maps": "Payment.state"
|
|
2528
|
+
},
|
|
2529
|
+
{
|
|
2530
|
+
"name": "gateway_charge_id",
|
|
2531
|
+
"type": "text",
|
|
2532
|
+
"nullable": true,
|
|
2533
|
+
"maps": "Payment.gateway"
|
|
2534
|
+
},
|
|
2535
|
+
{
|
|
2536
|
+
"name": "requested_at",
|
|
2537
|
+
"type": "timestamptz",
|
|
2538
|
+
"nullable": false,
|
|
2539
|
+
"maps": "CaptureAttempt.requestedAt"
|
|
2540
|
+
}
|
|
2541
|
+
],
|
|
2542
|
+
"indexes": [
|
|
2543
|
+
{
|
|
2544
|
+
"name": "journal_entries_order_attempt_key",
|
|
2545
|
+
"columns": [
|
|
2546
|
+
"order_id",
|
|
2547
|
+
"attempt"
|
|
2548
|
+
],
|
|
2549
|
+
"unique": true
|
|
2550
|
+
},
|
|
2551
|
+
{
|
|
2552
|
+
"name": "journal_entries_state_idx",
|
|
2553
|
+
"columns": [
|
|
2554
|
+
"state"
|
|
2555
|
+
],
|
|
2556
|
+
"unique": false
|
|
2557
|
+
}
|
|
2558
|
+
]
|
|
2559
|
+
},
|
|
2560
|
+
{
|
|
2561
|
+
"id": "payments.ledger.pg.refunds",
|
|
2562
|
+
"name": "refunds",
|
|
2563
|
+
"doc": "Refunds against a captured payment.",
|
|
2564
|
+
"role": "child",
|
|
2565
|
+
"persists": {
|
|
2566
|
+
"aggregate": "payments.ledger.refund"
|
|
2567
|
+
},
|
|
2568
|
+
"columns": [
|
|
2569
|
+
{
|
|
2570
|
+
"name": "id",
|
|
2571
|
+
"type": "text",
|
|
2572
|
+
"nullable": false,
|
|
2573
|
+
"pk": true,
|
|
2574
|
+
"maps": "Refund.id"
|
|
2575
|
+
},
|
|
2576
|
+
{
|
|
2577
|
+
"name": "payment_id",
|
|
2578
|
+
"type": "text",
|
|
2579
|
+
"nullable": false,
|
|
2580
|
+
"maps": "Refund.paymentId",
|
|
2581
|
+
"fk": {
|
|
2582
|
+
"table": "payments.ledger.pg.journal_entries",
|
|
2583
|
+
"column": "id",
|
|
2584
|
+
"onDelete": "restrict"
|
|
2585
|
+
}
|
|
2586
|
+
},
|
|
2587
|
+
{
|
|
2588
|
+
"name": "amount_minor",
|
|
2589
|
+
"type": "bigint",
|
|
2590
|
+
"nullable": false,
|
|
2591
|
+
"maps": "Refund.amount"
|
|
2592
|
+
},
|
|
2593
|
+
{
|
|
2594
|
+
"name": "reason",
|
|
2595
|
+
"type": "text",
|
|
2596
|
+
"nullable": false,
|
|
2597
|
+
"maps": "Refund.reason"
|
|
2598
|
+
},
|
|
2599
|
+
{
|
|
2600
|
+
"name": "state",
|
|
2601
|
+
"type": "text",
|
|
2602
|
+
"nullable": false,
|
|
2603
|
+
"maps": "Refund.state"
|
|
2604
|
+
},
|
|
2605
|
+
{
|
|
2606
|
+
"name": "settled_at",
|
|
2607
|
+
"type": "timestamptz",
|
|
2608
|
+
"nullable": true,
|
|
2609
|
+
"maps": "Settlement.settledAt"
|
|
2610
|
+
}
|
|
2611
|
+
]
|
|
2612
|
+
}
|
|
2613
|
+
],
|
|
2614
|
+
"views": [
|
|
2615
|
+
{
|
|
2616
|
+
"id": "payments.ledger.pg.v_payment_state",
|
|
2617
|
+
"name": "v_payment_state",
|
|
2618
|
+
"doc": "Every payment with what has been refunded against it. One row per journal entry.",
|
|
2619
|
+
"persists": {
|
|
2620
|
+
"aggregate": "payments.ledger.payment"
|
|
2621
|
+
},
|
|
2622
|
+
"source": "payments/ledger/db/migrations/0011_payment_state.sql",
|
|
2623
|
+
"definition": "CREATE VIEW v_payment_state AS\nSELECT j.id AS payment_id, j.order_id, j.state,\n j.amount_minor, coalesce(sum(r.amount_minor), 0) AS refunded_minor\n FROM journal_entries j\n LEFT JOIN refunds r ON r.payment_id = j.id\n GROUP BY j.id;",
|
|
2624
|
+
"columns": [
|
|
2625
|
+
{
|
|
2626
|
+
"name": "payment_id",
|
|
2627
|
+
"type": "text",
|
|
2628
|
+
"nullable": false,
|
|
2629
|
+
"from": [
|
|
2630
|
+
"payments.ledger.pg.journal_entries.id"
|
|
2631
|
+
],
|
|
2632
|
+
"maps": "Payment.id"
|
|
2633
|
+
},
|
|
2634
|
+
{
|
|
2635
|
+
"name": "order_id",
|
|
2636
|
+
"type": "text",
|
|
2637
|
+
"nullable": false,
|
|
2638
|
+
"from": [
|
|
2639
|
+
"payments.ledger.pg.journal_entries.order_id"
|
|
2640
|
+
],
|
|
2641
|
+
"maps": "Payment.orderId"
|
|
2642
|
+
},
|
|
2643
|
+
{
|
|
2644
|
+
"name": "state",
|
|
2645
|
+
"type": "text",
|
|
2646
|
+
"nullable": false,
|
|
2647
|
+
"from": [
|
|
2648
|
+
"payments.ledger.pg.journal_entries.state"
|
|
2649
|
+
],
|
|
2650
|
+
"maps": "Payment.state"
|
|
2651
|
+
},
|
|
2652
|
+
{
|
|
2653
|
+
"name": "amount_minor",
|
|
2654
|
+
"type": "bigint",
|
|
2655
|
+
"nullable": false,
|
|
2656
|
+
"from": [
|
|
2657
|
+
"payments.ledger.pg.journal_entries.amount_minor"
|
|
2658
|
+
],
|
|
2659
|
+
"maps": "Payment.amount"
|
|
2660
|
+
},
|
|
2661
|
+
{
|
|
2662
|
+
"name": "refunded_minor",
|
|
2663
|
+
"type": "bigint",
|
|
2664
|
+
"nullable": false,
|
|
2665
|
+
"from": [
|
|
2666
|
+
"payments.ledger.pg.refunds.amount_minor"
|
|
2667
|
+
]
|
|
2668
|
+
}
|
|
2669
|
+
]
|
|
2670
|
+
}
|
|
2671
|
+
]
|
|
2672
|
+
},
|
|
2673
|
+
{
|
|
2674
|
+
"id": "delivery.core.pg",
|
|
2675
|
+
"slug": "pg",
|
|
2676
|
+
"name": "Delivery database",
|
|
2677
|
+
"kind": "postgres",
|
|
2678
|
+
"owner": "delivery.core",
|
|
2679
|
+
"source": "delivery/core/db/migrations",
|
|
2680
|
+
"tables": [
|
|
2681
|
+
{
|
|
2682
|
+
"id": "delivery.core.pg.packages",
|
|
2683
|
+
"name": "packages",
|
|
2684
|
+
"doc": "One row per shipment. order_id is a real foreign key into the OMS database — see Problems.",
|
|
2685
|
+
"role": "aggregate-root",
|
|
2686
|
+
"persists": {
|
|
2687
|
+
"aggregate": "delivery.core.shipment"
|
|
2688
|
+
},
|
|
2689
|
+
"columns": [
|
|
2690
|
+
{
|
|
2691
|
+
"name": "id",
|
|
2692
|
+
"type": "text",
|
|
2693
|
+
"nullable": false,
|
|
2694
|
+
"pk": true,
|
|
2695
|
+
"maps": "Shipment.id"
|
|
2696
|
+
},
|
|
2697
|
+
{
|
|
2698
|
+
"name": "order_id",
|
|
2699
|
+
"type": "uuid",
|
|
2700
|
+
"nullable": false,
|
|
2701
|
+
"maps": "Shipment.orderId",
|
|
2702
|
+
"fk": {
|
|
2703
|
+
"table": "shop.oms.pg.orders",
|
|
2704
|
+
"column": "id",
|
|
2705
|
+
"onDelete": "restrict"
|
|
2706
|
+
}
|
|
2707
|
+
},
|
|
2708
|
+
{
|
|
2709
|
+
"name": "state",
|
|
2710
|
+
"type": "text",
|
|
2711
|
+
"nullable": false,
|
|
2712
|
+
"maps": "Shipment.state"
|
|
2713
|
+
},
|
|
2714
|
+
{
|
|
2715
|
+
"name": "ship_to",
|
|
2716
|
+
"type": "jsonb",
|
|
2717
|
+
"nullable": false,
|
|
2718
|
+
"maps": "Shipment.shipTo",
|
|
2719
|
+
"from": [
|
|
2720
|
+
"shop.oms.pg.orders.ship_to"
|
|
2721
|
+
],
|
|
2722
|
+
"doc": "Copied off the order when the shipment is created; never read back from OMS."
|
|
2723
|
+
},
|
|
2724
|
+
{
|
|
2725
|
+
"name": "dispatched_at",
|
|
2726
|
+
"type": "timestamptz",
|
|
2727
|
+
"nullable": true
|
|
2728
|
+
}
|
|
2729
|
+
]
|
|
2730
|
+
},
|
|
2731
|
+
{
|
|
2732
|
+
"id": "delivery.core.pg.parcels",
|
|
2733
|
+
"name": "parcels",
|
|
2734
|
+
"doc": "Parcels inside a shipment.",
|
|
2735
|
+
"role": "child",
|
|
2736
|
+
"persists": {
|
|
2737
|
+
"aggregate": "delivery.core.shipment",
|
|
2738
|
+
"block": "delivery.core.shipment.parcel"
|
|
2739
|
+
},
|
|
2740
|
+
"columns": [
|
|
2741
|
+
{
|
|
2742
|
+
"name": "id",
|
|
2743
|
+
"type": "text",
|
|
2744
|
+
"nullable": false,
|
|
2745
|
+
"pk": true,
|
|
2746
|
+
"maps": "Parcel.id"
|
|
2747
|
+
},
|
|
2748
|
+
{
|
|
2749
|
+
"name": "package_id",
|
|
2750
|
+
"type": "text",
|
|
2751
|
+
"nullable": false,
|
|
2752
|
+
"fk": {
|
|
2753
|
+
"table": "delivery.core.pg.packages",
|
|
2754
|
+
"column": "id",
|
|
2755
|
+
"onDelete": "cascade"
|
|
2756
|
+
}
|
|
2757
|
+
},
|
|
2758
|
+
{
|
|
2759
|
+
"name": "weight_grams",
|
|
2760
|
+
"type": "integer",
|
|
2761
|
+
"nullable": false,
|
|
2762
|
+
"maps": "Parcel.weightGrams"
|
|
2763
|
+
},
|
|
2764
|
+
{
|
|
2765
|
+
"name": "tracking",
|
|
2766
|
+
"type": "text",
|
|
2767
|
+
"nullable": false,
|
|
2768
|
+
"maps": "Parcel.tracking"
|
|
2769
|
+
}
|
|
2770
|
+
]
|
|
2771
|
+
},
|
|
2772
|
+
{
|
|
2773
|
+
"id": "delivery.core.pg.route_stops",
|
|
2774
|
+
"name": "route_stops",
|
|
2775
|
+
"doc": "Planned stops, rebuilt from RoutePlanned. Derived, not a source of truth.",
|
|
2776
|
+
"role": "projection",
|
|
2777
|
+
"persists": {
|
|
2778
|
+
"aggregate": "delivery.core.route"
|
|
2779
|
+
},
|
|
2780
|
+
"columns": [
|
|
2781
|
+
{
|
|
2782
|
+
"name": "route_id",
|
|
2783
|
+
"type": "text",
|
|
2784
|
+
"nullable": false,
|
|
2785
|
+
"pk": true
|
|
2786
|
+
},
|
|
2787
|
+
{
|
|
2788
|
+
"name": "seq",
|
|
2789
|
+
"type": "integer",
|
|
2790
|
+
"nullable": false,
|
|
2791
|
+
"pk": true
|
|
2792
|
+
},
|
|
2793
|
+
{
|
|
2794
|
+
"name": "package_id",
|
|
2795
|
+
"type": "text",
|
|
2796
|
+
"nullable": true,
|
|
2797
|
+
"fk": {
|
|
2798
|
+
"table": "delivery.core.pg.packages",
|
|
2799
|
+
"column": "id"
|
|
2800
|
+
}
|
|
2801
|
+
},
|
|
2802
|
+
{
|
|
2803
|
+
"name": "address",
|
|
2804
|
+
"type": "jsonb",
|
|
2805
|
+
"nullable": false,
|
|
2806
|
+
"from": [
|
|
2807
|
+
"delivery.core.pg.packages.ship_to"
|
|
2808
|
+
],
|
|
2809
|
+
"doc": "Denormalised from the package, so a route can be printed without a join."
|
|
2810
|
+
}
|
|
2811
|
+
]
|
|
2812
|
+
}
|
|
2813
|
+
],
|
|
2814
|
+
"views": [
|
|
2815
|
+
{
|
|
2816
|
+
"id": "delivery.core.pg.mv_route_load",
|
|
2817
|
+
"name": "mv_route_load",
|
|
2818
|
+
"materialized": true,
|
|
2819
|
+
"doc": "Stops and weight per route, refreshed after each plan. The catalog knows what it reads, not which column feeds which.",
|
|
2820
|
+
"reads": [
|
|
2821
|
+
"delivery.core.pg.route_stops",
|
|
2822
|
+
"delivery.core.pg.parcels"
|
|
2823
|
+
],
|
|
2824
|
+
"source": "delivery/core/db/migrations/0005_route_load.sql",
|
|
2825
|
+
"definition": "CREATE MATERIALIZED VIEW mv_route_load AS\nSELECT s.route_id, count(*) AS stops, sum(p.weight_grams) AS weight_grams\n FROM route_stops s\n LEFT JOIN parcels p ON p.package_id = s.package_id\n GROUP BY s.route_id;",
|
|
2826
|
+
"columns": [
|
|
2827
|
+
{
|
|
2828
|
+
"name": "route_id",
|
|
2829
|
+
"type": "text",
|
|
2830
|
+
"nullable": false
|
|
2831
|
+
},
|
|
2832
|
+
{
|
|
2833
|
+
"name": "stops",
|
|
2834
|
+
"type": "bigint",
|
|
2835
|
+
"nullable": false
|
|
2836
|
+
},
|
|
2837
|
+
{
|
|
2838
|
+
"name": "weight_grams",
|
|
2839
|
+
"type": "bigint",
|
|
2840
|
+
"nullable": true
|
|
2841
|
+
}
|
|
2842
|
+
]
|
|
2843
|
+
}
|
|
2844
|
+
]
|
|
2845
|
+
}
|
|
2846
|
+
]
|
|
2847
|
+
}
|