sikhaid-payment
Use when working with donations, payments, or Razorpay integration in SikhAid project. Covers Razorpay setup, payment validation, donation flow, success/failure handling, and payment configuration.
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- Updated: 2025-11-22
Download the latest version (v1.0.0) from this page.
SKILL.md
---
name: sikhaid-payment
description: Use when working with donations, payments, or Razorpay integration in SikhAid project. Covers Razorpay setup, payment validation, donation flow, success/failure handling, and payment configuration.
allowed-tools: Read, Edit, Write
---
# SikhAid Payment Integration
## Razorpay Integration Overview
### Payment Gateway
**Provider:** Razorpay
**Configuration File:** `src/lib/razorpay.ts`
**Payment Component:** `src/lib/components/PaymentForm.svelte`
**Success Page:** `src/routes/payment/success/+page.svelte`
**Failure Page:** `src/routes/payment/failure/+page.svelte`
### Razorpay Script Loading
**Location:** `src/routes/+layout.svelte`
```svelte
<script lang="ts">
import { onMount } from 'svelte';
onMount(() => {
// Load Razorpay checkout script
const script = document.createElement('script');
script.src = 'https://checkout.razorpay.com/v1/checkout.js';
script.async = true;
document.body.appendChild(script);
});
</script>
```
## Razorpay Configuration
### Environment Variables
**File:** `.env`
```bash
VITE_RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxxxx # Razorpay Key ID
VITE_ORGANIZATION_NAME=Sikh Aid Charitable Trust
VITE_SITE_URL=www.sikhaid.ngo
```
### Configuration Object
**File:** `src/lib/razorpay.ts`
```typescript
import { env } from '$env/dynamic/public';
export const razorpayConfig = {
key: env.VITE_RAZORPAY_KEY_ID,
currency: 'INR',
name: env.VITE_ORGANIZATION_NAME || 'Sikh Aid Charitable Trust',
description: 'Punjab Floods Relief Aid 2025',
image: '/sikhaidLogo.png',
theme: {
color: '#1a237e' // Navy color
}
};
```
## Donation Data Types
### DonationData Interface
```typescript
export interface DonationData {
amount: number; // Amount in INR
name: string; // Donor name
phone: string; // 10-digit mobile number
email?: string; // Optional email
panCard?: string; // Required for amounts >= ₹2,000 (uppercase, 10 chars)
}
```
### RazorpayResponse Interface
```typescript
export interface RazorpayResponse {
razorpay_payment_id: string; // Payment ID
razorpay_order_id?: string; // Order ID (if order created)
razorpay_signature?: string; // Signature for verification
}
```
## Validation Functions
### Amount Validation
**File:** `src/lib/razorpay.ts`
```typescript
const MIN_AMOUNT = 10; // Minimum ₹10
const MAX_AMOUNT = 500000; // Maximum ₹5,00,000
export function validateDonationAmount(amount: number): {
valid: boolean;
error: string;
} {
if (!amount || amount <= 0) {
return {
valid: false,
error: 'Please enter a valid amount'
};
}
if (amount < MIN_AMOUNT) {
return {
valid: false,
error: `Minimum donation amount is ₹${MIN_AMOUNT}`
};
}
if (amount > MAX_AMOUNT) {
return {
valid: false,
error: `Maximum donation amount is ₹${MAX_AMOUNT.toLocaleString('en-IN')}`
};
}
return { valid: true, error: '' };
}
```
### Phone Number Validation
```typescript
export function validatePhoneNumber(phone: string): {
valid: boolean;
error: string;
} {
// Indian mobile: 10 digits, starts with 6-9
const phoneRegex = /^[6-9]\d{9}$/;
if (!phone || !phone.trim()) {
return {
valid: false,
error: 'Phone number is required'
};
}
if (!phoneRegex.test(phone)) {
return {
valid: false,
error: 'Please enter a valid 10-digit mobile number starting with 6-9'
};
}
return { valid: true, error: '' };
}
```
### Name Validation
```typescript
export function validateName(name: string): {
valid: boolean;
error: string;
} {
if (!name || !name.trim()) {
return {
valid: false,
error: 'Name is required'
};
}
if (name.trim().length < 2) {
return {
valid: false,
error: 'Name must be at least 2 characters'
};
}
return { valid: true, error: '' };
}
```
### PAN Card Validation
**Added:** Nov 2024 - PAN required for donations >= ₹2,000
```typescript
export function validatePanCard(panCard: string, amount: number): string | null {
// PAN is required for donations >= 2000
if (amount >= 2000) {
if (!panCard || panCard.trim().length === 0) {
return 'PAN Card is required for donations of ₹2,000 or more';
}
// PAN format: 5 letters, 4 digits, 1 letter (e.g., ABCDE1234F)
const panRegex = /^[A-Z]{5}[0-9]{4}[A-Z]$/;
if (!panRegex.test(panCard.toUpperCase())) {
return 'Invalid PAN format (e.g., ABCDE1234F)';
}
}
return null;
}
```
**PAN Input Implementation:**
```svelte
<!-- PAN Card Input (Conditional) -->
{#if amount >= 2000}
<div class="animate-fadeIn">
<label for="panCard" class="block text-sm font-medium text-gray-700 mb-2">
PAN Card Number <span class="text-red-500">*</span>
</label>
<input
id="panCard"
type="text"
bind:value={panCard}
on:input={(e) => {
panCard = e.currentTarget.value.toUpperCase().slice(0, 10);
clearError('panCard');
}}
placeholder="ABCDE1234F"
maxlength="10"
class="block w-full px-3 py-3 border border-gray-300 rounded-lg uppercase font-mono"
class:border-red-300={errors.panCard}
/>
{#if errors.panCard}
<p class="mt-1 text-sm text-red-600">{errors.panCard}</p>
{/if}
<p class="mt-1 text-xs text-gray-500">
⚠️ PAN Card is required for donations of ₹2,000 or more
</p>
</div>
{/if}
```
### Complete Validation Check
```typescript
export function validateRazorpaySetup(): boolean {
if (!razorpayConfig.key) {
console.error('❌ Razorpay key not configured');
return false;
}
if (typeof window === 'undefined' || !window.Razorpay) {
console.error('❌ Razorpay script not loaded');
return false;
}
return true;
}
```
## Payment Flow
### Opening Razorpay Checkout
**File:** `src/lib/razorpay.ts`
```typescript
export function openRazorpayCheckout(
donationData: DonationData,
onSuccess: (response: RazorpayResponse) => void,
onFailure: (error: any) => void
): void {
// 1. Validate setup
if (!validateRazorpaySetup()) {
onFailure(new Error('Razorpay not configured properly'));
return;
}
// 2. Validate donation data
const amountValidation = validateDonationAmount(donationData.amount);
if (!amountValidation.valid) {
onFailure(new Error(amountValidation.error));
return;
}
const phoneValidation = validatePhoneNumber(donationData.phone);
if (!phoneValidation.valid) {
onFailure(new Error(phoneValidation.error));
return;
}
const nameValidation = validateName(donationData.name);
if (!nameValidation.valid) {
onFailure(new Error(nameValidation.error));
return;
}
// 3. Create Razorpay options
const options = {
...razorpayConfig,
amount: donationData.amount * 100, // Convert to paise
prefill: {
name: donationData.name,
contact: donationData.phone,
email: donationData.email || ''
},
handler: function (response: RazorpayResponse) {
// 4. Success callback
console.log('✅ Payment successful:', response.razorpay_payment_id);
onSuccess(response);
},
modal: {
ondismiss: function () {
// 5. User closed modal without completing payment
console.log('⚠️ Payment cancelled by user');
onFailure(new Error('Payment cancelled'));
}
}
};
// 6. Open Razorpay checkout
try {
const razorpay = new window.Razorpay(options);
razorpay.open();
} catch (error) {
console.error('❌ Error opening Razorpay:', error);
onFailure(error);
}
}
```
## PaymentForm Component
### Complete Payment Form Implementation
**File:** `src/lib/components/PaymentForm.svelte`
```svelte
<script lang="ts">
import { goto } from '$app/navigation';
import { selectedAmount } from '$lib/stores/donation';
import {
openRazorpayCheckout,
validateDonationAmount,
validatePhoneNumber,
validateName,
type DonationData,
type RazorpayResponse
} from '$lib/razorpay';
// Form data
let formData: DonationData = {
amount: 0,
name: '',
phone: '',
email: ''
};
// UI state
let isSubmitting = false;
let errors = {
amount: '',
name: '',
phone: ''
};
// Reactive: sync with store
$: formData.amount = $selectedAmount;
// Validation functions
function validateAmount() {
const result = validateDonationAmount(formData.amount);
errors.amount = result.error;
return result.valid;
}
function validateNameField() {
const result = validateName(formData.name);
errors.name = result.error;
return result.valid;
}
function validatePhoneField() {
const result = validatePhoneNumber(formData.phone);
errors.phone = result.error;
return result.valid;
}
function validateForm(): boolean {
const amountValid = validateAmount();
const nameValid = validateNameField();
const phoneValid = validatePhoneField();
return amountValid && nameValid && phoneValid;
}
// Submit handler
function handleDonate() {
// Validate form
if (!validateForm()) {
return;
}
// Start processing
isSubmitting = true;
// Open Razorpay checkout
openRazorpayCheckout(
formData,
// Success callback
(response: RazorpayResponse) => {
console.log('✅ Payment successful:', response);
// Redirect to success page with payment ID
goto(`/payment/success?payment_id=${response.razorpay_payment_id}`);
},
// Failure callback
(error: any) => {
console.error('❌ Payment failed:', error);
isSubmitting = false;
// Optionally redirect to failure page
// goto('/payment/failure');
}
);
}
// Predefined amounts
const presetAmounts = [500, 1000, 2000, 5000, 10000];
function selectAmount(amount: number) {
selectedAmount.set(amount);
errors.amount = '';
}
</script>
<div id="payment-form" class="max-w-2xl mx-auto p-6 bg-white rounded-xl shadow-lg">
<h2 class="text-3xl font-bold text-navy-custom mb-6">Make a Donation</h2>
<!-- Preset amounts -->
<div class="mb-6">
<label class="block text-gray-700 font-semibold mb-3">
Select Amount
</label>
<div class="grid grid-cols-3 md:grid-cols-5 gap-3">
{#each presetAmounts as amount}
<button
type="button"
on:click={() => selectAmount(amount)}
class="py-3 px-4 border-2 rounded-lg font-semibold transition-all"
class:border-orange-custom={$selectedAmount === amount}
class:bg-orange-custom={$selectedAmount === amount}
class:text-white={$selectedAmount === amount}
class:border-gray-300={$selectedAmount !== amount}
class:hover:border-orange-custom={$selectedAmount !== amount}
>
₹{amount.toLocaleString('en-IN')}
</button>
{/each}
</div>
</div>
<!-- Custom amount -->
<div class="mb-6">
<label for="amount" class="block text-gray-700 font-semibold mb-2">
Or Enter Custom Amount *
</label>
<div class="flex">
<span class="inline-flex items-center px-4 bg-gray-100 border border-r-0 border-gray-300 rounded-l-lg text-gray-600 font-semibold">
₹
</span>
<input
id="amount"
type="number"
bind:value={formData.amount}
on:blur={validateAmount}
min="10"
max="500000"
class="flex-1 px-4 py-3 border border-gray-300 rounded-r-lg focus:outline-none focus:ring-2 focus:ring-orange-custom"
class:border-red-500={errors.amount}
placeholder="Enter amount"
/>
</div>
{#if errors.amount}
<p class="text-red-500 text-sm mt-1">{errors.amount}</p>
{:else}
<p class="text-sm text-gray-500 mt-1">Minimum ₹10, Maximum ₹5,00,000</p>
{/if}
</div>
<!-- Donor details -->
<div class="mb-4">
<label for="name" class="block text-gray-700 font-semibold mb-2">
Full Name *
</label>
<input
id="name"
type="text"
bind:value={formData.name}
on:blur={validateNameField}
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-custom"
class:border-red-500={errors.name}
placeholder="Enter your name"
/>
{#if errors.name}
<p class="text-red-500 text-sm mt-1">{errors.name}</p>
{/if}
</div>
<div class="mb-4">
<label for="phone" class="block text-gray-700 font-semibold mb-2">
Mobile Number *
</label>
<div class="flex">
<span class="inline-flex items-center px-4 bg-gray-100 border border-r-0 border-gray-300 rounded-l-lg text-gray-600">
+91
</span>
<input
id="phone"
type="tel"
bind:value={formData.phone}
on:blur={validatePhoneField}
maxlength="10"
class="flex-1 px-4 py-3 border border-gray-300 rounded-r-lg focus:outline-none focus:ring-2 focus:ring-orange-custom"
class:border-red-500={errors.phone}
placeholder="9876543210"
/>
</div>
{#if errors.phone}
<p class="text-red-500 text-sm mt-1">{errors.phone}</p>
{/if}
</div>
<div class="mb-6">
<label for="email" class="block text-gray-700 font-semibold mb-2">
Email (Optional)
</label>
<input
id="email"
type="email"
bind:value={formData.email}
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-custom"
placeholder="your@email.com"
/>
</div>
<!-- Submit button -->
<button
type="button"
on:click={handleDonate}
disabled={isSubmitting}
class="w-full bg-orange-custom hover:bg-orange-dark text-white font-bold text-lg py-4 px-6 rounded-lg transition-all duration-300 shadow-md hover:shadow-lg"
class:opacity-50={isSubmitting}
class:cursor-not-allowed={isSubmitting}
>
{#if isSubmitting}
Processing...
{:else}
Donate ₹{formData.amount.toLocaleString('en-IN')}
{/if}
</button>
<p class="text-sm text-gray-500 text-center mt-4">
Your donation is secure and will be processed by Razorpay
</p>
</div>
```
## Success Page
### Payment Success Handler
**File:** `src/routes/payment/success/+page.svelte`
```svelte
<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { Icon } from '@iconify/svelte';
// Get payment ID from URL
$: paymentId = $page.url.searchParams.get('payment_id');
// Optional: Save payment details to database here
</script>
<svelte:head>
<title>Payment Successful - Sikh Aid</title>
</svelte:head>
<div class="container mx-auto px-4 py-16">
<div class="max-w-2xl mx-auto text-center">
<div class="mb-6 flex justify-center">
<div class="w-24 h-24 bg-green-100 rounded-full flex items-center justify-center">
<Icon icon="mdi:check-circle" class="text-6xl text-green-500" />
</div>
</div>
<h1 class="text-4xl font-bold text-navy-custom mb-4">
Thank You for Your Donation!
</h1>
<p class="text-lg text-gray-700 mb-6">
Your generous contribution will help us make a difference in people's lives.
</p>
{#if paymentId}
<div class="bg-gray-100 rounded-lg p-4 mb-6">
<p class="text-sm text-gray-600 mb-1">Payment ID</p>
<p class="text-lg font-mono font-semibold text-navy-custom">
{paymentId}
</p>
</div>
{/if}
<p class="text-gray-600 mb-8">
You will receive a confirmation email shortly with your donation receipt.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<button
on:click={() => goto('/')}
class="bg-orange-custom hover:bg-orange-dark text-white font-semibold py-3 px-6 rounded-lg transition-colors"
>
Back to Home
</button>
<button
on:click={() => goto('/campaigns')}
class="border-2 border-orange-custom text-orange-custom hover:bg-orange-custom hover:text-white font-semibold py-3 px-6 rounded-lg transition-all"
>
View Our Campaigns
</button>
</div>
</div>
</div>
```
## Firestore Integration for Donations
### Saving Donations to Firebase
**Added:** Nov 2024 - Auto-save donation records
**Collection:** `donations`
**File:** `src/lib/firestore.ts`
```typescript
export interface Donation {
id?: string;
donorName: string;
phone: string;
panCard?: string;
amount: number;
razorpayPaymentId: string;
razorpayOrderId?: string;
timestamp: string;
firestoreTimestamp?: Timestamp;
}
export async function addDonationToFirestore(
donation: Omit<Donation, 'id' | 'timestamp' | 'firestoreTimestamp'>
): Promise<string> {
await ensureFirebaseInitialized();
if (!db) {
throw new Error('Firestore is not initialized');
}
try {
const donationData: Omit<Donation, 'id'> = {
...donation,
timestamp: new Date().toISOString(),
firestoreTimestamp: serverTimestamp() as any
};
const cleanedData = removeUndefinedFields(donationData);
const docRef = await addDoc(collection(db, 'donations'), cleanedData);
console.log('✅ Donation saved to Firestore with ID:', docRef.id);
return docRef.id;
} catch (error) {
console.error('❌ Error adding donation to Firestore:', error);
throw error;
}
}
export async function getDonations(): Promise<Donation[]> {
await ensureFirebaseInitialized();
if (!db) {
throw new Error('Firestore is not initialized');
}
try {
const q = query(collection(db, 'donations'), orderBy('firestoreTimestamp', 'desc'));
const querySnapshot = await getDocs(q);
const donations: Donation[] = [];
querySnapshot.forEach((doc) => {
donations.push({
id: doc.id,
...doc.data()
} as Donation);
});
return donations;
} catch (error) {
console.error('❌ Error fetching donations:', error);
throw error;
}
}
```
### Payment Success with Firestore Save
**File:** `src/routes/payment/success/+page.svelte`
```svelte
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { addDonationToFirestore } from '$lib/firestore';
let paymentId: string = '';
let amount: string = '';
let donorName: string = '';
let phone: string = '';
let panCard: string = '';
let isSaving: boolean = false;
let saveError: string = '';
let savedSuccessfully: boolean = false;
onMount(async () => {
// Extract URL parameters
paymentId = $page.url.searchParams.get('payment_id') || '';
amount = $page.url.searchParams.get('amount') || '';
donorName = $page.url.searchParams.get('name') || 'Anonymous';
phone = $page.url.searchParams.get('phone') || '';
panCard = $page.url.searchParams.get('pan_card') || '';
if (!paymentId) {
goto('/');
return;
}
// Save donation to Firestore
await saveDonation();
});
async function saveDonation() {
if (!paymentId || !amount) return;
isSaving = true;
saveError = '';
try {
await addDonationToFirestore({
donorName,
phone,
...(panCard && { panCard }),
amount: parseFloat(amount),
razorpayPaymentId: paymentId
});
savedSuccessfully = true;
console.log('✅ Donation saved successfully');
} catch (error) {
console.error('Failed to save donation:', error);
saveError = 'Failed to save donation record. Please contact support.';
} finally {
isSaving = false;
}
}
</script>
<!-- Success UI with error handling -->
{#if saveError}
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
<p class="text-sm text-yellow-900">{saveError}</p>
<p class="text-xs text-yellow-700 mt-1">Payment ID: {paymentId}</p>
</div>
{/if}
```
### Firestore Security Rules for Donations
**F