Creating a Donation Form through SKY API into Raiser’s Edge NXT – Part 3 (Server back end)

This follows on from the second part of my guide to creating a donation from through SKY API…

The source code for this and the other parts of this series is available on Github.

In this part we are going to actually doing something with the donor details and their credit card.

We are using .NET on our server. You could use any language to do this but I am mostly familiar with working with .NET. I am going to assume here that you are either familiar with .NET or you are able to translate the concepts into your own language.

I created a new project in Visual Studio 2017. I chose an ASP.NET Web Application and selected the WebAPI option without authentication.  I added a new controller to my project – “Web API 2 Controller – Empty” called DonationController.

The first thing we need to do here is to capture the incoming data. In WebAPI we need to create a complex type to capture this incoming data. (I was always baffled by the fact that it does not appear to be possible to simple accept the raw data. Maybe there is a way but I am simply missing it).

We create a DTO – data transfer object. This is then the input into our method as shown below:

public class DonationController : ApiController
{

   public class DonationDTO
   {
      public string FirstName { get; set; }
      public string LastName { get; set; }
      public string Email { get; set; }
      public string Token { get; set; }
      public string Amount { get; set; }
      public string TextOther { get; set; }
   }
   
   [Route("donation")]
   [HttpPost]
   public async Task<IHttpActionResult> CaptureDonation([FromBody] DonationDTO formData)
   {  
      return Ok("All good");
   }

   private async Task SendToRaisersEdge()
   {
      return true;
   }

   private async Task SendToStripe(DonationDTO formData)
   {
      return true;
   }
}

This includes all of our incoming variables. If the TextOther variable is not “null” then the donor has selected that amount rather than the value in Amount.

Let’s look at what we would put in the SendToStripe method first. I have copied the example almost exactly from the Stripe example. We are using the Stripe .NET library found in NuGet or by entering the following in the Package Manager Console:

Install-Package Stripe.net

Our SendToStripe method creates a charge with options including the token and the incoming amount. This example is the bare bones minimum to charge the customer.

 private async Task SendToStripe(DonationDTO formData)
 {

     //This could really do with some better validation than just assuming the values to be numbers.
     int amount;
     if(formData.TextOther !=null)
     {
        amount = int.Parse(formData.TextOther) * 100;
     }
     else
     {
        amount = int.Parse(formData.Amount) * 100;
     }
     
     // See your keys here: https://dashboard.stripe.com/account/apikeys
     StripeConfiguration.SetApiKey("<Your Stripe secret key>");
     var token = formData.Token; 

     try
     {
        var options = new ChargeCreateOptions
        {
           Amount = amount,
           Currency = "usd",
           Description = "Generous Donation to Zeidman Development",
           SourceId = token,
        };
        var service = new ChargeService();
        Charge charge = await service.CreateAsync(options);

     }
     catch (Exception)
     {
        //Again better exception handling would be required.
        return false;
     }

     return true;    
 }

Now let’s move on to Raiser’s Edge.

There are a couple of things we need to do in order to add our donation. In this case we are going to make some assumptions about our data. You may need to adjust these assumptions based on your instance of RE.

  1. We don’t need to update existing constituents (we only have the first name, last name and emails address although you could capture more details if you wanted).
  2. For new constituents we do not need to add anything else to their record i.e. there are no other required fields such as constituent codes or addressee/salutations. (In a real world example you may want to add these types of values whether they are required or not).
  3. We are only going to look up by email address and assume that if we find it in RE then this is the correct record. In a real world example you would want to check to make sure that the names match. If the names did not match you may want to flag this for additional checks by a member of staff.
  4. We are adding the gift directly to the record and not into a batch. At the time of writing this is not currently possible in the SKY API.
  5. We are going to assume that you have authentication covered. This is a complex topic that Blackbaud covers well and there is little reason for me to go over it.

OK let’s get on with the code…

We are using Newtonsoft JSON.NET as a method of handling in json in .NET. This can be found in Nuget.

Firstly we need a couple of utility methods. These will build our request message and get our access token. (As per my assumptions I am leaving the implementation of that to you). You will need to get hold of your BB API subscription key from your developer account.

private HttpRequestMessage GetNXTRequest(string uri, HttpMethod method)
{
	var request = new HttpRequestMessage()
	{
		RequestUri = new Uri(uri),
		Method = method,
	};
	
	request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
	request.Headers.Add("Authorization", "Bearer " + GetAccessToken());
	request.Headers.Add("bb-api-subscription-key", "");

	return request;
}

private string GetAccessToken()
{
    //You will need to figure out your own routine here. For testing generate an access token from the "Try it" app on the BB developer site.
    return "<Generated Token>";
}

Now that we have these, we need to look up the constituent by email address. We make use of the SKY API search method to search for email address. This returns the system id if found otherwise an empty string which we can test for.

private async Task LookupConstituentByEmail(DonationDTO formData)
{

  string id = string.Empty;

  using (var http = new HttpClient())
  {
    //This is our call to the SKY API to look up the constituent
    var uri = "https://api.sky.blackbaud.com/constituent/v1/constituents/search?search_text=" + WebUtility.UrlEncode(formData.Email) + "&include_inactive=true&strict_search=false";
    var request = GetNXTRequest(uri, HttpMethod.Get);
    HttpResponseMessage response = await http.SendAsync(request);

    if (response.IsSuccessStatusCode)
    {

      JObject jContent = JObject.Parse(await response.Content.ReadAsStringAsync());
      int totalItems = Convert.ToInt32(jContent.SelectToken("count").ToString());

      if (totalItems == 0)
      {
        return id;
      }

      JArray values = (JArray)jContent.SelectToken("value");

      if (values != null)
      {
        //We are being lazy and just taking the first one that we find. You would want to ensure that you find the correct one.
        id = values[0].SelectToken("id").Value();
         
      }
         
    }
  }

  return id;
}

If it is an empty string we need to create our constituent.

There are two parts to our creation. One is the construction of our json object and the other is posting that object to the SKY API. This is shown below.

private async Task CreateConstituent(DonationDTO formData)
{

   string id = string.Empty;

   using (var http = new HttpClient())
   {
       //This is our call to the SKY API to look up the constituent
       var uri = "https://api.sky.blackbaud.com/constituent/v1/constituents";
       var request = GetNXTRequest(uri, HttpMethod.Post);
       request.Content = new StringContent(ConstructConstituent(formData), Encoding.UTF8, "application/json");

       HttpResponseMessage response = await http.SendAsync(request);

       if (response.IsSuccessStatusCode)
       {
          JObject jContent = JObject.Parse(await response.Content.ReadAsStringAsync());
                    
          //We are being lazy and just assuming that the constituent was created successfully here...
          id = jContent.SelectToken("id").Value();                    
       }
    }

   return id;

}

private string ConstructConstituent(DonationDTO formData)
{

    JObject constituent = new JObject();
    constituent.Add("type", "Individual");    
    constituent.Add("first", formData.FirstName);
    constituent.Add("last", formData.LastName);
    constituent.Add(new JProperty("email",
        new JObject(
            new JProperty("address", formData.Email),
            new JProperty("type", "Email"),
            new JProperty("primary", true)
            )
       )
    );

    return constituent.ToString();
}

We are now on to the final piece of our code. We have the constituent id of either the looked up constituent or of the newly created constituent so we can create our gift. This will be similar to the constituent creation code so I will not repeat it all.

Instead of the ConstructConstituent method we will use a similar ConstructGift method as shown below. You will need to adjust some of the values for your instance of RE e.g. fund, campaign and appeal ids.

private string ConstructGift(DonationDTO formData, string id)
{

    //As previously if the TextOther field is null we use the amount value otherwise the TextOther value. 
    double amount = 0;
    if (formData.TextOther == null)
        amount = double.Parse(formData.Amount);
    else
        amount = double.Parse(formData.TextOther);

    JObject gift = new JObject();
    gift.Add("constituent_id", id);
    gift.Add("type", "Donation");
    gift.Add("date", DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss"));
    gift.Add("post_date", DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss"));
    gift.Add("post_status", "NotPosted");

    JProperty jAmount = new JProperty("amount",
        new JObject(
            new JProperty("value", amount)
       )
    );

    gift.Add(jAmount);
    gift.Add(new JProperty("gift_splits", new JArray(
        new JObject(
            jAmount,      
            new JProperty("appeal_id", "15"),
            new JProperty("campaign_id", "1"),
            new JProperty("fund_id", "41")
        ))
        )
    );

    gift.Add(new JProperty("payments", new JArray(
        new JObject(new JProperty("payment_method", "Cash"))
       ))
    );

    return gift.ToString();
}

We are also copying and changing the CreateConstituent method so that it calls the corresponding gift post url and we now specify POST instead of GET in our call to the GetNXTRequest method i.e:

var uri = "https://api.sky.blackbaud.com/gift/v1/gifts";
var request = GetNXTRequest(uri, HttpMethod.Post);
request.Content = new StringContent(ConstructGift(formData,constitId), Encoding.UTF8, "application/json");

 

We now need to put all the pieces together. We just update our initial methods as shown below:

[Route("donation")]
[HttpPost]
public async Task CaptureDonation([FromBody] DonationDTO formData)
{

    var stripeResult = await SendToStripe(formData);
    var reResult = await SendToRaisersEdge(formData);
    

    return Ok(stripeResult && reResult);
}

private async Task SendToRaisersEdge(DonationDTO formData)
{
    string giftId = string.Empty;
    string id = await LookupConstituentByEmail(formData);
    if (string.IsNullOrEmpty(id))
    {
        id = await CreateConstituent(formData);
    }

    if (!string.IsNullOrEmpty(id))
    {
        giftId = await CreateGift(formData, id);
    }

    return (!string.IsNullOrEmpty(giftId));
}

And there we have it. A working example albeit somewhat rough around the edges. As I have stressed several times there is a lot of room for improvement to make the solution more robust. It does not handle validation or exceptions every well. It also does not handle Blackbaud’s throttling either. (However if all you are doing is a limited number of calls like this solution then that ought not be a problem. If it is a problem then you are getting a lot of donations in so count yourself lucky!)

If you would like any help in implementing this or any other solution then please do get in touch via our website: zeidman.info.