| 1 | import { withRequestOptions } from "@distilled.cloud/stripe"; |
| 2 | import { |
| 3 | GetPaymentMethodConfigurations, |
| 4 | GetPaymentMethodConfiguration, |
| 5 | CreatePaymentMethodConfiguration, |
| 6 | UpdatePaymentMethodConfiguration, |
| 7 | type PaymentMethodConfigResourcePaymentMethodProperties, |
| 8 | type PaymentMethodConfiguration as StripePaymentMethodConfiguration, |
| 9 | } from "@distilled.cloud/stripe/stripe"; |
| 10 | import * as Data from "effect/Data"; |
| 11 | import * as Effect from "effect/Effect"; |
| 12 | import { Unowned } from "../AdoptPolicy.ts"; |
| 13 | import { isResolved } from "../Diff.ts"; |
| 14 | import { createPhysicalName } from "../PhysicalName.ts"; |
| 15 | import * as Provider from "../Provider.ts"; |
| 16 | import { Resource } from "../Resource.ts"; |
| 17 | import type { Providers } from "./Providers.ts"; |
| 18 | import { isMissingStripeResource } from "./missing.ts"; |
| 19 | |
| 20 | const NAME_MAX_LENGTH = 100; |
| 21 | const LIST_PAGE_SIZE = 100; |
| 22 | const LIST_MAX_PAGES = 100; |
| 23 | |
| 24 | /** Account preference for whether a payment method is displayed. */ |
| 25 | export type PaymentMethodPreference = "none" | "off" | "on"; |
| 26 | |
| 27 | /** Effective display value after parent/override resolution. */ |
| 28 | export type PaymentMethodEffectivePreference = "off" | "on"; |
| 29 | |
| 30 | export interface PaymentMethodDisplayPreference { |
| 31 | /** |
| 32 | * Account preference for whether this payment method is displayed at |
| 33 | * checkout. `on` shows it, `off` hides it, `none` defers to the parent |
| 34 | * configuration or Stripe's default. |
| 35 | */ |
| 36 | preference?: PaymentMethodPreference; |
| 37 | } |
| 38 | |
| 39 | export interface PaymentMethodPreferenceInput { |
| 40 | /** |
| 41 | * Whether or not the payment method should be displayed at checkout. |
| 42 | */ |
| 43 | displayPreference?: PaymentMethodDisplayPreference; |
| 44 | } |
| 45 | |
| 46 | export interface PaymentMethodState { |
| 47 | /** |
| 48 | * Whether this payment method may be offered at checkout. True when |
| 49 | * `displayPreference` is `on` and the payment method's capability is |
| 50 | * active. |
| 51 | */ |
| 52 | available: boolean; |
| 53 | /** |
| 54 | * Observed display preference, including the effective value. |
| 55 | */ |
| 56 | displayPreference: { |
| 57 | /** |
| 58 | * For child configs, whether the connected account may override. |
| 59 | */ |
| 60 | overridable: boolean | undefined; |
| 61 | /** |
| 62 | * Configured preference (`on` / `off` / `none`). |
| 63 | */ |
| 64 | preference: PaymentMethodPreference; |
| 65 | /** |
| 66 | * Effective display value after parent/override resolution. |
| 67 | */ |
| 68 | value: PaymentMethodEffectivePreference; |
| 69 | }; |
| 70 | } |
| 71 | |
| 72 | export interface PaymentMethodConfigurationProps { |
| 73 | /** |
| 74 | * Configuration name. If omitted, a unique name is generated from the |
| 75 | * stack, stage, and logical id. Mutable. Max 100 characters. Used as |
| 76 | * the lookup key when the Stripe id is missing — this object has no |
| 77 | * metadata. |
| 78 | */ |
| 79 | name?: string; |
| 80 | /** |
| 81 | * Whether the configuration can be used for new payments. New |
| 82 | * configurations are created active. Destroy sets this to `false`. |
| 83 | * @default true |
| 84 | */ |
| 85 | active?: boolean; |
| 86 | /** |
| 87 | * Parent configuration id. Specify to create a child configuration for |
| 88 | * a connected account. Create-only — changing it replaces the |
| 89 | * configuration. |
| 90 | */ |
| 91 | parent?: string; |
| 92 | /** Display preference for ACSS Debit. */ |
| 93 | acssDebit?: PaymentMethodPreferenceInput; |
| 94 | /** Display preference for Affirm. */ |
| 95 | affirm?: PaymentMethodPreferenceInput; |
| 96 | /** Display preference for Afterpay / Clearpay. */ |
| 97 | afterpayClearpay?: PaymentMethodPreferenceInput; |
| 98 | /** Display preference for Alipay. */ |
| 99 | alipay?: PaymentMethodPreferenceInput; |
| 100 | /** Display preference for Alma. */ |
| 101 | alma?: PaymentMethodPreferenceInput; |
| 102 | /** Display preference for Amazon Pay. */ |
| 103 | amazonPay?: PaymentMethodPreferenceInput; |
| 104 | /** Display preference for Apple Pay. */ |
| 105 | applePay?: PaymentMethodPreferenceInput; |
| 106 | /** Display preference for AU BECS Debit. */ |
| 107 | auBecsDebit?: PaymentMethodPreferenceInput; |
| 108 | /** Display preference for Bacs Direct Debit. */ |
| 109 | bacsDebit?: PaymentMethodPreferenceInput; |
| 110 | /** Display preference for Bancontact. */ |
| 111 | bancontact?: PaymentMethodPreferenceInput; |
| 112 | /** Display preference for Billie. */ |
| 113 | billie?: PaymentMethodPreferenceInput; |
| 114 | /** Display preference for Bizum. */ |
| 115 | bizum?: PaymentMethodPreferenceInput; |
| 116 | /** Display preference for BLIK. */ |
| 117 | blik?: PaymentMethodPreferenceInput; |
| 118 | /** Display preference for Boleto. */ |
| 119 | boleto?: PaymentMethodPreferenceInput; |
| 120 | /** Display preference for cards. */ |
| 121 | card?: PaymentMethodPreferenceInput; |
| 122 | /** Display preference for Cartes Bancaires. */ |
| 123 | cartesBancaires?: PaymentMethodPreferenceInput; |
| 124 | /** Display preference for Cash App Pay. */ |
| 125 | cashapp?: PaymentMethodPreferenceInput; |
| 126 | /** Display preference for crypto / stablecoin payments. */ |
| 127 | crypto?: PaymentMethodPreferenceInput; |
| 128 | /** Display preference for customer balance (bank transfer). */ |
| 129 | customerBalance?: PaymentMethodPreferenceInput; |
| 130 | /** Display preference for EPS. */ |
| 131 | eps?: PaymentMethodPreferenceInput; |
| 132 | /** Display preference for FPX. */ |
| 133 | fpx?: PaymentMethodPreferenceInput; |
| 134 | /** Display preference for giropay. */ |
| 135 | giropay?: PaymentMethodPreferenceInput; |
| 136 | /** Display preference for Google Pay. */ |
| 137 | googlePay?: PaymentMethodPreferenceInput; |
| 138 | /** Display preference for GrabPay. */ |
| 139 | grabpay?: PaymentMethodPreferenceInput; |
| 140 | /** Display preference for iDEAL. */ |
| 141 | ideal?: PaymentMethodPreferenceInput; |
| 142 | /** Display preference for JCB. */ |
| 143 | jcb?: PaymentMethodPreferenceInput; |
| 144 | /** Display preference for Kakao Pay. */ |
| 145 | kakaoPay?: PaymentMethodPreferenceInput; |
| 146 | /** Display preference for Klarna. */ |
| 147 | klarna?: PaymentMethodPreferenceInput; |
| 148 | /** Display preference for Konbini. */ |
| 149 | konbini?: PaymentMethodPreferenceInput; |
| 150 | /** Display preference for Korean cards. */ |
| 151 | krCard?: PaymentMethodPreferenceInput; |
| 152 | /** Display preference for Link. */ |
| 153 | link?: PaymentMethodPreferenceInput; |
| 154 | /** Display preference for MB WAY. */ |
| 155 | mbWay?: PaymentMethodPreferenceInput; |
| 156 | /** Display preference for MobilePay. */ |
| 157 | mobilepay?: PaymentMethodPreferenceInput; |
| 158 | /** Display preference for Multibanco. */ |
| 159 | multibanco?: PaymentMethodPreferenceInput; |
| 160 | /** Display preference for Naver Pay. */ |
| 161 | naverPay?: PaymentMethodPreferenceInput; |
| 162 | /** Display preference for NZ BECS Debit. */ |
| 163 | nzBankAccount?: PaymentMethodPreferenceInput; |
| 164 | /** Display preference for OXXO. */ |
| 165 | oxxo?: PaymentMethodPreferenceInput; |
| 166 | /** Display preference for Przelewy24. */ |
| 167 | p24?: PaymentMethodPreferenceInput; |
| 168 | /** Display preference for Pay by Bank. */ |
| 169 | payByBank?: PaymentMethodPreferenceInput; |
| 170 | /** Display preference for PAYCO. */ |
| 171 | payco?: PaymentMethodPreferenceInput; |
| 172 | /** Display preference for PayNow. */ |
| 173 | paynow?: PaymentMethodPreferenceInput; |
| 174 | /** Display preference for PayPal. */ |
| 175 | paypal?: PaymentMethodPreferenceInput; |
| 176 | /** Display preference for PayTo. */ |
| 177 | payto?: PaymentMethodPreferenceInput; |
| 178 | /** Display preference for Pix. */ |
| 179 | pix?: PaymentMethodPreferenceInput; |
| 180 | /** Display preference for PromptPay. */ |
| 181 | promptpay?: PaymentMethodPreferenceInput; |
| 182 | /** Display preference for Revolut Pay. */ |
| 183 | revolutPay?: PaymentMethodPreferenceInput; |
| 184 | /** Display preference for Samsung Pay. */ |
| 185 | samsungPay?: PaymentMethodPreferenceInput; |
| 186 | /** Display preference for Satispay. */ |
| 187 | satispay?: PaymentMethodPreferenceInput; |
| 188 | /** Display preference for Scalapay. */ |
| 189 | scalapay?: PaymentMethodPreferenceInput; |
| 190 | /** Display preference for SEPA Debit. */ |
| 191 | sepaDebit?: PaymentMethodPreferenceInput; |
| 192 | /** Display preference for Sofort. */ |
| 193 | sofort?: PaymentMethodPreferenceInput; |
| 194 | /** Display preference for Sunbit. */ |
| 195 | sunbit?: PaymentMethodPreferenceInput; |
| 196 | /** Display preference for Swish. */ |
| 197 | swish?: PaymentMethodPreferenceInput; |
| 198 | /** Display preference for Twint. */ |
| 199 | twint?: PaymentMethodPreferenceInput; |
| 200 | /** Display preference for UPI. */ |
| 201 | upi?: PaymentMethodPreferenceInput; |
| 202 | /** Display preference for US bank account (ACH). */ |
| 203 | usBankAccount?: PaymentMethodPreferenceInput; |
| 204 | /** Display preference for WeChat Pay. */ |
| 205 | wechatPay?: PaymentMethodPreferenceInput; |
| 206 | /** Display preference for Zip. */ |
| 207 | zip?: PaymentMethodPreferenceInput; |
| 208 | } |
| 209 | |
| 210 | type MethodKey = Exclude< |
| 211 | keyof PaymentMethodConfigurationProps, |
| 212 | "name" | "active" | "parent" |
| 213 | >; |
| 214 | |
| 215 | type SnakeMethodKey = (typeof METHOD_MAP)[number][1]; |
| 216 | |
| 217 | const METHOD_MAP = [ |
| 218 | ["acssDebit", "acss_debit"], |
| 219 | ["affirm", "affirm"], |
| 220 | ["afterpayClearpay", "afterpay_clearpay"], |
| 221 | ["alipay", "alipay"], |
| 222 | ["alma", "alma"], |
| 223 | ["amazonPay", "amazon_pay"], |
| 224 | ["applePay", "apple_pay"], |
| 225 | ["auBecsDebit", "au_becs_debit"], |
| 226 | ["bacsDebit", "bacs_debit"], |
| 227 | ["bancontact", "bancontact"], |
| 228 | ["billie", "billie"], |
| 229 | ["bizum", "bizum"], |
| 230 | ["blik", "blik"], |
| 231 | ["boleto", "boleto"], |
| 232 | ["card", "card"], |
| 233 | ["cartesBancaires", "cartes_bancaires"], |
| 234 | ["cashapp", "cashapp"], |
| 235 | ["crypto", "crypto"], |
| 236 | ["customerBalance", "customer_balance"], |
| 237 | ["eps", "eps"], |
| 238 | ["fpx", "fpx"], |
| 239 | ["giropay", "giropay"], |
| 240 | ["googlePay", "google_pay"], |
| 241 | ["grabpay", "grabpay"], |
| 242 | ["ideal", "ideal"], |
| 243 | ["jcb", "jcb"], |
| 244 | ["kakaoPay", "kakao_pay"], |
| 245 | ["klarna", "klarna"], |
| 246 | ["konbini", "konbini"], |
| 247 | ["krCard", "kr_card"], |
| 248 | ["link", "link"], |
| 249 | ["mbWay", "mb_way"], |
| 250 | ["mobilepay", "mobilepay"], |
| 251 | ["multibanco", "multibanco"], |
| 252 | ["naverPay", "naver_pay"], |
| 253 | ["nzBankAccount", "nz_bank_account"], |
| 254 | ["oxxo", "oxxo"], |
| 255 | ["p24", "p24"], |
| 256 | ["payByBank", "pay_by_bank"], |
| 257 | ["payco", "payco"], |
| 258 | ["paynow", "paynow"], |
| 259 | ["paypal", "paypal"], |
| 260 | ["payto", "payto"], |
| 261 | ["pix", "pix"], |
| 262 | ["promptpay", "promptpay"], |
| 263 | ["revolutPay", "revolut_pay"], |
| 264 | ["samsungPay", "samsung_pay"], |
| 265 | ["satispay", "satispay"], |
| 266 | ["scalapay", "scalapay"], |
| 267 | ["sepaDebit", "sepa_debit"], |
| 268 | ["sofort", "sofort"], |
| 269 | ["sunbit", "sunbit"], |
| 270 | ["swish", "swish"], |
| 271 | ["twint", "twint"], |
| 272 | ["upi", "upi"], |
| 273 | ["usBankAccount", "us_bank_account"], |
| 274 | ["wechatPay", "wechat_pay"], |
| 275 | ["zip", "zip"], |
| 276 | ] as const satisfies ReadonlyArray<readonly [MethodKey, string]>; |
| 277 | |
| 278 | type PaymentMethodAttributes = { |
| 279 | [K in MethodKey]: PaymentMethodState | undefined; |
| 280 | }; |
| 281 | |
| 282 | export type PaymentMethodConfiguration = Resource< |
| 283 | "Stripe.PaymentMethodConfiguration", |
| 284 | PaymentMethodConfigurationProps, |
| 285 | { |
| 286 | /** Stripe payment method configuration id (`pmc_…`). */ |
| 287 | id: string; |
| 288 | /** Configuration name. */ |
| 289 | name: string; |
| 290 | /** Whether the configuration can be used for new payments. */ |
| 291 | active: boolean; |
| 292 | /** Whether this is the account's default configuration. */ |
| 293 | isDefault: boolean; |
| 294 | /** Parent configuration id, if this is a child config. */ |
| 295 | parent: string | undefined; |
| 296 | /** Connect application id for child configs, if any. */ |
| 297 | application: string | undefined; |
| 298 | /** Whether the configuration exists in live mode. */ |
| 299 | livemode: boolean; |
| 300 | } & PaymentMethodAttributes, |
| 301 | never, |
| 302 | Providers |
| 303 | >; |
| 304 | |
| 305 | /** |
| 306 | * A Stripe Payment Method Configuration — which payment methods Checkout, |
| 307 | * PaymentIntents, and SetupIntents show when payment method types are not |
| 308 | * set explicitly. `name` and per-method `displayPreference` update in |
| 309 | * place. `parent` is create-only and changing it replaces the |
| 310 | * configuration. |
| 311 | * |
| 312 | * Payment method configurations have no metadata field and cannot be |
| 313 | * hard-deleted. Destroying this resource deactivates it (`active: false`). |
| 314 | * Identity is the Stripe id plus the unique name. |
| 315 | * |
| 316 | * @see https://docs.stripe.com/api/payment_method_configurations |
| 317 | * |
| 318 | * ### Creating a Configuration |
| 319 | * **Example:** Named checkout configuration |
| 320 | * ```typescript |
| 321 | * const checkout = yield* Stripe.PaymentMethodConfiguration("checkout", { |
| 322 | * name: "Checkout", |
| 323 | * card: { displayPreference: { preference: "on" } }, |
| 324 | * link: { displayPreference: { preference: "on" } }, |
| 325 | * }); |
| 326 | * ``` |
| 327 | * |
| 328 | * **Example:** Cards only |
| 329 | * ```typescript |
| 330 | * const cards = yield* Stripe.PaymentMethodConfiguration("cards", { |
| 331 | * name: "Cards only", |
| 332 | * card: { displayPreference: { preference: "on" } }, |
| 333 | * link: { displayPreference: { preference: "off" } }, |
| 334 | * applePay: { displayPreference: { preference: "off" } }, |
| 335 | * googlePay: { displayPreference: { preference: "off" } }, |
| 336 | * }); |
| 337 | * ``` |
| 338 | * |
| 339 | * ### Updating a Configuration |
| 340 | * **Example:** Rename and enable US bank account |
| 341 | * ```typescript |
| 342 | * const checkout = yield* Stripe.PaymentMethodConfiguration("checkout", { |
| 343 | * name: "Checkout (updated)", |
| 344 | * card: { displayPreference: { preference: "on" } }, |
| 345 | * usBankAccount: { displayPreference: { preference: "on" } }, |
| 346 | * }); |
| 347 | * ``` |
| 348 | * |
| 349 | * ### Deactivating a Configuration |
| 350 | * **Example:** Destroy deactivates rather than deleting |
| 351 | * ```typescript |
| 352 | * // stack.destroy() / resource removal sets active: false |
| 353 | * const checkout = yield* Stripe.PaymentMethodConfiguration("checkout", { |
| 354 | * name: "Checkout", |
| 355 | * }); |
| 356 | * ``` |
| 357 | * |
| 358 | * @resource |
| 359 | * @product Payment Methods |
| 360 | */ |
| 361 | export const PaymentMethodConfiguration = Resource<PaymentMethodConfiguration>( |
| 362 | "Stripe.PaymentMethodConfiguration", |
| 363 | ); |
| 364 | |
| 365 | export class PaymentMethodConfigurationNotResolved extends Data.TaggedError( |
| 366 | "Stripe.PaymentMethodConfigurationNotResolved", |
| 367 | )<{ |
| 368 | name: string; |
| 369 | }> {} |
| 370 | |
| 371 | type ConfigurationAttributes = PaymentMethodConfiguration["Attributes"]; |
| 372 | |
| 373 | type WireMethod = { |
| 374 | display_preference: { preference: PaymentMethodPreference }; |
| 375 | }; |
| 376 | |
| 377 | const toName = (id: string, name: string | undefined, existing?: string) => |
| 378 | Effect.gen(function* () { |
| 379 | return ( |
| 380 | name ?? |
| 381 | existing ?? |
| 382 | (yield* createPhysicalName({ id, maxLength: NAME_MAX_LENGTH })) |
| 383 | ); |
| 384 | }); |
| 385 | |
| 386 | const toMethodState = ( |
| 387 | value: PaymentMethodConfigResourcePaymentMethodProperties | undefined, |
| 388 | ): PaymentMethodState | undefined => { |
| 389 | if (value === undefined) return undefined; |
| 390 | return { |
| 391 | available: value.available, |
| 392 | displayPreference: { |
| 393 | overridable: value.display_preference.overridable ?? undefined, |
| 394 | preference: value.display_preference.preference, |
| 395 | value: value.display_preference.value, |
| 396 | }, |
| 397 | }; |
| 398 | }; |
| 399 | |
| 400 | const readMethod = ( |
| 401 | configuration: StripePaymentMethodConfiguration, |
| 402 | snake: SnakeMethodKey, |
| 403 | ): PaymentMethodState | undefined => toMethodState(configuration[snake]); |
| 404 | |
| 405 | const toAttrs = ( |
| 406 | configuration: StripePaymentMethodConfiguration, |
| 407 | ): ConfigurationAttributes => { |
| 408 | const methods = {} as PaymentMethodAttributes; |
| 409 | for (const [camel, snake] of METHOD_MAP) { |
| 410 | methods[camel] = readMethod(configuration, snake); |
| 411 | } |
| 412 | return { |
| 413 | id: configuration.id, |
| 414 | name: configuration.name, |
| 415 | active: configuration.active, |
| 416 | isDefault: configuration.is_default, |
| 417 | parent: configuration.parent ?? undefined, |
| 418 | application: configuration.application ?? undefined, |
| 419 | livemode: configuration.livemode, |
| 420 | ...methods, |
| 421 | }; |
| 422 | }; |
| 423 | |
| 424 | const toWireMethods = ( |
| 425 | news: PaymentMethodConfigurationProps, |
| 426 | ): Partial<Record<SnakeMethodKey, WireMethod>> => { |
| 427 | const payload: Partial<Record<SnakeMethodKey, WireMethod>> = {}; |
| 428 | for (const [camel, snake] of METHOD_MAP) { |
| 429 | const preference = news[camel]?.displayPreference?.preference; |
| 430 | if (preference !== undefined) { |
| 431 | payload[snake] = { display_preference: { preference } }; |
| 432 | } |
| 433 | } |
| 434 | return payload; |
| 435 | }; |
| 436 | |
| 437 | const toChangedWireMethods = ( |
| 438 | news: PaymentMethodConfigurationProps, |
| 439 | current: StripePaymentMethodConfiguration, |
| 440 | ): Partial<Record<SnakeMethodKey, WireMethod>> => { |
| 441 | const payload: Partial<Record<SnakeMethodKey, WireMethod>> = {}; |
| 442 | for (const [camel, snake] of METHOD_MAP) { |
| 443 | const desired = news[camel]?.displayPreference?.preference; |
| 444 | if (desired === undefined) continue; |
| 445 | const observed = readMethod(current, snake)?.displayPreference.preference; |
| 446 | if (desired !== observed) { |
| 447 | payload[snake] = { display_preference: { preference: desired } }; |
| 448 | } |
| 449 | } |
| 450 | return payload; |
| 451 | }; |
| 452 | |
| 453 | const isMissingConfiguration = isMissingStripeResource; |
| 454 | |
| 455 | const getById = (configuration: string) => |
| 456 | GetPaymentMethodConfiguration({ configuration }).pipe( |
| 457 | Effect.catchIf(isMissingConfiguration, () => Effect.succeed(undefined)), |
| 458 | ); |
| 459 | |
| 460 | const listByActive = Effect.fn(function* (active: boolean) { |
| 461 | const configurations: StripePaymentMethodConfiguration[] = []; |
| 462 | let startingAfter: string | undefined; |
| 463 | for (let page = 0; page < LIST_MAX_PAGES; page++) { |
| 464 | const response = yield* GetPaymentMethodConfigurations({ |
| 465 | active, |
| 466 | limit: LIST_PAGE_SIZE, |
| 467 | ...(startingAfter !== undefined ? { starting_after: startingAfter } : {}), |
| 468 | }); |
| 469 | configurations.push(...response.data); |
| 470 | if (!response.has_more || response.data.length === 0) { |
| 471 | break; |
| 472 | } |
| 473 | startingAfter = response.data[response.data.length - 1]?.id; |
| 474 | if (startingAfter === undefined) { |
| 475 | break; |
| 476 | } |
| 477 | } |
| 478 | return configurations; |
| 479 | }); |
| 480 | |
| 481 | const listAllConfigurations = Effect.fn(function* () { |
| 482 | const [active, inactive] = yield* Effect.all( |
| 483 | [listByActive(true), listByActive(false)], |
| 484 | { concurrency: 2 }, |
| 485 | ); |
| 486 | const seen = new Set<string>(); |
| 487 | const configurations: StripePaymentMethodConfiguration[] = []; |
| 488 | for (const configuration of [...active, ...inactive]) { |
| 489 | if (seen.has(configuration.id)) continue; |
| 490 | seen.add(configuration.id); |
| 491 | configurations.push(configuration); |
| 492 | } |
| 493 | return configurations; |
| 494 | }); |
| 495 | |
| 496 | const findByName = Effect.fn(function* (name: string) { |
| 497 | const configurations = yield* listAllConfigurations(); |
| 498 | const matches = configurations.filter( |
| 499 | (configuration) => configuration.name === name && !configuration.is_default, |
| 500 | ); |
| 501 | return matches.find((configuration) => configuration.active) ?? matches[0]; |
| 502 | }); |
| 503 | |
| 504 | const observe = Effect.fn(function* (input: { id?: string; name?: string }) { |
| 505 | if (input.id !== undefined) { |
| 506 | const byId = yield* getById(input.id); |
| 507 | if (byId !== undefined) return byId; |
| 508 | } |
| 509 | if (input.name !== undefined) { |
| 510 | return yield* findByName(input.name); |
| 511 | } |
| 512 | return undefined; |
| 513 | }); |
| 514 | |
| 515 | const shouldReplace = ( |
| 516 | news: PaymentMethodConfigurationProps, |
| 517 | output: ConfigurationAttributes | undefined, |
| 518 | ): boolean => { |
| 519 | if (output === undefined) return false; |
| 520 | if (news.parent !== undefined && news.parent !== output.parent) { |
| 521 | return true; |
| 522 | } |
| 523 | return false; |
| 524 | }; |
| 525 | |
| 526 | export const PaymentMethodConfigurationProvider = () => |
| 527 | Provider.succeed(PaymentMethodConfiguration, { |
| 528 | stables: ["id", "parent", "application", "isDefault", "livemode"], |
| 529 | |
| 530 | diff: Effect.fn(function* ({ news, output }) { |
| 531 | if (!isResolved(news)) return undefined; |
| 532 | if (shouldReplace(news, output)) { |
| 533 | return { action: "replace" } as const; |
| 534 | } |
| 535 | return undefined; |
| 536 | }), |
| 537 | |
| 538 | read: Effect.fn(function* ({ output }) { |
| 539 | const existing = yield* observe({ |
| 540 | id: output?.id, |
| 541 | name: output?.name, |
| 542 | }); |
| 543 | if (existing === undefined) return undefined; |
| 544 | const attrs = toAttrs(existing); |
| 545 | // No metadata. Identity is the Stripe id and unique name. Never |
| 546 | // take over the account default configuration. |
| 547 | return existing.is_default ? Unowned(attrs) : attrs; |
| 548 | }), |
| 549 | |
| 550 | list: Effect.fn(function* () { |
| 551 | // No metadata on this resource. Default list is active, non-default |
| 552 | // configurations; deactivated rows stay in Stripe but must not |
| 553 | // re-enter nuke. The account default is never returned. |
| 554 | const configurations = yield* listByActive(true); |
| 555 | return configurations |
| 556 | .filter((configuration) => !configuration.is_default) |
| 557 | .map(toAttrs); |
| 558 | }), |
| 559 | |
| 560 | reconcile: Effect.fn(function* ({ id, news, output, instanceId }) { |
| 561 | const name = yield* toName(id, news.name, output?.name); |
| 562 | const desiredActive = news.active ?? true; |
| 563 | const wireMethods = toWireMethods(news); |
| 564 | |
| 565 | let current = yield* observe({ |
| 566 | id: output?.id, |
| 567 | name, |
| 568 | }); |
| 569 | if (current !== undefined && shouldReplace(news, toAttrs(current))) { |
| 570 | current = undefined; |
| 571 | } |
| 572 | if (current?.is_default) { |
| 573 | current = undefined; |
| 574 | } |
| 575 | |
| 576 | if (current === undefined) { |
| 577 | current = yield* CreatePaymentMethodConfiguration({ |
| 578 | name, |
| 579 | ...(news.parent !== undefined ? { parent: news.parent } : {}), |
| 580 | ...wireMethods, |
| 581 | }).pipe( |
| 582 | withRequestOptions({ |
| 583 | idempotencyKey: `alchemy-payment-method-configuration-${instanceId}`, |
| 584 | }), |
| 585 | ); |
| 586 | } |
| 587 | |
| 588 | if (current === undefined) { |
| 589 | return yield* new PaymentMethodConfigurationNotResolved({ name }); |
| 590 | } |
| 591 | |
| 592 | const nameChanged = current.name !== name; |
| 593 | const activeChanged = current.active !== desiredActive; |
| 594 | const changedMethods = toChangedWireMethods(news, current); |
| 595 | const methodsChanged = Object.keys(changedMethods).length > 0; |
| 596 | |
| 597 | if (!nameChanged && !activeChanged && !methodsChanged) { |
| 598 | return toAttrs(current); |
| 599 | } |
| 600 | |
| 601 | const updated = yield* UpdatePaymentMethodConfiguration({ |
| 602 | configuration: current.id, |
| 603 | ...(nameChanged ? { name } : {}), |
| 604 | ...(activeChanged ? { active: desiredActive } : {}), |
| 605 | ...(methodsChanged ? changedMethods : {}), |
| 606 | }); |
| 607 | return toAttrs(updated); |
| 608 | }), |
| 609 | |
| 610 | delete: Effect.fn(function* ({ output }) { |
| 611 | const existing = yield* getById(output.id); |
| 612 | if (existing === undefined || !existing.active || existing.is_default) { |
| 613 | return; |
| 614 | } |
| 615 | yield* UpdatePaymentMethodConfiguration({ |
| 616 | configuration: existing.id, |
| 617 | active: false, |
| 618 | }).pipe(Effect.catchIf(isMissingConfiguration, () => Effect.void)); |
| 619 | }), |
| 620 | }); |