Overview
The Church Management System provides comprehensive donation campaign management with Stripe integration for payment processing. This guide covers creating campaigns, accepting donations, verifying payments, and tracking metrics.
Donation Campaign Structure
Each donation campaign includes:
- Church Reference - Associated church ID
- Campaign Details - Name, description, dates
- Status - on, off, or completed
- Bank Details - Account information for offline donations
- Metrics - Target amount, minimum donation, total collected
- Donators - Array of donations with verification status
- Contact Information - Support phone and email
Creating a Donation Campaign
Prerequisites
Only the pastor of a church can create donation campaigns for that church. Ensure you have:
- Pastor role
- Created a church
- Your pastor profile ID
Create Campaign Endpoint
POST /api/church/createdonation/:pastorId
Authorization: Bearer <token>
Content-Type: application/json
{
"church": "507f1f77bcf86cd799439011",
"donationName": "Building Fund 2024",
"donationStatus": "on",
"startDate": "2024-12-01T00:00:00.000Z",
"endDate": "2024-12-31T23:59:59.000Z",
"donationDescription": "Help us build our new community center to serve more families.",
"bankDetails": {
"accountName": "Grace Community Church",
"accountNumber": "1234567890",
"bankName": "First National Bank"
},
"donationSupportContact": {
"phone": "+1-555-0123",
"email": "[email protected]"
},
"donationMetrics": {
"targetAmount": 50000,
"minAmount": 10
}
}
Field Requirements
| Field | Type | Validation | Required |
|---|
church | ObjectId | Must be a valid church ID | Yes |
donationName | String | Unique per church | Yes |
donationStatus | String | ”on”, “off”, or “completed” | Yes |
startDate | Date | Must be in the future | Yes |
endDate | Date | Must be after startDate and in future | Yes |
donationDescription | String | Any text | Yes |
bankDetails | Object | Account name, number, bank name | Yes |
donationSupportContact | Object | Valid phone and email | Yes |
donationMetrics.targetAmount | Number | Target fundraising amount | Yes |
donationMetrics.minAmount | Number | Minimum donation amount | Yes |
Campaign Creation Logic
const createADonation = async (req, res) => {
const churchId = req.body.church;
const pastorID = req.params.pastorId;
try {
// Verify church exists
const theChurch = await churchObject.findById(churchId);
if (theChurch) {
// Verify user is the pastor of this church
const heIsThePastor = await churchObject.findOne({
_id: churchId,
pastor: pastorID,
});
if (heIsThePastor) {
// Check for duplicate donation name in this church
const alreadyexists = await donationObject.findOne({
donationName: req.body.donationName,
church: churchId,
});
if (alreadyexists) {
res.status(400).send({
success: false,
message: "Unable to create donation",
data: "Donation with this name already exists in this church",
});
} else {
// Create the donation campaign
const theDonationBody = donationObject({
church: churchId,
...req.body,
});
await theDonationBody.save();
const theDonation = await donationObject
.findById(theDonationBody._id)
.populate("church");
res.status(200).send({
success: true,
message: `Your ${theDonationBody.donationName} donation is now open`,
data: theDonation,
});
}
} else {
res.status(400).send({
success: false,
message: "Unable to create donation",
data: "You are not the pastor of this church",
});
}
}
} catch (err) {
res.status(500).send({
success: false,
message: "Unable to create a donation",
data: err.message,
});
}
};
Making a Donation
Donation Process Flow
The system uses Stripe Checkout for secure payment processing:
Initiate donation
Member selects a donation amount and provides their email
Create Stripe session
System creates a Stripe Checkout session and returns payment URL
Process payment
User completes payment on Stripe’s secure checkout page
Verify payment
Pastor verifies the payment using the transaction ID
Update metrics
System updates total donations and marks payment as verified
Donate to Campaign
PATCH /api/church/makedonation/:id
Authorization: Bearer <token>
Content-Type: application/json
{
"donationId": "507f1f77bcf86cd799439015",
"useremail": "[email protected]",
"donated": 100
}
Donation Implementation
const makeADonation = async (req, res) => {
const userID = req.params.id;
const { donationId, useremail } = req.body;
try {
const donationExists = await donationObject
.findOne({ _id: donationId })
.populate("donators.user", "name -_id");
if (donationExists) {
const currentDate = new Date();
// Check if campaign is still active
if (currentDate <= donationExists.endDate) {
// Check campaign status
if (donationExists.donationStatus === "on") {
// Validate minimum donation amount
if (req.body.donated >= donationExists.donationMetrics.minAmount) {
// Create Stripe checkout session
const session = await stripe.checkout.sessions.create({
client_reference_id: userID,
customer_email: useremail,
line_items: [
{
price_data: {
currency: "usd",
product_data: {
name: donationExists.donationName,
},
unit_amount: req.body.donated * 100, // Convert to cents
},
quantity: 1,
},
],
mode: "payment",
success_url: `${process.env.BASE_URL}/complete?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.BASE_URL}/cancel`,
});
const paymentId = session.id;
const paymentUrl = session.url;
// Record donation with pending verification
donationExists.donators.push({
user: userID,
donated: req.body.donated,
transactionId: paymentId,
paymentVerified: false,
});
await donationExists.save();
res.status(200).send({
success: true,
message: `Your donation to ${donationExists.donationName} has been initiated. Kindly visit the url below to complete payment`,
data: {
Id: paymentId,
url: paymentUrl,
},
});
} else {
res.status(404).send({
success: false,
message: "Unable to make donation",
data: `Minimum donation amount is ${donationExists.donationMetrics.minAmount}`,
});
}
} else if (donationExists.donationStatus === "completed") {
res.status(200).send({
success: false,
message: "Unable to make donation",
data: "The target for this donation has been achieved",
});
} else {
res.status(200).send({
success: false,
message: "Unable to make donation",
data: "This donation is not open yet",
});
}
} else {
return res.status(400).send({
success: false,
message: "Unable to make donation",
error: `This donation ended on ${donationExists.endDate.toLocaleDateString()}`,
});
}
}
} catch (err) {
res.status(500).send({
success: false,
message: "Unable to make donation",
data: err.message,
});
}
};
Donation Response
{
"success": true,
"message": "Your donation to Building Fund 2024 has been initiated. Kindly visit the url below to complete payment",
"data": {
"Id": "cs_test_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0",
"url": "https://checkout.stripe.com/pay/cs_test_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"
}
}
After receiving the payment URL, redirect the user to complete payment on Stripe’s secure checkout page. The payment is recorded with paymentVerified: false until manually verified.
Payment Verification
Verify Donation Payment
Pastors must verify payments after users complete the Stripe checkout:
PATCH /api/church/verifydonation
Authorization: Bearer <token>
Content-Type: application/json
{
"donationId": "507f1f77bcf86cd799439015",
"transactionId": "cs_test_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"
}
Verification Logic
const verifyDonation = async (req, res) => {
const { donationId, transactionId } = req.body;
try {
// Check if already verified
const hasbeenVerified = await donationObject.findOne({
_id: donationId,
"donators.transactionId": transactionId,
"donators.paymentVerified": true,
});
if (hasbeenVerified) {
res.status(200).send({
success: false,
message: "Unable to verify donations",
data: "This donation has already been verified",
});
} else {
const theDonation = await donationObject.findOne({
_id: donationId,
"donators.transactionId": transactionId,
});
// Find the specific donation entry
const donatorIndex = theDonation.donators.findIndex(
(d) => d.transactionId.toString() === transactionId
);
if (donatorIndex !== -1) {
const donaetedAmount = theDonation.donators[donatorIndex].donated;
// Retrieve session from Stripe
const session = await stripe.checkout.sessions.retrieve(transactionId);
// Verify payment was successful
if (session.payment_status === "paid") {
// Update donation: mark as verified and add to total
const updatedDonation = await donationObject
.findOneAndUpdate(
{
_id: donationId,
"donators.transactionId": transactionId,
},
{
$set: { "donators.$.paymentVerified": true },
$inc: { "donationMetrics.totalGotten": donaetedAmount },
},
{ new: true }
)
.populate("donators.user", "name phoneNumber");
res.status(200).send({
success: true,
message: "Payment verified",
data: updatedDonation,
});
} else {
return res.status(200).send({
success: true,
message: "Payment not successful",
error: "This payment's transaction was not successful",
});
}
}
}
} catch (err) {
res.status(500).send({
success: false,
message: "Unable to verify donation",
data: err.message,
});
}
};
Verification is a critical step! The system:
- Checks Stripe to confirm payment status is “paid”
- Prevents double-verification of the same transaction
- Only adds to
totalGotten after verification
- Uses
$inc operator to atomically update the total
Managing Campaign Status
Update Donation Status
Pastors can change the campaign status:
PATCH /api/church/editdonationstatus
Authorization: Bearer <token>
Content-Type: application/json
{
"donationId": "507f1f77bcf86cd799439015",
"churchId": "507f1f77bcf86cd799439011",
"status": "completed"
}
Valid Status Values
- on - Campaign is active and accepting donations
- off - Campaign is paused (temporarily not accepting donations)
- completed - Campaign has reached its goal or ended
Status Update Logic
const editDonationStatus = async (req, res) => {
const { donationId, churchId, status } = req.body;
try {
const donationExists = await donationObject
.findOne({
_id: donationId,
church: churchId,
})
.select("-donators");
if (donationExists) {
if (["on", "off", "completed"].includes(status)) {
donationExists.donationStatus = status;
await donationExists.save();
res.status(200).send({
success: true,
message: "Donation status changed",
data: donationExists,
});
} else {
res.status(400).send({
success: false,
message: "Unable to edit donation status",
data: "Donation status must be on, off or completed",
});
}
} else {
res.status(400).send({
success: false,
message: "Unable to edit donation status",
data: "Donation not found",
});
}
} catch (err) {
res.status(500).send({
success: false,
message: "Unable to edit donation status",
data: err.message,
});
}
};
Viewing Donations
Get All Donations for a Church
GET /api/church/getdonations/:churchId
Authorization: Bearer <token>
Response Example
{
"success": true,
"message": "These are the available donations for Grace Community Church",
"data": [
{
"_id": "507f1f77bcf86cd799439015",
"church": "507f1f77bcf86cd799439011",
"donationName": "Building Fund 2024",
"donationStatus": "on",
"startDate": "2024-12-01T00:00:00.000Z",
"endDate": "2024-12-31T23:59:59.000Z",
"donationDescription": "Help us build our new community center.",
"donationMetrics": {
"targetAmount": 50000,
"minAmount": 10,
"totalGotten": 15750
},
"donators": [
{
"user": {
"name": "John Doe",
"phoneNumber": 5551234567
},
"donated": 100,
"transactionId": "cs_test_abc123",
"paymentVerified": true
},
{
"user": {
"name": "Jane Smith",
"phoneNumber": 5559876543
},
"donated": 250,
"transactionId": "cs_test_xyz789",
"paymentVerified": true
}
]
}
]
}
Complete Donation Workflow
Pastor creates campaign
curl -X POST http://localhost:3001/api/church/createdonation/507f1f77bcf86cd799439012 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <pastor-token>" \
-d '{
"church": "507f1f77bcf86cd799439011",
"donationName": "Building Fund 2024",
"donationStatus": "on",
"startDate": "2024-12-01T00:00:00.000Z",
"endDate": "2024-12-31T23:59:59.000Z",
"donationDescription": "New community center",
"bankDetails": {
"accountName": "Grace Community Church",
"accountNumber": "1234567890",
"bankName": "First National Bank"
},
"donationSupportContact": {
"phone": "+1-555-0123",
"email": "[email protected]"
},
"donationMetrics": {
"targetAmount": 50000,
"minAmount": 10
}
}'
Member initiates donation
curl -X PATCH http://localhost:3001/api/church/makedonation/507f1f77bcf86cd799439013 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <member-token>" \
-d '{
"donationId": "507f1f77bcf86cd799439015",
"useremail": "[email protected]",
"donated": 100
}'
Save the returned payment URL.Member completes payment
User visits the Stripe Checkout URL and completes the payment securely.
Pastor verifies payment
curl -X PATCH http://localhost:3001/api/church/verifydonation \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <pastor-token>" \
-d '{
"donationId": "507f1f77bcf86cd799439015",
"transactionId": "cs_test_a1b2c3d4e5f6g7h8i9j0"
}'
View donation metrics
curl -X GET http://localhost:3001/api/church/getdonations/507f1f77bcf86cd799439011 \
-H "Authorization: Bearer <token>"
Tracking Metrics
Available Metrics
Each campaign tracks:
- targetAmount - Fundraising goal
- minAmount - Minimum allowed donation
- totalGotten - Total verified donations received
Progress Calculation
const progress = (totalGotten / targetAmount) * 100;
const remaining = targetAmount - totalGotten;
const donorCount = donators.filter(d => d.paymentVerified).length;
Use these metrics to:
- Display progress bars in your UI
- Send milestone notifications
- Automatically set status to “completed” when target is reached
- Generate donor reports
Error Handling
Common Errors
Campaign Not Found:
{
"success": false,
"message": "Unable to make donation",
"data": "Donation with this Id does not exist"
}
Below Minimum Amount:
{
"success": false,
"message": "Unable to make donation",
"data": "Minimum donation amount is 10"
}
Campaign Expired:
{
"success": false,
"message": "Unable to make donation",
"error": "This donation ended on 12/31/2024"
}
Campaign Completed:
{
"success": false,
"message": "Unable to make donation",
"data": "The target for this donation has been achieved"
}
Already Verified:
{
"success": false,
"message": "Unable to verify donations",
"data": "This donation has already been verified"
}
Security Considerations
Critical Security Practices:
- Never store credit card information in your database
- Always use Stripe’s secure checkout (never handle card data directly)
- Verify payments server-side using Stripe API
- Check
payment_status === "paid" before marking as verified
- Require pastor authentication for creating and verifying donations
- Validate donation amounts against minimums
- Check campaign dates and status before accepting donations
- Use atomic operations (
$inc) for updating totals to prevent race conditions
Best Practices
Recommendations:
- Set realistic fundraising goals
- Keep minimum donation amounts accessible (e.g., 5−10)
- Provide clear descriptions of what donations will fund
- Send thank-you emails after verified donations
- Update campaign status to “completed” when goals are reached
- Generate contribution statements for tax purposes
- Monitor campaign progress regularly
- Set up webhook handlers for automatic payment verification
- Implement refund handling for cancelled donations
Stripe Webhook Integration
For production, consider implementing Stripe webhooks for automatic verification:
// Example webhook handler (not in current implementation)
app.post('/webhook', async (req, res) => {
const sig = req.headers['stripe-signature'];
const event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
if (event.type === 'checkout.session.completed') {
const session = event.data.object;
// Automatically verify the donation
await verifyDonationFromWebhook(session.id);
}
res.json({received: true});
});
Next Steps