Preventing Duplicate Payments in Production
This guide explores two approaches for preventing duplicate payments (and costly returns) in our React example. First, we demonstrate how to disable the submit button after clicking it, preventing accidental multiple submissions. Then, we implement backend logic to block duplicate payment attempts within a five-minute window. By combining these approaches, developers can create a seamless and reliable checkout process.
Disabling the Submit Button in Your Payment Form
To enhance user experience and prevent duplicate submissions, you can disable the submit button once it has been clicked. Here’s an example implementation in your PaymentForm component:
const [buttonDisabled, setButtonDisabled] = useState(false);
const onSubmit = async (data: FormValues) => {
setButtonDisabled(true); // Disable the button immediately upon submission
try {
// Your submission logic
} catch (error) {
console.error(error);
setButtonDisabled(false); // Re-enable the button if an error occurs
}
};
<SubmitButton
handler={handleSubmit(onSubmit as SubmitHandler<FormValues>)}
disabled={buttonDisabled}
/>Key Benefits:
- Prevents users from unintentionally submitting the form multiple times.
- Provides visual feedback to the user, indicating that the action is in progress.
Preventing Duplicate Payments in the Last 5 Minutes
To avoid processing duplicate payments within a short timeframe, you can implement server-side logic that checks for recent payment attempts. Here’s an example using Node.js:
app.post(
"/payment-intents",
async (
req: Request & {
headers: {
payments_account: string;
};
body: {
payment_intent: PaymentIntentCreateParams;
prevent_duplicates?: boolean;
};
},
res: Response & {
json: any;
send: any;
status: any;
}
) => {
const { payments_account } = req.headers;
const { payment_intent, prevent_duplicates = true } = req.body;
try {
if (prevent_duplicates) {
const now = new Date();
const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000); // 5 minutes ago
// Check for duplicate payment intents
const response = await paymentIntentsApi.listPaymentIntents({
payments_account,
q: (payment_intent.amount/100).toFixed(2),
created_at_gte: fiveMinutesAgo.toISOString(),
metadata: payment_intent.metadata,
});
const data = response.data;
console.log(data);
if (data.items.length > 0) {
// If duplicate found, return early with a 409 status
return res.status(409).json(data.items);
}
}
// If no duplicate found, create the payment intent
const createResponse = await paymentIntentsApi.createPaymentIntent({
payments_account,
PaymentIntentCreateParams: payment_intent,
});
return res.json(createResponse.data);
} catch (error) {
console.error(error);
return res.status(404).json(error);
}
}
);How It Works:
- The prevent_duplicates flag is checked. If true, the backend searches for payments made within the last 5 minutes that match the same amount and metadata.
- If duplicates are detected, a 409 Conflict response is returned with the duplicate payment details.
- Otherwise, a new payment is created and processed as usual.
Frontend Error Handling: To handle the 409 status code in your frontend and allow users to retry, you can implement the following in your Error component:
<Error
message="A duplicate payment attempt was detected."
tryAgain={() => {
fetchPaymentIntent({ preventDuplicates: false });
}}
/>
By combining these two approaches, you can ensure a smoother and more reliable payment experience for your users.
Calculating Fees from a Payment Intent
In this guide, we demonstrate how to calculate the fees associated with a Paydude payment intent by iterating over its charges and refunds. This approach is helpful for merchants and developers who want to understand the fee breakdown for transactions processed through Paydude.
Code Overview
The provided code fetches balance transactions related to a payment intent and filters for entries of type “fee”. It calculates the total fees by summing these filtered amounts. Here’s how the code works step-by-step:
- Extract IDs from Charges and Refunds: The code first extracts ids from the charges and refunds in the payment intent.
- Fetch Balance Transactions: Using the extracted ids, the code queries the Paydude API for all balance transactions associated with each charge and refund.
- Filter for Fee Transactions: From the response, the code filters transactions with a type that includes “fee”.
- Calculate Total Fees: Finally, it sums the amounts from the filtered transactions to compute the total fee.
Code Implementation
Below is the Typescript implementation of a function to calculate fees from a given payment intent:
import { PaymentIntent, BalanceTransaction } from "tilled-node";
async function calculateFeesFromPaymentIntent(paymentIntent: PaymentIntent) {
const ids: string[] = [];
let feeTotal: number = 0;
// Step 1: Extract charge and refund IDs
for (const charge of paymentIntent.charges) {
ids.push(charge.id);
for (const refund of charge.refunds) {
ids.push(refund.id);
}
}
// Step 2: Fetch and calculate fees
const feePromises = ids.map(async (id) => {
const response = await fetch(
`https://api.sandbox-paymentsonline.io/v1/balance-transactions?source_id=${id}`,
{
headers: {
"payments-account": "acct_xxxxxxxxxxxxx",
"payments-api-key": "YOUR_TOKEN",
},
}
);
if (!response.ok) {
throw new Error(`Failed to fetch transactions for source_id=${id}`);
}
const txns: BalanceTransaction[] = await response.json();
// Filter transactions of type "fee" and sum the amounts
return txns
.filter((txn) => txn.type.includes("fee"))
.reduce((acc, txn) => acc + txn.amount, 0);
});
// Wait for all promises to resolve and sum up the total fees
const fees = await Promise.all(feePromises);
feeTotal = fees.reduce((acc, fee) => acc + fee, 0);
console.log(`Total Fees: ${feeTotal}`);
return feeTotal;
}Detailed Explanation
- Data Extraction: The charges array is accessed from the PaymentIntent object, and the ids of all charges and refunds are collected.
- API Requests with fetch: For each id, a GET request is sent to fetch balance transactions using the Paydude API.
- Filtering and Summing Fees: Transactions are filtered to include only those of type “fee”, and their amounts are summed up to calculate the total fees.
- Error Handling: If the API request fails, an error is thrown with a helpful message.
- Async Promises: Promise.all ensures all asynchronous requests are completed before calculating the total fees.
Usage
To use this function:
- Pass in a PaymentIntent object fetched from Paydude.
- Ensure you provide the correct Paydude-Account and Paydude-Api-Key in the headers.
- The function returns the total fees as a number.
Important Notes
- API Keys and Security: Never expose your API keys in client-side code. Use server-side environments for security.
- Error Handling: Ensure comprehensive error handling to manage API failures and unexpected responses.
- Performance Consideration: The number of API requests scales with the number of charge and refund IDs. Consider caching for large datasets.
By implementing this function, you can efficiently calculate and track fees associated with your payment intents in Paydude, providing valuable insights into transaction costs.
