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 defaultInvoiceMonth,
11 desiredRepricingConfig,
12 findCustomerRepricing,
13 jsonEqual,
14 normalizeDate,
15 normalizeName,
16 replaceOnIdentity,
17 toCustomerRepricingAttrs,
18 toRepricingConfigName,
19} from "./internal.ts";
20
21export type CustomersCustomerRepricingConfigProps = {
22 /**
23 * Customer that receives this repricing config. Full name
24 * `accounts/{account}/customers/{customer}`. Immutable — changing it
25 * replaces the config.
26 */
27 parent: string;
28 /**
29 * Config id (last segment of the resource name). Server-assigned on
30 * create. Immutable — changing it replaces the config.
31 */
32 configId?: string;
33 /**
34 * Year/month when the adjustment activates. Day must be `0`. You can
35 * only create or update configs for a future month. Immutable —
36 * changing it replaces the config.
37 */
38 effectiveInvoiceMonth?: cloudchannel.GoogleTypeDate;
39 /**
40 * Rebilling basis used for the bill.
41 * @default "COST_AT_LIST"
42 */
43 rebillingBasis?:
44 | cloudchannel.GoogleCloudChannelV1RepricingConfigRebillingBasisEnum
45 | (string & {});
46 /**
47 * Markup or markdown percentage (`"1.00"` is +1%, `"-1.00"` is -1%,
48 * `"0.00"` is pass-through). Ignored when `adjustment` is set.
49 * @default "0.00"
50 */
51 adjustmentPercentage?: string;
52 /**
53 * Full adjustment. Defaults to a percentage adjustment of
54 * `adjustmentPercentage`.
55 */
56 adjustment?: cloudchannel.GoogleCloudChannelV1RepricingAdjustment;
57 /**
58 * Entitlement this config applies to
59 * (`accounts/{account}/customers/{customer}/entitlements/{entitlement}`).
60 */
61 entitlement?: string;
62 /**
63 * Entitlement granularity. Takes precedence over `entitlement`.
64 */
65 entitlementGranularity?: cloudchannel.GoogleCloudChannelV1RepricingConfigEntitlementGranularity;
66 /**
67 * Conditional overrides applied before the default adjustment.
68 */
69 conditionalOverrides?: cloudchannel.GoogleCloudChannelV1ConditionalOverrideList;
70};
71
72export type CustomersCustomerRepricingConfig = Resource<
73 "GCP.CloudChannel.CustomersCustomerRepricingConfig",
74 CustomersCustomerRepricingConfigProps,
75 {
76 /** Resource name `accounts/{account}/customers/{customer}/customerRepricingConfigs/{id}`. */
77 name: string;
78 /** Config id (last path segment). */
79 configId: string;
80 /** Parent customer resource name. */
81 parent: string;
82 /** RFC3339 last-update timestamp. */
83 updateTime: string | undefined;
84 /** Repricing configuration. */
85 repricingConfig:
86 | cloudchannel.GoogleCloudChannelV1RepricingConfig
87 | undefined;
88 },
89 never,
90 Providers
91>;
92
93/**
94 * A Cloud Channel customer repricing config.
95 *
96 * Repricing configs have no labels field: `read` reports a config it
97 * finds without prior state as unowned (adopt it with `--adopt`). Parent
98 * customer and effective invoice month are identity — changing either
99 * replaces the config. Adjustment, rebilling basis, entitlement, and
100 * overrides update in place for a future month.
101 *
102 * Creating configs requires Cloud Channel reseller access and a
103 * provisioned entitlement.
104 *
105 * ### Creating a Customer Repricing Config
106 * **Example:** Pass-through for a future month
107 * ```typescript
108 * const config = yield* GCP.CloudChannel.CustomersCustomerRepricingConfig(
109 * "AcmeBill",
110 * {
111 * parent: customer.name,
112 * effectiveInvoiceMonth: { year: 2099, month: 1, day: 0 },
113 * entitlement: `${customer.name}/entitlements/ent-1`,
114 * },
115 * );
116 * ```
117 *
118 * **Example:** One percent markup
119 * ```typescript
120 * const config = yield* GCP.CloudChannel.CustomersCustomerRepricingConfig(
121 * "AcmeBill",
122 * {
123 * parent: customer.name,
124 * effectiveInvoiceMonth: { year: 2099, month: 1, day: 0 },
125 * adjustmentPercentage: "1.00",
126 * entitlement: `${customer.name}/entitlements/ent-1`,
127 * },
128 * );
129 * ```
130 *
131 * @resource
132 * @category CloudChannel
133 */
134export const CustomersCustomerRepricingConfig =
135 Resource<CustomersCustomerRepricingConfig>(
136 "GCP.CloudChannel.CustomersCustomerRepricingConfig",
137 );
138
139export class CustomersCustomerRepricingConfigNotResolved extends Data.TaggedError(
140 "GCP.CloudChannel.CustomersCustomerRepricingConfigNotResolved",
141)<{
142 name: string;
143}> {}
144
145const monthOf = (
146 news: CustomersCustomerRepricingConfigProps,
147 outputMonth?: cloudchannel.GoogleTypeDate,
148) =>
149 Effect.gen(function* () {
150 return (
151 normalizeDate(news.effectiveInvoiceMonth) ??
152 normalizeDate(outputMonth) ??
153 (yield* defaultInvoiceMonth())
154 );
155 });
156
157export const CustomersCustomerRepricingConfigProvider = () =>
158 Provider.succeed(CustomersCustomerRepricingConfig, {
159 stables: ["name", "configId", "parent"],
160
161 diff: Effect.fn(function* ({ news, olds, output }) {
162 if (!isResolved(news)) return undefined;
163 const previousMonth = normalizeDate(
164 olds?.effectiveInvoiceMonth ??
165 output?.repricingConfig?.effectiveInvoiceMonth,
166 );
167 const nextMonth = normalizeDate(news.effectiveInvoiceMonth);
168 return replaceOnIdentity({
169 previousId: olds?.configId ?? output?.configId,
170 nextId: news.configId,
171 previousParent: olds?.parent ?? output?.parent,
172 nextParent: news.parent,
173 extra:
174 previousMonth !== undefined &&
175 nextMonth !== undefined &&
176 !jsonEqual(previousMonth, nextMonth),
177 });
178 }),
179
180 read: Effect.fn(function* ({ olds, output }) {
181 const parent = normalizeName(olds?.parent ?? output?.parent ?? "");
182 const name = toRepricingConfigName(
183 parent,
184 olds?.configId ?? output?.configId ?? output?.name,
185 "customerRepricingConfigs",
186 );
187 const existing = yield* findCustomerRepricing(
188 parent,
189 output?.name ?? name,
190 olds?.effectiveInvoiceMonth ??
191 output?.repricingConfig?.effectiveInvoiceMonth,
192 );
193 if (existing === undefined) return undefined;
194 const attrs = toCustomerRepricingAttrs(existing);
195 // No labels field: without prior state the config may not be ours.
196 return output !== undefined ? attrs : Unowned(attrs);
197 }),
198
199 reconcile: Effect.fn(function* ({ news, output }) {
200 const parent = normalizeName(news.parent);
201 const month = yield* monthOf(
202 news,
203 output?.repricingConfig?.effectiveInvoiceMonth,
204 );
205 const repricingConfig = desiredRepricingConfig({
206 effectiveInvoiceMonth: month,
207 rebillingBasis: news.rebillingBasis,
208 adjustmentPercentage: news.adjustmentPercentage,
209 adjustment: news.adjustment,
210 entitlementGranularity:
211 news.entitlementGranularity ??
212 (news.entitlement
213 ? { entitlement: news.entitlement }
214 : output?.repricingConfig?.entitlementGranularity),
215 conditionalOverrides: news.conditionalOverrides,
216 });
217 const name = toRepricingConfigName(
218 parent,
219 news.configId ?? output?.configId ?? output?.name,
220 "customerRepricingConfigs",
221 );
222
223 let current = yield* findCustomerRepricing(
224 parent,
225 output?.name ?? name,
226 month,
227 );
228
229 if (current === undefined) {
230 const created = yield* cloudchannel
231 .createAccountsCustomersCustomerRepricingConfigs({
232 parent,
233 body: { repricingConfig },
234 })
235 .pipe(
236 Effect.catchTag("Conflict", () =>
237 findCustomerRepricing(parent, name, month),
238 ),
239 );
240 current = created ?? undefined;
241 }
242
243 if (current === undefined) {
244 return yield* new CustomersCustomerRepricingConfigNotResolved({
245 name: name || `${parent}/customerRepricingConfigs`,
246 });
247 }
248
249 const currentName = current.name ?? name;
250 const observed = current.repricingConfig;
251 const changed =
252 !jsonEqual(
253 normalizeDate(observed?.effectiveInvoiceMonth),
254 normalizeDate(repricingConfig.effectiveInvoiceMonth),
255 ) ||
256 (observed?.rebillingBasis ?? "") !==
257 (repricingConfig.rebillingBasis ?? "") ||
258 !jsonEqual(observed?.adjustment, repricingConfig.adjustment) ||
259 !jsonEqual(
260 observed?.entitlementGranularity,
261 repricingConfig.entitlementGranularity,
262 ) ||
263 !jsonEqual(
264 observed?.conditionalOverrides,
265 repricingConfig.conditionalOverrides,
266 );
267
268 if (changed && currentName.length > 0) {
269 current =
270 yield* cloudchannel.patchAccountsCustomersCustomerRepricingConfigs({
271 name: currentName,
272 body: { name: currentName, repricingConfig },
273 });
274 }
275
276 return toCustomerRepricingAttrs(current);
277 }),
278
279 delete: Effect.fn(function* ({ output }) {
280 const name = output.name;
281 if (name.length === 0) return;
282 yield* cloudchannel
283 .deleteAccountsCustomersCustomerRepricingConfigs({ name })
284 .pipe(Effect.catchTag("NotFound", () => Effect.void));
285 }),
286 });