Fee Structure
How donation fees are calculated, what rates apply per payment method, and how the total charged amount is derived.
Source files: app/Services/FeeCalculator.php · config/fees.php · app/Http/Controllers/Api/DonationController.php
The frontend (Donate.tsx) never computes fees itself. It always calls POST /donations/quote and displays the response verbatim — this guarantees the number a donor is shown is exactly the number they're charged, since both paths run through the same FeeCalculator::calculate() call.
Two components, always kept separate
Every donation carries two distinct fee lines, stored separately on the Donation record and never summed into one "fee":
| Component | Meaning | Column |
|---|---|---|
convenience_fee | The payment gateway's own processing fee (MDR), passed through to the donor | donations.convenience_fee |
system_fee | Batchmates' platform service fee | donations.system_fee |
convenience_fee is a pass-through cost, not platform income — see Financial Reports for how reporting keeps this separate from actual revenue (system_fee).
Fee Rates — by gateway and method
Donors select a payment method in the UI before checkout, so the exact published rate for that method is charged — there is no more flat "hosted checkout" rate.
PayMongo — by method
| Method | Rate | Fixed | Env keys |
|---|---|---|---|
| Card (domestic Visa/MC) | 3.125% | ₱13.39 | PAYMONGO_CARD_RATE, PAYMONGO_CARD_FIXED |
| GCash | 2.23% | — | PAYMONGO_GCASH_RATE, PAYMONGO_GCASH_FIXED |
| Maya (e-wallet) | 1.79% | — | PAYMONGO_PAYMAYA_RATE, PAYMONGO_PAYMAYA_FIXED |
| GrabPay | 1.96% | — | PAYMONGO_GRABPAY_RATE, PAYMONGO_GRABPAY_FIXED |
| QR Ph | 1.34% | — | PAYMONGO_QRPH_RATE, PAYMONGO_QRPH_FIXED |
unknown (fallback only) | 3.125% | ₱13.39 | PAYMONGO_FALLBACK_RATE, PAYMONGO_FALLBACK_FIXED |
International Visa/MC (4.02%) is not modeled. Volume is low and no card_country is captured today — domestic card rate applies to all cards. unknown mirrors the card rate as a conservative fallback and should not be hit in the normal checkout flow — it exists only in case a payment method somehow isn't resolved.
Platform service fee
| Env key | Default |
|---|---|
APP_SERVICE_FEE_RATE | 1.5% |
Environment Variables
# .env
APP_SERVICE_FEE_RATE=0.015
# PayMongo — per method (paymongo.com/pricing)
PAYMONGO_CARD_RATE=0.03125
PAYMONGO_CARD_FIXED=13.39
PAYMONGO_GCASH_RATE=0.0223
PAYMONGO_GCASH_FIXED=0.0
PAYMONGO_PAYMAYA_RATE=0.0179
PAYMONGO_PAYMAYA_FIXED=0.0
PAYMONGO_GRABPAY_RATE=0.0196
PAYMONGO_GRABPAY_FIXED=0.0
PAYMONGO_QRPH_RATE=0.0134
PAYMONGO_QRPH_FIXED=0.0
PAYMONGO_FALLBACK_RATE=0.03125
PAYMONGO_FALLBACK_FIXED=13.39
# Platform operating cost — reporting only, never charged to donors
PAYMONGO_PAYOUT_FEE=10.0
Usage
// app/Services/FeeCalculator.php
$fees = FeeCalculator::calculate('paymongo', 1000.00, 'gcash');
// [
// 'convenience_fee' => 22.30, // 2.23% × 1000
// 'system_fee' => 15.00, // 1.5% × 1000
// 'total_amount' => 1037.30,
// ]
Fee Quote Endpoint
Server-authoritative fee preview. Creates and persists nothing — it calls the exact same FeeCalculator::calculate() path the real charge uses, so the quoted total can never drift from what's actually charged. Safe to call on every keystroke; rate-limited to guard against abuse.
Authentication: Not required (same public/optional-auth route group as donation creation)
Rate limit: throttle:30,1 (30 requests/minute)
Request Body
- Name
amount- Type
- number
- Description
Base donation amount, ₱1–₱99,999,999.99, up to 2 decimal places
- Name
payment_gateway- Type
- string
- Description
Must be
paymongo
- Name
payment_method- Type
- string
- Description
card|gcash|paymaya|grab_pay|qrph
Request
{
"amount": 1000,
"payment_gateway": "paymongo",
"payment_method": "gcash"
}
Response
{
"success": true,
"data": {
"amount": 1000,
"convenience_fee": 22.30,
"system_fee": 15.00,
"total_amount": 1037.30,
"breakdown": {
"gateway_label": "GCash",
"rate_percent": 2.23,
"fixed_fee": 0,
"service_fee_percent": 1.5
}
}
}
Error (missing method for PayMongo)
{
"message": "The given data was invalid.",
"errors": {
"payment_method": ["The payment method field is required."]
}
}
Worked Examples
PayMongo — ₱1,000 donation, GCash
| Component | Calculation | Amount |
|---|---|---|
| Base amount | — | ₱1,000.00 |
| Transaction Fee/s (convenience) | ₱1,000 × 2.23% | ₱22.30 |
| Platform Service Fee (system) | ₱1,000 × 1.5% | ₱15.00 |
| Total charged | ₱1,037.30 | |
| Campaign receives | — | ₱1,000.00 |
PayMongo — ₱1,000 donation, Card
| Component | Calculation | Amount |
|---|---|---|
| Base amount | — | ₱1,000.00 |
| Transaction Fee/s | ₱1,000 × 3.125% + ₱13.39 | ₱44.64 |
| Platform Service Fee | ₱1,000 × 1.5% | ₱15.00 |
| Total charged | ₱1,059.64 | |
| Campaign receives | — | ₱1,000.00 |
Prior to this fee model, PayMongo hosted checkout charged donors only the 1% (now 1.5%) system fee — the gateway's real MDR was silently absorbed out of the platform's payout regardless of which method the donor picked on PayMongo's page. Donors now pick their method in Batchmates' own UI first, so the exact rate for that method is charged up front. This is a real increase in what hosted-checkout donors pay (previously amount + service fee only, now amount + service fee + the method's real MDR).
Recurring (Subscription) Donations
PayMongo subscriptions bill the fee-inclusive total, not just the base pledge — the PayMongo Plan is created for FeeCalculator::calculate('paymongo', $amount, 'card')['total_amount'] (subscriptions are always billed against a vaulted card, so the card tier applies). Each subscription.invoice.paid webhook re-derives the same convenience_fee/system_fee/total_amount split from the subscription's stored base amount, and credits the campaign by the base amount only — fees are platform revenue, same as one-time donations.
This only affects new subscriptions created after this change — an existing PayMongo Plan bills whatever amount it was created with and cannot be changed retroactively; a donor would need to cancel and re-subscribe to move to fee-inclusive billing.
See app/Services/PayMongoSubscriptionService.php (createSubscription(), handleInvoicePaid()).
What Gets Sent to the Gateway
'amount' => (int) round($fees['total_amount'] * 100), // integer centavos
PayMongo hosted checkout also narrows payment_method_types to a single-element array containing the donor's chosen method — the amount quoted is guaranteed to be the only method PayMongo's page will actually offer:
'payment_method_types' => [$donation->payment_method], // e.g. ['gcash']
What Gets Credited to the Campaign
Campaign balances are incremented by the base donation amount only — fees are excluded, for both one-time and recurring donations:
$campaign->increment('raised_amount', $donation->amount); // base amount
$campaign->increment('available_amount', $donation->amount); // base amount
Donation Record Fields
| Field | Value |
|---|---|
amount | Base donation (what the donor intended to give; what the campaign is credited) |
payment_method | The method actually charged — card, gcash, paymaya, grab_pay, qrph |
convenience_fee | Gateway processing fee for that method |
system_fee | Platform service fee |
total_amount | Sum of the above three — what is actually charged |