@satorio/machinery 0.2.1
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 +71 -0
- package/dictionary/all.json +4656 -0
- package/dictionary/all.toon +639 -0
- package/dictionary/core.json +221 -0
- package/dictionary/core.toon +33 -0
- package/dictionary/graphql.json +212 -0
- package/dictionary/graphql.toon +32 -0
- package/dictionary/http.json +113 -0
- package/dictionary/http.toon +21 -0
- package/dictionary/identifiers.json +266 -0
- package/dictionary/identifiers.toon +38 -0
- package/dictionary/index.json +96 -0
- package/dictionary/index.toon +18 -0
- package/dictionary/integration.json +131 -0
- package/dictionary/integration.toon +23 -0
- package/dictionary/money.json +383 -0
- package/dictionary/money.toon +51 -0
- package/dictionary/ports.json +392 -0
- package/dictionary/ports.toon +52 -0
- package/dictionary/postgres.json +473 -0
- package/dictionary/postgres.toon +61 -0
- package/dictionary/publishing.json +131 -0
- package/dictionary/publishing.toon +23 -0
- package/dictionary/queue.json +365 -0
- package/dictionary/queue.toon +49 -0
- package/dictionary/reference/api.mdx +843 -0
- package/dictionary/reference/uk/api.mdx +844 -0
- package/dictionary/storage.json +194 -0
- package/dictionary/storage.toon +30 -0
- package/dictionary/testing.json +707 -0
- package/dictionary/testing.toon +87 -0
- package/dictionary/time.json +311 -0
- package/dictionary/time.toon +43 -0
- package/dictionary/vitest.json +356 -0
- package/dictionary/vitest.toon +48 -0
- package/dictionary/workflow.json +185 -0
- package/dictionary/workflow.toon +29 -0
- package/dist/bin/command.d.ts +45 -0
- package/dist/bin/command.js +113 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +11 -0
- package/dist/core/bigint-json.d.ts +6 -0
- package/dist/core/bigint-json.js +6 -0
- package/dist/core/closed-set.d.ts +14 -0
- package/dist/core/closed-set.js +8 -0
- package/dist/core/domain-error.d.ts +8 -0
- package/dist/core/domain-error.js +11 -0
- package/dist/core/environment.d.ts +124 -0
- package/dist/core/environment.js +115 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +13 -0
- package/dist/core/ulid.d.ts +27 -0
- package/dist/core/ulid.js +27 -0
- package/dist/graphql/builder.d.ts +36 -0
- package/dist/graphql/builder.js +35 -0
- package/dist/graphql/context.d.ts +36 -0
- package/dist/graphql/context.js +69 -0
- package/dist/graphql/failure-classification.d.ts +11 -0
- package/dist/graphql/failure-classification.js +1 -0
- package/dist/graphql/failures.d.ts +62 -0
- package/dist/graphql/failures.js +30 -0
- package/dist/graphql/graphiql.d.ts +45 -0
- package/dist/graphql/graphiql.js +56 -0
- package/dist/graphql/index.d.ts +15 -0
- package/dist/graphql/index.js +14 -0
- package/dist/graphql/layer.d.ts +15 -0
- package/dist/graphql/layer.js +23 -0
- package/dist/graphql/request-handler.d.ts +26 -0
- package/dist/graphql/request-handler.js +33 -0
- package/dist/graphql/server.d.ts +19 -0
- package/dist/graphql/server.js +16 -0
- package/dist/graphql/settings.d.ts +55 -0
- package/dist/graphql/settings.js +82 -0
- package/dist/http/health.d.ts +70 -0
- package/dist/http/health.js +60 -0
- package/dist/http/index.d.ts +5 -0
- package/dist/http/index.js +5 -0
- package/dist/identifiers/client-code.d.ts +37 -0
- package/dist/identifiers/client-code.js +41 -0
- package/dist/identifiers/creditor-reference.d.ts +38 -0
- package/dist/identifiers/creditor-reference.js +32 -0
- package/dist/identifiers/damm.d.ts +22 -0
- package/dist/identifiers/damm.js +50 -0
- package/dist/identifiers/document-number.d.ts +68 -0
- package/dist/identifiers/document-number.js +73 -0
- package/dist/identifiers/document-numbers.d.ts +27 -0
- package/dist/identifiers/document-numbers.js +16 -0
- package/dist/identifiers/failures.d.ts +46 -0
- package/dist/identifiers/failures.js +48 -0
- package/dist/identifiers/index.d.ts +14 -0
- package/dist/identifiers/index.js +13 -0
- package/dist/identifiers/next-document-number.d.ts +26 -0
- package/dist/identifiers/next-document-number.js +31 -0
- package/dist/identifiers/ulid.d.ts +28 -0
- package/dist/identifiers/ulid.js +71 -0
- package/dist/integration/external-http-client.d.ts +22 -0
- package/dist/integration/external-http-client.js +74 -0
- package/dist/integration/index.d.ts +12 -0
- package/dist/integration/index.js +12 -0
- package/dist/integration/layer.d.ts +19 -0
- package/dist/integration/layer.js +30 -0
- package/dist/integration/retry.d.ts +65 -0
- package/dist/integration/retry.js +54 -0
- package/dist/integration/settings.d.ts +73 -0
- package/dist/integration/settings.js +92 -0
- package/dist/money/amount.d.ts +60 -0
- package/dist/money/amount.js +67 -0
- package/dist/money/arithmetic.d.ts +60 -0
- package/dist/money/arithmetic.js +73 -0
- package/dist/money/currency.d.ts +25 -0
- package/dist/money/currency.js +24 -0
- package/dist/money/decimal.d.ts +29 -0
- package/dist/money/decimal.js +39 -0
- package/dist/money/exchange-rate.d.ts +32 -0
- package/dist/money/exchange-rate.js +28 -0
- package/dist/money/failures.d.ts +86 -0
- package/dist/money/failures.js +97 -0
- package/dist/money/index.d.ts +15 -0
- package/dist/money/index.js +15 -0
- package/dist/money/percent.d.ts +35 -0
- package/dist/money/percent.js +50 -0
- package/dist/money/schema.d.ts +25 -0
- package/dist/money/schema.js +41 -0
- package/dist/ports/blob-store.d.ts +42 -0
- package/dist/ports/blob-store.js +13 -0
- package/dist/ports/document-number-counter.d.ts +36 -0
- package/dist/ports/document-number-counter.js +12 -0
- package/dist/ports/event-publisher.d.ts +29 -0
- package/dist/ports/event-publisher.js +13 -0
- package/dist/ports/external-http-client.d.ts +60 -0
- package/dist/ports/external-http-client.js +25 -0
- package/dist/ports/index.d.ts +17 -0
- package/dist/ports/index.js +17 -0
- package/dist/ports/integration-event-outbox.d.ts +44 -0
- package/dist/ports/integration-event-outbox.js +13 -0
- package/dist/ports/port-unavailable.d.ts +21 -0
- package/dist/ports/port-unavailable.js +30 -0
- package/dist/ports/processed-event-registry.d.ts +57 -0
- package/dist/ports/processed-event-registry.js +24 -0
- package/dist/ports/queue-message-producer.d.ts +26 -0
- package/dist/ports/queue-message-producer.js +12 -0
- package/dist/ports/queue-message-source.d.ts +39 -0
- package/dist/ports/queue-message-source.js +13 -0
- package/dist/ports/readiness-probe.d.ts +27 -0
- package/dist/ports/readiness-probe.js +13 -0
- package/dist/ports/transaction-boundary.d.ts +28 -0
- package/dist/ports/transaction-boundary.js +13 -0
- package/dist/postgres/client.d.ts +51 -0
- package/dist/postgres/client.js +81 -0
- package/dist/postgres/document-number-counter.d.ts +14 -0
- package/dist/postgres/document-number-counter.js +27 -0
- package/dist/postgres/failures.d.ts +28 -0
- package/dist/postgres/failures.js +18 -0
- package/dist/postgres/index.d.ts +18 -0
- package/dist/postgres/index.js +18 -0
- package/dist/postgres/integration-event-outbox.d.ts +15 -0
- package/dist/postgres/integration-event-outbox.js +60 -0
- package/dist/postgres/jsonb-value.d.ts +10 -0
- package/dist/postgres/jsonb-value.js +9 -0
- package/dist/postgres/layer.d.ts +57 -0
- package/dist/postgres/layer.js +72 -0
- package/dist/postgres/migrations/advisory-lock.d.ts +47 -0
- package/dist/postgres/migrations/advisory-lock.js +59 -0
- package/dist/postgres/migrations/runner.d.ts +56 -0
- package/dist/postgres/migrations/runner.js +69 -0
- package/dist/postgres/migrations/sql-file-loader.d.ts +59 -0
- package/dist/postgres/migrations/sql-file-loader.js +99 -0
- package/dist/postgres/processed-event-registry.d.ts +13 -0
- package/dist/postgres/processed-event-registry.js +44 -0
- package/dist/postgres/readiness-probe.d.ts +27 -0
- package/dist/postgres/readiness-probe.js +33 -0
- package/dist/postgres/settings.d.ts +134 -0
- package/dist/postgres/settings.js +210 -0
- package/dist/postgres/sql/0001_processed_event.sql +4 -0
- package/dist/postgres/sql/0002_integration_event_outbox.sql +13 -0
- package/dist/postgres/sql/0003_document_number_counter.sql +6 -0
- package/dist/postgres/sql-failure.d.ts +19 -0
- package/dist/postgres/sql-failure.js +22 -0
- package/dist/postgres/store-failure.d.ts +17 -0
- package/dist/postgres/store-failure.js +16 -0
- package/dist/postgres/transaction-boundary.d.ts +13 -0
- package/dist/postgres/transaction-boundary.js +21 -0
- package/dist/publishing/event-publisher.sns.d.ts +66 -0
- package/dist/publishing/event-publisher.sns.js +75 -0
- package/dist/publishing/event-publisher.stdout.d.ts +15 -0
- package/dist/publishing/event-publisher.stdout.js +24 -0
- package/dist/publishing/index.d.ts +10 -0
- package/dist/publishing/index.js +10 -0
- package/dist/publishing/layer.d.ts +25 -0
- package/dist/publishing/layer.js +39 -0
- package/dist/publishing/settings.d.ts +60 -0
- package/dist/publishing/settings.js +85 -0
- package/dist/queue/consumer.d.ts +91 -0
- package/dist/queue/consumer.js +92 -0
- package/dist/queue/envelope.d.ts +39 -0
- package/dist/queue/envelope.js +39 -0
- package/dist/queue/failures.d.ts +33 -0
- package/dist/queue/failures.js +15 -0
- package/dist/queue/index.d.ts +14 -0
- package/dist/queue/index.js +14 -0
- package/dist/queue/layer.d.ts +33 -0
- package/dist/queue/layer.js +58 -0
- package/dist/queue/message-codec.d.ts +43 -0
- package/dist/queue/message-codec.js +33 -0
- package/dist/queue/queue-message-producer.d.ts +61 -0
- package/dist/queue/queue-message-producer.js +52 -0
- package/dist/queue/queue-message-source.d.ts +93 -0
- package/dist/queue/queue-message-source.js +86 -0
- package/dist/queue/readiness.d.ts +78 -0
- package/dist/queue/readiness.js +86 -0
- package/dist/queue/settings.d.ts +106 -0
- package/dist/queue/settings.js +160 -0
- package/dist/storage/blob-store.d.ts +84 -0
- package/dist/storage/blob-store.js +109 -0
- package/dist/storage/index.d.ts +9 -0
- package/dist/storage/index.js +9 -0
- package/dist/storage/layer.d.ts +17 -0
- package/dist/storage/layer.js +30 -0
- package/dist/storage/settings.d.ts +48 -0
- package/dist/storage/settings.js +65 -0
- package/dist/testing/blob-store.d.ts +27 -0
- package/dist/testing/blob-store.js +45 -0
- package/dist/testing/clock.d.ts +29 -0
- package/dist/testing/clock.js +41 -0
- package/dist/testing/dna-perturbator/attention.d.ts +55 -0
- package/dist/testing/dna-perturbator/attention.js +49 -0
- package/dist/testing/dna-perturbator/chains.d.ts +31 -0
- package/dist/testing/dna-perturbator/chains.js +53 -0
- package/dist/testing/dna-perturbator/report.d.ts +12 -0
- package/dist/testing/dna-perturbator/report.js +22 -0
- package/dist/testing/dna-perturbator/verdict.d.ts +26 -0
- package/dist/testing/dna-perturbator/verdict.js +28 -0
- package/dist/testing/dna-perturbator.d.ts +48 -0
- package/dist/testing/dna-perturbator.js +145 -0
- package/dist/testing/document-number-counter.d.ts +29 -0
- package/dist/testing/document-number-counter.js +39 -0
- package/dist/testing/event-publisher.d.ts +28 -0
- package/dist/testing/event-publisher.js +33 -0
- package/dist/testing/external-http-client.d.ts +29 -0
- package/dist/testing/external-http-client.js +49 -0
- package/dist/testing/index.d.ts +24 -0
- package/dist/testing/index.js +24 -0
- package/dist/testing/integration-event-outbox.d.ts +37 -0
- package/dist/testing/integration-event-outbox.js +67 -0
- package/dist/testing/mutation-settings.d.ts +71 -0
- package/dist/testing/mutation-settings.js +57 -0
- package/dist/testing/postgres-rows.d.ts +12 -0
- package/dist/testing/postgres-rows.js +19 -0
- package/dist/testing/processed-event-registry.d.ts +25 -0
- package/dist/testing/processed-event-registry.js +39 -0
- package/dist/testing/queue-message-producer.d.ts +27 -0
- package/dist/testing/queue-message-producer.js +41 -0
- package/dist/testing/queue-message-source.d.ts +54 -0
- package/dist/testing/queue-message-source.js +71 -0
- package/dist/testing/quiet-reporter.d.ts +86 -0
- package/dist/testing/quiet-reporter.js +81 -0
- package/dist/testing/readiness-probe.d.ts +27 -0
- package/dist/testing/readiness-probe.js +33 -0
- package/dist/testing/scenario.d.ts +94 -0
- package/dist/testing/scenario.js +81 -0
- package/dist/testing/transaction-boundary.d.ts +30 -0
- package/dist/testing/transaction-boundary.js +36 -0
- package/dist/testing/typed-failure.d.ts +8 -0
- package/dist/testing/typed-failure.js +15 -0
- package/dist/time/calendar-day.d.ts +83 -0
- package/dist/time/calendar-day.js +112 -0
- package/dist/time/failures.d.ts +51 -0
- package/dist/time/failures.js +56 -0
- package/dist/time/index.d.ts +11 -0
- package/dist/time/index.js +11 -0
- package/dist/time/instant.d.ts +64 -0
- package/dist/time/instant.js +67 -0
- package/dist/time/period.d.ts +45 -0
- package/dist/time/period.js +55 -0
- package/dist/vitest/container-reuse.d.ts +14 -0
- package/dist/vitest/container-reuse.js +15 -0
- package/dist/vitest/dotenv.d.ts +16 -0
- package/dist/vitest/dotenv.js +48 -0
- package/dist/vitest/effect-single-instance.d.ts +36 -0
- package/dist/vitest/effect-single-instance.js +125 -0
- package/dist/vitest/fast-check-single-instance.d.ts +11 -0
- package/dist/vitest/fast-check-single-instance.js +26 -0
- package/dist/vitest/index.d.ts +31 -0
- package/dist/vitest/index.js +30 -0
- package/dist/vitest/localstack-container.d.ts +32 -0
- package/dist/vitest/localstack-container.js +55 -0
- package/dist/vitest/port-contract-tester.d.ts +8 -0
- package/dist/vitest/port-contract-tester.js +1 -0
- package/dist/vitest/port-contracts/blob-store.contract.d.ts +22 -0
- package/dist/vitest/port-contracts/blob-store.contract.js +108 -0
- package/dist/vitest/port-contracts/document-number-counter.contract.d.ts +18 -0
- package/dist/vitest/port-contracts/document-number-counter.contract.js +90 -0
- package/dist/vitest/port-contracts/event-publisher.contract.d.ts +35 -0
- package/dist/vitest/port-contracts/event-publisher.contract.js +67 -0
- package/dist/vitest/port-contracts/external-http-client.contract.d.ts +35 -0
- package/dist/vitest/port-contracts/external-http-client.contract.js +52 -0
- package/dist/vitest/port-contracts/integration-event-outbox.contract.d.ts +23 -0
- package/dist/vitest/port-contracts/integration-event-outbox.contract.js +75 -0
- package/dist/vitest/port-contracts/processed-event-registry.contract.d.ts +18 -0
- package/dist/vitest/port-contracts/processed-event-registry.contract.js +84 -0
- package/dist/vitest/port-contracts/queue-message-producer.contract.d.ts +26 -0
- package/dist/vitest/port-contracts/queue-message-producer.contract.js +48 -0
- package/dist/vitest/port-contracts/queue-message-source.contract.d.ts +29 -0
- package/dist/vitest/port-contracts/queue-message-source.contract.js +71 -0
- package/dist/vitest/port-contracts/readiness-probe.contract.d.ts +18 -0
- package/dist/vitest/port-contracts/readiness-probe.contract.js +41 -0
- package/dist/vitest/port-contracts/transaction-boundary.contract.d.ts +21 -0
- package/dist/vitest/port-contracts/transaction-boundary.contract.js +64 -0
- package/dist/vitest/postgres-container.d.ts +57 -0
- package/dist/vitest/postgres-container.js +118 -0
- package/dist/vitest/reader-port-contract.d.ts +41 -0
- package/dist/vitest/reader-port-contract.js +43 -0
- package/dist/vitest/repo-source-files.d.ts +37 -0
- package/dist/vitest/repo-source-files.js +61 -0
- package/dist/vitest/reset-tables.d.ts +18 -0
- package/dist/vitest/reset-tables.js +30 -0
- package/dist/vitest/settings-contract.d.ts +38 -0
- package/dist/vitest/settings-contract.js +77 -0
- package/dist/vitest/stub-server.d.ts +43 -0
- package/dist/vitest/stub-server.js +60 -0
- package/dist/vitest/term-registry.json +730 -0
- package/dist/vitest/testclock-fork-isolated-files.d.ts +13 -0
- package/dist/vitest/testclock-fork-isolated-files.js +18 -0
- package/dist/vitest/testclock-fork-isolation.d.ts +77 -0
- package/dist/vitest/testclock-fork-isolation.js +188 -0
- package/dist/workflow/claim-once.d.ts +16 -0
- package/dist/workflow/claim-once.js +9 -0
- package/dist/workflow/failures.d.ts +47 -0
- package/dist/workflow/failures.js +22 -0
- package/dist/workflow/index.d.ts +12 -0
- package/dist/workflow/index.js +12 -0
- package/dist/workflow/on-claim.d.ts +17 -0
- package/dist/workflow/on-claim.js +8 -0
- package/dist/workflow/publish-pending-events.d.ts +31 -0
- package/dist/workflow/publish-pending-events.js +31 -0
- package/dist/workflow/transactionally.d.ts +8 -0
- package/dist/workflow/transactionally.js +8 -0
- package/dist/workflow/versioned-payload.d.ts +35 -0
- package/dist/workflow/versioned-payload.js +43 -0
- package/package.json +309 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Effect, Exit } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* The path that answers whether this process is alive at all, without checking
|
|
4
|
+
* a single dependency.
|
|
5
|
+
*/
|
|
6
|
+
export declare const LIVEZ_PATH = "/livez";
|
|
7
|
+
/**
|
|
8
|
+
* The path that answers whether this service can take traffic, after every
|
|
9
|
+
* mounted dependency has been asked.
|
|
10
|
+
*/
|
|
11
|
+
export declare const READYZ_PATH = "/readyz";
|
|
12
|
+
/**
|
|
13
|
+
* Same check as READYZ_PATH, under a second name. Orchestrators expect
|
|
14
|
+
* different words for the same question: can you send traffic here.
|
|
15
|
+
*/
|
|
16
|
+
export declare const HEALTHZ_PATH = "/healthz";
|
|
17
|
+
/**
|
|
18
|
+
* How one dependency came back: it answered, or it is down.
|
|
19
|
+
*/
|
|
20
|
+
export type HealthCheckStatus = 'ok' | 'down';
|
|
21
|
+
/**
|
|
22
|
+
* One dependency to check, under the name the answer reports it by.
|
|
23
|
+
*/
|
|
24
|
+
export interface NamedHealthCheck<R> {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly verify: Effect.Effect<void, unknown, R>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* What the readiness endpoint answers: whether the service is ready, and how
|
|
30
|
+
* each dependency came back.
|
|
31
|
+
*/
|
|
32
|
+
export interface ReadinessOutcome {
|
|
33
|
+
readonly ready: boolean;
|
|
34
|
+
readonly checks: Readonly<Record<string, HealthCheckStatus>>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Runs a readiness check and returns its Exit. A resolver borrows the same
|
|
38
|
+
* shape from the runtime as the GraphQL context does, so both sides of the
|
|
39
|
+
* machinery talk to the runtime through one interface.
|
|
40
|
+
*/
|
|
41
|
+
export type RunHealthCheck<R> = <A, E>(program: Effect.Effect<A, E, R>) => Promise<Exit.Exit<A, E>>;
|
|
42
|
+
/**
|
|
43
|
+
* How long one check gets before it counts as down. Without a deadline a
|
|
44
|
+
* check that hangs, rather than failing outright, keeps /readyz open
|
|
45
|
+
* indefinitely instead of returning in time.
|
|
46
|
+
*/
|
|
47
|
+
export declare const DEFAULT_HEALTH_CHECK_DEADLINE_MILLIS = 3000;
|
|
48
|
+
/**
|
|
49
|
+
* What the readiness routes need: the runtime that runs a check, the checks
|
|
50
|
+
* themselves, and the deadline one check gets.
|
|
51
|
+
*/
|
|
52
|
+
export interface HealthRoutesOptions<R> {
|
|
53
|
+
readonly runCheck: RunHealthCheck<R>;
|
|
54
|
+
readonly checks: readonly NamedHealthCheck<R>[];
|
|
55
|
+
readonly checkDeadlineMillis?: number;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Runs every check independently, in parallel, under its own deadline. One
|
|
59
|
+
* dependency that fails or hangs does not hide the others' results and does
|
|
60
|
+
* not hold /readyz open past checkDeadlineMillis. An empty checks list
|
|
61
|
+
* reports ready: true — an app with no mounted dependencies is ready by
|
|
62
|
+
* itself.
|
|
63
|
+
*/
|
|
64
|
+
export declare const checkReadiness: <R>(options: HealthRoutesOptions<R>) => Effect.Effect<ReadinessOutcome>;
|
|
65
|
+
/**
|
|
66
|
+
* Handles the three readiness endpoints. Returns null on any other path, so
|
|
67
|
+
* the caller passes the request on down its own chain — one Bun.serve
|
|
68
|
+
* handler can then serve both readiness probes and GraphQL.
|
|
69
|
+
*/
|
|
70
|
+
export declare const healthRoutesFetch: <R>(options: HealthRoutesOptions<R>) => ((request: Request) => Promise<Response | null>);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Duration, Effect, Exit } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* The path that answers whether this process is alive at all, without checking
|
|
4
|
+
* a single dependency.
|
|
5
|
+
*/
|
|
6
|
+
export const LIVEZ_PATH = '/livez';
|
|
7
|
+
/**
|
|
8
|
+
* The path that answers whether this service can take traffic, after every
|
|
9
|
+
* mounted dependency has been asked.
|
|
10
|
+
*/
|
|
11
|
+
export const READYZ_PATH = '/readyz';
|
|
12
|
+
/**
|
|
13
|
+
* Same check as READYZ_PATH, under a second name. Orchestrators expect
|
|
14
|
+
* different words for the same question: can you send traffic here.
|
|
15
|
+
*/
|
|
16
|
+
export const HEALTHZ_PATH = '/healthz';
|
|
17
|
+
/**
|
|
18
|
+
* How long one check gets before it counts as down. Without a deadline a
|
|
19
|
+
* check that hangs, rather than failing outright, keeps /readyz open
|
|
20
|
+
* indefinitely instead of returning in time.
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_HEALTH_CHECK_DEADLINE_MILLIS = 3_000;
|
|
23
|
+
const oneCheckOutcome = (runCheck, check, deadlineMillis) => Effect.promise(() => runCheck(check.verify.pipe(Effect.timeout(Duration.millis(deadlineMillis))))).pipe(Effect.map((exit) => [check.name, Exit.isSuccess(exit) ? 'ok' : 'down']));
|
|
24
|
+
/**
|
|
25
|
+
* Runs every check independently, in parallel, under its own deadline. One
|
|
26
|
+
* dependency that fails or hangs does not hide the others' results and does
|
|
27
|
+
* not hold /readyz open past checkDeadlineMillis. An empty checks list
|
|
28
|
+
* reports ready: true — an app with no mounted dependencies is ready by
|
|
29
|
+
* itself.
|
|
30
|
+
*/
|
|
31
|
+
export const checkReadiness = (options) => {
|
|
32
|
+
const deadlineMillis = options.checkDeadlineMillis ?? DEFAULT_HEALTH_CHECK_DEADLINE_MILLIS;
|
|
33
|
+
return Effect.forEach(options.checks, (check) => oneCheckOutcome(options.runCheck, check, deadlineMillis), { concurrency: 'unbounded' }).pipe(Effect.map((pairs) => {
|
|
34
|
+
const checks = Object.fromEntries(pairs);
|
|
35
|
+
const ready = pairs.every(([, status]) => status === 'ok');
|
|
36
|
+
return { ready, checks };
|
|
37
|
+
}));
|
|
38
|
+
};
|
|
39
|
+
const jsonResponse = (status, body) => new Response(JSON.stringify(body), {
|
|
40
|
+
status,
|
|
41
|
+
headers: { 'content-type': 'application/json' },
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* Handles the three readiness endpoints. Returns null on any other path, so
|
|
45
|
+
* the caller passes the request on down its own chain — one Bun.serve
|
|
46
|
+
* handler can then serve both readiness probes and GraphQL.
|
|
47
|
+
*/
|
|
48
|
+
export const healthRoutesFetch = (options) => {
|
|
49
|
+
return async (request) => {
|
|
50
|
+
const url = new URL(request.url);
|
|
51
|
+
if (url.pathname === LIVEZ_PATH) {
|
|
52
|
+
return jsonResponse(200, { status: 'ok' });
|
|
53
|
+
}
|
|
54
|
+
if (url.pathname === READYZ_PATH || url.pathname === HEALTHZ_PATH) {
|
|
55
|
+
const outcome = await Effect.runPromise(checkReadiness(options));
|
|
56
|
+
return jsonResponse(outcome.ready ? 200 : 503, outcome);
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry to the http mechanism: readiness probes mounted on the same port
|
|
3
|
+
* as GraphQL. See machinery/graphql/server.ts for where they attach.
|
|
4
|
+
*/
|
|
5
|
+
export { checkReadiness, DEFAULT_HEALTH_CHECK_DEADLINE_MILLIS, HEALTHZ_PATH, type HealthCheckStatus, type HealthRoutesOptions, healthRoutesFetch, LIVEZ_PATH, type NamedHealthCheck, READYZ_PATH, type ReadinessOutcome, type RunHealthCheck, } from './health.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry to the http mechanism: readiness probes mounted on the same port
|
|
3
|
+
* as GraphQL. See machinery/graphql/server.ts for where they attach.
|
|
4
|
+
*/
|
|
5
|
+
export { checkReadiness, DEFAULT_HEALTH_CHECK_DEADLINE_MILLIS, HEALTHZ_PATH, healthRoutesFetch, LIVEZ_PATH, READYZ_PATH, } from './health.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Effect, Either } from 'effect';
|
|
2
|
+
import { NotAClientCodeError } from './failures.js';
|
|
3
|
+
declare const clientCodeBrand: unique symbol;
|
|
4
|
+
/**
|
|
5
|
+
* The number a client quotes back to you: eight digits, the last of which
|
|
6
|
+
* checks the other seven. It is a label and never an identity — the
|
|
7
|
+
* identity of an account is its own identifier — and it is never a key of
|
|
8
|
+
* anything either, so knowing one opens nothing.
|
|
9
|
+
*/
|
|
10
|
+
export type ClientCode = string & {
|
|
11
|
+
readonly [clientCodeBrand]: 'ClientCode';
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* A fresh client code, drawn from the randomness the service already
|
|
15
|
+
* shares. The seven drawn digits never start with a zero, so the code
|
|
16
|
+
* reads back the same whether a person writes it or a spreadsheet does,
|
|
17
|
+
* and the eighth digit is the check the whole code stands on. Nothing
|
|
18
|
+
* here counts: two codes issued one after the other say nothing about
|
|
19
|
+
* how many clients came between them.
|
|
20
|
+
*/
|
|
21
|
+
export declare const newClientCode: Effect.Effect<ClientCode>;
|
|
22
|
+
/**
|
|
23
|
+
* Reads a client code somebody typed, forgiving the spaces and hyphens a
|
|
24
|
+
* person puts between the groups. A code whose check digit does not hold
|
|
25
|
+
* comes back as a refusal, and the refusal names no character.
|
|
26
|
+
*/
|
|
27
|
+
export declare const clientCodeFrom: (text: string) => Either.Either<ClientCode, NotAClientCodeError>;
|
|
28
|
+
/**
|
|
29
|
+
* The eight digits with nothing between them, which is the form a
|
|
30
|
+
* document number and a payment reference are built from.
|
|
31
|
+
*/
|
|
32
|
+
export declare const digitsOfClientCode: (code: ClientCode) => string;
|
|
33
|
+
/**
|
|
34
|
+
* The form a person reads off a document: two groups of four.
|
|
35
|
+
*/
|
|
36
|
+
export declare const clientCodeInGroupedForm: (code: ClientCode) => string;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Effect, Either, Random } from 'effect';
|
|
2
|
+
import { dammHolds, withDammCheckDigit } from './damm.js';
|
|
3
|
+
import { NotAClientCodeError } from './failures.js';
|
|
4
|
+
const DRAWN_DIGITS = 7;
|
|
5
|
+
const EIGHT_DIGITS_WITHOUT_A_LEADING_ZERO = /^[1-9]\d{7}$/;
|
|
6
|
+
const NOT_A_DIGIT = /[\s-]+/g;
|
|
7
|
+
const GROUP = 4;
|
|
8
|
+
/**
|
|
9
|
+
* A fresh client code, drawn from the randomness the service already
|
|
10
|
+
* shares. The seven drawn digits never start with a zero, so the code
|
|
11
|
+
* reads back the same whether a person writes it or a spreadsheet does,
|
|
12
|
+
* and the eighth digit is the check the whole code stands on. Nothing
|
|
13
|
+
* here counts: two codes issued one after the other say nothing about
|
|
14
|
+
* how many clients came between them.
|
|
15
|
+
*/
|
|
16
|
+
export const newClientCode = Effect.gen(function* () {
|
|
17
|
+
const first = yield* Random.nextIntBetween(1, 10);
|
|
18
|
+
const rest = yield* Effect.replicateEffect(Random.nextIntBetween(0, 10), DRAWN_DIGITS - 1);
|
|
19
|
+
const drawn = `${first}${rest.join('')}`;
|
|
20
|
+
return (withDammCheckDigit(drawn) ?? drawn);
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Reads a client code somebody typed, forgiving the spaces and hyphens a
|
|
24
|
+
* person puts between the groups. A code whose check digit does not hold
|
|
25
|
+
* comes back as a refusal, and the refusal names no character.
|
|
26
|
+
*/
|
|
27
|
+
export const clientCodeFrom = (text) => {
|
|
28
|
+
const digits = text.replace(NOT_A_DIGIT, '');
|
|
29
|
+
return EIGHT_DIGITS_WITHOUT_A_LEADING_ZERO.test(digits) && dammHolds(digits)
|
|
30
|
+
? Either.right(digits)
|
|
31
|
+
: Either.left(new NotAClientCodeError({ text }));
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* The eight digits with nothing between them, which is the form a
|
|
35
|
+
* document number and a payment reference are built from.
|
|
36
|
+
*/
|
|
37
|
+
export const digitsOfClientCode = (code) => code;
|
|
38
|
+
/**
|
|
39
|
+
* The form a person reads off a document: two groups of four.
|
|
40
|
+
*/
|
|
41
|
+
export const clientCodeInGroupedForm = (code) => `${code.slice(0, GROUP)}-${code.slice(GROUP)}`;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Either } from 'effect';
|
|
2
|
+
import type { DocumentNumber } from './document-number.js';
|
|
3
|
+
import { NotACreditorReferenceError } from './failures.js';
|
|
4
|
+
declare const creditorReferenceBrand: unique symbol;
|
|
5
|
+
/**
|
|
6
|
+
* The reference a payer copies into a bank transfer, with its own check
|
|
7
|
+
* digits.
|
|
8
|
+
*
|
|
9
|
+
* The reference a payer copies into their bank transfer: the letters `RF`, two
|
|
10
|
+
* check digits over everything after them, and the digits of the document
|
|
11
|
+
* number. It follows ISO 11649, so a bank anywhere reads it and a typo in it
|
|
12
|
+
* does not reach anybody's ledger.
|
|
13
|
+
*/
|
|
14
|
+
export type CreditorReference = string & {
|
|
15
|
+
readonly [creditorReferenceBrand]: 'CreditorReference';
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The payment reference for a document that somebody has to pay.
|
|
19
|
+
*/
|
|
20
|
+
export declare const creditorReferenceOf: (number: DocumentNumber) => CreditorReference;
|
|
21
|
+
/**
|
|
22
|
+
* Reads a reference somebody typed, forgiving case, spaces and the
|
|
23
|
+
* hyphens a person copies along with it. A reference whose check digits
|
|
24
|
+
* do not hold comes back as a refusal that names no character: the
|
|
25
|
+
* payer retypes the whole reference, and nobody guessing at references
|
|
26
|
+
* learns which part they got closest on.
|
|
27
|
+
*/
|
|
28
|
+
export declare const creditorReferenceFrom: (text: string) => Either.Either<CreditorReference, NotACreditorReferenceError>;
|
|
29
|
+
/**
|
|
30
|
+
* The digits of the document number this reference points at.
|
|
31
|
+
*/
|
|
32
|
+
export declare const digitsOfCreditorReference: (reference: CreditorReference) => string;
|
|
33
|
+
/**
|
|
34
|
+
* The form a person reads off a document and types into a bank: groups
|
|
35
|
+
* of four, the way ISO 11649 prints one.
|
|
36
|
+
*/
|
|
37
|
+
export declare const creditorReferenceInGroupedForm: (reference: CreditorReference) => string;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Either } from 'effect';
|
|
2
|
+
import { generate, parse } from 'node-iso11649';
|
|
3
|
+
import { digitsOfDocumentNumber } from './document-number.js';
|
|
4
|
+
import { NotACreditorReferenceError } from './failures.js';
|
|
5
|
+
const NOT_A_REFERENCE_CHARACTER = /[\s-]+/g;
|
|
6
|
+
const GROUP = 4;
|
|
7
|
+
/**
|
|
8
|
+
* The payment reference for a document that somebody has to pay.
|
|
9
|
+
*/
|
|
10
|
+
export const creditorReferenceOf = (number) => generate(digitsOfDocumentNumber(number));
|
|
11
|
+
/**
|
|
12
|
+
* Reads a reference somebody typed, forgiving case, spaces and the
|
|
13
|
+
* hyphens a person copies along with it. A reference whose check digits
|
|
14
|
+
* do not hold comes back as a refusal that names no character: the
|
|
15
|
+
* payer retypes the whole reference, and nobody guessing at references
|
|
16
|
+
* learns which part they got closest on.
|
|
17
|
+
*/
|
|
18
|
+
export const creditorReferenceFrom = (text) => {
|
|
19
|
+
const written = text.replace(NOT_A_REFERENCE_CHARACTER, '').toUpperCase();
|
|
20
|
+
return parse(written) === null
|
|
21
|
+
? Either.left(new NotACreditorReferenceError({ text }))
|
|
22
|
+
: Either.right(written);
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The digits of the document number this reference points at.
|
|
26
|
+
*/
|
|
27
|
+
export const digitsOfCreditorReference = (reference) => reference.slice(GROUP);
|
|
28
|
+
/**
|
|
29
|
+
* The form a person reads off a document and types into a bank: groups
|
|
30
|
+
* of four, the way ISO 11649 prints one.
|
|
31
|
+
*/
|
|
32
|
+
export const creditorReferenceInGroupedForm = (reference) => (reference.match(/.{1,4}/g) ?? [reference]).join(' ');
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The check digit that makes a run of digits self-checking, by Damm's
|
|
3
|
+
* algorithm. The table above is the order-ten totally anti-symmetric
|
|
4
|
+
* quasigroup of the 2004 paper *Total anti-symmetrische Quasigruppen*
|
|
5
|
+
* (Philipps-Universitat Marburg), which is what makes the digit catch
|
|
6
|
+
* every single wrong digit and every swap of two neighbours. A text
|
|
7
|
+
* holding anything but digits gets `null` — the caller turns that into
|
|
8
|
+
* its own refusal.
|
|
9
|
+
*
|
|
10
|
+
* No package on the registry carries this algorithm at a version a
|
|
11
|
+
* service should depend on, so it stands here as one pure function with
|
|
12
|
+
* its source named, rather than as an unmaintained peer dependency.
|
|
13
|
+
*/
|
|
14
|
+
export declare const dammCheckDigit: (digits: string) => string | null;
|
|
15
|
+
/**
|
|
16
|
+
* The same digits with their check digit written on the end.
|
|
17
|
+
*/
|
|
18
|
+
export declare const withDammCheckDigit: (digits: string) => string | null;
|
|
19
|
+
/**
|
|
20
|
+
* Whether a run of digits already carries a check digit that holds.
|
|
21
|
+
*/
|
|
22
|
+
export declare const dammHolds: (digits: string) => boolean;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const OPERATION_TABLE = [
|
|
2
|
+
[0, 3, 1, 7, 5, 9, 8, 6, 4, 2],
|
|
3
|
+
[7, 0, 9, 2, 1, 5, 4, 8, 6, 3],
|
|
4
|
+
[4, 2, 0, 6, 8, 7, 1, 3, 5, 9],
|
|
5
|
+
[1, 7, 5, 0, 9, 8, 3, 4, 2, 6],
|
|
6
|
+
[6, 1, 2, 3, 0, 4, 5, 9, 7, 8],
|
|
7
|
+
[3, 6, 7, 4, 2, 0, 9, 5, 8, 1],
|
|
8
|
+
[5, 8, 6, 9, 7, 2, 0, 1, 3, 4],
|
|
9
|
+
[8, 9, 4, 5, 3, 6, 2, 0, 1, 7],
|
|
10
|
+
[9, 4, 3, 8, 6, 1, 7, 2, 0, 5],
|
|
11
|
+
[2, 5, 8, 1, 4, 3, 6, 7, 9, 0],
|
|
12
|
+
];
|
|
13
|
+
const ONLY_DIGITS = /^\d+$/;
|
|
14
|
+
const interimOf = (digits) => {
|
|
15
|
+
if (!ONLY_DIGITS.test(digits))
|
|
16
|
+
return null;
|
|
17
|
+
let row = 0;
|
|
18
|
+
for (const digit of digits) {
|
|
19
|
+
row = OPERATION_TABLE[row]?.[Number(digit)] ?? 0;
|
|
20
|
+
}
|
|
21
|
+
return row;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The check digit that makes a run of digits self-checking, by Damm's
|
|
25
|
+
* algorithm. The table above is the order-ten totally anti-symmetric
|
|
26
|
+
* quasigroup of the 2004 paper *Total anti-symmetrische Quasigruppen*
|
|
27
|
+
* (Philipps-Universitat Marburg), which is what makes the digit catch
|
|
28
|
+
* every single wrong digit and every swap of two neighbours. A text
|
|
29
|
+
* holding anything but digits gets `null` — the caller turns that into
|
|
30
|
+
* its own refusal.
|
|
31
|
+
*
|
|
32
|
+
* No package on the registry carries this algorithm at a version a
|
|
33
|
+
* service should depend on, so it stands here as one pure function with
|
|
34
|
+
* its source named, rather than as an unmaintained peer dependency.
|
|
35
|
+
*/
|
|
36
|
+
export const dammCheckDigit = (digits) => {
|
|
37
|
+
const interim = interimOf(digits);
|
|
38
|
+
return interim === null ? null : String(interim);
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The same digits with their check digit written on the end.
|
|
42
|
+
*/
|
|
43
|
+
export const withDammCheckDigit = (digits) => {
|
|
44
|
+
const check = dammCheckDigit(digits);
|
|
45
|
+
return check === null ? null : `${digits}${check}`;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Whether a run of digits already carries a check digit that holds.
|
|
49
|
+
*/
|
|
50
|
+
export const dammHolds = (digits) => interimOf(digits) === 0;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ClosedSet } from '@satorio/machinery/core';
|
|
2
|
+
import { type CalendarDay } from '@satorio/machinery/time';
|
|
3
|
+
import { Either } from 'effect';
|
|
4
|
+
import { type ClientCode } from './client-code.js';
|
|
5
|
+
import { NotADocumentNumberError } from './failures.js';
|
|
6
|
+
declare const documentNumberBrand: unique symbol;
|
|
7
|
+
/**
|
|
8
|
+
* The number printed on a document, in the form a person reads it.
|
|
9
|
+
*
|
|
10
|
+
* The number printed on a document: its series, the client code, the reporting
|
|
11
|
+
* month as two digits of year and two of month, and the count of documents
|
|
12
|
+
* that client has been sent that month.
|
|
13
|
+
*/
|
|
14
|
+
export type DocumentNumber = string & {
|
|
15
|
+
readonly [documentNumberBrand]: 'DocumentNumber';
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* What a document number is made of. The count is shared across every
|
|
19
|
+
* series, so a client never meets two documents carrying the same digits
|
|
20
|
+
* in one month.
|
|
21
|
+
*/
|
|
22
|
+
export interface DocumentNumberParts<Series extends string = string> {
|
|
23
|
+
readonly series: Series;
|
|
24
|
+
readonly clientCode: ClientCode;
|
|
25
|
+
readonly reportingMonth: CalendarDay;
|
|
26
|
+
readonly count: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The two digits of year and two of month a reporting period is written
|
|
30
|
+
* as: March 2026 reads `2603`.
|
|
31
|
+
*/
|
|
32
|
+
export declare const reportingMonthOf: (day: CalendarDay) => string;
|
|
33
|
+
/**
|
|
34
|
+
* Builds a document number out of its parts. A count below one, or one
|
|
35
|
+
* that is no whole number, comes back as a refusal: a document nobody
|
|
36
|
+
* counted has no number.
|
|
37
|
+
*
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
export declare const documentNumberOf: (parts: DocumentNumberParts) => Either.Either<DocumentNumber, NotADocumentNumberError>;
|
|
41
|
+
/**
|
|
42
|
+
* Reads a document number somebody typed against the series a service
|
|
43
|
+
* issues. The series has to stand in that set, and the client code
|
|
44
|
+
* inside the number has to pass its own check digit, so a number
|
|
45
|
+
* invented by hand does not read back.
|
|
46
|
+
*
|
|
47
|
+
* @internal
|
|
48
|
+
*/
|
|
49
|
+
export declare const documentNumberReadFrom: (text: string, series: ClosedSet<readonly string[]>) => Either.Either<DocumentNumber, NotADocumentNumberError>;
|
|
50
|
+
/**
|
|
51
|
+
* The series this document belongs to, as the number itself spells it.
|
|
52
|
+
*
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
export declare const seriesOfDocumentNumber: (number: DocumentNumber) => string;
|
|
56
|
+
/**
|
|
57
|
+
* The digits of a document number with the series and the hyphens taken
|
|
58
|
+
* off, which is what a payment reference is built over.
|
|
59
|
+
*/
|
|
60
|
+
export declare const digitsOfDocumentNumber: (number: DocumentNumber) => string;
|
|
61
|
+
/**
|
|
62
|
+
* The client code printed inside a document number. Every document
|
|
63
|
+
* number that exists was either built from a client code or read back
|
|
64
|
+
* through that code's own check digit, so the eight digits at the front
|
|
65
|
+
* of one are always a code that holds.
|
|
66
|
+
*/
|
|
67
|
+
export declare const clientCodeOfDocumentNumber: (number: DocumentNumber) => ClientCode;
|
|
68
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { dayInIsoForm } from '@satorio/machinery/time';
|
|
2
|
+
import { Either } from 'effect';
|
|
3
|
+
import { clientCodeFrom, clientCodeInGroupedForm, } from './client-code.js';
|
|
4
|
+
import { NotADocumentNumberError } from './failures.js';
|
|
5
|
+
const SMALLEST_COUNT = 1;
|
|
6
|
+
const COUNT_DIGITS = 2;
|
|
7
|
+
const CLIENT_CODE_DIGITS = 8;
|
|
8
|
+
const WRITTEN_FORM = /^([A-Z]{2,3})-(\d{4})-(\d{4})-(\d{4})-(\d{2,})$/;
|
|
9
|
+
const YEAR_IN_MONTH = 4;
|
|
10
|
+
const NOT_A_DIGIT = /[\s-]+/g;
|
|
11
|
+
/**
|
|
12
|
+
* The two digits of year and two of month a reporting period is written
|
|
13
|
+
* as: March 2026 reads `2603`.
|
|
14
|
+
*/
|
|
15
|
+
export const reportingMonthOf = (day) => {
|
|
16
|
+
const iso = dayInIsoForm(day);
|
|
17
|
+
return `${iso.slice(2, YEAR_IN_MONTH)}${iso.slice(5, 7)}`;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Builds a document number out of its parts. A count below one, or one
|
|
21
|
+
* that is no whole number, comes back as a refusal: a document nobody
|
|
22
|
+
* counted has no number.
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
export const documentNumberOf = (parts) => {
|
|
27
|
+
const counted = Number.isInteger(parts.count) && parts.count >= SMALLEST_COUNT;
|
|
28
|
+
const count = String(parts.count).padStart(COUNT_DIGITS, '0');
|
|
29
|
+
const written = `${parts.series}-${clientCodeInGroupedForm(parts.clientCode)}` +
|
|
30
|
+
`-${reportingMonthOf(parts.reportingMonth)}-${count}`;
|
|
31
|
+
return counted
|
|
32
|
+
? Either.right(written)
|
|
33
|
+
: Either.left(new NotADocumentNumberError({ text: written }));
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Reads a document number somebody typed against the series a service
|
|
37
|
+
* issues. The series has to stand in that set, and the client code
|
|
38
|
+
* inside the number has to pass its own check digit, so a number
|
|
39
|
+
* invented by hand does not read back.
|
|
40
|
+
*
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
export const documentNumberReadFrom = (text, series) => {
|
|
44
|
+
const written = text.trim().toUpperCase();
|
|
45
|
+
const parts = WRITTEN_FORM.exec(written);
|
|
46
|
+
const refused = Either.left(new NotADocumentNumberError({ text }));
|
|
47
|
+
if (parts === null)
|
|
48
|
+
return refused;
|
|
49
|
+
const [, written_series = '', first = '', second = ''] = parts;
|
|
50
|
+
if (!series.is(written_series))
|
|
51
|
+
return refused;
|
|
52
|
+
return Either.isRight(clientCodeFrom(`${first}${second}`))
|
|
53
|
+
? Either.right(written)
|
|
54
|
+
: refused;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* The series this document belongs to, as the number itself spells it.
|
|
58
|
+
*
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
export const seriesOfDocumentNumber = (number) => number.slice(0, number.indexOf('-'));
|
|
62
|
+
/**
|
|
63
|
+
* The digits of a document number with the series and the hyphens taken
|
|
64
|
+
* off, which is what a payment reference is built over.
|
|
65
|
+
*/
|
|
66
|
+
export const digitsOfDocumentNumber = (number) => number.slice(number.indexOf('-') + 1).replace(NOT_A_DIGIT, '');
|
|
67
|
+
/**
|
|
68
|
+
* The client code printed inside a document number. Every document
|
|
69
|
+
* number that exists was either built from a client code or read back
|
|
70
|
+
* through that code's own check digit, so the eight digits at the front
|
|
71
|
+
* of one are always a code that holds.
|
|
72
|
+
*/
|
|
73
|
+
export const clientCodeOfDocumentNumber = (number) => digitsOfDocumentNumber(number).slice(0, CLIENT_CODE_DIGITS);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ClosedSet } from '@satorio/machinery/core';
|
|
2
|
+
import type { DocumentNumberCounter, DocumentNumberCounterUnavailableError } from '@satorio/machinery/ports';
|
|
3
|
+
import type { Effect, Either } from 'effect';
|
|
4
|
+
import { type DocumentNumber, type DocumentNumberParts } from './document-number.js';
|
|
5
|
+
import type { NotADocumentNumberError } from './failures.js';
|
|
6
|
+
import { type NextDocumentNumberInput } from './next-document-number.js';
|
|
7
|
+
/**
|
|
8
|
+
* The document numbers of one closed set of series: minting bound to
|
|
9
|
+
* that set, reading that refuses any series outside it, and the set
|
|
10
|
+
* itself, so a caller lists its own series in one place and reads them
|
|
11
|
+
* back from the same one.
|
|
12
|
+
*/
|
|
13
|
+
export interface DocumentNumbers<Series extends readonly string[]> {
|
|
14
|
+
readonly series: ClosedSet<Series>;
|
|
15
|
+
readonly of: (parts: DocumentNumberParts<Series[number]>) => Either.Either<DocumentNumber, NotADocumentNumberError>;
|
|
16
|
+
readonly from: (text: string) => Either.Either<DocumentNumber, NotADocumentNumberError>;
|
|
17
|
+
readonly seriesOf: (number: DocumentNumber) => Series[number];
|
|
18
|
+
readonly next: (input: NextDocumentNumberInput<Series[number]>) => Effect.Effect<DocumentNumber, DocumentNumberCounterUnavailableError, DocumentNumberCounter>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Binds document numbers to the series one service issues. Which kinds
|
|
22
|
+
* of document exist is that service's own canon, never this package's:
|
|
23
|
+
* a proforma, a monthly invoice and a credit note belong to whoever
|
|
24
|
+
* sends them, and a package that hard-codes those four sends every
|
|
25
|
+
* other business back to writing its own numbering.
|
|
26
|
+
*/
|
|
27
|
+
export declare const makeDocumentNumbers: <const Series extends readonly string[]>(series: ClosedSet<Series>) => DocumentNumbers<Series>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { documentNumberOf, documentNumberReadFrom, seriesOfDocumentNumber, } from './document-number.js';
|
|
2
|
+
import { nextDocumentNumber, } from './next-document-number.js';
|
|
3
|
+
/**
|
|
4
|
+
* Binds document numbers to the series one service issues. Which kinds
|
|
5
|
+
* of document exist is that service's own canon, never this package's:
|
|
6
|
+
* a proforma, a monthly invoice and a credit note belong to whoever
|
|
7
|
+
* sends them, and a package that hard-codes those four sends every
|
|
8
|
+
* other business back to writing its own numbering.
|
|
9
|
+
*/
|
|
10
|
+
export const makeDocumentNumbers = (series) => ({
|
|
11
|
+
series,
|
|
12
|
+
of: (parts) => documentNumberOf(parts),
|
|
13
|
+
from: (text) => documentNumberReadFrom(text, series),
|
|
14
|
+
seriesOf: (number) => seriesOfDocumentNumber(number),
|
|
15
|
+
next: (input) => nextDocumentNumber(input),
|
|
16
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Refusal } from '@satorio/machinery/core';
|
|
2
|
+
declare const NotAClientCodeError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
3
|
+
readonly _tag: "NotAClientCodeError";
|
|
4
|
+
} & Readonly<A>;
|
|
5
|
+
/**
|
|
6
|
+
* The text handed in is not a client code. The refusal never says which
|
|
7
|
+
* character failed: a client code is a public label, and a message that
|
|
8
|
+
* narrows the search helps whoever is guessing at codes more than it
|
|
9
|
+
* helps the person who typed one wrong.
|
|
10
|
+
*/
|
|
11
|
+
export declare class NotAClientCodeError extends NotAClientCodeError_base<{
|
|
12
|
+
readonly text: string;
|
|
13
|
+
}> {
|
|
14
|
+
get refusal(): Refusal;
|
|
15
|
+
}
|
|
16
|
+
declare const NotADocumentNumberError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
17
|
+
readonly _tag: "NotADocumentNumberError";
|
|
18
|
+
} & Readonly<A>;
|
|
19
|
+
/**
|
|
20
|
+
* The parts handed in cannot make a document number, or the text handed
|
|
21
|
+
* in does not read as one.
|
|
22
|
+
*/
|
|
23
|
+
export declare class NotADocumentNumberError extends NotADocumentNumberError_base<{
|
|
24
|
+
readonly text: string;
|
|
25
|
+
}> {
|
|
26
|
+
get refusal(): Refusal;
|
|
27
|
+
}
|
|
28
|
+
declare const NotACreditorReferenceError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
29
|
+
readonly _tag: "NotACreditorReferenceError";
|
|
30
|
+
} & Readonly<A>;
|
|
31
|
+
/**
|
|
32
|
+
* The text handed in is not a payment reference this mechanism issued.
|
|
33
|
+
* Like a client code, the refusal names no character: whoever typed one
|
|
34
|
+
* wrong retypes the whole reference, and whoever is guessing learns
|
|
35
|
+
* nothing.
|
|
36
|
+
*/
|
|
37
|
+
export declare class NotACreditorReferenceError extends NotACreditorReferenceError_base<{
|
|
38
|
+
readonly text: string;
|
|
39
|
+
}> {
|
|
40
|
+
get refusal(): Refusal;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Every way this mechanism refuses a value.
|
|
44
|
+
*/
|
|
45
|
+
export type IdentifierFailure = NotAClientCodeError | NotADocumentNumberError | NotACreditorReferenceError;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Data } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* The text handed in is not a client code. The refusal never says which
|
|
4
|
+
* character failed: a client code is a public label, and a message that
|
|
5
|
+
* narrows the search helps whoever is guessing at codes more than it
|
|
6
|
+
* helps the person who typed one wrong.
|
|
7
|
+
*/
|
|
8
|
+
export class NotAClientCodeError extends Data.TaggedError('NotAClientCodeError') {
|
|
9
|
+
get refusal() {
|
|
10
|
+
return {
|
|
11
|
+
what: `${JSON.stringify(this.text)} is not a client code.`,
|
|
12
|
+
why: 'A client code is eight digits, the last of which checks the other ' +
|
|
13
|
+
'seven, and this text does not pass that check.',
|
|
14
|
+
whatToDo: 'Read the code off the document again, keeping all eight digits, ' +
|
|
15
|
+
'then hand it back.',
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The parts handed in cannot make a document number, or the text handed
|
|
21
|
+
* in does not read as one.
|
|
22
|
+
*/
|
|
23
|
+
export class NotADocumentNumberError extends Data.TaggedError('NotADocumentNumberError') {
|
|
24
|
+
get refusal() {
|
|
25
|
+
return {
|
|
26
|
+
what: `${JSON.stringify(this.text)} is not a document number.`,
|
|
27
|
+
why: 'A document number is a series, a client code, the reporting month ' +
|
|
28
|
+
'as four digits, and a count, joined by hyphens.',
|
|
29
|
+
whatToDo: 'Write it as INV-4173-8297-2609-02, then hand it back.',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The text handed in is not a payment reference this mechanism issued.
|
|
35
|
+
* Like a client code, the refusal names no character: whoever typed one
|
|
36
|
+
* wrong retypes the whole reference, and whoever is guessing learns
|
|
37
|
+
* nothing.
|
|
38
|
+
*/
|
|
39
|
+
export class NotACreditorReferenceError extends Data.TaggedError('NotACreditorReferenceError') {
|
|
40
|
+
get refusal() {
|
|
41
|
+
return {
|
|
42
|
+
what: `${JSON.stringify(this.text)} is not a payment reference that exists.`,
|
|
43
|
+
why: 'A payment reference carries its own two check digits, and this ' +
|
|
44
|
+
'text does not pass them.',
|
|
45
|
+
whatToDo: 'Copy the reference from the document again, then hand it back.',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|