1import { withRequestOptions } from "@distilled.cloud/stripe";
2import {
3 DeleteAccount,
4 GetAccounts,
5 GetAccountByAccount,
6 CreateAccount,
7 UpdateAccount,
8 type Account as StripeAccount,
9 type AccountBusinessType as StripeAccountBusinessType,
10 type AccountType as StripeAccountType,
11 type CreateAccountRequestBusinessProfile,
12 type CreateAccountRequestCapabilities,
13 type CreateAccountRequestCompany,
14 type CreateAccountRequestController,
15 type CreateAccountRequestIndividual,
16 type CreateAccountRequestSettings,
17 type CreateAccountRequestTosAcceptance,
18} from "@distilled.cloud/stripe/stripe";
19import * as Effect from "effect/Effect";
20import { Unowned } from "../AdoptPolicy.ts";
21import { deepEqual, isResolved } from "../Diff.ts";
22import * as Provider from "../Provider.ts";
23import { Resource } from "../Resource.ts";
24import { tagRecord } from "../Tags.ts";
25import {
26 alchemyMetadataKeys,
27 createInternalMetadata,
28 diffMetadata,
29 hasAlchemyMetadata,
30 stripInternalMetadata,
31 toMetadata,
32} from "./Metadata.ts";
33import type { Providers } from "./Providers.ts";
34import { isMissingStripeResource } from "./missing.ts";
35
36const LIST_PAGE_SIZE = 100;
37const LIST_MAX_PAGES = 100;
38
39/** Connected-account kinds accepted by `POST /v1/accounts`. */
40export type AccountKind = "custom" | "express" | "standard";
41
42/** Legal structure of the account holder. */
43export type AccountBusinessType = StripeAccountBusinessType;
44
45/** Stripe-reported account type, including `none` for controller-created accounts. */
46export type AccountType = StripeAccountType;
47
48export interface AccountBusinessProfile {
49 /**
50 * Customer-facing business name.
51 */
52 name?: string;
53 /**
54 * Public website of the business.
55 */
56 url?: string;
57 /**
58 * Merchant category code (MCC).
59 */
60 mcc?: string;
61 /**
62 * Internal description of the product or service. Used by Stripe for
63 * underwriting.
64 */
65 productDescription?: string;
66 /**
67 * Public support email.
68 */
69 supportEmail?: string;
70 /**
71 * Public support phone.
72 */
73 supportPhone?: string;
74 /**
75 * Public support URL.
76 */
77 supportUrl?: string;
78}
79
80export interface AccountControllerFees {
81 /**
82 * Who pays Stripe fees on this account.
83 * @default "account"
84 */
85 payer?: "account" | "application";
86}
87
88export interface AccountControllerLosses {
89 /**
90 * Who is liable for negative balances.
91 * @default "stripe"
92 */
93 payments?: "application" | "stripe";
94}
95
96export interface AccountControllerDashboard {
97 /**
98 * Stripe-hosted dashboard access: full Dashboard, Express, or none.
99 * @default "full"
100 */
101 type?: "express" | "full" | "none";
102}
103
104export interface AccountController {
105 /**
106 * Who pays Stripe fees for product usage on this account.
107 */
108 fees?: AccountControllerFees;
109 /**
110 * Who is liable for negative balances on this account.
111 */
112 losses?: AccountControllerLosses;
113 /**
114 * Who collects updated information when requirements are due.
115 * @default "stripe"
116 */
117 requirementCollection?: "application" | "stripe";
118 /**
119 * Stripe-hosted dashboard access.
120 */
121 stripeDashboard?: AccountControllerDashboard;
122}
123
124export interface AccountTosAcceptance {
125 /**
126 * Unix timestamp when the account representative accepted the service
127 * agreement.
128 */
129 date?: number;
130 /**
131 * IP address from which the representative accepted the agreement.
132 */
133 ip?: string;
134 /**
135 * User agent of the browser that accepted the agreement.
136 */
137 userAgent?: string;
138 /**
139 * Service agreement type.
140 */
141 serviceAgreement?: string;
142}
143
144export interface AccountProps {
145 /**
146 * Kind of connected account to create — `standard`, `express`, or
147 * `custom`. Create-only — changing it replaces the account.
148 *
149 * Mutually exclusive with {@link AccountProps.controller}. Omitting both
150 * makes Stripe default to a Standard account.
151 */
152 type?: AccountKind;
153 /**
154 * Two-letter ISO country code of the account holder (e.g. `"US"`).
155 * Create-only — changing it replaces the account. When omitted, Stripe
156 * defaults to the platform's country.
157 */
158 country?: string;
159 /**
160 * Email address of the account holder. Used to identify the account;
161 * Stripe does not market to it. Mutable.
162 */
163 email?: string;
164 /**
165 * Legal structure of the account holder. Mutable until Connect
166 * onboarding starts on Standard and Express accounts.
167 */
168 businessType?: AccountBusinessType;
169 /**
170 * Public-facing business information (name, URL, support contacts, MCC).
171 * Mutable.
172 */
173 businessProfile?: AccountBusinessProfile;
174 /**
175 * Capabilities to request, keyed by Stripe capability name
176 * (`card_payments`, `transfers`, …) with `{ requested: boolean }`.
177 * Mutable — requesting a capability does not activate it; Stripe
178 * activates it once requirements are met.
179 *
180 * Nested fields use Stripe's wire names.
181 */
182 capabilities?: CreateAccountRequestCapabilities;
183 /**
184 * Account behaviour settings (branding, payouts, card payments, …).
185 * Mutable. Nested fields use Stripe's wire names.
186 */
187 settings?: CreateAccountRequestSettings;
188 /**
189 * Three-letter ISO currency code used as the account's default currency.
190 * Must be a currency Stripe supports in the account's country. Mutable.
191 */
192 defaultCurrency?: string;
193 /**
194 * Company or business details. Available for any `businessType`. Mutable
195 * while `controller.requirement_collection` is `application` (Custom).
196 * Nested fields use Stripe's wire names.
197 */
198 company?: CreateAccountRequestCompany;
199 /**
200 * Individual represented by the account. Only meaningful when
201 * `businessType` is `individual`. Nested fields use Stripe's wire names.
202 */
203 individual?: CreateAccountRequestIndividual;
204 /**
205 * Record of the account holder accepting the Stripe Services Agreement.
206 * Only settable for Custom accounts.
207 */
208 tosAcceptance?: AccountTosAcceptance;
209 /**
210 * Who is responsible for fees, losses, requirement collection, and
211 * dashboard access. Create-only — changing it replaces the account.
212 *
213 * Mutually exclusive with {@link AccountProps.type}.
214 */
215 controller?: AccountController;
216 /**
217 * User-defined metadata. Alchemy ownership keys (`alchemy_stack` /
218 * `alchemy_stage` / `alchemy_id`) are merged in automatically. Keys may
219 * not contain `:`.
220 */
221 metadata?: Record<string, string>;
222}
223
224export type Account = Resource<
225 "Stripe.Account",
226 AccountProps,
227 {
228 /** Stripe account id (`acct_…`). */
229 id: string;
230 /**
231 * Stripe account type — `standard`, `express`, `custom`, or `none`
232 * for accounts created with an explicit `controller`.
233 */
234 type: AccountType | undefined;
235 /** Two-letter ISO country code the account is registered in. */
236 country: string | undefined;
237 /** Email address associated with the account. */
238 email: string | undefined;
239 /** Legal structure of the account holder, once known. */
240 businessType: AccountBusinessType | undefined;
241 /** Three-letter ISO code of the account's default currency. */
242 defaultCurrency: string | undefined;
243 /** Customer-facing business name, if set. */
244 businessProfileName: string | undefined;
245 /** Public website of the business, if set. */
246 businessProfileUrl: string | undefined;
247 /** Whether the account can currently process charges. */
248 chargesEnabled: boolean;
249 /** Whether funds in the account can currently be paid out. */
250 payoutsEnabled: boolean;
251 /**
252 * Whether the account has finished submitting its details. Accounts
253 * with Dashboard access cannot receive payouts until this is `true`.
254 */
255 detailsSubmitted: boolean;
256 /**
257 * Capability name → status (`active`, `pending`, `inactive`) for every
258 * capability Stripe reports on the account.
259 */
260 capabilities: Record<string, string>;
261 /**
262 * Fields Stripe needs before the account's capabilities stay enabled.
263 */
264 requirementsCurrentlyDue: string[];
265 /** Why the account is currently disabled, if it is. */
266 requirementsDisabledReason: string | undefined;
267 /** Unix timestamp when the account was connected. */
268 created: number | undefined;
269 /** User-defined metadata (Alchemy ownership keys stripped). */
270 metadata: Record<string, string>;
271 },
272 never,
273 Providers
274>;
275
276/**
277 * A Stripe Connect connected account — a Standard, Express, or Custom
278 * account created by your platform on behalf of a user.
279 *
280 * Creating an account is only the first step of onboarding: Stripe
281 * activates requested capabilities once the account satisfies their
282 * requirements, so a freshly created account normally reports
283 * `chargesEnabled: false` and a non-empty `requirementsCurrentlyDue`.
284 * Account Links are one-shot URLs and are not modeled as resources.
285 *
286 * `type`, `country`, and `controller` are fixed at creation. Email,
287 * business profile, capabilities, settings, and metadata update in
288 * place. Deleting the resource deletes the connected account.
289 *
290 * @see https://docs.stripe.com/api/accounts
291 *
292 * ### Creating an Account
293 * **Example:** A Standard connected account
294 * ```typescript
295 * const account = yield* Stripe.Account("Merchant", {
296 * type: "standard",
297 * country: "US",
298 * email: "merchant@example.com",
299 * });
300 * ```
301 *
302 * **Example:** An Express account requesting payment capabilities
303 * ```typescript
304 * const account = yield* Stripe.Account("Merchant", {
305 * type: "express",
306 * country: "US",
307 * email: "merchant@example.com",
308 * capabilities: {
309 * card_payments: { requested: true },
310 * transfers: { requested: true },
311 * },
312 * });
313 * ```
314 *
315 * ### Fully configuring an Account
316 * **Example:** Business profile, payouts, and metadata
317 * ```typescript
318 * const account = yield* Stripe.Account("Merchant", {
319 * type: "express",
320 * country: "US",
321 * email: "merchant@example.com",
322 * businessType: "company",
323 * defaultCurrency: "usd",
324 * businessProfile: {
325 * name: "Example Merchant",
326 * url: "https://example.com",
327 * mcc: "5734",
328 * supportEmail: "support@example.com",
329 * },
330 * capabilities: {
331 * card_payments: { requested: true },
332 * transfers: { requested: true },
333 * },
334 * settings: {
335 * payouts: { schedule: { interval: "manual" } },
336 * },
337 * metadata: { tier: "gold" },
338 * });
339 * ```
340 *
341 * ### Controller instead of type
342 * **Example:** Custom responsibilities without `type`
343 * ```typescript
344 * const account = yield* Stripe.Account("Merchant", {
345 * country: "US",
346 * controller: {
347 * losses: { payments: "application" },
348 * fees: { payer: "application" },
349 * requirementCollection: "application",
350 * stripeDashboard: { type: "none" },
351 * },
352 * });
353 * ```
354 *
355 * @resource
356 * @product Account
357 */
358export const Account = Resource<Account>("Stripe.Account");
359
360const userMetadata = (
361 metadata: Record<string, string | undefined> | null | undefined,
362): Record<string, string> => stripInternalMetadata(tagRecord(metadata));
363
364const capabilityStatuses = (
365 capabilities: StripeAccount["capabilities"],
366): Record<string, string> =>
367 Object.fromEntries(
368 Object.entries(capabilities ?? {}).filter(
369 (entry): entry is [string, string] => typeof entry[1] === "string",
370 ),
371 );
372
373const toWireBusinessProfile = (
374 profile: AccountBusinessProfile,
375): CreateAccountRequestBusinessProfile => ({
376 ...(profile.name !== undefined ? { name: profile.name } : {}),
377 ...(profile.url !== undefined ? { url: profile.url } : {}),
378 ...(profile.mcc !== undefined ? { mcc: profile.mcc } : {}),
379 ...(profile.productDescription !== undefined
380 ? { product_description: profile.productDescription }
381 : {}),
382 ...(profile.supportEmail !== undefined
383 ? { support_email: profile.supportEmail }
384 : {}),
385 ...(profile.supportPhone !== undefined
386 ? { support_phone: profile.supportPhone }
387 : {}),
388 ...(profile.supportUrl !== undefined
389 ? { support_url: profile.supportUrl }
390 : {}),
391});
392
393const toWireController = (
394 controller: AccountController,
395): CreateAccountRequestController => ({
396 ...(controller.fees !== undefined
397 ? {
398 fees: {
399 ...(controller.fees.payer !== undefined
400 ? { payer: controller.fees.payer }
401 : {}),
402 },
403 }
404 : {}),
405 ...(controller.losses !== undefined
406 ? {
407 losses: {
408 ...(controller.losses.payments !== undefined
409 ? { payments: controller.losses.payments }
410 : {}),
411 },
412 }
413 : {}),
414 ...(controller.requirementCollection !== undefined
415 ? { requirement_collection: controller.requirementCollection }
416 : {}),
417 ...(controller.stripeDashboard !== undefined
418 ? {
419 stripe_dashboard: {
420 ...(controller.stripeDashboard.type !== undefined
421 ? { type: controller.stripeDashboard.type }
422 : {}),
423 },
424 }
425 : {}),
426});
427
428const toWireTosAcceptance = (
429 tos: AccountTosAcceptance,
430): CreateAccountRequestTosAcceptance => ({
431 ...(tos.date !== undefined ? { date: tos.date } : {}),
432 ...(tos.ip !== undefined ? { ip: tos.ip } : {}),
433 ...(tos.userAgent !== undefined ? { user_agent: tos.userAgent } : {}),
434 ...(tos.serviceAgreement !== undefined
435 ? { service_agreement: tos.serviceAgreement }
436 : {}),
437});
438
439const toAttrs = (account: StripeAccount) => ({
440 id: account.id,
441 type: account.type,
442 country: account.country,
443 email: account.email ?? undefined,
444 businessType: account.business_type ?? undefined,
445 defaultCurrency: account.default_currency,
446 businessProfileName: account.business_profile?.name ?? undefined,
447 businessProfileUrl: account.business_profile?.url ?? undefined,
448 chargesEnabled: account.charges_enabled ?? false,
449 payoutsEnabled: account.payouts_enabled ?? false,
450 detailsSubmitted: account.details_submitted ?? false,
451 capabilities: capabilityStatuses(account.capabilities),
452 requirementsCurrentlyDue: [...(account.requirements?.currently_due ?? [])],
453 requirementsDisabledReason:
454 account.requirements?.disabled_reason ?? undefined,
455 created: account.created,
456 metadata: userMetadata(account.metadata),
457});
458
459const isMissingAccount = isMissingStripeResource;
460
461const getById = (account: string) =>
462 GetAccountByAccount({ account }).pipe(
463 Effect.catchIf(isMissingAccount, () => Effect.succeed(undefined)),
464 );
465
466const listAllAccounts = Effect.fn(function* () {
467 const accounts: StripeAccount[] = [];
468 let startingAfter: string | undefined;
469 for (let page = 0; page < LIST_MAX_PAGES; page++) {
470 const response = yield* GetAccounts({
471 limit: LIST_PAGE_SIZE,
472 ...(startingAfter !== undefined ? { starting_after: startingAfter } : {}),
473 });
474 accounts.push(...response.data);
475 if (!response.has_more || response.data.length === 0) {
476 break;
477 }
478 startingAfter = response.data[response.data.length - 1]?.id;
479 if (startingAfter === undefined) {
480 break;
481 }
482 }
483 return accounts;
484});
485
486const findByAlchemyId = Effect.fn(function* (id: string) {
487 const accounts = yield* listAllAccounts();
488 const matches: StripeAccount[] = [];
489 for (const account of accounts) {
490 if (yield* hasAlchemyMetadata(id, tagRecord(account.metadata))) {
491 matches.push(account);
492 }
493 }
494 matches.sort((a, b) => (b.created ?? 0) - (a.created ?? 0));
495 return matches[0];
496});
497
498const observe = Effect.fn(function* (input: {
499 id?: string;
500 logicalId: string;
501}) {
502 if (input.id !== undefined) {
503 const byId = yield* getById(input.id);
504 if (byId !== undefined) return byId;
505 }
506 return yield* findByAlchemyId(input.logicalId);
507});
508
509const desiredMetadata = Effect.fn(function* (
510 id: string,
511 metadata: Record<string, string> | undefined,
512) {
513 return {
514 ...toMetadata(metadata),
515 ...(yield* createInternalMetadata(id)),
516 };
517});
518
519const businessProfileChanged = (
520 desired: AccountBusinessProfile | undefined,
521 observed: StripeAccount["business_profile"],
522): boolean => {
523 if (desired === undefined) return false;
524 if (
525 desired.name !== undefined &&
526 desired.name !== (observed?.name ?? undefined)
527 ) {
528 return true;
529 }
530 if (
531 desired.url !== undefined &&
532 desired.url !== (observed?.url ?? undefined)
533 ) {
534 return true;
535 }
536 if (
537 desired.mcc !== undefined &&
538 desired.mcc !== (observed?.mcc ?? undefined)
539 ) {
540 return true;
541 }
542 if (
543 desired.productDescription !== undefined &&
544 desired.productDescription !== (observed?.product_description ?? undefined)
545 ) {
546 return true;
547 }
548 if (
549 desired.supportEmail !== undefined &&
550 desired.supportEmail !== (observed?.support_email ?? undefined)
551 ) {
552 return true;
553 }
554 if (
555 desired.supportPhone !== undefined &&
556 desired.supportPhone !== (observed?.support_phone ?? undefined)
557 ) {
558 return true;
559 }
560 if (
561 desired.supportUrl !== undefined &&
562 desired.supportUrl !== (observed?.support_url ?? undefined)
563 ) {
564 return true;
565 }
566 return false;
567};
568
569const capabilitiesDiverge = (
570 desired: CreateAccountRequestCapabilities | undefined,
571 observed: Record<string, string>,
572): boolean => {
573 if (desired === undefined) return false;
574 return Object.entries(desired).some(([name, value]) => {
575 const requested = value?.requested;
576 if (requested === undefined) return false;
577 const status = observed[name];
578 const onFile = status === "active" || status === "pending";
579 return requested !== onFile;
580 });
581};
582
583const shouldReplace = (
584 news: AccountProps,
585 olds: AccountProps | undefined,
586 output: Account["Attributes"] | undefined,
587): boolean => {
588 if (
589 news.type !== undefined &&
590 output?.type !== undefined &&
591 news.type !== output.type
592 ) {
593 return true;
594 }
595 if (
596 news.country !== undefined &&
597 output?.country !== undefined &&
598 news.country !== output.country
599 ) {
600 return true;
601 }
602 if (
603 olds !== undefined &&
604 news.controller !== undefined &&
605 !deepEqual(news.controller, olds.controller, { stripNullish: true })
606 ) {
607 return true;
608 }
609 return false;
610};
611
612export const AccountProvider = () =>
613 Provider.succeed(Account, {
614 stables: ["id", "type", "country", "created"],
615
616 diff: Effect.fn(function* ({ olds, news, output }) {
617 if (!isResolved(news)) return undefined;
618 if (shouldReplace(news, olds, output)) {
619 return { action: "replace" as const };
620 }
621 return undefined;
622 }),
623
624 read: Effect.fn(function* ({ id, output }) {
625 const existing = yield* observe({
626 id: output?.id,
627 logicalId: id,
628 });
629 if (existing === undefined) return undefined;
630 const attrs = toAttrs(existing);
631 return (yield* hasAlchemyMetadata(id, tagRecord(existing.metadata)))
632 ? attrs
633 : Unowned(attrs);
634 }),
635
636 list: Effect.fn(function* () {
637 const accounts = yield* listAllAccounts();
638 return accounts
639 .filter((account) => {
640 const metadata = tagRecord(account.metadata);
641 return metadata[alchemyMetadataKeys.stack] !== undefined;
642 })
643 .map(toAttrs);
644 }),
645
646 reconcile: Effect.fn(function* ({ id, news, olds, output, instanceId }) {
647 const metadata = yield* desiredMetadata(id, news.metadata);
648 const businessProfile =
649 news.businessProfile !== undefined
650 ? toWireBusinessProfile(news.businessProfile)
651 : undefined;
652 const controller =
653 news.controller !== undefined
654 ? toWireController(news.controller)
655 : undefined;
656 const tosAcceptance =
657 news.tosAcceptance !== undefined
658 ? toWireTosAcceptance(news.tosAcceptance)
659 : undefined;
660
661 let current = yield* observe({
662 id: output?.id,
663 logicalId: id,
664 });
665 let createdThisPass = false;
666
667 if (current === undefined) {
668 current = yield* CreateAccount({
669 ...(news.type !== undefined ? { type: news.type } : {}),
670 ...(news.country !== undefined ? { country: news.country } : {}),
671 ...(controller !== undefined ? { controller } : {}),
672 ...(news.email !== undefined ? { email: news.email } : {}),
673 ...(news.businessType !== undefined
674 ? { business_type: news.businessType }
675 : {}),
676 ...(businessProfile !== undefined
677 ? { business_profile: businessProfile }
678 : {}),
679 ...(news.capabilities !== undefined
680 ? { capabilities: news.capabilities }
681 : {}),
682 ...(news.settings !== undefined ? { settings: news.settings } : {}),
683 ...(news.defaultCurrency !== undefined
684 ? { default_currency: news.defaultCurrency }
685 : {}),
686 ...(news.company !== undefined ? { company: news.company } : {}),
687 ...(news.individual !== undefined
688 ? { individual: news.individual }
689 : {}),
690 ...(tosAcceptance !== undefined
691 ? { tos_acceptance: tosAcceptance }
692 : {}),
693 metadata,
694 }).pipe(
695 withRequestOptions({
696 idempotencyKey: `alchemy-account-${instanceId}`,
697 }),
698 );
699 createdThisPass = true;
700 }
701
702 const observedMetadata = tagRecord(current.metadata);
703 const { upsert, removed } = diffMetadata(observedMetadata, metadata);
704 const metadataChanged = upsert.length > 0 || removed.length > 0;
705 const emailChanged =
706 news.email !== undefined && (current.email ?? undefined) !== news.email;
707 const businessTypeChanged =
708 news.businessType !== undefined &&
709 (current.business_type ?? undefined) !== news.businessType;
710 const defaultCurrencyChanged =
711 news.defaultCurrency !== undefined &&
712 current.default_currency !== news.defaultCurrency;
713 const profileChanged = businessProfileChanged(
714 news.businessProfile,
715 current.business_profile,
716 );
717 const capsChanged = capabilitiesDiverge(
718 news.capabilities,
719 capabilityStatuses(current.capabilities),
720 );
721 const writeOnlyChanged =
722 !createdThisPass &&
723 ((news.settings !== undefined &&
724 (olds === undefined ||
725 !deepEqual(news.settings, olds.settings, {
726 stripNullish: true,
727 }))) ||
728 (news.company !== undefined &&
729 (olds === undefined ||
730 !deepEqual(news.company, olds.company, {
731 stripNullish: true,
732 }))) ||
733 (news.individual !== undefined &&
734 (olds === undefined ||
735 !deepEqual(news.individual, olds.individual, {
736 stripNullish: true,
737 }))) ||
738 (news.tosAcceptance !== undefined &&
739 (olds === undefined ||
740 !deepEqual(news.tosAcceptance, olds.tosAcceptance, {
741 stripNullish: true,
742 }))));
743
744 if (
745 !emailChanged &&
746 !businessTypeChanged &&
747 !defaultCurrencyChanged &&
748 !profileChanged &&
749 !capsChanged &&
750 !writeOnlyChanged &&
751 !metadataChanged
752 ) {
753 return toAttrs(current);
754 }
755
756 const updated = yield* UpdateAccount({
757 account: current.id,
758 ...(emailChanged ? { email: news.email } : {}),
759 ...(businessTypeChanged ? { business_type: news.businessType } : {}),
760 ...(defaultCurrencyChanged
761 ? { default_currency: news.defaultCurrency }
762 : {}),
763 ...(profileChanged ? { business_profile: businessProfile } : {}),
764 ...(capsChanged ? { capabilities: news.capabilities } : {}),
765 ...(writeOnlyChanged && news.settings !== undefined
766 ? { settings: news.settings }
767 : {}),
768 ...(writeOnlyChanged && news.company !== undefined
769 ? { company: news.company }
770 : {}),
771 ...(writeOnlyChanged && news.individual !== undefined
772 ? { individual: news.individual }
773 : {}),
774 ...(writeOnlyChanged && tosAcceptance !== undefined
775 ? { tos_acceptance: tosAcceptance }
776 : {}),
777 ...(metadataChanged
778 ? {
779 metadata: {
780 ...Object.fromEntries(
781 upsert.map((tag) => [tag.Key, tag.Value]),
782 ),
783 ...Object.fromEntries(removed.map((key) => [key, ""])),
784 },
785 }
786 : {}),
787 });
788 return toAttrs(updated);
789 }),
790
791 delete: Effect.fn(function* ({ output }) {
792 yield* DeleteAccount({ account: output.id }).pipe(
793 Effect.catchIf(isMissingAccount, () => Effect.void),
794 );
795 }),
796 });