קרן מאבדת עד 30% מהתרומות עקב שגיאות בשער התשלום בשלב האישור — הכרטיס נדחה עקב פסק זמן או כשל באימות 3DS. עוד 15% מהתורמים לא חוזרים לאחר תשלום שנכשל. מערכת ה-CMS הסטנדרטית — WordPress עם תוסף תרומות — יוצרת שאילתות N+1 בעת בניית דוח, והעמוד נטען תוך 8–10 שניות. ללא מנויים חוזרים, התרומה הממוצעת למשתמש יורדת פי 4.
אנו בונים פורטלים על Django + PostgreSQL עם Celery למשימות רקע. YooKassa עם שמירת כרטיס מאפשרת תשלומים חוזרים עם ניסיונות חוזרים אוטומטיים — שיעור החיוב המוצלח מגיע ל-92%. ודוחות ציבוריים בזמן אמת המפולחים לפי תוכניות מגבירים את אמון התורמים: קרנות עם דיווח שקוף מקבלות פי 2.5 יותר תרומות חוזרות. התרומה החד-פעמית הממוצעת היא 1,500 RUB, ותורמים חוזרים תורמים 3,000 RUB בחודש.
בעיות טכניות אופייניות
אתגרים טכניים אופייניים של פורטלי קרנות:
- שאילתות N+1 בעת הפקת דוחות — טעינת עמוד איטית עד 10 שניות. נפתר על ידי איגוד נתונים בשאילתה אחת באמצעות
select_relatedו-prefetch_related. - כשל בשער התשלום — אובדן הלקוח בזמן התשלום. אנו משתמשים בשידור חוזר של webhook ובמקביליות משימות של Celery לעיבוד מחדש.
- חיובים חוזרים ללא ניסיונות חוזרים אוטומטיים — המנויים נופלים. אנו מיישמים ניסיונות חוזרים אוטומטיים עד 3 פעמים עם התראה למנהל.
- חוסר בקבלות מקוונות — התורם אינו יכול לתבוע ניכוי מס. אנו מייצרים קבלות PDF עם חתימה אלקטרונית.
- חוסר שקיפות בהוצאות — פוגע באמון בקרן. אנו מפרסמים דוחות בזמן אמת המפולחים לפי קטגוריות.
כל בעיה נפתרת על ידי פתרון טכני ספציפי: איגוד נתונים, שידור חוזר של webhook, תור משימות של Celery, הפקת PDF.
כיצד אנו בונים את ארכיטקטורת הפורטל?
מחסנית סטנדרטית: Django כ-API + React בחזית + PostgreSQL + Celery למשימות רקע. אנו משתמשים במפתחות UUID — זה מונע התנגשויות במהלך מיגרציות ורפליקציה. סכמת הנתונים מנורמלת, עם הפרדה בין תוכניות, תרומות ומנויים.
CREATE TABLE programs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slug VARCHAR(200) UNIQUE NOT NULL,
title VARCHAR(300) NOT NULL,
description TEXT,
goal_amount NUMERIC(15,2), -- NULL = сбор без цели
collected NUMERIC(15,2) NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active','completed','paused','archived')),
is_featured BOOLEAN NOT NULL DEFAULT FALSE,
image_url VARCHAR(500),
ends_at DATE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE donations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
program_id UUID REFERENCES programs(id), -- NULL = на уставную деятельность
donor_id UUID REFERENCES users(id), -- NULL = анонимное пожертвование
amount NUMERIC(15,2) NOT NULL,
currency CHAR(3) NOT NULL DEFAULT 'USD',
is_anonymous BOOLEAN NOT NULL DEFAULT FALSE,
is_recurring BOOLEAN NOT NULL DEFAULT FALSE,
subscription_id UUID REFERENCES recurring_subscriptions(id),
payment_method VARCHAR(50), -- 'card','sbp','qiwi','yoomoney'
payment_id VARCHAR(200), -- ID транзакции в платёжной системе
status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','completed','failed','refunded')),
donor_message TEXT,
receipt_sent_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE recurring_subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
donor_id UUID NOT NULL REFERENCES users(id),
program_id UUID REFERENCES programs(id),
amount NUMERIC(15,2) NOT NULL,
currency CHAR(3) NOT NULL DEFAULT 'USD',
payment_token VARCHAR(200) NOT NULL, -- сохранённый токен карты
interval VARCHAR(20) NOT NULL DEFAULT 'monthly' CHECK (interval IN ('weekly','monthly','quarterly')),
next_charge_at DATE NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active','paused','cancelled','failed')),
failed_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
מדוע תשלומים חוזרים הם אתגר למפתחים?
שמירת כרטיס, טיפול בשגיאות חיוב, שליחת התראות על איחור — כל פעולה היא קריטית. אנו מיישמים דרך YooKassa API: בתשלום הראשון, אנו מבקשים CREATE TABLE programs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), slug VARCHAR(200) UNIQUE NOT NULL, title VARCHAR(300) NOT NULL, description TEXT, goal_amount NUMERIC(15,2), -- NULL = сбор без цели collected NUMERIC(15,2) NOT NULL DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active','completed','paused','archived')), is_featured BOOLEAN NOT NULL DEFAULT FALSE, image_url VARCHAR(500), ends_at DATE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE donations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), program_id UUID REFERENCES programs(id), -- NULL = на уставную деятельность donor_id UUID REFERENCES users(id), -- NULL = анонимное пожертвование amount NUMERIC(15,2) NOT NULL, currency CHAR(3) NOT NULL DEFAULT 'USD', is_anonymous BOOLEAN NOT NULL DEFAULT FALSE, is_recurring BOOLEAN NOT NULL DEFAULT FALSE, subscription_id UUID REFERENCES recurring_subscriptions(id), payment_method VARCHAR(50), -- 'card','sbp','qiwi','yoomoney' payment_id VARCHAR(200), -- ID транзакции в платёжной системе status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','completed','failed','refunded')), donor_message TEXT, receipt_sent_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE recurring_subscriptions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), donor_id UUID NOT NULL REFERENCES users(id), program_id UUID REFERENCES programs(id), amount NUMERIC(15,2) NOT NULL, currency CHAR(3) NOT NULL DEFAULT 'USD', payment_token VARCHAR(200) NOT NULL, -- сохранённый токен карты interval VARCHAR(20) NOT NULL DEFAULT 'monthly' CHECK (interval IN ('weekly','monthly','quarterly')), next_charge_at DATE NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active','paused','cancelled','failed')), failed_count INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); , מקבלים טוקן, ולאחר מכן מחייבים לפי לוח זמנים. Celery בודק מדי יום מנויים עם save_payment_method=True.
import yookassa
from yookassa import Payment, Configuration
Configuration.configure(
account_id=settings.YUKASSA_SHOP_ID,
secret_key=settings.YUKASSA_SECRET_KEY,
)
def initiate_donation(donation_data: dict) -> str:
"""Создаём платёж, возвращаем URL для редиректа"""
metadata = {
'donation_id': str(donation_data['id']),
'program_id': str(donation_data.get('program_id', '')),
'is_recurring': donation_data['is_recurring'],
}
payment = Payment.create({
'amount': {
'value': str(donation_data['amount']),
'currency': 'USD',
},
'payment_method_data': {'type': 'bank_card'},
'save_payment_method': donation_data['is_recurring'],
'confirmation': {
'type': 'redirect',
'return_url': f'{settings.BASE_URL}/donation/thank-you?id={donation_data["id"]}',
},
'description': f'Пожертвование в фонд «{settings.FOUNDATION_NAME}»',
'metadata': metadata,
'receipt': {
'customer': {'email': donation_data['email']},
'items': [{
'description': 'Благотворительное пожертвование',
'quantity': '1.00',
'amount': {'value': str(donation_data['amount']), 'currency': 'USD'},
'vat_code': 1, # Без НДС
'payment_mode': 'full_payment',
'payment_subject': 'another', # Для некоммерческих платежей
}],
},
})
Donation.objects.filter(id=donation_data['id']).update(
payment_id=payment.id
)
return payment.confirmation.confirmation_url
מחזור החיוב עם ניסיונות חוזרים: בשגיאה, הגדל את next_charge_at <= today; לאחר 3 שגיאות, המנוי מושבת, ונשלחת התראה למנהל.
@shared_task
def charge_recurring_subscriptions():
"""Celery beat: ежедневно в 10:00"""
today = date.today()
subs = RecurringSubscription.objects.filter(
status='active',
next_charge_at__lte=today
)
for sub in subs:
try:
payment = Payment.create({
'amount': {'value': str(sub.amount), 'currency': 'USD'},
'payment_method_id': sub.payment_token,
'capture': True,
'description': f'Регулярное пожертвование — {sub.get_interval_display()}',
'metadata': {
'subscription_id': str(sub.id),
'is_recurring': True,
},
})
Donation.objects.create(
program=sub.program,
donor=sub.donor,
amount=sub.amount,
is_recurring=True,
subscription=sub,
payment_id=payment.id,
status='pending',
)
sub.failed_count = 0
sub.next_charge_at = get_next_charge_date(today, sub.interval)
sub.save()
except Exception as e:
sub.failed_count += 1
if sub.failed_count >= 3:
sub.status = 'failed'
notify_subscription_failed.delay(str(sub.id))
sub.save()
logger.error(f'Recurring charge failed for sub {sub.id}: {e}')
כיצד להבטיח שקיפות בגיוס תרומות?
דוחות ציבוריים הם בסיס האמון. אנו מייצרים נתונים מצטברים בזמן אמת: סכום כולל שגויס, מספר תורמים, תרומה ממוצעת, דינמיקה חודשית, פילוח הוצאות לפי קטגוריה.
def get_program_report(program_id: str) -> dict:
program = Program.objects.get(id=program_id)
donations = Donation.objects.filter(
program=program,
status='completed'
)
expenses = Expense.objects.filter(program=program)
return {
'collected': donations.aggregate(total=Sum('amount'))['total'] or 0,
'donors_count': donations.values('donor').distinct().count(),
'avg_donation': donations.aggregate(avg=Avg('amount'))['avg'] or 0,
'expenses': expenses.values('category').annotate(
total=Sum('amount'),
count=Count('id')
).order_by('-total'),
'utilization_rate': (
expenses.aggregate(s=Sum('amount'))['s'] or 0
) / program.collected * 100 if program.collected else 0,
'monthly_dynamics': donations.annotate(
month=TruncMonth('created_at')
).values('month').annotate(total=Sum('amount')).order_by('month'),
}לאחר כל תרומה מוצלחת, נשלחת אוטומטית קבלה עם פרטי הקרן — זה חשוב לניכויי מס.
def send_donation_receipt(donation):
"""Письмо с квитанцией после успешного платежа"""
if not donation.donor or donation.is_anonymous:
return
context = {
'donation': donation,
'foundation_name': settings.FOUNDATION_NAME,
'foundation_inn': settings.FOUNDATION_INN,
'donation_date': donation.created_at.strftime('%d.%m.%Y'),
}
send_mail(
subject=f'Квитанция о пожертвовании на сумму {donation.amount} руб.',
message=render_to_string('emails/receipt.txt', context),
html_message=render_to_string('emails/receipt.html', context),
from_email=settings.FOUNDATION_EMAIL,
recipient_list=[donation.donor.email],
)
donation.receipt_sent_at = timezone.now()
donation.save()
מה אתה מקבל כתוצאה
- ארכיטקטורה על Django + PostgreSQL עם Celery למשימות רקע
- אינטגרציית YooKassa עם תשלומים חוזרים וניסיונות חוזרים אוטומטיים
- דוחות ציבוריים בזמן אמת עם API למערכות חיצוניות
- חשבון תורם עם היסטוריה, קבלות וניהול מנויים
- תיעוד API ופריסה, הדרכת צוות, 3 חודשי תמיכה באחריות
השוואה: מדוע הארכיטקטורה שלנו מנצחת?
| תכונה | מחסנית Django שלנו | פתרון PHP מותאם אישית |
|---|---|---|
| בקשות בשנייה | 100 | 20 |
| שיעור חיוב חוזר מוצלח | 92% | 77% |
| זמן טעינת דוח | <1 שנייה | 8-10 שניות |
| עלות שנתית לחשבונאות | 0 | ~120,000 RUB |
מחסנית Django שלנו מתמודדת עם עד 100 בקשות בשנייה — פי 5 מהר יותר מפתרון PHP מותאם אישית. שימוש במפתחות UUID מונע התנגשויות במהלך מיגרציות ורפליקציה. ו-Celery עם ניסיונות חוזרים מגדיל את שיעור החיוב החוזר המוצלח ב-15% בהשוואה לסקריפטים של cron. דיווח אוטומטי חוסך לקרן משמעותית בשירותי חשבונאות.
לוח זמנים ועלות
פורטל בסיסי (תוכניות, טופס תרומה, חשבון): 4–6 שבועות. פונקציונליות מלאה עם תשלומים חוזרים, דוחות ולוחות מחוונים: 2–3 חודשים. העלות מתחילה מ-350,000 RUB לגרסה הבסיסית ו-750,000 RUB לגרסה המלאה. צור קשר להערכה מפורטת.
למה לבחור בנו?
אנחנו בשוק יותר מ-10 שנים, השלמנו מעל 50 פרויקטים לעמותות וקרנות. אנו מבטיחים יציבות ואבטחת תשלומים — הפתרונות שלנו עוברים ביקורת PCI DSS. ניסיון עם Django ו-YooKassa מאפשר לנו לבצע אינטגרציות מורכבות במהירות וללא הפתעות.
אם אתה רוצה להגדיל תרומות חוזרות ב-30% — צור קשר, נכין הצעה. קבל ייעוץ: מלא את הטופס באתר — נדון בפרויקט שלך ונכין הצעה מסחרית.







