September 27, 2024

Fundraising nástroje pre neziskovky

Vediete alebo pracujete pre neziskovú organizáciu?
Máte problém so správou darcov, darov a komunikáciou?
Pozrime sa, ako môžu digitálne nástroje zjednodušiť váš život.

Cloud API

Fundraising 101

Ak ste niekedy darovali neziskovej organizácii na Slovensku, je veľká pravdepodobnosť, že ste to urobili prostredníctvom portálu https://www.darujme.sk. Vďaka tomuto portálu môžu neziskové organizácie prijímať platby rýchlo a bezpečne. Je to dobre známa a dôveryhodná platforma, ktorá ponúka rôzne možnosti platby. Okrem toho môžu byť dary realizované buď ako jednorazový príspevok, alebo na pravidelnej báze, a to ako pre jednotlivcov, tak aj pre spoločnosti.

Neziskové organizácie môžu pristupovať k informáciám o daroch a darcoch prostredníctvom webovej aplikácie alebo cez API.

Prečo je to dôležité?

Aby organizácie mohli komunikovať s darcami, umožňujúc im vidieť, ako ich finančné príspevky vytvárajú lepšiu budúcnosť a pomáhajú plniť ciele organizácie. Jednoducho povedané, transparentná a efektívna komunikácia vedie k lepšiemu vzťahu s darcami, čo môže následne viesť k stabilnému alebo dokonca rastúcemu objemu darov.

Efektívna komunikácia je automatizovaná

Informácie o daroch sú konsolidované do jednej databázy, kde sú systematicky vyhodnocované – darcovia sú kategorizovaní a komunikácia je následne zahájená na základe ich preferencií. Môže to znieť komplikovane, ale myšlienka je jednoduchá: napríklad, pravidelní darcovia dostávajú informácie častejšie, zatiaľ čo jednorazoví darcovia sú kontaktovaní po dlhšom čase. Samozrejme má zmysel, aby organizácie kontaktovali veľkých alebo firemných darcov osobne.

Populárnou voľbou pre jednu databázu je https://salesforce.com. Najmä preto, že ponúkajú veľké zľavy pre neziskové organizácie (až po bezplatné).

V tomto blogu ukážeme, ako prepojiť Salesforce.com a Darujme.sk.

Blog bude veľmi technický, ale je to kompletný recept. Salesforce admin si s nim poradí ale ak vaša organizácia nemá technických členov tímu, radi vám pomôžeme s nasadením.

Prehľad integrácie

  1. Integrácia bude postavená vo vnútri Salesforce.com, ako plánovaná trieda APEX.
  2. Použijeme API darujme.sk API na získanie platieb.
  3. Každá platba bude premenená na InuTransaction a ak darca ešte neexistuje v našej databáze, na Contact a Account.
  4. Contact bude reprezentovať darcu jednotlivca a budeme ho párovať podľa e-mailu.
  5. Account bude reprezentovať darcu pre dar od spoločnosti.

Dátový model

Vytvoríme nový Salesforce objekt na uchovávanie darov – nazvaný InuTransaction. Samozrejme potrebujeme polia pre finančnú sumu, dátum a darcu. Keďže dar môže byť urobený fyzickou osobou, pridáme vzťahy k objektom Contact a Account. Account - spoločnosť - je voliteľný.
Contact je povinný, v skutočnosti je to Master-Detail vzťah. To je nevyhnutné pre vytváranie zhrňovacích polí, ktoré chceme vytvoriť pre kontakty, ako napríklad celkové dary za rok, priemerná výška daru, atď.

Pozrime si celý zoznam:

NÁZOV POĽA NÁZOV V SYSTÉME DÁTOVÝ TYP
Účet Account__c Lookup(Account)
Názov bankového účtu Bank_Account_Name__c Text(100)
Kampaň Campaign__c Lookup(Campaign)
Názov kampane Campaign_Name__c Text(100)
Kontakt Contact__c Master-Detail(Contact)
Vytvorené CreatedById Lookup(User)
darujme_id darujme_id__c Text(100)
Typ darcu Donor_Kind__c Text(100)
Informačný text Info_Text__c Text(100)
Posledná úprava LastModifiedById Lookup(User)
IBAN platiaceho Payer_IBAN__c Text(100)
Špecifické číslo Specific_Number__c Text(100)
Transakcia Name Auto Number
Suma transakcie Transaction_Amount__c Currency(16, 2)
Dátum transakcie Transaction_Date__c Date
Periodickosť transakcie Transaction_Periodicity__c Text(100)
Číslo sekvencie transakcie Transaction_Sequence_no__c Number(18, 0)
Stav transakcie Transaction_Status__c Text(100)
Variabilný symbol Variable_Number__c Text(100)
Zdroj daru Zdroj_daru__c Picklist

Poznámka: Tento objekt už obsahuje polia, ktoré budeme potrebovať na spracovanie darov realizovaných bankovým prevodom (IBAN atď.). Nezabudnite si prečítať druhú časť tejto série.

Volanie Darujme.sk API z APEX-u

API darujme.sk je pomerne jednoduché.
Každá nezisková organizácia, ktorá má účet na darujme.sk, môže požiadať o prístup k API a získať API kľúč a tajný kľúč.

Predtým, než budeme môcť zavolať ďalšie API, musíme vymeniť používateľské meno a heslo za prístupový token.
Takže poďme na to.

global class InukoDarujmeSync implements Schedulable {
	public class TokenResponse {
        public TokenResponseInner response;
    } 
    public class TokenResponseInner {
        public String token;
    }
	private static string getDarujmeToken() {
		object r = darujmeFetch('POST', '/v1/tokens/', '{"username": "'+DARUJME_USER+'", "password": "'+DARUJME_PWD+'"}', null, null);
		TokenResponse resp = (TokenResponse)JSON.deserialize((string)r, TokenResponse.class);
		string token = resp.response.token;
		return token;
	}
}

Metóda darujmeFetch pridáva všetky požadované hlavičky, ktoré sú spoločné pre všetky volania API, a vykoná HTTP požiadavku.
Iba metóda generateSignature je trochu nezvyčajná - používa API_SECRET na výpočet podpisu z cesty api a post dát (ak existujú). Je to len SHA256 hash vyššie uvedeného. Ako som spomínal, len trochu nezvyčajné.

global class InukoDarujmeSync implements Schedulable {
  private static object darujmeFetch(string method, string api, string sb, string token, string apiParams) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://api.darujme.sk'+api+(apiParams != null ?apiParams:''));
        req.setMethod(method);   
        if (token != null)
        	req.setHeader('Authorization', 'TOKEN '+token);
        
        req.setHeader('Content-Type', 'application/json');
		req.setHeader('Accept','application/json');
		req.setHeader('X-ApiKey', DARUJME_API_KEY);
		req.setHeader('X-Organisation', DARUJME_ORGANIZATION_ID);
		req.setHeader('X-Signature', generateSignature(api, sb));
        
        if(method == 'POST')
            req.setBody(sb);
        
        Http http = new Http();
        if (Test.isRunningTest()) {
          

 return null;
        }
        try {
            HttpResponse res = http.send(req);
            return res.getBody();
        }
        catch(Exception e) {
        	return null;
        }
    }
}

Volanie Darujme.sk API z Javascrip-u

Pod textom nájdete Javascript/NodeJS kód, pre komunikáciu s API. Môže sa vám zísť ak budete robiť masový export/import historických záznamov.

const getDarujmeToken = ()=>{
	const body = {
		username: DARUJME_USER,
		password: DARUJME_PWD
	}
	const tokenResp = await darujmeFetch('POST', '/v1/tokens/', body, undefined);
	if (!tokenResp)
		throw Error("No token!");
	const token = tokenResp.response.token;
	return token;
}

const darujmeFetch = async (method, api, body, token, apiParams) => {

	let sb = "";
	if (body)
		sb = JSON.stringify(body);

	const init = {
		headers: {
			'Content-Type': 'application/json',
			'Accept': 'application/json',
			'X-ApiKey': DARUJME_API_KEY,
			'X-Organisation': DARUJME_ORGANIZATION_ID, // Guid can be taken from web - org info url
			'X-Signature': generateSignature(api, sb),
		},
		method: method
	};
	if (token) {
		init.headers["Authorization"] = 'TOKEN ' + token;
	}

	if (method === "POST" && sb) {
		init.body = sb;
	}

	const url = "https://api.darujme.sk"
	const reqUrl = url + api + (apiParams||"");
	const resp = await fetch(reqUrl, init);
	if (!resp.ok) {
		const t = await resp.text();
		throw Error(t);
	} else {
		const j = await resp.json();
		return j;
	}
}
const generateSignature = (api, sb) => {
	const path = api;
	const payload = sb + ":" + path;
	const secret = DARUJME_API_SECRET;
	const r = crypto.createHmac('sha256', Buffer.from(secret)).update(Buffer.from(payload)).digest("hex");
	return r;
}

Plánovaný APEX

Kód nižšie je konceptuálne jednoduchý. Získa záznamy o daroch z darujme.sk a vytvorí zodpovedajúce objekty v salesforce.com.

  1. Zavoláme API pre platby a požiadame o všetky nové dary od včera. Očakávame, že kód bude spustený denne. Keďže si v databáze salesforce uchovávame ID darov z darujme.sk, môžeme rýchlo preskočiť dary, ktoré sme už spracovali.
  2. Hľadáme dary poskytnuté firmami a vytvoríme zodpovedajúce účty, ak ešte neexistujú.
  3. Hľadáme kontakty pomocou emailu darcu a vytvoríme nové kontakty, ak email ešte nie je v salesforce.
  4. Nakoniec vytvoríme záznamy InuTransaction, jeden pre každý dar.

Dôvod, prečo je kód štruktúrovaný týmto spôsobom, je jednoduchý. Apex kód môže vykonať iba 100 SOQL/DML volaní, a preto nemôžeme dotazovať alebo vytvárať každý záznam zvlášť. (No, môžeme, ale nebolo by to veľmi výkonné.)

Najskôr niekoľko objektov používaných na deserializáciu JSON odpovedí z API. API poskytuje viac polí, ale toto sú tie, ktoré používame.

global class InukoDarujmeSync implements Schedulable {
    
    public class TokenResponse {
        public TokenResponseInner response;
    } 
    public class TokenResponseInner {
        public String token;
    }
    
    public class PaymentResponse {
        public PaymentItem[] items; 
        public PaymentMeta metadata;
    }
    public class PaymentMeta {
        Integer page, pages, total;
    }
    public class PaymentItem {
        public string id, status, created_at, happened_at;
        public Decimal value;
        public Integer sequence_no;
        public PaymentDonation donation;
    }
    public class PaymentDonation {
        public string periodicity, kind;
        public PaymentDonor donor;
        public PaymentCompany company_data;
    }
    public class PaymentDonor {
        public string id,email, name,surname,note;
    } 
    public class PaymentCompany {
        public string tax_id, vat_id, business_id, business_name, business_address;
    } 
}

Now let's login and fetch donations (payments). Then we will filter any donations we already have in salesforce.

global class InukoDarujmeSync implements Schedulable {
	global void execute(SchedulableContext SC) {       
        String token = getDarujmeToken();
       
        String since = datetime.now().addDays(-1).formatGMT('yyyy-MM-dd');
        String apiParams = '?created_gte='+since+'T00%3A00%3A00Z&limit=100';

 		for (Integer i=1; i<1000; i++)
        {
            String apiParams2 = apiParams + '&page='+i;
            object z = darujmeFetch('GET', '/v1/payments/', '', token, apiParams2);
            PaymentResponse payments = (PaymentResponse)JSON.deserialize((string)z, PaymentResponse.class);
            
            Map<string, PaymentItem> itemsMap = new Map<string, PaymentItem>();
            for (PaymentItem item : payments.items) {
                String email = item.donation.donor.email;
                item.donation.donor.email = email.toLowerCase(); // just in case...
                itemsMap.put(item.id, item);
            }
           
            Set<string> tids = itemsMap.keySet();
            for (InuTransaction__c t : [SELECT Id, darujme_id__c From InuTransaction__c where darujme_id__c = :tids]) {
                itemsMap.remove(t.darujme_id__c);
                system.debug('SKIP Payment with id already exists:'+t.darujme_id__c);
            }
            List<PaymentItem> paymentItems = itemsMap.values();
            
            if (paymentItems.size() > 0) {
            
                Map<string, string> accounts = prepareAccounts(paymentItems);
                
                Map<string, string> contacts = prepareContacts(paymentItems, accounts);
                
                processPayments(paymentItems, accounts, contacts);
                break; // We have to break here, because we cannot make HTTP calles in APEX with uncommited data. 
				// If this actually ever happends we will have to schedule more APEX jobs 
            }
            // Processes all
            if (payments.metadata == null || payments.metadata.page >= payments.metadata.pages)
                break;
        }
    }
}

Ak sú nejaké dary na spracovanie paymentItems.size() > 0, vytvoríme účty pre firemné dary (ak nejaké sú). Zároveň vrátime cache pre preklad názvov firiem na accountId v salesforce.

	// Returns a Map of name (and ico) to accountId. 
    // First find the accounts by payments donor company name.
    // If not found create the accounts.
    private static Map<string, string> prepareAccounts(List<PaymentItem> paymentsItems) {
        Map<string, PaymentItem> emap = new Map<string, PaymentItem>();
        for (PaymentItem item : paymentsItems) {
            PaymentCompany company = item.donation.company_data;
            if (item.donation.kind == 'company' && company != null) {
                if (company.business_name != null)
                    emap.put(company.business_name, item);
                if (company.business_id != null)
                    emap.put(company.business_id, item);
            }
          
        }
        Map<string, string> accounts = new Map<string, string>(); // companyName -> account.Id
        
        // 1. Find existing accounts by name
        Set<string> names = emap.keySet();
        for (Account l : [SELECT Id, Name, AccountNumber From Account where name = :names OR AccountNumber= :names ]) {
            system.debug('SKIP: account:'+l.Name);
            if (l.Name != null) {
                accounts.put(l.Name, l.id);
                PaymentItem x = emap.get(l.Name);
                if (x != null)
                    emap.remove(x.donation.company_data.business_id);
                emap.remove(l.Name);
            }
            if (l.AccountNumber != null) {
                accounts.put(l.AccountNumber, l.id);
                PaymentItem x = emap.get(l.AccountNumber);
                if (x != null)
                    emap.remove(x.donation.company_data.business_name);
                emap.remove(l.AccountNumber);
            }
        }
        
        // 2. Create missing accounts
        List<Account> accountsToCreate = new List<Account>();
        Set<PaymentItem> itemsWithoutAccount = new Set<PaymentItem>(emap.values()); // remove duplicates
        for (PaymentItem item : itemsWithoutAccount) {
            
            PaymentCompany company = item.donation.company_data;
            if (item.donation.kind == 'company' && company != null && (company.business_name != null || company.business_id != null)) {
              	Account aid = new Account(Name=company.business_name, AccountNumber=company.business_id, ICO__c=company.business_id, IC_DPH__c=company.vat_id, Tax_ID__c = company.tax_id);
                if (company.business_address != null) {
                    aid.BillingStreet = company.business_address;
                }
                system.debug('CREATE: account:'+company.business_name+' ico:'+company.business_id);
           		accountsToCreate.add(aid);
            }
        }
       	insert accountsToCreate;
        for (Account c : accountsToCreate) {
            if (c.Name != null)
            	accounts.put(c.Name, c.Id);
            if (c.AccountNumber != null) {
                 accounts.put(c.AccountNumber, c.Id);
            }
        }
        
        return accounts;
    }

Teraz vytvoríme kontakty v Salesforce pre darcov, ak ešte neexistujú. Opäť vrátime cache emailov na Salesforce contact ID. Všimnite si, že používame cache accountov na to, aby sme priradili kontakt pod firemný účet, ak bol dar poskytnutý firmou.

    // Returns a Map of email to contactid. 
    // First find the contacts by payments donor email.
    // If not found create the contacts.
    private static Map<string, string> prepareContacts(List<PaymentItem> paymentsItems, Map<string, string> accounts) {
        Map<string, PaymentItem> emap = new Map<string, PaymentItem>();
        for (PaymentItem item : paymentsItems) {
        	emap.put(item.donation.donor.email, item);
        }
        Map<string, string> contacts = new Map<string, string>(); // email -> contact.Id
        
        // 1. Find existing contacts by email
        Set<string> emails = emap.keySet();
        for (Contact l : [SELECT Id, Name, accountid,email,Secondary_Email__c From Contact where email = :emails or Secondary_Email__c = :emails]) {
            system.debug('SKIP: contact:'+l.Name);
            if (l.email != null) {
                contacts.put(l.email, l.id);
                emap.remove(l.email);
            }
            if (l.Secondary_Email__c != null) {
                contacts.put(l.Secondary_Email__c, l.id);
                emap.remove(l.Secondary_Email__c);
            }
        }
        
        // 2. Create missing contacts
        string defaultAccount = null;
        List<Contact> contactsToCreate = new List<Contact>();
        Set<PaymentItem> itemsWithoutContact = new Set(emap.values());
        for (PaymentItem item : itemsWithoutContact) {
            string aid = null;
            
            PaymentCompany company = item.donation.company_data;
            
            if (accounts != null && item.donation.kind == 'company' && company != null && (company.business_name != null || company.business_id != null)) {
                if (company.business_name != null)
                	aid = accounts.get(company.business_name);
                if (aid == null && company.business_id != null)
               		aid = accounts.get(company.business_id);
            }
            if (aid == null) {
                if (defaultAccount == null)
                    defaultAccount = getDefaultAccount();
                aid = defaultAccount;
            }
            system.debug('CREATE: contact:'+item.donation.donor.name+' email:'+item.donation.donor.email);
            contactsToCreate.add(new Contact(firstname=item.donation.donor.name, lastname=item.donation.donor.surname, email=item.donation.donor.email, AccountId=aid));
        }
       	insert contactsToCreate;
        for (Contact c : contactsToCreate) {
            contacts.put(c.Email.toLowerCase(), c.Id);
        }
        
        return contacts;
    }
	private static string getDefaultAccount() {
        Account defaultAccount = null;
        String defaultAccountName = 'Fyzická osoba'; // Natural Persons
        List<Account> accs = [SELECT Id, Name From Account where name = :defaultAccountName];
        if (accs.size()>0) {
            return accs[0].Id;
        } else {
            Account d = new Account(name = defaultAccountName);
            insert d;
            return d.Id;
        }
    }

Vyzbrojený cache-ov contactov a accountov je finále príbehu iba jednoduchý cyklus.

	private static void processPayments(List<PaymentItem> paymentItems, Map<string, string> accounts, Map<string, string> contacts) {
        List<InuTransaction__c> trans = new List<InuTransaction__c>();
        for(PaymentItem item : paymentItems) {
            String email = item.donation.donor.email;
            Decimal value = item.value;
           
            String aid = null;
            PaymentCompany company = item.donation.company_data;
            if (item.donation.kind == 'company' && company != null) {
                if (company.business_name != null)
                    aid = accounts.get(company.business_name);
                if (aid == null && company.business_id != null)
                    aid = accounts.get(company.business_id);
            }
            
            String cid = contacts.get(email);
            
            InuTransaction__c t = new InuTransaction__c(contact__c = cid);
            if (aid != null)
                t.account__c = aid;
            t.Info_Text__c = 'Darujme';
            t.Zdroj_daru__c = 'Darujme';
            t.darujme_id__c = item.id;
            t.Donor_kind__c = item.donation.kind;
            t.Transaction_Amount__c = value;
            t.Transaction_Status__c = item.status;
            t.Transaction_Sequence_no__c = item.sequence_no;
            t.Transaction_Periodicity__c = item.donation.periodicity;
            string[] dt_parts = item.happened_at.split('T')[0].split('-'); // We only want to date part.
            t.Transaction_Date__c = Date.newInstance(integer.valueOf(dt_parts[0]),integer.valueOf(dt_parts[1]),integer.valueOf(dt_parts[2]));
            system.debug('CREATE: transaction:'+email+' kind:'+item.donation.kind+' value:'+value);
            trans.add(t);
        }
        insert trans;
    }

Naplánovanie APEX úlohy

Jediné, čo zostáva, je skutočne periodicky spúšťať vyššie uvedený kód. Nastavíme to v Salesforce ako 'Scheduled Jobs' (cron).

Scheduled_Jobs_Apex

Ak potrebujete denne spracovať viac ako sto darov, budete musieť vytvoriť viacero denných úloh.
Alebo nás kontaktujte pre iné riešenie.

Zhrnutie

Darujme.sk a Salesforce.com sú skvelé nástroje pre neziskové organizácie.
Avšak deď ich spojíte, otvára to ešte množstvo dalších možností.
Či už ide o manažment darcov a ich segmentáciu, personalizovanejšiu a efektívnejšiu komunikáciu, centralizované dashboardy organizácie a oveľa viac.

Ak potrebujete pomoc s integráciou (nielen) Salesforce.com a darujme.sk, neváhajte nás kontaktovať.

Úspešné fundraisovanie!

Čítajte viac

Kto sú Zebry

May 16, 2024
Kto sú Zebry

Na jeseň 2023 začalo občianske združenie Cesta von rozbiehať svoj nový program Zebra a my sme ich podporili hneď od začiatku.

Vývoj softvéru pre program, ktorý sa spúšťa, neustále mení, prispôsobuje a hľadá, je zaujímavá výzva.

Apka Úsmev od Inuko

May 07, 2024
Apka Úsmev od Inuko

V predošlých blogoch sme vám priblížili aplikácie, ktoré sme vytvorili pre mimovládne organizácie, respektíve neziskovky. Medzi nimi sa najviac vynímali naše apky pre organizáciu Cesta von, ktorá po celom Slovensku pomáha marginalizovaným komunitám, aby sa dostali z pasce chudoby a aby pre ďalšie generácie zabezpečili lepší život.

Hodina slovenčiny s Amalkou

March 05, 2024
Hodina slovenčiny s Amalkou

Tretím úspešným programom Cesty von je program jazykovej podpory Amal, ktorý rozvíja najmä komunikačné zručnosti v slovenskom jazyku u omám a rodičov z komunít a to aj vďaka prepojeniu ľudí z majority a minority, ktorí by sa možno inak nikdy nestretli…

©2022-2024 Inuko s.r.o
Privacy PolicyCloud
ENSKCZ
ICO 54507006
VAT SK2121695400
Vajanskeho 5
Senec 90301
Slovakia EU

contact@inuko.net