1import * as cloudchannel from "@distilled.cloud/gcp/cloudchannel_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import type { Providers } from "../Providers.ts";
9import {
10 accountOf,
11 desiredCustomer,
12 findCustomerByDomain,
13 getPartnerCustomer,
14 jsonEqual,
15 lastSegment,
16 listPartnerCustomers,
17 replaceOnIdentity,
18 sameText,
19 toChannelPartnerLinkName,
20 toCustomerAttrs,
21 toCustomerName,
22 toDomain,
23 toOrgDisplayName,
24 updateMaskOf,
25} from "./internal.ts";
26
27export type ChannelPartnerLinksCustomerProps = {
28 /**
29 * Channel partner link that owns the customer. Full name
30 * `accounts/{account}/channelPartnerLinks/{channelPartner}` or the
31 * partner id (combined with `account`). Immutable — changing it
32 * replaces the customer.
33 */
34 parent: string;
35 /**
36 * Reseller account used when `parent` is a bare partner id.
37 */
38 account?: string;
39 /**
40 * Customer id (last segment of `accounts/{account}/customers/{customer}`).
41 * Server-assigned on create. Immutable — changing it replaces the
42 * customer.
43 */
44 customerId?: string;
45 /**
46 * Organization display name. If omitted, a unique name is generated.
47 */
48 orgDisplayName?: string;
49 /**
50 * Primary domain. Must match `primaryContactInfo.email`'s domain. If
51 * omitted, a unique `{name}.example.com` domain is generated.
52 */
53 domain?: string;
54 /**
55 * Organization postal address. A US address is used when omitted.
56 */
57 orgPostalAddress?: cloudchannel.GoogleTypePostalAddress;
58 /**
59 * Primary contact. Email defaults to `admin@{domain}`.
60 */
61 primaryContactInfo?: cloudchannel.GoogleCloudChannelV1ContactInfo;
62 /**
63 * Secondary contact email used as a recovery address.
64 */
65 alternateEmail?: string;
66 /**
67 * BCP-47 language code such as `en-US`.
68 * @default "en-US"
69 */
70 languageCode?: string;
71 /**
72 * External CRM id.
73 */
74 correlationId?: string;
75 /**
76 * Attestation that provided information is correct. Required for GCP
77 * entitlements.
78 */
79 customerAttestationState?:
80 | cloudchannel.GoogleCloudChannelV1CustomerCustomerAttestationStateEnum
81 | (string & {});
82};
83
84export type ChannelPartnerLinksCustomer = Resource<
85 "GCP.CloudChannel.ChannelPartnerLinksCustomer",
86 ChannelPartnerLinksCustomerProps,
87 {
88 /** Resource name `accounts/{account}/customers/{customer}`. */
89 name: string;
90 /** Customer id (last path segment). */
91 customerId: string;
92 /** Channel partner link parent. */
93 parent: string;
94 /** Reseller account `accounts/{account}`. */
95 account: string;
96 /** Organization display name. */
97 orgDisplayName: string | undefined;
98 /** Primary domain. */
99 domain: string | undefined;
100 /** Organization postal address. */
101 orgPostalAddress: cloudchannel.GoogleTypePostalAddress | undefined;
102 /** Primary contact. */
103 primaryContactInfo:
104 | cloudchannel.GoogleCloudChannelV1ContactInfo
105 | undefined;
106 /** Alternate email. */
107 alternateEmail: string | undefined;
108 /** BCP-47 language code. */
109 languageCode: string | undefined;
110 /** External CRM id. */
111 correlationId: string | undefined;
112 /** Customer attestation state. */
113 customerAttestationState: string | undefined;
114 /** Channel partner Cloud Identity id. */
115 channelPartnerId: string | undefined;
116 /** Cloud Identity id, once provisioned. */
117 cloudIdentityId: string | undefined;
118 /** RFC3339 creation timestamp. */
119 createTime: string | undefined;
120 /** RFC3339 last-update timestamp. */
121 updateTime: string | undefined;
122 },
123 never,
124 Providers
125>;
126
127/**
128 * A Cloud Channel customer created under a channel partner link.
129 *
130 * Partner customers have no labels field, so ownership rests on the
131 * deterministic domain: `read` reports a customer it finds without prior
132 * state as unowned (adopt it with `--adopt`). The channel partner link (`parent`)
133 * and customer id are identity. Display name, address, contact, domain,
134 * language, and attestation update in place.
135 *
136 * Creating partner customers requires Cloud Channel distributor access.
137 *
138 * ### Creating a Partner Customer
139 * **Example:** Generated domain
140 * ```typescript
141 * const customer = yield* GCP.CloudChannel.ChannelPartnerLinksCustomer(
142 * "Resold",
143 * {
144 * parent: "accounts/C012345/channelPartnerLinks/C987654",
145 * orgDisplayName: "Resold Corp",
146 * },
147 * );
148 * ```
149 *
150 * **Example:** Explicit domain
151 * ```typescript
152 * const customer = yield* GCP.CloudChannel.ChannelPartnerLinksCustomer(
153 * "Resold",
154 * {
155 * parent: "accounts/C012345/channelPartnerLinks/C987654",
156 * orgDisplayName: "Resold Corp",
157 * domain: "resold.example.com",
158 * primaryContactInfo: {
159 * email: "admin@resold.example.com",
160 * },
161 * },
162 * );
163 * ```
164 *
165 * @resource
166 * @category CloudChannel
167 */
168export const ChannelPartnerLinksCustomer =
169 Resource<ChannelPartnerLinksCustomer>(
170 "GCP.CloudChannel.ChannelPartnerLinksCustomer",
171 );
172
173export class ChannelPartnerLinksCustomerNotResolved extends Data.TaggedError(
174 "GCP.CloudChannel.ChannelPartnerLinksCustomerNotResolved",
175)<{
176 name: string;
177}> {}
178
179const resolveParent = (newsParent: string, account?: string) =>
180 toChannelPartnerLinkName(newsParent, account ?? accountOf(newsParent));
181
182export const ChannelPartnerLinksCustomerProvider = () =>
183 Provider.succeed(ChannelPartnerLinksCustomer, {
184 stables: ["name", "customerId", "account", "createTime"],
185
186 diff: Effect.fn(function* ({ news, olds, output }) {
187 if (!isResolved(news)) return undefined;
188 const previousParent = olds?.parent ?? output?.parent;
189 const nextParent = resolveParent(news.parent, news.account);
190 return replaceOnIdentity({
191 previousId: olds?.customerId ?? output?.customerId,
192 nextId: news.customerId,
193 previousParent,
194 nextParent,
195 });
196 }),
197
198 read: Effect.fn(function* ({ id, olds, output }) {
199 const parent = resolveParent(
200 olds?.parent ?? output?.parent ?? "",
201 olds?.account ?? output?.account,
202 );
203 const name = toCustomerName(
204 parent,
205 olds?.customerId ?? output?.customerId ?? output?.name,
206 );
207 let existing = yield* getPartnerCustomer(output?.name ?? name);
208 if (existing === undefined) {
209 const domain = yield* toDomain(id, olds?.domain, output?.domain);
210 existing = yield* findCustomerByDomain(
211 parent,
212 domain,
213 listPartnerCustomers,
214 );
215 }
216 if (existing === undefined) return undefined;
217 const attrs = toCustomerAttrs(existing, parent);
218 // No labels field: without prior state the customer may not be ours.
219 return output !== undefined ? attrs : Unowned(attrs);
220 }),
221
222 reconcile: Effect.fn(function* ({ id, news, output }) {
223 const parent = resolveParent(
224 news.parent,
225 news.account ?? output?.account,
226 );
227 const channelPartnerId = lastSegment(parent);
228 const orgDisplayName = yield* toOrgDisplayName(
229 id,
230 news.orgDisplayName,
231 output?.orgDisplayName,
232 );
233 const domain = yield* toDomain(id, news.domain, output?.domain);
234 const body = desiredCustomer({
235 orgDisplayName,
236 domain,
237 orgPostalAddress: news.orgPostalAddress ?? output?.orgPostalAddress,
238 primaryContactInfo:
239 news.primaryContactInfo ?? output?.primaryContactInfo,
240 alternateEmail: news.alternateEmail,
241 languageCode: news.languageCode ?? output?.languageCode,
242 correlationId: news.correlationId,
243 customerAttestationState: news.customerAttestationState,
244 channelPartnerId,
245 });
246 const name = toCustomerName(
247 parent,
248 news.customerId ?? output?.customerId ?? output?.name,
249 );
250
251 let current = yield* getPartnerCustomer(output?.name ?? name);
252 if (current === undefined) {
253 current = yield* findCustomerByDomain(
254 parent,
255 domain,
256 listPartnerCustomers,
257 );
258 }
259
260 if (current === undefined) {
261 const created = yield* cloudchannel
262 .createAccountsChannelPartnerLinksCustomers({
263 parent,
264 body,
265 })
266 .pipe(
267 Effect.catchTag("Conflict", () =>
268 findCustomerByDomain(parent, domain, listPartnerCustomers),
269 ),
270 );
271 current = created ?? undefined;
272 }
273
274 if (current === undefined) {
275 return yield* new ChannelPartnerLinksCustomerNotResolved({
276 name: name || `${parent}/customers/${domain}`,
277 });
278 }
279
280 const currentName = current.name ?? name;
281 const displayChanged = !sameText(current.orgDisplayName, orgDisplayName);
282 const domainChanged = !sameText(current.domain, domain);
283 const addressChanged = !jsonEqual(
284 current.orgPostalAddress,
285 body.orgPostalAddress,
286 );
287 const contactChanged = !jsonEqual(
288 {
289 firstName: current.primaryContactInfo?.firstName,
290 lastName: current.primaryContactInfo?.lastName,
291 email: current.primaryContactInfo?.email,
292 phone: current.primaryContactInfo?.phone,
293 title: current.primaryContactInfo?.title,
294 },
295 {
296 firstName: body.primaryContactInfo?.firstName,
297 lastName: body.primaryContactInfo?.lastName,
298 email: body.primaryContactInfo?.email,
299 phone: body.primaryContactInfo?.phone,
300 title: body.primaryContactInfo?.title,
301 },
302 );
303 const alternateChanged = !sameText(
304 current.alternateEmail,
305 news.alternateEmail,
306 );
307 const languageChanged = !sameText(
308 current.languageCode,
309 body.languageCode,
310 );
311 const correlationChanged = !sameText(
312 current.correlationId,
313 news.correlationId,
314 );
315 const attestationChanged =
316 news.customerAttestationState !== undefined &&
317 !sameText(
318 current.customerAttestationState,
319 news.customerAttestationState,
320 );
321
322 const updateMask = updateMaskOf(
323 displayChanged ? "org_display_name" : undefined,
324 domainChanged ? "domain" : undefined,
325 addressChanged ? "org_postal_address" : undefined,
326 contactChanged ? "primary_contact_info" : undefined,
327 alternateChanged ? "alternate_email" : undefined,
328 languageChanged ? "language_code" : undefined,
329 correlationChanged ? "correlation_id" : undefined,
330 attestationChanged ? "customer_attestation_state" : undefined,
331 );
332
333 if (updateMask.length > 0 && currentName.length > 0) {
334 current = yield* cloudchannel.patchAccountsChannelPartnerLinksCustomers(
335 {
336 name: currentName,
337 updateMask,
338 body,
339 },
340 );
341 }
342
343 return toCustomerAttrs(current, parent);
344 }),
345
346 delete: Effect.fn(function* ({ output }) {
347 const name = output.name;
348 if (name.length === 0) return;
349 yield* cloudchannel
350 .deleteAccountsChannelPartnerLinksCustomers({ name })
351 .pipe(Effect.catchTag("NotFound", () => Effect.void));
352 }),
353 });