1import * as b2bi from "@distilled.cloud/aws/b2bi";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import { createInternalTags, hasAlchemyTags } from "../../Tags.ts";
9import type { Providers } from "../Providers.ts";
10import { readB2biTags, syncB2biTags, toWireTags } from "./internal.ts";
11
12export interface PartnershipProps {
13 /**
14 * The unique identifier for the profile connected to this partnership.
15 * May reference a {@link Profile}'s `profileId` output. Changing the
16 * profile replaces the partnership.
17 */
18 profileId: string;
19 /**
20 * The name of the partnership.
21 */
22 name: string;
23 /**
24 * The email address associated with the trading partner.
25 */
26 email: string;
27 /**
28 * The phone number associated with the trading partner, in E.164 format.
29 */
30 phone?: string;
31 /**
32 * A list of capability IDs (see {@link Capability}) enabled for this
33 * partnership.
34 */
35 capabilities?: string[];
36 /**
37 * Inbound/outbound EDI options that override the defaults for this
38 * partnership.
39 */
40 capabilityOptions?: b2bi.CapabilityOptions;
41 /**
42 * User-defined tags for the partnership.
43 */
44 tags?: Record<string, string>;
45}
46
47export interface Partnership extends Resource<
48 "AWS.B2BI.Partnership",
49 PartnershipProps,
50 {
51 /**
52 * Service-assigned unique ID of the partnership.
53 */
54 partnershipId: string;
55 /**
56 * ARN of the partnership.
57 */
58 partnershipArn: string;
59 /**
60 * ID of the profile the partnership belongs to.
61 */
62 profileId: string;
63 /**
64 * Service-assigned ID of the trading partner.
65 */
66 tradingPartnerId: string | undefined;
67 },
68 never,
69 Providers
70> {}
71
72/**
73 * An AWS B2B Data Interchange (B2BI) partnership. A partnership connects a
74 * customer {@link Profile} to a trading partner and enables a set of
75 * {@link Capability | capabilities} for exchanging EDI documents.
76 * ### Creating a Partnership
77 * **Example:** Basic Partnership
78 * ```typescript
79 * const partnership = yield* B2BI.Partnership("AcmeToPartner", {
80 * profileId: profile.profileId,
81 * name: "acme-partner",
82 * email: "edi@partner.example",
83 * capabilities: [capability.capabilityId],
84 * });
85 * ```
86 *
87 * @resource
88 */
89export const Partnership = Resource<Partnership>("AWS.B2BI.Partnership");
90
91const toAttrs = (
92 r:
93 | b2bi.GetPartnershipResponse
94 | b2bi.CreatePartnershipResponse
95 | b2bi.UpdatePartnershipResponse,
96) => ({
97 partnershipId: r.partnershipId,
98 partnershipArn: r.partnershipArn,
99 profileId: r.profileId,
100 tradingPartnerId: r.tradingPartnerId,
101});
102
103export const PartnershipProvider = () =>
104 Provider.effect(
105 Partnership,
106 Effect.gen(function* () {
107 const findByName = (name: string) =>
108 b2bi.listPartnerships.items({}).pipe(
109 Stream.filter((s) => s.name === name),
110 Stream.runHead,
111 Effect.flatMap((head) =>
112 head._tag === "Some"
113 ? b2bi
114 .getPartnership({ partnershipId: head.value.partnershipId })
115 .pipe(
116 Effect.catchTag("ResourceNotFoundException", () =>
117 Effect.succeed(undefined),
118 ),
119 )
120 : Effect.succeed(undefined),
121 ),
122 );
123
124 return {
125 stables: ["partnershipId", "partnershipArn", "profileId"],
126
127 diff: Effect.fn(function* ({ olds = {}, news }) {
128 if (!isResolved(news)) return undefined;
129 // The profile a partnership belongs to is immutable. Delete-first:
130 // the replacement keeps the same user-facing name, which is how
131 // lost state is recovered (findByName), so the old instance must
132 // be gone before the new one is created.
133 if (
134 olds.profileId !== undefined &&
135 olds.profileId !== news.profileId
136 ) {
137 return { action: "replace", deleteFirst: true } as const;
138 }
139 }),
140
141 read: Effect.fn(function* ({ id, olds, output }) {
142 const found = output?.partnershipId
143 ? yield* b2bi
144 .getPartnership({ partnershipId: output.partnershipId })
145 .pipe(
146 Effect.catchTag("ResourceNotFoundException", () =>
147 Effect.succeed(undefined),
148 ),
149 )
150 : yield* findByName(olds?.name ?? "");
151 if (found === undefined) return undefined;
152 const attrs = toAttrs(found);
153 const tags = yield* readB2biTags(attrs.partnershipArn);
154 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
155 }),
156
157 reconcile: Effect.fn(function* ({ id, news, output, session }) {
158 const internalTags = yield* createInternalTags(id);
159 const desiredTags = { ...internalTags, ...news.tags };
160
161 // 1. Observe.
162 let live = output?.partnershipId
163 ? yield* b2bi
164 .getPartnership({ partnershipId: output.partnershipId })
165 .pipe(
166 Effect.catchTag("ResourceNotFoundException", () =>
167 Effect.succeed(undefined),
168 ),
169 )
170 : yield* findByName(news.name);
171
172 // 2. Ensure.
173 if (live === undefined) {
174 const created = yield* b2bi.createPartnership({
175 profileId: news.profileId,
176 name: news.name,
177 email: news.email,
178 phone: news.phone,
179 capabilities: news.capabilities ?? [],
180 capabilityOptions: news.capabilityOptions,
181 });
182 live = yield* b2bi.getPartnership({
183 partnershipId: created.partnershipId,
184 });
185 } else {
186 // 3. Sync — converge name, capabilities, and capability options
187 // (email/phone/profile are set only at create time).
188 const nameDrift = live.name !== news.name;
189 const capsDrift =
190 JSON.stringify([...(live.capabilities ?? [])].sort()) !==
191 JSON.stringify([...(news.capabilities ?? [])].sort());
192 const optsDrift =
193 JSON.stringify(live.capabilityOptions ?? null) !==
194 JSON.stringify(news.capabilityOptions ?? null);
195 if (nameDrift || capsDrift || optsDrift) {
196 const updated = yield* b2bi.updatePartnership({
197 partnershipId: live.partnershipId,
198 name: news.name,
199 capabilities: news.capabilities,
200 capabilityOptions: news.capabilityOptions,
201 });
202 live = { ...live, ...updated };
203 }
204 }
205
206 // 3b. Sync tags.
207 yield* syncB2biTags(live.partnershipArn, desiredTags);
208
209 yield* session.note(live.partnershipId);
210 return toAttrs(live);
211 }),
212
213 delete: Effect.fn(function* ({ output }) {
214 yield* b2bi
215 .deletePartnership({ partnershipId: output.partnershipId })
216 .pipe(
217 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
218 );
219 }),
220
221 list: () =>
222 b2bi.listPartnerships.items({}).pipe(
223 Stream.mapEffect((s) =>
224 b2bi.getPartnership({ partnershipId: s.partnershipId }).pipe(
225 Effect.map(toAttrs),
226 Effect.catchTag("ResourceNotFoundException", () =>
227 Effect.succeed(undefined),
228 ),
229 ),
230 ),
231 Stream.filter((item) => item !== undefined),
232 Stream.runCollect,
233 Effect.map((chunk) => Array.from(chunk)),
234 ),
235 };
236 }),
237 );