1import * as cloudchannel from "@distilled.cloud/gcp/cloudchannel_v1";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { createPhysicalName } from "../../PhysicalName.ts";
5
6export const MAX_ORG_DISPLAY_NAME_LENGTH = 128;
7export const MAX_DOMAIN_LABEL_LENGTH = 32;
8export const DEFAULT_LANGUAGE = "en-US";
9export const DEFAULT_REBILLING_BASIS = "COST_AT_LIST";
10export const DEFAULT_ADJUSTMENT = "0.00";
11
12export const DEFAULT_POSTAL_ADDRESS: cloudchannel.GoogleTypePostalAddress = {
13 regionCode: "US",
14 postalCode: "94105",
15 administrativeArea: "CA",
16 locality: "San Francisco",
17 addressLines: ["100 Market Street"],
18};
19
20export const lastSegment = (value: string) => {
21 const trimmed = value.replace(/\/+$/, "");
22 const parts = trimmed.split("/");
23 return parts[parts.length - 1] || trimmed;
24};
25
26export const parentOf = (name: string) => {
27 const parts = name.split("/").filter((part) => part.length > 0);
28 return parts.slice(0, -2).join("/");
29};
30
31export const normalizeName = (value: string) => value.replace(/\/+$/, "");
32
33export const toAccountName = (value: string) => {
34 const trimmed = normalizeName(value);
35 if (trimmed.startsWith("accounts/")) {
36 const parts = trimmed.split("/").filter((part) => part.length > 0);
37 return `accounts/${parts[1] ?? trimmed}`;
38 }
39 return `accounts/${trimmed}`;
40};
41
42export const accountOf = (value: string) => {
43 const trimmed = normalizeName(value);
44 if (trimmed.length === 0) return "";
45 const parts = trimmed.split("/").filter((part) => part.length > 0);
46 if (parts[0] === "accounts" && parts[1]) {
47 return `accounts/${parts[1]}`;
48 }
49 return toAccountName(trimmed);
50};
51
52export const toCustomerName = (parent: string, customerId?: string) => {
53 if (customerId !== undefined && customerId.includes("/")) {
54 return normalizeName(customerId);
55 }
56 if (customerId !== undefined && customerId.length > 0) {
57 return `${accountOf(parent)}/customers/${customerId}`;
58 }
59 return "";
60};
61
62export const toChannelPartnerLinkName = (value: string, account?: string) => {
63 const trimmed = normalizeName(value);
64 if (trimmed.includes("/channelPartnerLinks/")) return trimmed;
65 const acct = account ? toAccountName(account) : accountOf(trimmed);
66 if (acct.length === 0) return trimmed;
67 return `${acct}/channelPartnerLinks/${lastSegment(trimmed)}`;
68};
69
70export const toRepricingConfigName = (
71 parent: string,
72 configId: string | undefined,
73 collection: "customerRepricingConfigs" | "channelPartnerRepricingConfigs",
74) => {
75 if (configId !== undefined && configId.includes("/")) {
76 return normalizeName(configId);
77 }
78 if (configId !== undefined && configId.length > 0 && parent.length > 0) {
79 return `${normalizeName(parent)}/${collection}/${configId}`;
80 }
81 return "";
82};
83
84export const sameText = (left: string | undefined, right: string | undefined) =>
85 (left ?? "") === (right ?? "");
86
87export const jsonEqual = (left: unknown, right: unknown) =>
88 JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
89
90export const updateMaskOf = (...fields: Array<string | undefined>) =>
91 fields.filter((field): field is string => field !== undefined).join(",");
92
93export const replaceOnIdentity = (input: {
94 previousId?: string;
95 nextId?: string;
96 previousParent?: string;
97 nextParent?: string;
98 extra?: boolean;
99}) => {
100 if (input.extra === true) {
101 return { action: "replace" as const, deleteFirst: false };
102 }
103 if (
104 input.previousId !== undefined &&
105 input.nextId !== undefined &&
106 input.previousId !== input.nextId
107 ) {
108 return { action: "replace" as const, deleteFirst: false };
109 }
110 if (
111 input.previousParent !== undefined &&
112 input.nextParent !== undefined &&
113 input.previousParent !== input.nextParent
114 ) {
115 return { action: "replace" as const, deleteFirst: false };
116 }
117 return undefined;
118};
119
120export const toOrgDisplayName = (
121 id: string,
122 requested: string | undefined,
123 existing: string | undefined,
124) =>
125 Effect.gen(function* () {
126 if (requested !== undefined && requested.length > 0) return requested;
127 if (existing !== undefined && existing.length > 0) return existing;
128 return yield* createPhysicalName({
129 id,
130 maxLength: 40,
131 lowercase: true,
132 });
133 });
134
135export const toDomain = (
136 id: string,
137 requested: string | undefined,
138 existing: string | undefined,
139) =>
140 Effect.gen(function* () {
141 if (requested !== undefined && requested.length > 0) return requested;
142 if (existing !== undefined && existing.length > 0) return existing;
143 const local = yield* createPhysicalName({
144 id,
145 maxLength: MAX_DOMAIN_LABEL_LENGTH,
146 lowercase: true,
147 });
148 return `${local}.example.com`;
149 });
150
151export const defaultInvoiceMonth = () =>
152 Effect.sync(() => {
153 const now = new Date();
154 const utcMonthIndex = now.getUTCMonth() + 2;
155 const year = now.getUTCFullYear() + Math.floor(utcMonthIndex / 12);
156 const month = (utcMonthIndex % 12) + 1;
157 return { year, month, day: 0 };
158 });
159
160export const normalizeDate = (
161 date: cloudchannel.GoogleTypeDate | undefined,
162): cloudchannel.GoogleTypeDate | undefined => {
163 if (date === undefined) return undefined;
164 return {
165 year: date.year ?? 0,
166 month: date.month ?? 0,
167 day: date.day ?? 0,
168 };
169};
170
171const emptyList = <A>() => Effect.succeed([] as A[]);
172
173export const collectPages = <A, Page, E, R>(
174 pages: Stream.Stream<Page, E, R>,
175 pick: (page: Page) => readonly A[] | undefined,
176) =>
177 pages.pipe(
178 Stream.flatMap((page) => Stream.fromIterable(pick(page) ?? [])),
179 Stream.runCollect,
180 Effect.map((chunk) => Array.from(chunk)),
181 );
182
183export const getCustomer = (name: string) =>
184 name.length === 0
185 ? Effect.succeed(undefined)
186 : cloudchannel
187 .getAccountsCustomers({ name })
188 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
189
190export const getPartnerCustomer = (name: string) =>
191 name.length === 0
192 ? Effect.succeed(undefined)
193 : cloudchannel
194 .getAccountsChannelPartnerLinksCustomers({ name })
195 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
196
197export const listCustomers = (parent: string) =>
198 parent.length === 0
199 ? emptyList<cloudchannel.GoogleCloudChannelV1Customer>()
200 : collectPages(
201 cloudchannel.listAccountsCustomers.pages({
202 parent,
203 pageSize: 50,
204 }),
205 (page) => page.customers,
206 ).pipe(
207 Effect.catchTag("NotFound", () =>
208 emptyList<cloudchannel.GoogleCloudChannelV1Customer>(),
209 ),
210 );
211
212export const listPartnerCustomers = (parent: string) =>
213 parent.length === 0
214 ? emptyList<cloudchannel.GoogleCloudChannelV1Customer>()
215 : collectPages(
216 cloudchannel.listAccountsChannelPartnerLinksCustomers.pages({
217 parent,
218 pageSize: 50,
219 }),
220 (page) => page.customers,
221 ).pipe(
222 Effect.catchTag("NotFound", () =>
223 emptyList<cloudchannel.GoogleCloudChannelV1Customer>(),
224 ),
225 );
226
227export const listChannelPartnerLinks = (parent: string) =>
228 parent.length === 0
229 ? emptyList<cloudchannel.GoogleCloudChannelV1ChannelPartnerLink>()
230 : collectPages(
231 cloudchannel.listAccountsChannelPartnerLinks.pages({
232 parent,
233 pageSize: 50,
234 view: "BASIC",
235 }),
236 (page) => page.channelPartnerLinks,
237 ).pipe(
238 Effect.catchTag("NotFound", () =>
239 emptyList<cloudchannel.GoogleCloudChannelV1ChannelPartnerLink>(),
240 ),
241 );
242
243export const findCustomerByDomain = (
244 parent: string,
245 domain: string,
246 list: typeof listCustomers = listCustomers,
247) =>
248 Effect.gen(function* () {
249 if (domain.length === 0) return undefined;
250 const customers = yield* list(parent);
251 return customers.find((customer) => sameText(customer.domain, domain));
252 });
253
254export const toCustomerAttrs = (
255 customer: cloudchannel.GoogleCloudChannelV1Customer,
256 parent?: string,
257) => {
258 const name = customer.name ?? "";
259 const account = accountOf(name || parent || "");
260 const derivedParent =
261 parent !== undefined && parent.includes("/channelPartnerLinks/")
262 ? parent
263 : customer.channelPartnerId
264 ? `${account}/channelPartnerLinks/${customer.channelPartnerId}`
265 : account;
266 return {
267 name,
268 customerId: lastSegment(name),
269 parent: derivedParent,
270 account,
271 orgDisplayName: customer.orgDisplayName,
272 domain: customer.domain,
273 orgPostalAddress: customer.orgPostalAddress,
274 primaryContactInfo: customer.primaryContactInfo,
275 alternateEmail: customer.alternateEmail,
276 languageCode: customer.languageCode,
277 correlationId: customer.correlationId,
278 customerAttestationState: customer.customerAttestationState,
279 channelPartnerId: customer.channelPartnerId,
280 cloudIdentityId: customer.cloudIdentityId,
281 createTime: customer.createTime,
282 updateTime: customer.updateTime,
283 };
284};
285
286export const desiredCustomer = (input: {
287 orgDisplayName: string;
288 domain: string;
289 orgPostalAddress?: cloudchannel.GoogleTypePostalAddress;
290 primaryContactInfo?: cloudchannel.GoogleCloudChannelV1ContactInfo;
291 alternateEmail?: string;
292 languageCode?: string;
293 correlationId?: string;
294 customerAttestationState?:
295 | cloudchannel.GoogleCloudChannelV1CustomerCustomerAttestationStateEnum
296 | (string & {});
297 channelPartnerId?: string;
298}): cloudchannel.GoogleCloudChannelV1Customer => {
299 const address = input.orgPostalAddress ?? DEFAULT_POSTAL_ADDRESS;
300 const email = input.primaryContactInfo?.email ?? `admin@${input.domain}`;
301 return {
302 orgDisplayName: input.orgDisplayName,
303 domain: input.domain,
304 orgPostalAddress: address,
305 primaryContactInfo: {
306 firstName: input.primaryContactInfo?.firstName ?? "Alchemy",
307 lastName: input.primaryContactInfo?.lastName ?? "Test",
308 email,
309 phone: input.primaryContactInfo?.phone,
310 title: input.primaryContactInfo?.title,
311 },
312 alternateEmail: input.alternateEmail,
313 languageCode: input.languageCode ?? DEFAULT_LANGUAGE,
314 correlationId: input.correlationId,
315 customerAttestationState: input.customerAttestationState,
316 channelPartnerId: input.channelPartnerId,
317 };
318};
319
320export const getCustomerRepricingConfig = (name: string) =>
321 name.length === 0
322 ? Effect.succeed(undefined)
323 : cloudchannel
324 .getAccountsCustomersCustomerRepricingConfigs({ name })
325 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
326
327export const getChannelPartnerRepricingConfig = (name: string) =>
328 name.length === 0
329 ? Effect.succeed(undefined)
330 : cloudchannel
331 .getAccountsChannelPartnerLinksChannelPartnerRepricingConfigs({
332 name,
333 })
334 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
335
336export const listCustomerRepricingConfigs = (parent: string) =>
337 parent.length === 0
338 ? emptyList<cloudchannel.GoogleCloudChannelV1CustomerRepricingConfig>()
339 : collectPages(
340 cloudchannel.listAccountsCustomersCustomerRepricingConfigs.pages({
341 parent,
342 pageSize: 100,
343 }),
344 (page) => page.customerRepricingConfigs,
345 ).pipe(
346 Effect.catchTag("NotFound", () =>
347 emptyList<cloudchannel.GoogleCloudChannelV1CustomerRepricingConfig>(),
348 ),
349 );
350
351export const listChannelPartnerRepricingConfigs = (parent: string) =>
352 parent.length === 0
353 ? emptyList<cloudchannel.GoogleCloudChannelV1ChannelPartnerRepricingConfig>()
354 : collectPages(
355 cloudchannel.listAccountsChannelPartnerLinksChannelPartnerRepricingConfigs.pages(
356 {
357 parent,
358 pageSize: 100,
359 },
360 ),
361 (page) => page.channelPartnerRepricingConfigs,
362 ).pipe(
363 Effect.catchTag("NotFound", () =>
364 emptyList<cloudchannel.GoogleCloudChannelV1ChannelPartnerRepricingConfig>(),
365 ),
366 );
367
368export const desiredRepricingConfig = (input: {
369 effectiveInvoiceMonth: cloudchannel.GoogleTypeDate;
370 rebillingBasis?:
371 | cloudchannel.GoogleCloudChannelV1RepricingConfigRebillingBasisEnum
372 | (string & {});
373 adjustmentPercentage?: string;
374 adjustment?: cloudchannel.GoogleCloudChannelV1RepricingAdjustment;
375 entitlementGranularity?: cloudchannel.GoogleCloudChannelV1RepricingConfigEntitlementGranularity;
376 conditionalOverrides?: cloudchannel.GoogleCloudChannelV1ConditionalOverrideList;
377}): cloudchannel.GoogleCloudChannelV1RepricingConfig => ({
378 effectiveInvoiceMonth: normalizeDate(input.effectiveInvoiceMonth),
379 rebillingBasis: input.rebillingBasis ?? DEFAULT_REBILLING_BASIS,
380 adjustment:
381 input.adjustment ??
382 ({
383 percentageAdjustment: {
384 percentage: { value: input.adjustmentPercentage ?? DEFAULT_ADJUSTMENT },
385 },
386 } satisfies cloudchannel.GoogleCloudChannelV1RepricingAdjustment),
387 entitlementGranularity: input.entitlementGranularity,
388 conditionalOverrides: input.conditionalOverrides,
389});
390
391export const toCustomerRepricingAttrs = (
392 config: cloudchannel.GoogleCloudChannelV1CustomerRepricingConfig,
393) => {
394 const name = config.name ?? "";
395 return {
396 name,
397 configId: lastSegment(name),
398 parent: parentOf(name),
399 updateTime: config.updateTime,
400 repricingConfig: config.repricingConfig,
401 };
402};
403
404export const toChannelPartnerRepricingAttrs = (
405 config: cloudchannel.GoogleCloudChannelV1ChannelPartnerRepricingConfig,
406) => {
407 const name = config.name ?? "";
408 return {
409 name,
410 configId: lastSegment(name),
411 parent: parentOf(name),
412 updateTime: config.updateTime,
413 repricingConfig: config.repricingConfig,
414 };
415};
416
417export const findCustomerRepricing = (
418 parent: string,
419 name: string | undefined,
420 month: cloudchannel.GoogleTypeDate | undefined,
421) =>
422 Effect.gen(function* () {
423 if (name !== undefined && name.length > 0) {
424 const existing = yield* getCustomerRepricingConfig(name);
425 if (existing !== undefined) return existing;
426 }
427 const configs = yield* listCustomerRepricingConfigs(parent);
428 if (month === undefined) return undefined;
429 const wanted = normalizeDate(month);
430 return configs.find((config) =>
431 jsonEqual(
432 normalizeDate(config.repricingConfig?.effectiveInvoiceMonth),
433 wanted,
434 ),
435 );
436 });
437
438export const findChannelPartnerRepricing = (
439 parent: string,
440 name: string | undefined,
441 month: cloudchannel.GoogleTypeDate | undefined,
442) =>
443 Effect.gen(function* () {
444 if (name !== undefined && name.length > 0) {
445 const existing = yield* getChannelPartnerRepricingConfig(name);
446 if (existing !== undefined) return existing;
447 }
448 const configs = yield* listChannelPartnerRepricingConfigs(parent);
449 if (month === undefined) return undefined;
450 const wanted = normalizeDate(month);
451 return configs.find((config) =>
452 jsonEqual(
453 normalizeDate(config.repricingConfig?.effectiveInvoiceMonth),
454 wanted,
455 ),
456 );
457 });