@cullet/erp-core 1.0.2 → 1.0.3
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/README.md +24 -24
- package/dist/gate-engine-registry.js.map +1 -1
- package/dist/gate-v1-payload.schema.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/outcome.js.map +1 -1
- package/dist/policies/engines/v1/gate/index.js.map +1 -1
- package/dist/policy-service.js.map +1 -1
- package/dist/validation-code.js.map +1 -1
- package/dist/validation-error.js.map +1 -1
- package/dist/validation-field.js.map +1 -1
- package/meta.json +67 -67
- package/package.json +56 -56
- package/src/core/application/commands/command.ts +3 -3
- package/src/core/application/commands/requested-by.ts +60 -60
- package/src/core/application/index.ts +21 -21
- package/src/core/application/policy-error-mapper.ts +54 -54
- package/src/core/application/ports/index.ts +6 -6
- package/src/core/application/ports/logger.port.ts +4 -4
- package/src/core/application/ports/metrics.port.ts +3 -3
- package/src/core/application/ports/policy-port.ts +7 -7
- package/src/core/application/ports/repository.port.ts +3 -3
- package/src/core/application/ports/temporal-repository.port.ts +4 -4
- package/src/core/application/ports/tracer.port.ts +7 -7
- package/src/core/application/queries/query.ts +13 -13
- package/src/core/application/temporal/index.ts +7 -7
- package/src/core/application/temporal/temporal-context.ts +20 -20
- package/src/core/application/temporal/temporal-use-case.ts +22 -22
- package/src/core/application/use-case.ts +10 -10
- package/src/core/config/core-config.instance.ts +1 -1
- package/src/core/config/core-config.ts +59 -59
- package/src/core/config/index.ts +6 -6
- package/src/core/config/policy-reporter.ts +31 -31
- package/src/core/config/silent-policy-reporter.ts +1 -1
- package/src/core/domain/entity.ts +44 -44
- package/src/core/domain/rulesets/entity-ruleset.contracts.ts +6 -6
- package/src/core/domain/rulesets/ruleset-registry.ts +49 -49
- package/src/core/domain/rulesets/ruleset.contracts.ts +6 -6
- package/src/core/domain/rulesets/value-object-ruleset.contracts.ts +5 -5
- package/src/core/domain/temporal/index.ts +11 -11
- package/src/core/domain/temporal/temporal-range.ts +16 -16
- package/src/core/domain/temporal/temporal-snapshot.ts +24 -24
- package/src/core/domain/temporal/transaction-time.ts +30 -30
- package/src/core/domain/temporal/valid-time.ts +29 -29
- package/src/core/domain/value-object.ts +16 -16
- package/src/core/errors/app-error.ts +64 -64
- package/src/core/errors/authentication-error.ts +134 -134
- package/src/core/errors/authorization-error.ts +154 -154
- package/src/core/errors/business-rule-violation-error.ts +15 -15
- package/src/core/errors/conflict-error.ts +202 -201
- package/src/core/errors/error-codes.ts +48 -48
- package/src/core/errors/idempotency-error.ts +324 -320
- package/src/core/errors/index.ts +50 -50
- package/src/core/errors/integration-error.ts +120 -120
- package/src/core/errors/legacy-incompatible-error.ts +13 -13
- package/src/core/errors/not-found-error.ts +14 -14
- package/src/core/errors/serialization-error.ts +167 -167
- package/src/core/errors/temporal-error.ts +109 -107
- package/src/core/errors/types.ts +35 -35
- package/src/core/errors/unexpected-error.ts +10 -10
- package/src/core/errors/utils/index.ts +2 -2
- package/src/core/errors/utils/json-safe.ts +53 -53
- package/src/core/errors/validation-error.ts +18 -18
- package/src/core/exceptions/business-rule-violation-exception.ts +6 -6
- package/src/core/exceptions/domain-exception.ts +5 -5
- package/src/core/exceptions/entity-not-found-exception.ts +6 -6
- package/src/core/exceptions/invalid-state-transition-exception.ts +8 -8
- package/src/core/exceptions/invariant-violation-exception.ts +3 -3
- package/src/core/exceptions/validation-code.ts +35 -29
- package/src/core/exceptions/validation-exception.ts +19 -19
- package/src/core/exceptions/validation-field.ts +20 -19
- package/src/core/index.ts +7 -7
- package/src/core/policies/asof/asof.ts +47 -47
- package/src/core/policies/catalog/catalog.instance.ts +8 -6
- package/src/core/policies/catalog/index.ts +4 -4
- package/src/core/policies/catalog/policy-catalog-entry.ts +199 -197
- package/src/core/policies/catalog/policy-catalog.ts +101 -101
- package/src/core/policies/catalog/policy-key.ts +51 -51
- package/src/core/policies/context/context-builder.ts +335 -313
- package/src/core/policies/context/context-registry.instance.ts +9 -9
- package/src/core/policies/context/context-registry.ts +61 -61
- package/src/core/policies/context/context-resolver.ts +12 -12
- package/src/core/policies/context/context-seed.ts +19 -15
- package/src/core/policies/context/index.ts +9 -9
- package/src/core/policies/context/path.ts +135 -128
- package/src/core/policies/defs/in-memory-policy-definition-repo.ts +56 -55
- package/src/core/policies/defs/index.ts +10 -10
- package/src/core/policies/defs/policy-definition-repository.ts +3 -3
- package/src/core/policies/defs/policy-definition.ts +143 -142
- package/src/core/policies/defs/policy-scope.ts +21 -19
- package/src/core/policies/defs/policy-semver.ts +51 -51
- package/src/core/policies/engines/compute-engine-registry.ts +43 -43
- package/src/core/policies/engines/compute-evaluator-registry.ts +20 -20
- package/src/core/policies/engines/compute-registry.ts +45 -45
- package/src/core/policies/engines/compute-types.ts +15 -15
- package/src/core/policies/engines/condition-evaluator-reporter.ts +17 -17
- package/src/core/policies/engines/gate-engine-registry.ts +27 -25
- package/src/core/policies/engines/gate-types.ts +28 -28
- package/src/core/policies/engines/index.ts +28 -28
- package/src/core/policies/engines/parse-compute-payload.ts +51 -45
- package/src/core/policies/engines/parse-gate-payload.ts +42 -42
- package/src/core/policies/engines/v1/compute/compute-engine-v1.ts +39 -37
- package/src/core/policies/engines/v1/compute/compute-payload.schema.ts +43 -41
- package/src/core/policies/engines/v1/compute/index.ts +6 -6
- package/src/core/policies/engines/v1/compute/resolve-decision-table-v1.ts +41 -39
- package/src/core/policies/engines/v1/condition-evaluator.ts +547 -505
- package/src/core/policies/engines/v1/condition-schema.ts +35 -35
- package/src/core/policies/engines/v1/condition-types.ts +21 -21
- package/src/core/policies/engines/v1/gate/gate-engine-v1.ts +134 -130
- package/src/core/policies/engines/v1/gate/gate-types-v1.ts +5 -5
- package/src/core/policies/engines/v1/gate/gate-v1-payload.schema.ts +32 -30
- package/src/core/policies/engines/v1/gate/index.ts +4 -4
- package/src/core/policies/engines/v1/index.ts +8 -8
- package/src/core/policies/index.ts +72 -72
- package/src/core/policies/package/policy-package.ts +3 -3
- package/src/core/policies/policy-ids.ts +25 -25
- package/src/core/policies/resolver/policy-resolver.ts +34 -34
- package/src/core/policies/service/index.ts +6 -6
- package/src/core/policies/service/policy-evaluation-error.ts +141 -133
- package/src/core/policies/service/policy-service.ts +432 -419
- package/src/core/policies/utils/date.ts +3 -3
- package/src/core/policies/utils/hash.ts +61 -61
- package/src/core/result/outcome.ts +131 -126
- package/src/core/result/result.ts +130 -127
- package/src/core/shared/aggregate-version.ts +5 -5
- package/src/core/shared/hashing.ts +18 -18
- package/src/core/shared/immutable.ts +29 -29
- package/src/core/shared/temporal-guards.ts +9 -9
- package/src/core/versioning/domain-event-contracts.ts +40 -40
- package/src/core/versioning/version.ts +19 -19
- package/src/examples/rulesets/entity/order-creation-rules-v1.ts +18 -18
- package/src/examples/rulesets/entity/order-invariants-v1.ts +31 -29
- package/src/examples/rulesets/entity/order-invariants-v2.ts +12 -12
- package/src/examples/rulesets/entity/order.ts +127 -127
- package/src/examples/rulesets/value-object/cpf-rules-v1.ts +41 -39
- package/src/examples/rulesets/value-object/cpf-rules-v2.ts +14 -12
- package/src/examples/rulesets/value-object/cpf.ts +24 -24
- package/src/examples/rulesets/value-object/customer.ts +42 -39
- package/src/examples/rulesets/value-object/person-name-rules-v1.ts +27 -27
- package/src/examples/rulesets/value-object/person-name-rules-v2.ts +13 -13
- package/src/examples/rulesets/value-object/person-name.ts +29 -26
- package/src/index.ts +2 -2
- package/src/version.ts +1 -1
|
@@ -12,79 +12,79 @@ import { isValidDate } from "../../shared/temporal-guards";
|
|
|
12
12
|
* prevent semantically different payloads from collapsing to the same hash.
|
|
13
13
|
*/
|
|
14
14
|
export class PolicyHashing {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
private static assertHashable(value: unknown, path: string): void {
|
|
20
|
-
if (value === null) {
|
|
21
|
-
return;
|
|
15
|
+
static sha256(input: string): string {
|
|
16
|
+
return sha256Hex(input);
|
|
22
17
|
}
|
|
23
18
|
|
|
24
|
-
|
|
19
|
+
private static assertHashable(value: unknown, path: string): void {
|
|
20
|
+
if (value === null) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
throw new TypeError(
|
|
28
|
-
`canonicalJson does not accept undefined values (at ${path})`,
|
|
29
|
-
);
|
|
30
|
-
}
|
|
24
|
+
const type = typeof value;
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
if (type === "undefined") {
|
|
27
|
+
throw new TypeError(
|
|
28
|
+
`canonicalJson does not accept undefined values (at ${path})`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
37
31
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
32
|
+
if (type === "number" && !Number.isFinite(value)) {
|
|
33
|
+
throw new TypeError(
|
|
34
|
+
`canonicalJson does not accept non-finite numbers (at ${path}, value: ${String(value)})`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
43
37
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
if (type === "bigint" || type === "function" || type === "symbol") {
|
|
39
|
+
throw new TypeError(
|
|
40
|
+
`canonicalJson does not accept ${type} values (at ${path})`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
47
43
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
`canonicalJson does not accept Invalid Date (at ${path})`,
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
44
|
+
if (type !== "object") {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
56
47
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
48
|
+
if (value instanceof Date) {
|
|
49
|
+
if (!isValidDate(value)) {
|
|
50
|
+
throw new TypeError(
|
|
51
|
+
`canonicalJson does not accept Invalid Date (at ${path})`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (Array.isArray(value)) {
|
|
58
|
+
value.forEach((item, index) => {
|
|
59
|
+
PolicyHashing.assertHashable(item, `${path}[${index}]`);
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
for (const [key, nested] of Object.entries(
|
|
65
|
+
value as Record<string, unknown>,
|
|
66
|
+
)) {
|
|
67
|
+
PolicyHashing.assertHashable(nested, `${path}.${key}`);
|
|
68
|
+
}
|
|
68
69
|
}
|
|
69
|
-
}
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
static canonicalJson(value: unknown): string {
|
|
72
|
+
PolicyHashing.assertHashable(value, "$");
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
return stableStringify(value);
|
|
75
|
+
}
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
77
|
+
static computePayloadHash(
|
|
78
|
+
payload: unknown,
|
|
79
|
+
policyKey: string,
|
|
80
|
+
policyVersion: string,
|
|
81
|
+
): string {
|
|
82
|
+
const canonical = PolicyHashing.canonicalJson({
|
|
83
|
+
policyKey,
|
|
84
|
+
policyVersion,
|
|
85
|
+
payload,
|
|
86
|
+
});
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
return PolicyHashing.sha256(canonical);
|
|
89
|
+
}
|
|
90
90
|
}
|
|
@@ -19,140 +19,145 @@ import { InvariantViolationException } from "../exceptions/invariant-violation-e
|
|
|
19
19
|
* @typeParam D - Shape of the decision data carried by this outcome
|
|
20
20
|
*/
|
|
21
21
|
export class Outcome<S extends string, D = undefined> {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ─── Generic factory ────────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
static of<S extends string, D = undefined>(
|
|
43
|
-
status: S,
|
|
44
|
-
data: D,
|
|
45
|
-
reason?: string,
|
|
46
|
-
metadata?: Record<string, unknown>,
|
|
47
|
-
): Outcome<S, D> {
|
|
48
|
-
return new Outcome(status, data, reason, Object.freeze({ ...metadata }));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// ─── Convenience factories for common business statuses ─────────────
|
|
52
|
-
|
|
53
|
-
static approved<D = undefined>(
|
|
54
|
-
data: D,
|
|
55
|
-
reason?: string,
|
|
56
|
-
): Outcome<"APPROVED", D> {
|
|
57
|
-
return new Outcome("APPROVED", data, reason, Object.freeze({}));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
static rejected<D = undefined>(
|
|
61
|
-
data: D,
|
|
62
|
-
reason: string,
|
|
63
|
-
): Outcome<"REJECTED", D> {
|
|
64
|
-
return new Outcome("REJECTED", data, reason, Object.freeze({}));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
static noOp<D = undefined>(data: D, reason?: string): Outcome<"NO_OP", D> {
|
|
68
|
-
return new Outcome("NO_OP", data, reason, Object.freeze({}));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
static deferred<D = undefined>(
|
|
72
|
-
data: D,
|
|
73
|
-
reason: string,
|
|
74
|
-
): Outcome<"DEFERRED", D> {
|
|
75
|
-
return new Outcome("DEFERRED", data, reason, Object.freeze({}));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
static requiresReview<D = undefined>(
|
|
79
|
-
data: D,
|
|
80
|
-
reason: string,
|
|
81
|
-
): Outcome<"REQUIRES_REVIEW", D> {
|
|
82
|
-
return new Outcome("REQUIRES_REVIEW", data, reason, Object.freeze({}));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ─── Type narrowing ────────────────────────────────────────────────
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Narrows this Outcome to a specific status.
|
|
89
|
-
*
|
|
90
|
-
* @example
|
|
91
|
-
* if (outcome.is('DENY')) {
|
|
92
|
-
* outcome.data.violations // TS knows status is 'DENY'
|
|
93
|
-
* }
|
|
94
|
-
*/
|
|
95
|
-
is<T extends S>(status: T): this is Outcome<T, D> {
|
|
96
|
-
return (this.status as string) === status;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// ─── Exhaustive match ──────────────────────────────────────────────
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Exhaustive pattern match over all possible statuses.
|
|
103
|
-
* TypeScript enforces that every status has a handler.
|
|
104
|
-
*
|
|
105
|
-
* @example
|
|
106
|
-
* outcome.match({
|
|
107
|
-
* ALLOW: (o) => handleAllow(o.data),
|
|
108
|
-
* DENY: (o) => handleDeny(o.data.violations),
|
|
109
|
-
* })
|
|
110
|
-
*/
|
|
111
|
-
match<THandlers extends { [K in S]: (outcome: Outcome<K, D>) => unknown }>(
|
|
112
|
-
handlers: THandlers,
|
|
113
|
-
): ReturnType<THandlers[S]> {
|
|
114
|
-
const handler = (
|
|
115
|
-
handlers as Partial<Record<string, (o: unknown) => unknown>>
|
|
116
|
-
)[this.status];
|
|
117
|
-
|
|
118
|
-
if (typeof handler !== "function") {
|
|
119
|
-
const availableStatuses = Object.keys(handlers);
|
|
120
|
-
throw new InvariantViolationException(
|
|
121
|
-
`Outcome.match is missing a handler for runtime status "${this.status}". Available handlers: ${availableStatuses.join(", ") || "(none)"}`,
|
|
122
|
-
);
|
|
22
|
+
readonly status: S;
|
|
23
|
+
readonly data: D;
|
|
24
|
+
readonly reason: string | undefined;
|
|
25
|
+
readonly metadata: Readonly<Record<string, unknown>>;
|
|
26
|
+
|
|
27
|
+
private constructor(
|
|
28
|
+
status: S,
|
|
29
|
+
data: D,
|
|
30
|
+
reason: string | undefined,
|
|
31
|
+
metadata: Readonly<Record<string, unknown>>,
|
|
32
|
+
) {
|
|
33
|
+
this.status = status;
|
|
34
|
+
this.data = data;
|
|
35
|
+
this.reason = reason;
|
|
36
|
+
this.metadata = metadata;
|
|
37
|
+
Object.freeze(this);
|
|
123
38
|
}
|
|
124
39
|
|
|
125
|
-
|
|
126
|
-
|
|
40
|
+
// ─── Generic factory ────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
static of<S extends string, D = undefined>(
|
|
43
|
+
status: S,
|
|
44
|
+
data: D,
|
|
45
|
+
reason?: string,
|
|
46
|
+
metadata?: Record<string, unknown>,
|
|
47
|
+
): Outcome<S, D> {
|
|
48
|
+
return new Outcome(
|
|
49
|
+
status,
|
|
50
|
+
data,
|
|
51
|
+
reason,
|
|
52
|
+
Object.freeze({ ...metadata }),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Convenience factories for common business statuses ─────────────
|
|
57
|
+
|
|
58
|
+
static approved<D = undefined>(
|
|
59
|
+
data: D,
|
|
60
|
+
reason?: string,
|
|
61
|
+
): Outcome<"APPROVED", D> {
|
|
62
|
+
return new Outcome("APPROVED", data, reason, Object.freeze({}));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static rejected<D = undefined>(
|
|
66
|
+
data: D,
|
|
67
|
+
reason: string,
|
|
68
|
+
): Outcome<"REJECTED", D> {
|
|
69
|
+
return new Outcome("REJECTED", data, reason, Object.freeze({}));
|
|
70
|
+
}
|
|
127
71
|
|
|
128
|
-
|
|
72
|
+
static noOp<D = undefined>(data: D, reason?: string): Outcome<"NO_OP", D> {
|
|
73
|
+
return new Outcome("NO_OP", data, reason, Object.freeze({}));
|
|
74
|
+
}
|
|
129
75
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
);
|
|
137
|
-
}
|
|
76
|
+
static deferred<D = undefined>(
|
|
77
|
+
data: D,
|
|
78
|
+
reason: string,
|
|
79
|
+
): Outcome<"DEFERRED", D> {
|
|
80
|
+
return new Outcome("DEFERRED", data, reason, Object.freeze({}));
|
|
81
|
+
}
|
|
138
82
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
83
|
+
static requiresReview<D = undefined>(
|
|
84
|
+
data: D,
|
|
85
|
+
reason: string,
|
|
86
|
+
): Outcome<"REQUIRES_REVIEW", D> {
|
|
87
|
+
return new Outcome("REQUIRES_REVIEW", data, reason, Object.freeze({}));
|
|
88
|
+
}
|
|
142
89
|
|
|
143
|
-
|
|
90
|
+
// ─── Type narrowing ────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Narrows this Outcome to a specific status.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* if (outcome.is('DENY')) {
|
|
97
|
+
* outcome.data.violations // TS knows status is 'DENY'
|
|
98
|
+
* }
|
|
99
|
+
*/
|
|
100
|
+
is<T extends S>(status: T): this is Outcome<T, D> {
|
|
101
|
+
return (this.status as string) === status;
|
|
102
|
+
}
|
|
144
103
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
104
|
+
// ─── Exhaustive match ──────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Exhaustive pattern match over all possible statuses.
|
|
108
|
+
* TypeScript enforces that every status has a handler.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* outcome.match({
|
|
112
|
+
* ALLOW: (o) => handleAllow(o.data),
|
|
113
|
+
* DENY: (o) => handleDeny(o.data.violations),
|
|
114
|
+
* })
|
|
115
|
+
*/
|
|
116
|
+
match<THandlers extends { [K in S]: (outcome: Outcome<K, D>) => unknown }>(
|
|
117
|
+
handlers: THandlers,
|
|
118
|
+
): ReturnType<THandlers[S]> {
|
|
119
|
+
const handler = (
|
|
120
|
+
handlers as Partial<Record<string, (o: unknown) => unknown>>
|
|
121
|
+
)[this.status];
|
|
122
|
+
|
|
123
|
+
if (typeof handler !== "function") {
|
|
124
|
+
const availableStatuses = Object.keys(handlers);
|
|
125
|
+
throw new InvariantViolationException(
|
|
126
|
+
`Outcome.match is missing a handler for runtime status "${this.status}". Available handlers: ${availableStatuses.join(", ") || "(none)"}`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return handler(this) as ReturnType<THandlers[S]>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── Derived copies ────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
withMetadata(extra: Record<string, unknown>): Outcome<S, D> {
|
|
136
|
+
return new Outcome(
|
|
137
|
+
this.status,
|
|
138
|
+
this.data,
|
|
139
|
+
this.reason,
|
|
140
|
+
Object.freeze({ ...this.metadata, ...extra }),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
withReason(reason: string): Outcome<S, D> {
|
|
145
|
+
return new Outcome(this.status, this.data, reason, this.metadata);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─── Debug ─────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
toString(): string {
|
|
151
|
+
const parts: string[] = [this.status];
|
|
152
|
+
if (this.reason) parts.push(`reason="${this.reason}"`);
|
|
153
|
+
return `Outcome(${parts.join(", ")})`;
|
|
154
|
+
}
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
/** Common business outcome statuses. Extend per domain as needed. */
|
|
153
158
|
export type CommonOutcomeStatus =
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
+
| "APPROVED"
|
|
160
|
+
| "REJECTED"
|
|
161
|
+
| "NO_OP"
|
|
162
|
+
| "DEFERRED"
|
|
163
|
+
| "REQUIRES_REVIEW";
|