1import * as frauddetector from "@distilled.cloud/aws/frauddetector";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import { createPhysicalName } from "../../PhysicalName.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { createInternalTags, hasAlchemyTags } from "../../Tags.ts";
10import type { Providers } from "../Providers.ts";
11import { readFraudDetectorTags, syncFraudDetectorTags } from "./internal.ts";
12
13export interface OutcomeProps {
14 /**
15 * Name of the outcome. If omitted, a unique lowercase name is generated from
16 * the app, stage, and logical ID. Changing the name replaces the outcome.
17 */
18 name?: string;
19 /**
20 * Human-readable description. This is an in-place update.
21 */
22 description?: string;
23 /**
24 * User-defined tags for the outcome.
25 */
26 tags?: Record<string, string>;
27}
28
29export interface Outcome extends Resource<
30 "AWS.FraudDetector.Outcome",
31 OutcomeProps,
32 {
33 /** The name of the outcome. */
34 name: string;
35 /** The ARN of the outcome. */
36 arn: string;
37 },
38 never,
39 Providers
40> {}
41
42/**
43 * An Amazon Fraud Detector outcome — the result a rule produces when it matches
44 * (e.g. `approve`, `review`, `block`). Detector rules reference outcomes; they
45 * are cheap metadata objects.
46 *
47 * ### Creating an Outcome
48 * **Example:** Approve and Review Outcomes
49 * ```typescript
50 * const approve = yield* FraudDetector.Outcome("approve", {
51 * description: "let the transaction through",
52 * });
53 * const review = yield* FraudDetector.Outcome("review", {});
54 * ```
55 *
56 * @resource
57 */
58export const Outcome = Resource<Outcome>("AWS.FraudDetector.Outcome");
59
60export const OutcomeProvider = () =>
61 Provider.effect(
62 Outcome,
63 Effect.gen(function* () {
64 const createName = Effect.fn(function* (id: string, props: OutcomeProps) {
65 return (
66 props.name ??
67 (yield* createPhysicalName({ id, maxLength: 64, lowercase: true }))
68 );
69 });
70
71 /** Look an outcome up by name; typed not-found → undefined. */
72 const get = Effect.fn(function* (name: string) {
73 const response = yield* frauddetector
74 .getOutcomes({ name })
75 .pipe(
76 Effect.catchTag("ResourceNotFoundException", () =>
77 Effect.succeed(undefined),
78 ),
79 );
80 return response?.outcomes?.[0];
81 });
82
83 const toAttrs = (outcome: frauddetector.Outcome) => ({
84 name: outcome.name!,
85 arn: outcome.arn!,
86 });
87
88 return {
89 stables: ["name", "arn"],
90
91 diff: Effect.fn(function* ({ id, olds = {}, news }) {
92 if (!isResolved(news)) return undefined;
93 const oldName = yield* createName(id, olds);
94 const newName = yield* createName(id, news);
95 if (oldName !== newName) {
96 return { action: "replace" } as const;
97 }
98 }),
99
100 read: Effect.fn(function* ({ id, olds, output }) {
101 const name = output?.name ?? (yield* createName(id, olds ?? {}));
102 const outcome = yield* get(name);
103 if (outcome === undefined) return undefined;
104 const attrs = toAttrs(outcome);
105 const tags = yield* readFraudDetectorTags(outcome.arn!);
106 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
107 }),
108
109 reconcile: Effect.fn(function* ({ id, news = {}, session }) {
110 const name = yield* createName(id, news);
111 const internalTags = yield* createInternalTags(id);
112 const desiredTags = { ...internalTags, ...news.tags };
113
114 // putOutcome is an idempotent upsert — call it to converge whether
115 // creating or updating the description.
116 yield* frauddetector.putOutcome({
117 name,
118 description: news.description,
119 });
120
121 const outcome = yield* get(name);
122 // Sync tags — diff against OBSERVED cloud tags.
123 yield* syncFraudDetectorTags(outcome!.arn!, desiredTags);
124
125 yield* session.note(name);
126 return toAttrs(outcome!);
127 }),
128
129 delete: Effect.fn(function* ({ output }) {
130 yield* frauddetector.deleteOutcome({ name: output.name }).pipe(
131 // Deleting an already-removed outcome is a no-op for us; Fraud
132 // Detector surfaces a missing outcome as a validation error.
133 Effect.catchTag("ValidationException", () => Effect.void),
134 );
135 }),
136
137 list: () =>
138 frauddetector.getOutcomes.pages({}).pipe(
139 Stream.runCollect,
140 Effect.map((chunk) =>
141 Array.from(chunk).flatMap((page) =>
142 (page.outcomes ?? []).map(toAttrs),
143 ),
144 ),
145 ),
146 };
147 }),
148 );