Skip to main content

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

FieldTypeValidationRequired
churchObjectIdMust be a valid church IDYes
donationNameStringUnique per churchYes
donationStatusString”on”, “off”, or “completed”Yes
startDateDateMust be in the futureYes
endDateDateMust be after startDate and in futureYes
donationDescriptionStringAny textYes
bankDetailsObjectAccount name, number, bank nameYes
donationSupportContactObjectValid phone and emailYes
donationMetrics.targetAmountNumberTarget fundraising amountYes
donationMetrics.minAmountNumberMinimum donation amountYes

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:
1

Initiate donation

Member selects a donation amount and provides their email
2

Create Stripe session

System creates a Stripe Checkout session and returns payment URL
3

Process payment

User completes payment on Stripe’s secure checkout page
4

Verify payment

Pastor verifies the payment using the transaction ID
5

Update metrics

System updates total donations and marks payment as verified
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

1

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
    }
  }'
2

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.
3

Member completes payment

User visits the Stripe Checkout URL and completes the payment securely.
4

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"
  }'
5

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., 55-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

Build docs developers (and LLMs) love