Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion components/Funding/PaymentRequestButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
7 changes: 3 additions & 4 deletions components/modals/ContributeToFundraiseModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 } =
Expand Down
59 changes: 59 additions & 0 deletions services/funding-pool.service.ts
Original file line number Diff line number Diff line change
@@ -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<FundingPool> {
const response = await ApiClient.post<any>(
`${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<FundingPool> {
const response = await ApiClient.post<any>(`${this.POOL_BASE_PATH}/${poolId}/distribute/`, {
amount: roundRscAmount(params.amount),
application_id: params.applicationId,
});
return transformFundingPool(response);
}
}
28 changes: 18 additions & 10 deletions services/payment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,34 +30,42 @@ export interface PaymentIntentResponse {
stripeAmountCents: number;
}

export type PaymentIntentTarget =
| { fundraiseId: ID; fundingPoolId?: never }
| { fundingPoolId: ID; fundraiseId?: never };

/**
* Service for handling payment-related API calls.
*/
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<PaymentIntentResponse> {
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<PaymentIntentApiResponse>(
`${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,
Expand Down
Loading