diff --git a/apps/lfx-one/src/app/modules/crowdfunding/initiative-detail/components/initiative-settings-drawer/initiative-settings-drawer.component.html b/apps/lfx-one/src/app/modules/crowdfunding/initiative-detail/components/initiative-settings-drawer/initiative-settings-drawer.component.html index 4bc626ab1..c2bf61414 100644 --- a/apps/lfx-one/src/app/modules/crowdfunding/initiative-detail/components/initiative-settings-drawer/initiative-settings-drawer.component.html +++ b/apps/lfx-one/src/app/modules/crowdfunding/initiative-detail/components/initiative-settings-drawer/initiative-settings-drawer.component.html @@ -97,6 +97,181 @@

General Fund + + +
+ + +
+ + + @if (isProjectType()) { +
+ + +
+ } + + + @if (isEventType()) { +
+

Event Details

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ } + + + @if (isSecurityAudit()) { +
+

Security Audit Details

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +

Enter the total budget in US dollars.

+
+ + +
+
+

Contacts

+
+ + @for (group of contactGroups(); track $index) { +
+
+ + @for (ct of CONTACT_TYPES; track ct.value) { + @if (ct.value === group.value['contactType']) { {{ ct.label }} } + } + + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ } + +
+ @for (ct of CONTACT_TYPES; track ct.value) { + @if (!usedContactTypes().includes(ct.value)) { + + } + } +
+
+
+ } } diff --git a/apps/lfx-one/src/app/modules/crowdfunding/initiative-detail/components/initiative-settings-drawer/initiative-settings-drawer.component.ts b/apps/lfx-one/src/app/modules/crowdfunding/initiative-detail/components/initiative-settings-drawer/initiative-settings-drawer.component.ts index 477eff2ca..679919bd0 100644 --- a/apps/lfx-one/src/app/modules/crowdfunding/initiative-detail/components/initiative-settings-drawer/initiative-settings-drawer.component.ts +++ b/apps/lfx-one/src/app/modules/crowdfunding/initiative-detail/components/initiative-settings-drawer/initiative-settings-drawer.component.ts @@ -22,6 +22,7 @@ import { DEFAULT_FUND_DISTRIBUTION, MAX_LOGO_SIZE_BYTES, } from '@lfx-one/shared/constants'; +import { FundType } from '@lfx-one/shared/enums'; import { FundDistributionItem, InitiativeDetail, TabOption, UpdateInitiativeInput } from '@lfx-one/shared/interfaces'; import { CrowdfundingService } from '@services/crowdfunding.service'; @@ -70,7 +71,23 @@ export class InitiativeSettingsDrawerComponent { description: new FormControl('', [Validators.required, Validators.maxLength(500)]), topics: new FormControl([], Validators.required), websiteUrl: new FormControl(''), + cocUrl: new FormControl(''), goal: new FormControl(null, [Validators.min(0)]), + // project-only + ciiProjectId: new FormControl(''), + // event-type only + eventStartDate: new FormControl(''), + eventEndDate: new FormControl(''), + applicationUrl: new FormControl(''), + eventbriteUrl: new FormControl(''), + country: new FormControl(''), + city: new FormControl(''), + isOnline: new FormControl(false), + // security_audit (ostif) only + monetizationStrategy: new FormControl(''), + currentSecurityStrategy: new FormControl(''), + licenseType: new FormControl(''), + totalBudgetCents: new FormControl(null, [Validators.min(0)]), }); protected readonly saving = signal(false); @@ -79,6 +96,13 @@ export class InitiativeSettingsDrawerComponent { protected readonly logoUploadError = signal(null); protected readonly distributionItems = signal(DEFAULT_FUND_DISTRIBUTION.map((i) => ({ ...i }))); protected readonly beneficiaryGroups = signal([]); + protected readonly contactGroups = signal([]); + + protected readonly CONTACT_TYPES = [ + { value: 'primary', label: 'Primary Contact' }, + { value: 'secondary', label: 'Secondary Contact' }, + { value: 'technical_lead', label: 'Technical Lead' }, + ]; protected readonly hasEnabledCategories = computed(() => this.distributionItems().some((i) => i.enabled)); protected readonly totalAllocated = computed(() => @@ -101,6 +125,22 @@ export class InitiativeSettingsDrawerComponent { protected readonly nameLength = computed(() => this.formValue().name?.length ?? 0); protected readonly descriptionLength = computed(() => this.formValue().description?.length ?? 0); protected readonly initiativeInitial = computed(() => this.initiative().name.charAt(0)); + protected readonly isEventType = computed(() => this.initiative().initiativeType === FundType.EVENT); + protected readonly isSecurityAudit = computed(() => this.initiative().initiativeType === FundType.SECURITY_AUDIT); + protected readonly isProjectType = computed(() => this.initiative().initiativeType === FundType.GENERAL_FUND); + + protected get eventStartDateControl(): FormControl { + return this.form.controls['eventStartDate'] as FormControl; + } + protected get eventEndDateControl(): FormControl { + return this.form.controls['eventEndDate'] as FormControl; + } + protected get isOnlineControl(): FormControl { + return this.form.controls['isOnline'] as FormControl; + } + protected get totalBudgetCentsControl(): FormControl { + return this.form.controls['totalBudgetCents'] as FormControl; + } public constructor() { toObservable(this.visible) @@ -118,7 +158,20 @@ export class InitiativeSettingsDrawerComponent { description: init.description, topics: existingTopics, websiteUrl: init.websiteUrl ?? '', + cocUrl: init.cocUrl ?? '', goal: init.fundingStatus?.goalsTotalCents != null ? init.fundingStatus.goalsTotalCents / 100 : null, + ciiProjectId: init.ciiProjectId ?? '', + eventStartDate: init.eventStartDate ? init.eventStartDate.substring(0, 10) : '', + eventEndDate: init.eventEndDate ? init.eventEndDate.substring(0, 10) : '', + applicationUrl: init.applicationUrl ?? '', + eventbriteUrl: init.eventbriteUrl ?? '', + country: init.country ?? '', + city: init.city ?? '', + isOnline: init.isOnline ?? false, + monetizationStrategy: init.ostifDetail?.monetizationStrategy ?? '', + currentSecurityStrategy: init.ostifDetail?.currentSecurityStrategy ?? '', + licenseType: init.ostifDetail?.licenseType ?? '', + totalBudgetCents: init.ostifDetail?.totalBudgetCents != null ? init.ostifDetail.totalBudgetCents / 100 : null, }); this.logoUrl.set(init.logoUrl ?? ''); this.logoUploadError.set(null); @@ -134,6 +187,7 @@ export class InitiativeSettingsDrawerComponent { }) ); this.beneficiaryGroups.set([]); + this.contactGroups.set((init.contacts ?? []).map((c) => this.makeContactGroup(c))); this.activeSettingsTab.set('details'); }); } @@ -151,12 +205,44 @@ export class InitiativeSettingsDrawerComponent { this.saving.set(true); try { - const { name, description, topics, websiteUrl, goal } = this.form.value as { + const { + name, + description, + topics, + websiteUrl, + cocUrl, + goal, + ciiProjectId, + eventStartDate, + eventEndDate, + applicationUrl, + eventbriteUrl, + country, + city, + isOnline, + monetizationStrategy, + currentSecurityStrategy, + licenseType, + totalBudgetCents, + } = this.form.value as { name: string; description: string; topics: string[]; websiteUrl: string; + cocUrl: string; goal: number | null; + ciiProjectId: string; + eventStartDate: string; + eventEndDate: string; + applicationUrl: string; + eventbriteUrl: string; + country: string; + city: string; + isOnline: boolean; + monetizationStrategy: string; + currentSecurityStrategy: string; + licenseType: string; + totalBudgetCents: number | null; }; const input: UpdateInitiativeInput = { @@ -165,8 +251,40 @@ export class InitiativeSettingsDrawerComponent { industry: topics.join(','), logoUrl: this.logoUrl(), websiteUrl: websiteUrl || undefined, + cocUrl: cocUrl || undefined, }; + if (this.isProjectType()) { + input.ciiProjectId = ciiProjectId || undefined; + } + + if (this.isEventType()) { + input.eventStartDate = eventStartDate || undefined; + input.eventEndDate = eventEndDate || undefined; + input.applicationUrl = applicationUrl || undefined; + input.eventbriteUrl = eventbriteUrl || undefined; + input.country = country || undefined; + input.city = city || undefined; + input.isOnline = isOnline; + } + + if (this.isSecurityAudit()) { + input.ostifDetail = { + monetizationStrategy: monetizationStrategy || undefined, + currentSecurityStrategy: currentSecurityStrategy || undefined, + licenseType: licenseType || undefined, + totalBudgetCents: totalBudgetCents != null ? Math.round(totalBudgetCents * 100) : undefined, + }; + input.contacts = this.contactGroups().map((g) => ({ + contactType: g.value.contactType as string, + firstName: (g.value.firstName as string) || undefined, + lastName: (g.value.lastName as string) || undefined, + email: (g.value.email as string) || undefined, + phoneNumber: (g.value.phoneNumber as string) || undefined, + preferredContactMethod: (g.value.preferredContactMethod as string) || undefined, + })); + } + if (goal != null) { const goalCents = Math.round(goal * 100); const enabledItems = this.distributionItems().filter((i) => i.enabled); @@ -293,4 +411,34 @@ export class InitiativeSettingsDrawerComponent { protected removeBeneficiary(index: number): void { this.beneficiaryGroups.update((groups) => groups.filter((_, i) => i !== index)); } + + protected addContact(contactType: string): void { + this.contactGroups.update((groups) => [...groups, this.makeContactGroup({ contactType })]); + } + + protected removeContact(index: number): void { + this.contactGroups.update((groups) => groups.filter((_, i) => i !== index)); + } + + protected usedContactTypes(): string[] { + return this.contactGroups().map((g) => g.value.contactType as string); + } + + private makeContactGroup(c: { + contactType: string; + firstName?: string; + lastName?: string; + email?: string; + phoneNumber?: string; + preferredContactMethod?: string; + }): FormGroup { + return new FormGroup({ + contactType: new FormControl(c.contactType ?? ''), + firstName: new FormControl(c.firstName ?? ''), + lastName: new FormControl(c.lastName ?? ''), + email: new FormControl(c.email ?? '', Validators.email), + phoneNumber: new FormControl(c.phoneNumber ?? ''), + preferredContactMethod: new FormControl(c.preferredContactMethod ?? 'email'), + }); + } } diff --git a/apps/lfx-one/src/server/controllers/crowdfunding.controller.ts b/apps/lfx-one/src/server/controllers/crowdfunding.controller.ts index c31d7a3c0..c4f97c218 100644 --- a/apps/lfx-one/src/server/controllers/crowdfunding.controller.ts +++ b/apps/lfx-one/src/server/controllers/crowdfunding.controller.ts @@ -281,6 +281,40 @@ export class CrowdfundingController { input.status = rawStatus as CrowdfundingInitiativeStatus; } + if (typeof body['cocUrl'] === 'string') input.cocUrl = body['cocUrl'].trim() || undefined; + if (typeof body['acceptFunding'] === 'boolean') input.acceptFunding = body['acceptFunding']; + if (typeof body['ciiProjectId'] === 'string') input.ciiProjectId = body['ciiProjectId'].trim() || undefined; + if (typeof body['eventStartDate'] === 'string') input.eventStartDate = body['eventStartDate'].trim() || undefined; + if (typeof body['eventEndDate'] === 'string') input.eventEndDate = body['eventEndDate'].trim() || undefined; + if (typeof body['applicationUrl'] === 'string') input.applicationUrl = body['applicationUrl'].trim() || undefined; + if (typeof body['eventbriteUrl'] === 'string') input.eventbriteUrl = body['eventbriteUrl'].trim() || undefined; + if (typeof body['country'] === 'string') input.country = body['country'].trim() || undefined; + if (typeof body['city'] === 'string') input.city = body['city'].trim() || undefined; + if (typeof body['isOnline'] === 'boolean') input.isOnline = body['isOnline']; + + if (body['ostifDetail'] !== null && typeof body['ostifDetail'] === 'object') { + const o = body['ostifDetail'] as Record; + input.ostifDetail = { + monetizationStrategy: typeof o['monetizationStrategy'] === 'string' ? o['monetizationStrategy'] : undefined, + currentSecurityStrategy: typeof o['currentSecurityStrategy'] === 'string' ? o['currentSecurityStrategy'] : undefined, + licenseType: typeof o['licenseType'] === 'string' ? o['licenseType'] : undefined, + totalBudgetCents: typeof o['totalBudgetCents'] === 'number' ? o['totalBudgetCents'] : undefined, + termsConditions: typeof o['termsConditions'] === 'boolean' ? o['termsConditions'] : undefined, + }; + } + + if (Array.isArray(body['contacts'])) { + input.contacts = (body['contacts'] as Record[]).map((c) => ({ + contactType: typeof c['contactType'] === 'string' ? c['contactType'] : '', + firstName: typeof c['firstName'] === 'string' ? c['firstName'] : undefined, + lastName: typeof c['lastName'] === 'string' ? c['lastName'] : undefined, + email: typeof c['email'] === 'string' ? c['email'] : undefined, + phoneNumber: typeof c['phoneNumber'] === 'string' ? c['phoneNumber'] : undefined, + otherContactOption: typeof c['otherContactOption'] === 'string' ? c['otherContactOption'] : undefined, + preferredContactMethod: typeof c['preferredContactMethod'] === 'string' ? c['preferredContactMethod'] : undefined, + })); + } + if (Array.isArray(body['goals'])) { input.goals = (body['goals'] as Record[]).map((g) => ({ name: typeof g['name'] === 'string' ? g['name'] : 'Annual Funding Goal', diff --git a/apps/lfx-one/src/server/services/crowdfunding.service.ts b/apps/lfx-one/src/server/services/crowdfunding.service.ts index 4989d865c..a8671714a 100644 --- a/apps/lfx-one/src/server/services/crowdfunding.service.ts +++ b/apps/lfx-one/src/server/services/crowdfunding.service.ts @@ -42,6 +42,11 @@ import { logger } from './logger.service'; const cfBaseUrl = (): string => (process.env['CROWDFUNDING_API_BASE_URL'] || '').replace(/\/+$/, ''); +/** Converts a YYYY-MM-DD date string to RFC3339 (required by the Go backend's time.Time JSON decoder). */ +function toRFC3339Date(date: string): string { + return /^\d{4}-\d{2}-\d{2}$/.test(date) ? `${date}T00:00:00Z` : date; +} + const CF_TIMEOUT_MS = 30_000; function throwCfNetworkError(operation: string, error: unknown): never { @@ -302,7 +307,37 @@ export class CrowdfundingService { if (input.industry !== undefined) body.industry = input.industry; if (input.logoUrl !== undefined) body.logo_url = input.logoUrl; if (input.websiteUrl !== undefined) body.website_url = input.websiteUrl; + if (input.cocUrl !== undefined) body.coc_url = input.cocUrl; + if (input.acceptFunding !== undefined) body.accept_funding = input.acceptFunding; if (input.status !== undefined) body.status = input.status; + if (input.ciiProjectId !== undefined) body.cii_project_id = input.ciiProjectId; + if (input.eventStartDate !== undefined) body.event_start_date = input.eventStartDate ? toRFC3339Date(input.eventStartDate) : input.eventStartDate; + if (input.eventEndDate !== undefined) body.event_end_date = input.eventEndDate ? toRFC3339Date(input.eventEndDate) : input.eventEndDate; + if (input.applicationUrl !== undefined) body.application_url = input.applicationUrl; + if (input.eventbriteUrl !== undefined) body.eventbrite_url = input.eventbriteUrl; + if (input.country !== undefined) body.country = input.country; + if (input.city !== undefined) body.city = input.city; + if (input.isOnline !== undefined) body.is_online = input.isOnline; + if (input.ostifDetail !== undefined) { + body.ostif_detail = { + monetization_strategy: input.ostifDetail.monetizationStrategy, + current_security_strategy: input.ostifDetail.currentSecurityStrategy, + license_type: input.ostifDetail.licenseType, + total_budget_cents: input.ostifDetail.totalBudgetCents, + terms_conditions: input.ostifDetail.termsConditions, + }; + } + if (input.contacts !== undefined) { + body.contacts = input.contacts.map((c) => ({ + contact_type: c.contactType, + first_name: c.firstName, + last_name: c.lastName, + email: c.email, + phone_number: c.phoneNumber, + other_contact_option: c.otherContactOption, + preferred_contact_method: c.preferredContactMethod, + })); + } if (input.goals !== undefined) { body.goals = input.goals.map((g): BackendGoalInput => ({ name: g.name, amount_cents: g.amountCents })); } diff --git a/apps/lfx-one/src/server/types/crowdfunding.types.ts b/apps/lfx-one/src/server/types/crowdfunding.types.ts index 1b2fe0dd6..264c7a634 100644 --- a/apps/lfx-one/src/server/types/crowdfunding.types.ts +++ b/apps/lfx-one/src/server/types/crowdfunding.types.ts @@ -21,6 +21,35 @@ export interface BackendSponsor { total_cents: number; } +export interface BackendOSTIFDetail { + monetization_strategy?: string; + current_security_strategy?: string; + license_type?: string; + total_budget_cents: number; + terms_conditions: boolean; +} + +export interface BackendContact { + id: string; + contact_type: string; + first_name?: string; + last_name?: string; + email?: string; + phone_number?: string; + other_contact_option?: string; + preferred_contact_method?: string; +} + +export interface BackendContactInput { + contact_type: string; + first_name?: string; + last_name?: string; + email?: string; + phone_number?: string; + other_contact_option?: string; + preferred_contact_method?: string; +} + export interface BackendInitiative { id: string; initiative_type: string; @@ -33,11 +62,19 @@ export interface BackendInitiative { color?: string; logo_url?: string; website_url?: string; + coc_url?: string; + accept_funding: boolean; country?: string; city?: string; + is_online: boolean; application_url?: string; + eventbrite_url?: string; event_start_date?: string; event_end_date?: string; + cii_project_id?: string; + entity_details?: Record; + ostif_detail?: BackendOSTIFDetail; + contacts?: BackendContact[]; created_on: string; updated_on: string; financials?: { @@ -137,6 +174,14 @@ export interface BackendBeneficiaryInput { email?: string; } +export interface BackendOSTIFDetailInput { + monetization_strategy?: string; + current_security_strategy?: string; + license_type?: string; + total_budget_cents?: number; + terms_conditions?: boolean; +} + /** Snake_case PATCH body sent to PATCH /v1/me/initiatives/{id} on the upstream crowdfunding service. */ export interface BackendUpdateInitiativeInput { name?: string; @@ -144,7 +189,22 @@ export interface BackendUpdateInitiativeInput { industry?: string; logo_url?: string; website_url?: string; + coc_url?: string; + accept_funding?: boolean; status?: string; + // project-only + cii_project_id?: string; + // event-only + event_start_date?: string; + event_end_date?: string; + application_url?: string; + eventbrite_url?: string; + country?: string; + city?: string; + is_online?: boolean; + // security_audit-only + ostif_detail?: BackendOSTIFDetailInput; + contacts?: BackendContactInput[]; goals?: BackendGoalInput[]; beneficiaries?: BackendBeneficiaryInput[]; } diff --git a/apps/lfx-one/src/server/utils/crowdfunding-mapper.ts b/apps/lfx-one/src/server/utils/crowdfunding-mapper.ts index 0c1b8a626..96605bb21 100644 --- a/apps/lfx-one/src/server/utils/crowdfunding-mapper.ts +++ b/apps/lfx-one/src/server/utils/crowdfunding-mapper.ts @@ -8,7 +8,9 @@ import { FinancialSummary, FundingGoal, InitiativeBase, + InitiativeContact, InitiativeDetail, + OSTIFDetail, SponsorEntry, CrowdfundingInitiativeStatus, MyDonation, @@ -20,9 +22,11 @@ import { import { FundType } from '@lfx-one/shared/enums'; import { + BackendContact, BackendDonation, BackendGoal, BackendInitiative, + BackendOSTIFDetail, BackendSponsor, BackendSubscription, BackendTransaction, @@ -58,6 +62,7 @@ export function mapToInitiativeBase(b: BackendInitiative): InitiativeBase { applicationUrl: b.application_url, eventStartDate: b.event_start_date, eventEndDate: b.event_end_date, + ciiProjectId: b.cii_project_id, fundingStatus: b.financials ? { goalsTotalCents: b.financials.goals_total_cents, @@ -71,6 +76,13 @@ export function mapToInitiativeBase(b: BackendInitiative): InitiativeBase { export function mapToInitiativeDetail(b: BackendInitiative): InitiativeDetail { return { ...mapToInitiativeBase(b), + cocUrl: b.coc_url, + acceptFunding: b.accept_funding, + isOnline: b.is_online, + eventbriteUrl: b.eventbrite_url, + entityDetails: b.entity_details, + ostifDetail: b.ostif_detail ? mapOSTIFDetail(b.ostif_detail) : undefined, + contacts: (b.contacts ?? []).map(mapContact), currentBalanceCents: b.financials?.available_balance_cents, sponsors: (b.sponsors ?? []).map(mapSponsor), fundingGoals: (b.goals ?? []).map(mapFundingGoal), @@ -83,6 +95,29 @@ export function mapToInitiativeDetail(b: BackendInitiative): InitiativeDetail { }; } +function mapOSTIFDetail(o: BackendOSTIFDetail): OSTIFDetail { + return { + monetizationStrategy: o.monetization_strategy, + currentSecurityStrategy: o.current_security_strategy, + licenseType: o.license_type, + totalBudgetCents: o.total_budget_cents, + termsConditions: o.terms_conditions, + }; +} + +function mapContact(c: BackendContact): InitiativeContact { + return { + id: c.id, + contactType: c.contact_type, + firstName: c.first_name, + lastName: c.last_name, + email: c.email, + phoneNumber: c.phone_number, + otherContactOption: c.other_contact_option, + preferredContactMethod: c.preferred_contact_method, + }; +} + function mapSponsor(s: BackendSponsor): SponsorEntry { return { id: s.id, diff --git a/packages/shared/src/interfaces/crowdfunding.interface.ts b/packages/shared/src/interfaces/crowdfunding.interface.ts index 1d392c29d..9840908d4 100644 --- a/packages/shared/src/interfaces/crowdfunding.interface.ts +++ b/packages/shared/src/interfaces/crowdfunding.interface.ts @@ -36,6 +36,7 @@ export interface InitiativeBase { applicationUrl?: string; eventStartDate?: string; eventEndDate?: string; + ciiProjectId?: string; initiativeStats?: InitiativeStats; fundingStatus?: FundingStatus; } @@ -137,8 +138,34 @@ export interface ProjectHealthStat { value: string; } +export interface OSTIFDetail { + monetizationStrategy?: string; + currentSecurityStrategy?: string; + licenseType?: string; + totalBudgetCents?: number; + termsConditions?: boolean; +} + +export interface InitiativeContact { + id: string; + contactType: string; + firstName?: string; + lastName?: string; + email?: string; + phoneNumber?: string; + otherContactOption?: string; + preferredContactMethod?: string; +} + /** Full initiative data returned by the GET /initiatives/:slug detail endpoint. */ export interface InitiativeDetail extends InitiativeBase { + cocUrl?: string; + acceptFunding?: boolean; + isOnline?: boolean; + eventbriteUrl?: string; + entityDetails?: Record; + ostifDetail?: OSTIFDetail; + contacts?: InitiativeContact[]; githubUrl?: string; currentBalanceCents?: number; sponsors?: SponsorEntry[]; @@ -265,13 +292,46 @@ export interface UpdateBeneficiaryInput { email?: string; } +export interface UpdateOSTIFDetailInput { + monetizationStrategy?: string; + currentSecurityStrategy?: string; + licenseType?: string; + totalBudgetCents?: number; + termsConditions?: boolean; +} + +export interface UpdateContactInput { + contactType: string; + firstName?: string; + lastName?: string; + email?: string; + phoneNumber?: string; + otherContactOption?: string; + preferredContactMethod?: string; +} + export interface UpdateInitiativeInput { name?: string; description?: string; industry?: string; logoUrl?: string; websiteUrl?: string; + cocUrl?: string; + acceptFunding?: boolean; status?: CrowdfundingInitiativeStatus; + // project-only + ciiProjectId?: string; + // event-only + eventStartDate?: string; + eventEndDate?: string; + applicationUrl?: string; + eventbriteUrl?: string; + country?: string; + city?: string; + isOnline?: boolean; + // security_audit-only + ostifDetail?: UpdateOSTIFDetailInput; + contacts?: UpdateContactInput[]; goals?: UpdateGoalInput[]; beneficiaries?: UpdateBeneficiaryInput[]; }