diff --git a/components/Funding/PaymentRequestButton.tsx b/components/Funding/PaymentRequestButton.tsx index 454d0a219..f822b9257 100644 --- a/components/Funding/PaymentRequestButton.tsx +++ b/components/Funding/PaymentRequestButton.tsx @@ -104,7 +104,9 @@ function PaymentRequestButtonInner({ try { // Create payment intent on our backend - const { clientSecret } = await PaymentService.createPaymentIntent(amountInRsc, fundraiseId); + const { clientSecret } = await PaymentService.createPaymentIntent(amountInRsc, { + fundraiseId, + }); // Confirm the payment with the payment method from Apple Pay/Google Pay const { error: confirmError, paymentIntent } = await stripe!.confirmCardPayment( diff --git a/components/modals/ContributeToFundraiseModal.tsx b/components/modals/ContributeToFundraiseModal.tsx index 5e3fb48ff..793108786 100644 --- a/components/modals/ContributeToFundraiseModal.tsx +++ b/components/modals/ContributeToFundraiseModal.tsx @@ -298,10 +298,9 @@ function ContributeToFundraiseModalInner({ const { stripe, cardElement } = stripeContext; // Step 1: Create payment intent with amount and fundraise ID (backend adds fees and handles contribution) - const { clientSecret } = await PaymentService.createPaymentIntent( - amountInRsc, - fundraise.id - ); + const { clientSecret } = await PaymentService.createPaymentIntent(amountInRsc, { + fundraiseId: fundraise.id, + }); // Step 2: Confirm payment with Stripe const { error: stripeError, paymentIntent: stripePaymentIntent } = diff --git a/services/funding-pool.service.ts b/services/funding-pool.service.ts new file mode 100644 index 000000000..06a263a6a --- /dev/null +++ b/services/funding-pool.service.ts @@ -0,0 +1,59 @@ +import { ApiClient } from './client'; +import { roundRscAmount } from './lib/serviceUtils'; +import { FundingPool, transformFundingPool } from '@/types/grant'; +import { ID } from '@/types/root'; + +export interface CreateFundingPoolContributionParams { + amount: number; + useCredits?: boolean; +} + +export interface DistributeFundingPoolParams { + amount: number; + applicationId: ID; +} + +/** + * Service for RFP community FundingPool APIs. + */ +export class FundingPoolService { + private static readonly POOL_BASE_PATH = '/api/funding_pool'; + + /** + * Contribute RSC (wallet balance or funding credits) into the pool. + * Pool contributions are RSC-only. + * + * @param poolId - Funding pool id + * @param params - Contribution amount and whether to use funding credits + * @returns The updated funding pool + */ + static async createContribution( + poolId: ID, + params: CreateFundingPoolContributionParams + ): Promise { + const response = await ApiClient.post( + `${this.POOL_BASE_PATH}/${poolId}/create_contribution/`, + { + amount: roundRscAmount(params.amount), + amount_currency: 'RSC', + use_credits: params.useCredits ?? false, + } + ); + return transformFundingPool(response); + } + + /** + * Allocate pool holdings into an open proposal fundraise via its grant application. + * + * @param poolId - Funding pool id + * @param params - RSC amount and application id + * @returns The updated funding pool + */ + static async distribute(poolId: ID, params: DistributeFundingPoolParams): Promise { + const response = await ApiClient.post(`${this.POOL_BASE_PATH}/${poolId}/distribute/`, { + amount: roundRscAmount(params.amount), + application_id: params.applicationId, + }); + return transformFundingPool(response); + } +} diff --git a/services/payment.service.ts b/services/payment.service.ts index 2a5ed90b1..30bec17d0 100644 --- a/services/payment.service.ts +++ b/services/payment.service.ts @@ -30,6 +30,10 @@ export interface PaymentIntentResponse { stripeAmountCents: number; } +export type PaymentIntentTarget = + | { fundraiseId: ID; fundingPoolId?: never } + | { fundingPoolId: ID; fundraiseId?: never }; + /** * Service for handling payment-related API calls. */ @@ -37,27 +41,31 @@ export class PaymentService { private static readonly BASE_PATH = '/api/payment'; /** - * Creates a payment intent for purchasing RSC and contributing to a fundraise. - * The backend will add fees to the amount and handle the contribution. + * Creates a payment intent for purchasing RSC and contributing to a fundraise + * or funding pool. The backend adds fees and handles the contribution. * * @param amount The RSC amount to purchase (without fees) - * @param fundraiseId The ID of the fundraise to contribute to + * @param target Exactly one of fundraiseId or fundingPoolId * @returns Promise containing the Stripe client secret and payment details */ static async createPaymentIntent( amount: number, - fundraiseId: ID + target: PaymentIntentTarget ): Promise { + const hasFundingPool = 'fundingPoolId' in target; + const body = { + amount: roundRscAmount(amount), + currency: 'RSC' as const, + ...(hasFundingPool + ? { funding_pool_id: target.fundingPoolId } + : { fundraise_id: target.fundraiseId }), + }; + const response = await ApiClient.post( `${this.BASE_PATH}/payment-intent/`, - { - amount: roundRscAmount(amount), - currency: 'RSC', - fundraise_id: fundraiseId, - } + body ); - // Transform snake_case to camelCase return { clientSecret: response.client_secret, paymentIntentId: response.payment_intent_id,