From 47c95354e4fa0665455f7a63675e15c2411b7521 Mon Sep 17 00:00:00 2001 From: Michal Lehotsky Date: Sat, 13 Jun 2026 22:23:17 -0700 Subject: [PATCH 1/6] fix(crowdfunding): expose type-specific fields for event and security audit initiatives Event dates (event_start_date / event_end_date) and security audit details (ostif_detail / contacts) were returned by the CF backend but not surfaced through the Self Serve BFF or edit UI, making it impossible for owners to view or update these fields after creation. - Add BackendOSTIFDetail, BackendContact, BackendContactInput types to the server wire shape (crowdfunding.types.ts) - Extend BackendInitiative with ostif_detail, contacts, entity_details, coc_url, accept_funding, is_online, and eventbrite_url - Extend BackendUpdateInitiativeInput with event_start_date, event_end_date, ostif_detail, and contacts - Add OSTIFDetail, InitiativeContact, UpdateOSTIFDetailInput, and UpdateContactInput to the shared interfaces - Extend InitiativeDetail with all type-specific read fields - Extend UpdateInitiativeInput with eventStartDate, eventEndDate, ostifDetail, and contacts - Map new fields in mapToInitiativeDetail (crowdfunding-mapper.ts) - Forward new fields in updateInitiative() in the server service and controller - Add event date pickers (shown only for event type) and security audit detail fields (shown only for security_audit type) to the settings drawer Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Michal Lehotsky --- .../initiative-settings-drawer.component.html | 82 +++++++++++++++++++ .../initiative-settings-drawer.component.ts | 61 ++++++++++++-- .../controllers/crowdfunding.controller.ts | 26 ++++++ .../server/services/crowdfunding.service.ts | 22 +++++ .../src/server/types/crowdfunding.types.ts | 48 +++++++++++ .../src/server/utils/crowdfunding-mapper.ts | 34 ++++++++ .../src/interfaces/crowdfunding.interface.ts | 48 +++++++++++ 7 files changed, 314 insertions(+), 7 deletions(-) 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..1296c6128 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,88 @@

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

Event Dates

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

Security Audit Details

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

Enter the total budget in US dollars.

+
+ +
+ + +
+
+ } } 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..dcdf240ab 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'; @@ -71,6 +72,15 @@ export class InitiativeSettingsDrawerComponent { topics: new FormControl([], Validators.required), websiteUrl: new FormControl(''), goal: new FormControl(null, [Validators.min(0)]), + // event-type only + eventStartDate: new FormControl(''), + eventEndDate: new FormControl(''), + // security_audit (ostif) only + monetizationStrategy: new FormControl(''), + currentSecurityStrategy: new FormControl(''), + licenseType: new FormControl(''), + totalBudgetCents: new FormControl(null, [Validators.min(0)]), + termsConditions: new FormControl(false), }); protected readonly saving = signal(false); @@ -101,6 +111,13 @@ 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 get eventStartDateControl(): FormControl { return this.form.controls['eventStartDate'] as FormControl; } + protected get eventEndDateControl(): FormControl { return this.form.controls['eventEndDate'] as FormControl; } + protected get totalBudgetCentsControl(): FormControl { return this.form.controls['totalBudgetCents'] as FormControl; } + protected get termsConditionsControl(): FormControl { return this.form.controls['termsConditions'] as FormControl; } public constructor() { toObservable(this.visible) @@ -119,6 +136,13 @@ export class InitiativeSettingsDrawerComponent { topics: existingTopics, websiteUrl: init.websiteUrl ?? '', goal: init.fundingStatus?.goalsTotalCents != null ? init.fundingStatus.goalsTotalCents / 100 : null, + eventStartDate: init.eventStartDate ? init.eventStartDate.substring(0, 10) : '', + eventEndDate: init.eventEndDate ? init.eventEndDate.substring(0, 10) : '', + monetizationStrategy: init.ostifDetail?.monetizationStrategy ?? '', + currentSecurityStrategy: init.ostifDetail?.currentSecurityStrategy ?? '', + licenseType: init.ostifDetail?.licenseType ?? '', + totalBudgetCents: init.ostifDetail?.totalBudgetCents ?? null, + termsConditions: init.ostifDetail?.termsConditions ?? false, }); this.logoUrl.set(init.logoUrl ?? ''); this.logoUploadError.set(null); @@ -151,13 +175,21 @@ export class InitiativeSettingsDrawerComponent { this.saving.set(true); try { - const { name, description, topics, websiteUrl, goal } = this.form.value as { - name: string; - description: string; - topics: string[]; - websiteUrl: string; - goal: number | null; - }; + const { name, description, topics, websiteUrl, goal, eventStartDate, eventEndDate, monetizationStrategy, currentSecurityStrategy, licenseType, totalBudgetCents, termsConditions } = + this.form.value as { + name: string; + description: string; + topics: string[]; + websiteUrl: string; + goal: number | null; + eventStartDate: string; + eventEndDate: string; + monetizationStrategy: string; + currentSecurityStrategy: string; + licenseType: string; + totalBudgetCents: number | null; + termsConditions: boolean; + }; const input: UpdateInitiativeInput = { name, @@ -167,6 +199,21 @@ export class InitiativeSettingsDrawerComponent { websiteUrl: websiteUrl || undefined, }; + if (this.isEventType()) { + input.eventStartDate = eventStartDate || undefined; + input.eventEndDate = eventEndDate || undefined; + } + + if (this.isSecurityAudit()) { + input.ostifDetail = { + monetizationStrategy: monetizationStrategy || undefined, + currentSecurityStrategy: currentSecurityStrategy || undefined, + licenseType: licenseType || undefined, + totalBudgetCents: totalBudgetCents != null ? Math.round(totalBudgetCents) : undefined, + termsConditions, + }; + } + if (goal != null) { const goalCents = Math.round(goal * 100); const enabledItems = this.distributionItems().filter((i) => i.enabled); diff --git a/apps/lfx-one/src/server/controllers/crowdfunding.controller.ts b/apps/lfx-one/src/server/controllers/crowdfunding.controller.ts index c31d7a3c0..8196206bf 100644 --- a/apps/lfx-one/src/server/controllers/crowdfunding.controller.ts +++ b/apps/lfx-one/src/server/controllers/crowdfunding.controller.ts @@ -281,6 +281,32 @@ export class CrowdfundingController { input.status = rawStatus as CrowdfundingInitiativeStatus; } + if (typeof body['eventStartDate'] === 'string') input.eventStartDate = body['eventStartDate']; + if (typeof body['eventEndDate'] === 'string') input.eventEndDate = body['eventEndDate']; + + 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..d1158cdfb 100644 --- a/apps/lfx-one/src/server/services/crowdfunding.service.ts +++ b/apps/lfx-one/src/server/services/crowdfunding.service.ts @@ -303,6 +303,28 @@ export class CrowdfundingService { if (input.logoUrl !== undefined) body.logo_url = input.logoUrl; if (input.websiteUrl !== undefined) body.website_url = input.websiteUrl; if (input.status !== undefined) body.status = input.status; + if (input.eventStartDate !== undefined) body.event_start_date = input.eventStartDate; + if (input.eventEndDate !== undefined) body.event_end_date = input.eventEndDate; + 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..0e670a847 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,18 @@ 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; + entity_details?: Record; + ostif_detail?: BackendOSTIFDetail; + contacts?: BackendContact[]; created_on: string; updated_on: string; financials?: { @@ -137,6 +173,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; @@ -145,6 +189,10 @@ export interface BackendUpdateInitiativeInput { logo_url?: string; website_url?: string; status?: string; + event_start_date?: string; + event_end_date?: string; + 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..f3ba31c79 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, @@ -71,6 +75,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 +94,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..c7b0d62c0 100644 --- a/packages/shared/src/interfaces/crowdfunding.interface.ts +++ b/packages/shared/src/interfaces/crowdfunding.interface.ts @@ -137,8 +137,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,6 +291,24 @@ 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; @@ -272,6 +316,10 @@ export interface UpdateInitiativeInput { logoUrl?: string; websiteUrl?: string; status?: CrowdfundingInitiativeStatus; + eventStartDate?: string; + eventEndDate?: string; + ostifDetail?: UpdateOSTIFDetailInput; + contacts?: UpdateContactInput[]; goals?: UpdateGoalInput[]; beneficiaries?: UpdateBeneficiaryInput[]; } From 72d70fd16c8cb30fa58f9b32a71ffbf3de9593f0 Mon Sep 17 00:00:00 2001 From: Michal Lehotsky Date: Sat, 13 Jun 2026 23:04:49 -0700 Subject: [PATCH 2/6] fix(crowdfunding): add missing type-specific fields to initiative settings drawer Follow-up to the initial type-specific fields fix. After auditing the CF initiative creation flow, several fields were found missing from the settings drawer and the BFF layer for each initiative type. All types: - Add Code of Conduct URL (coc_url) field Event type (expanded from just dates): - Add Registration URL (application_url) - Add Eventbrite URL (eventbrite_url) - Add City and Country location fields - Add Online event toggle (is_online) Project type: - Add CII Project ID (cii_project_id) field Security audit type: - Add contact management (primary, secondary, technical lead) with first/last name, email, phone, preferred contact method - Each contact type can only be added once (enforced client-side) Backend (BFF): - Extend BackendUpdateInitiativeInput and UpdateInitiativeInput with all new fields (cocUrl, ciiProjectId, applicationUrl, eventbriteUrl, country, city, isOnline, acceptFunding) - Add cii_project_id to BackendInitiative wire type - Map ciiProjectId in mapToInitiativeBase - Forward all new fields through controller and service to CF API - Fix date format: convert YYYY-MM-DD from date picker to RFC3339 (T00:00:00Z suffix) required by Go's time.Time JSON decoder Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Michal Lehotsky --- .../initiative-settings-drawer.component.html | 111 +++++++++++++++++- .../initiative-settings-drawer.component.ts | 109 ++++++++++++++--- .../controllers/crowdfunding.controller.ts | 8 ++ .../server/services/crowdfunding.service.ts | 17 ++- .../src/server/types/crowdfunding.types.ts | 12 ++ .../src/server/utils/crowdfunding-mapper.ts | 1 + .../src/interfaces/crowdfunding.interface.ts | 12 ++ 7 files changed, 248 insertions(+), 22 deletions(-) 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 1296c6128..7fd79446d 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 @@ -98,13 +98,27 @@

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

Event Dates

+
+

Event Details

- + Event Dates<
- + Event Dates< class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" data-testid="settings-event-end-input" />
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
} @@ -177,6 +220,64 @@

Security Aud inputId="settings-terms" />

+ + +
+
+

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 dcdf240ab..26518fd4d 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 @@ -71,10 +71,18 @@ 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(''), @@ -89,6 +97,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(() => @@ -113,9 +128,11 @@ export class InitiativeSettingsDrawerComponent { 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 === ('project' as FundType)); 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; } protected get termsConditionsControl(): FormControl { return this.form.controls['termsConditions'] as FormControl; } @@ -135,9 +152,16 @@ 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 ?? '', @@ -158,6 +182,7 @@ export class InitiativeSettingsDrawerComponent { }) ); this.beneficiaryGroups.set([]); + this.contactGroups.set((init.contacts ?? []).map((c) => this.makeContactGroup(c))); this.activeSettingsTab.set('details'); }); } @@ -175,21 +200,31 @@ export class InitiativeSettingsDrawerComponent { this.saving.set(true); try { - const { name, description, topics, websiteUrl, goal, eventStartDate, eventEndDate, monetizationStrategy, currentSecurityStrategy, licenseType, totalBudgetCents, termsConditions } = - this.form.value as { - name: string; - description: string; - topics: string[]; - websiteUrl: string; - goal: number | null; - eventStartDate: string; - eventEndDate: string; - monetizationStrategy: string; - currentSecurityStrategy: string; - licenseType: string; - totalBudgetCents: number | null; - termsConditions: boolean; - }; + const { + name, description, topics, websiteUrl, cocUrl, goal, ciiProjectId, + eventStartDate, eventEndDate, applicationUrl, eventbriteUrl, country, city, isOnline, + monetizationStrategy, currentSecurityStrategy, licenseType, totalBudgetCents, termsConditions, + } = 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; + termsConditions: boolean; + }; const input: UpdateInitiativeInput = { name, @@ -197,11 +232,21 @@ 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()) { @@ -212,6 +257,17 @@ export class InitiativeSettingsDrawerComponent { totalBudgetCents: totalBudgetCents != null ? Math.round(totalBudgetCents) : undefined, termsConditions, }; + const contactGs = this.contactGroups(); + if (contactGs.length > 0) { + input.contacts = contactGs.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) { @@ -340,4 +396,27 @@ 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 8196206bf..2f38f6d0f 100644 --- a/apps/lfx-one/src/server/controllers/crowdfunding.controller.ts +++ b/apps/lfx-one/src/server/controllers/crowdfunding.controller.ts @@ -281,8 +281,16 @@ 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']; if (typeof body['eventEndDate'] === 'string') input.eventEndDate = body['eventEndDate']; + 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; diff --git a/apps/lfx-one/src/server/services/crowdfunding.service.ts b/apps/lfx-one/src/server/services/crowdfunding.service.ts index d1158cdfb..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,9 +307,17 @@ 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.eventStartDate !== undefined) body.event_start_date = input.eventStartDate; - if (input.eventEndDate !== undefined) body.event_end_date = input.eventEndDate; + 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, diff --git a/apps/lfx-one/src/server/types/crowdfunding.types.ts b/apps/lfx-one/src/server/types/crowdfunding.types.ts index 0e670a847..264c7a634 100644 --- a/apps/lfx-one/src/server/types/crowdfunding.types.ts +++ b/apps/lfx-one/src/server/types/crowdfunding.types.ts @@ -71,6 +71,7 @@ export interface BackendInitiative { eventbrite_url?: string; event_start_date?: string; event_end_date?: string; + cii_project_id?: string; entity_details?: Record; ostif_detail?: BackendOSTIFDetail; contacts?: BackendContact[]; @@ -188,9 +189,20 @@ 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[]; diff --git a/apps/lfx-one/src/server/utils/crowdfunding-mapper.ts b/apps/lfx-one/src/server/utils/crowdfunding-mapper.ts index f3ba31c79..96605bb21 100644 --- a/apps/lfx-one/src/server/utils/crowdfunding-mapper.ts +++ b/apps/lfx-one/src/server/utils/crowdfunding-mapper.ts @@ -62,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, diff --git a/packages/shared/src/interfaces/crowdfunding.interface.ts b/packages/shared/src/interfaces/crowdfunding.interface.ts index c7b0d62c0..ff2436a8a 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; } @@ -315,9 +316,20 @@ export interface UpdateInitiativeInput { 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[]; From d3f6ee7b9abbfd745b9a217d279e28cb80040db1 Mon Sep 17 00:00:00 2001 From: Michal Lehotsky Date: Sat, 13 Jun 2026 23:27:16 -0700 Subject: [PATCH 3/6] fix(crowdfunding): remove terms & conditions toggle from initiative settings drawer Terms acceptance is a one-time attestation made at initiative creation and should not be editable after submission. Exposing it as a toggle in the settings drawer could allow owners to retract their acceptance, corrupting the compliance record. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Michal Lehotsky --- .../initiative-settings-drawer.component.html | 8 -------- .../initiative-settings-drawer.component.ts | 7 +------ 2 files changed, 1 insertion(+), 14 deletions(-) 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 7fd79446d..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 @@ -213,14 +213,6 @@

Security Aud

Enter the total budget in US dollars.

-
- - -
-
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 26518fd4d..d7737504c 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 @@ -88,7 +88,6 @@ export class InitiativeSettingsDrawerComponent { currentSecurityStrategy: new FormControl(''), licenseType: new FormControl(''), totalBudgetCents: new FormControl(null, [Validators.min(0)]), - termsConditions: new FormControl(false), }); protected readonly saving = signal(false); @@ -134,7 +133,6 @@ export class InitiativeSettingsDrawerComponent { 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; } - protected get termsConditionsControl(): FormControl { return this.form.controls['termsConditions'] as FormControl; } public constructor() { toObservable(this.visible) @@ -166,7 +164,6 @@ export class InitiativeSettingsDrawerComponent { currentSecurityStrategy: init.ostifDetail?.currentSecurityStrategy ?? '', licenseType: init.ostifDetail?.licenseType ?? '', totalBudgetCents: init.ostifDetail?.totalBudgetCents ?? null, - termsConditions: init.ostifDetail?.termsConditions ?? false, }); this.logoUrl.set(init.logoUrl ?? ''); this.logoUploadError.set(null); @@ -203,7 +200,7 @@ export class InitiativeSettingsDrawerComponent { const { name, description, topics, websiteUrl, cocUrl, goal, ciiProjectId, eventStartDate, eventEndDate, applicationUrl, eventbriteUrl, country, city, isOnline, - monetizationStrategy, currentSecurityStrategy, licenseType, totalBudgetCents, termsConditions, + monetizationStrategy, currentSecurityStrategy, licenseType, totalBudgetCents, } = this.form.value as { name: string; description: string; @@ -223,7 +220,6 @@ export class InitiativeSettingsDrawerComponent { currentSecurityStrategy: string; licenseType: string; totalBudgetCents: number | null; - termsConditions: boolean; }; const input: UpdateInitiativeInput = { @@ -255,7 +251,6 @@ export class InitiativeSettingsDrawerComponent { currentSecurityStrategy: currentSecurityStrategy || undefined, licenseType: licenseType || undefined, totalBudgetCents: totalBudgetCents != null ? Math.round(totalBudgetCents) : undefined, - termsConditions, }; const contactGs = this.contactGroups(); if (contactGs.length > 0) { From 9406c64c04066d3ef4f5c09c895a133d377dd39b Mon Sep 17 00:00:00 2001 From: Michal Lehotsky Date: Sun, 14 Jun 2026 21:46:37 -0700 Subject: [PATCH 4/6] fix(crowdfunding): fix totalBudgetCents unit mismatch and isProjectType comparison MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review comments from coderabbitai and copilot-pull-request-reviewer: - initiative-settings-drawer.component.ts: divide totalBudgetCents by 100 when patching form (display dollars, not cents) (per coderabbitai, copilot-pull-request-reviewer) - initiative-settings-drawer.component.ts: multiply by 100 before saving totalBudgetCents so backend receives cents not dollars (per coderabbitai, copilot-pull-request-reviewer) - initiative-settings-drawer.component.ts: fix isProjectType computed — replace non-existent 'project' FundType cast with FundType.GENERAL_FUND so the CII Project ID field is shown and saved for general fund initiatives (per coderabbitai, copilot-pull-request-reviewer) - crowdfunding.interface.ts: make OSTIFDetail.totalBudgetCents and termsConditions optional to match UI defensive handling and UpdateOSTIFDetailInput (per coderabbitai) Resolves 7 review threads. Signed-off-by: Michal Lehotsky --- .../initiative-settings-drawer.component.ts | 6 +++--- packages/shared/src/interfaces/crowdfunding.interface.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) 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 d7737504c..2cfa49883 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 @@ -127,7 +127,7 @@ export class InitiativeSettingsDrawerComponent { 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 === ('project' as FundType)); + 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; } @@ -163,7 +163,7 @@ export class InitiativeSettingsDrawerComponent { monetizationStrategy: init.ostifDetail?.monetizationStrategy ?? '', currentSecurityStrategy: init.ostifDetail?.currentSecurityStrategy ?? '', licenseType: init.ostifDetail?.licenseType ?? '', - totalBudgetCents: init.ostifDetail?.totalBudgetCents ?? null, + totalBudgetCents: init.ostifDetail?.totalBudgetCents != null ? init.ostifDetail.totalBudgetCents / 100 : null, }); this.logoUrl.set(init.logoUrl ?? ''); this.logoUploadError.set(null); @@ -250,7 +250,7 @@ export class InitiativeSettingsDrawerComponent { monetizationStrategy: monetizationStrategy || undefined, currentSecurityStrategy: currentSecurityStrategy || undefined, licenseType: licenseType || undefined, - totalBudgetCents: totalBudgetCents != null ? Math.round(totalBudgetCents) : undefined, + totalBudgetCents: totalBudgetCents != null ? Math.round(totalBudgetCents * 100) : undefined, }; const contactGs = this.contactGroups(); if (contactGs.length > 0) { diff --git a/packages/shared/src/interfaces/crowdfunding.interface.ts b/packages/shared/src/interfaces/crowdfunding.interface.ts index ff2436a8a..9840908d4 100644 --- a/packages/shared/src/interfaces/crowdfunding.interface.ts +++ b/packages/shared/src/interfaces/crowdfunding.interface.ts @@ -142,8 +142,8 @@ export interface OSTIFDetail { monetizationStrategy?: string; currentSecurityStrategy?: string; licenseType?: string; - totalBudgetCents: number; - termsConditions: boolean; + totalBudgetCents?: number; + termsConditions?: boolean; } export interface InitiativeContact { From f94951cc1ac1f9626163d6a7a7bbe5a59b41ff3d Mon Sep 17 00:00:00 2001 From: Michal Lehotsky Date: Sun, 14 Jun 2026 21:52:00 -0700 Subject: [PATCH 5/6] style(crowdfunding): apply prettier formatting to initiative-settings-drawer component Fix pre-existing formatting failures in Quality Checks CI. Signed-off-by: Michal Lehotsky --- .../initiative-settings-drawer.component.ts | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) 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 2cfa49883..c8863f535 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 @@ -129,10 +129,18 @@ export class InitiativeSettingsDrawerComponent { 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; } + 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) @@ -198,9 +206,24 @@ export class InitiativeSettingsDrawerComponent { try { const { - name, description, topics, websiteUrl, cocUrl, goal, ciiProjectId, - eventStartDate, eventEndDate, applicationUrl, eventbriteUrl, country, city, isOnline, - monetizationStrategy, currentSecurityStrategy, licenseType, totalBudgetCents, + 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; @@ -404,7 +427,14 @@ export class InitiativeSettingsDrawerComponent { 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 { + 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 ?? ''), From 50b1d88d51d9d9fbc3943cfaec0dee8310ffdbc0 Mon Sep 17 00:00:00 2001 From: Michal Lehotsky Date: Sun, 14 Jun 2026 22:31:19 -0700 Subject: [PATCH 6/6] fix(crowdfunding): fix contacts clear and date normalization in settings drawer Address review comments from copilot-pull-request-reviewer: - initiative-settings-drawer.component.ts: always set input.contacts for security audit initiatives (remove contactGroups.length guard) so clearing all contacts sends an empty array and the upstream service can clear the list - crowdfunding.controller.ts: normalize eventStartDate/eventEndDate with .trim() || undefined, consistent with all other string fields in the block, to avoid forwarding empty strings to toRFC3339Date() and the upstream API Resolves 2 review threads. Signed-off-by: Michal Lehotsky --- .../initiative-settings-drawer.component.ts | 19 ++++++++----------- .../controllers/crowdfunding.controller.ts | 4 ++-- 2 files changed, 10 insertions(+), 13 deletions(-) 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 c8863f535..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 @@ -275,17 +275,14 @@ export class InitiativeSettingsDrawerComponent { licenseType: licenseType || undefined, totalBudgetCents: totalBudgetCents != null ? Math.round(totalBudgetCents * 100) : undefined, }; - const contactGs = this.contactGroups(); - if (contactGs.length > 0) { - input.contacts = contactGs.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, - })); - } + 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) { diff --git a/apps/lfx-one/src/server/controllers/crowdfunding.controller.ts b/apps/lfx-one/src/server/controllers/crowdfunding.controller.ts index 2f38f6d0f..c4f97c218 100644 --- a/apps/lfx-one/src/server/controllers/crowdfunding.controller.ts +++ b/apps/lfx-one/src/server/controllers/crowdfunding.controller.ts @@ -284,8 +284,8 @@ export class CrowdfundingController { 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']; - if (typeof body['eventEndDate'] === 'string') input.eventEndDate = body['eventEndDate']; + 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;