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.
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.
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.
InuTransaction
a ak darca ešte neexistuje v našej databáze, na Contact
a Account
.Contact
bude reprezentovať darcu jednotlivca a budeme ho párovať podľa e-mailu.Account
bude reprezentovať darcu pre dar od spoločnosti.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.
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;
}
}
}
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;
}
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.
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;
}
Jediné, čo zostáva, je skutočne periodicky spúšťať vyššie uvedený kód. Nastavíme to v Salesforce ako 'Scheduled Jobs' (cron).
Ak potrebujete denne spracovať viac ako sto darov, budete musieť vytvoriť viacero denných úloh.
Alebo nás kontaktujte pre iné riešenie.
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!