Atomia Billing System developer guide

Atomia billing plug-ins

41 views 0

Overview

The base interface for all Atomia Billing plug-ins is IPlugin located in the Atomia.Billing.Core.Sdk.Plugins namespace. All more specific plug-in interfaces must inherit it and add its methods and property definitions.

namespace Atomia.Billing.Core.Sdk.Plugins
{
 /// <summary>
 /// Interface from which all Atomia Billing plug-ins have to inherit or implement.
 /// </summary>
 public interface IPlugin
 {
   /// <summary>
   /// Gets or sets the plug-in name.
   /// </summary>
   string Name { get; set; }
   
   /// <summary>
   /// Gets or sets IAtomiaBillingApi instance.
   /// </summary>
   IAtomiaBillingApi AtomiaBillingApi { get; set; }
 }
}

Plug-in interfaces

Here is list of all currently supported plug-in interfaces:

INTERFACE TYPE DESCRIPTION
IAddressFetchPlugin Use this interface to write a plugin for fetching customer address information from an address service based on the personal or corporate identification number.
ICampaignPlugin Used for applying specific campaign rules and discounts for an order.
IConditionHandler Used for creating custom conditions for campaign discounts.
IInvoiceTemplating Used for creating string representations of invoices for e-mails, PDF documents etc.
IMessagingPlugin Used for implementing message sending via specific protocols (email, SMS etc.).
IPayFileProcessorPlugin Used for parsing payments from payment report files.
IPaymentMethodPlugin Used for implementing payment operations (transaction handling) for payment gateways.
IPaymentRulePlugin Used for applying custom rules on payments (e.g. check balance or payment status, check if currency matches invoice etc.).
IProvisioningPlugin Used for defining provisioning operations (package/subscription handling, order validation etc.).
IScheduledEventHandler Used for handling scheduled tasks when “heart beat” occurs.
ITaxPlugin Used for applying tax to invoiced item (s).

Plug-in interfaces are placed in Atomia.Billing.Core.Sdk.Plugins . The documentation for plugin interfaces can be found in the API and SDK reference.

Campaign plug-in

Overview

The main object for campaigns is Campaign . It defines the campaign name, the period it is valid, codes and discounts. Campaign can have one or more campaign codes which are defined using CampaignCode class. Each code has its own validity period and maximum number of usages. Campaign is bound to an order by setting its custom attribute {{CampaignCode to a valid campaign code.

Campaign can have one or more discounts (defined by CampaignDiscount class). Discount can be specified using percentage or currency amount. It can have a list of items (identified by ItemNumber ) with a specific discount for every item. In this case, the item discount overrides the general discount. Also, discount limitations can be specified. This is done by specifying properties of the CampaignDiscountLimits class. A list of campaign discount conditions is defined for every discount. It contains the name of the condition handler (name of type which implements IConditionHandler interface) and a list of CampaignDiscountConditionParam objects. These parameters are used to check whether the order satisfies the discount conditions or not. In order to apply a discount to an order, every condition must be met. This logic should be implemented in plugins of type ICampaignPlugin and IConditionHandler .

A class diagram for mentioned types can be found here.

ICampaignPlugin example

ApplyCampaignRules method

ApplyCampaignRules is called when an order is created and it can be used to change order items, add more items (e.g. get three for two) etc.

public Order ApplyCampaignRules(Order order)
{
 OrderCustomAttribute codeAttribute = order.CustomAttributes.Find(x => x.Name == "CampaignCode");
 
 if (codeAttribute != null)
 {
   // Retreive campaign code object for checking if it can be used
   CampaignCode campaignCode = CampaignsHelper.GetCampaignCode(codeAttribute.Value);
   if (this.ValidateCampaignCode(campaignCode))
   {
     // Put logic for applying rules (change order data) here.
     // ...
     
     campaignCode.UsedTimes = campaignCode.UsedTimes + 1;
     CampaignsHelper.SaveCampaignCode(campaignCode);
   }
 }
 return order;
}

private bool ValidateCampaignCode(CampaignCode campaignCode)
{
 if (campaignCode == null)
 {
   return false;
 }
 return (campaignCode.CanBeUsedTimes > campaignCode.UsedTimes) &&
   (campaignCode.ValidFrom <= DateTime.Now) &&
   (campaignCode.ValidThru >= DateTime.Now);
}

ApplyCampaignDiscounts method

ApplyCampaignDiscounts is called when order totals are calculated. It should check if the order satisfies the campaign conditions and implement logic for changing order data by applying discount rules.

public Order ApplyCampaignDiscounts(Order order)
{
 OrderCustomAttribute codeAttribute = codeAttribute = order.CustomAttributes.Find(x => x.Name == "CampaignCode");
 
 if (codeAttribute != null)
 {
   // There is campaign code in order so its data should be retreived and discounts should be applied.
   CampaignCode campaignCode = CampaignsHelper.GetCampaignCode(codeAttribute.Value);
   if (this.ValidateCampaignCode(campaignCode))
   {
     // Put logic for applying discounts here
   }
 }
 
 return order;
}

private bool ValidateCampaignCode(CampaignCode campaignCode)
{
 if (campaignCode == null)
 {
   return false;
 }
 
  return (campaignCode.CanBeUsedTimes > campaignCode.UsedTimes) &&
    (campaignCode.ValidFrom <= DateTime.Now) &&
    (campaignCode.ValidThru >= DateTime.Now);
}

TestCondition method

TestCondition can be called after an order is created to check if it satisfies the conditions for applying campaign discounts. These conditions can be number of ordered items, list of specific items, new customer etc. Here is an example of an implementation which checks if an order contains items needed for a discount.

public class CampaignDiscountConditionParam
{
  public Guid Id { get; set; }

  public CampaignDiscountCondition CampaignDiscountCondition { get; set; }

  public string Name { get; set; }

  public string Value { get; set; }
}

public bool TestCondition(Order order, List<CampaignDiscountConditionParam> campaignDiscountConditionParams)
{
  List<OrderLine> orderLines = new List<OrderLine>();
  orderLines.AddRange(order.OrderLines);
  
  CampaignDiscountConditionParam operationParam = campaignDiscountConditionParams.Find(x => x.Name == "Operation");
  bool ok = operationParam == null || operationParam.Value == "AND";
  
  foreach (CampaignDiscountConditionParam campaignDiscountConditionParam in campaignDiscountConditionParams)
  {
    if (operationParam == null || operationParam.Value == "AND")
    {
      ok = ok && orderLines.Exists(x => x.ItemNumber == campaignDiscountConditionParam.Value);
    }
    else
    {
      ok = ok || orderLines.Exists(x => x.ItemNumber == campaignDiscountConditionParam.Value);
    }
  }
  
  return ok;
}

PayFileProcessor plug-in

Overview

Payment files are used for importing payments from various payment processors. Atomia Billing API allows customers to implement plugins for parsing different payment file formats. These plugins are used to parse file stream into defined sections and to create payments from them. After that, parsed payments are imported into the system by the API.

ParseFile method

ParseFile is used for parsing payments from a payments report file. These payments are later imported into the system.

public IList<Payment> ParseFile(Stream fileStream)
{
  // PaymentLine is custom type which represents one line from payment file.
  IList<PaymentLine> paymentLines = this.ParseLines(fileStream);
  
  IList<Payment> payments = new List<Payment>();
  
  // Payment is represented by PaymentLine.
  foreach (PaymentLine paymentLine in paymentLines)
  {
    Payment payment = new Payment();
    payment.Amount = paymentLine.IsCreditDeduction ? -paymentLine.Amount : paymentLine.Amount;
    payment.Currency = new Currency { Code = paymentLine.Currency };
    payment.InvoiceReferenceNumber = paymentLine.CustomerReference;
    payment.CreatedTime = paymentLine.AccountingDate;
    payment.LastChangedTime = DateTime.Now;
    payment.DocumentId = paymentLine.ReportNumber;
    
    payments.Add(payment);
  }
  
  return payments;
}
            
private IList<PaymentLine> ParseLines(Stream fileStream)
{
  // Logic for parsing file lines to custom PaymentLine type.
}

DetectFile method

DetectFile is used to check whether the pay file plugin can parse the specified file or not. When parsing payments from an uploaded file, Atomia Billing API loads the configured pay file plugins and uses DetectFile to select a suitable plugin for the uploaded file.

Payment method plug-in

Overview

Payment method plug-ins are used for performing processing of payment transactions by calling operations on various payment processing systems (payment gateways). Interface for payment method plug-ins,  IPaymentMethodPlugin, defines two standard operations for processing payment transactions (BeginTransaction and CloseTransaction ) with one additional method for getting transaction status (ProbeTransactionStatus). First two methods should perform needed transformation of transaction data to the format expected by payment gateway and call proper operations. On some payment gateways, where it is possible to do processing in one step (charge without some kind of authorization), implementing of BeginTransaction should be sufficient.

Authorize.Net example

In the following section, code for processing transaction on Authorize.Net payment gateway can be found. On Authorize.Net, it is possible to to authorize and capture in one step so implementation of CloseTransaction method is simple. In this example, Authorize.Net SDK is used for creating requests, sending them and getting response. In the end, payment transaction (with updated statuses) is saved using Atomia Billing SDK’s PaymentMethodsHelper.

/// <summary>
/// Begins the transaction on the payment gateway/engine.
/// </summary>
/// <param name="transaction">The transaction.</param>
/// <returns>Updated transaction.</returns>
/// <remarks>
/// It gets transaction object and according to the data stored in transaction object it creates transaction on the payment gateway.
/// For synchronous payments, it returns new transaction object with filled TransactionAttributes and other properties. Status of the transaction is set to "Completed"
/// For async payment, it also fills the properties, and sets that transaction is in progress.
/// It can receive PaymentProfile in the transaction. In that case, it should use that payment profile, otherwise, it should store payment profile and then use it.
/// </remarks>
public PaymentTransaction BeginTransaction(PaymentTransaction transaction)
{
  // Validate transactions's currency. Here only USD is supported.
  if (transaction.CurrencyCode != "USD")
  {
    throw new ArgumentException("CurrencyCode");
  }
  
  // Validate that all required credit card data.
  if (!transaction.Attributes.ContainsKey("cc_number") || !transaction.Attributes.ContainsKey("expires_month") ||
    !transaction.Attributes.ContainsKey("expires_year") || !transaction.Attributes.ContainsKey("ccv"))
  {
    throw new ArgumentException("CCData");
  }
  
  // Set defaults for transaction details.
  transaction.TransactionId = string.Empty;
  transaction.StatusCode = string.Empty;
  transaction.StatusCodeDescription = string.Empty;
  
  Dictionary<string, string> customAttributes = transaction.Attributes.Keys.ToDictionary(key => key, key => transaction.Attributes[key]);
  transaction.Attributes.Clear();
  
  // Get CC data.
  string cardNumber = customAttributes["cc_number"];
  string cardNumberExpireMonth = customAttributes["expires_month"];
  string cardNumberExpireYear = customAttributes["expires_year"];
  string cardNumberCvc = customAttributes["ccv"];
  
  // Create the request and add customer data.
  AuthorizationRequest request = new AuthorizationRequest(cardNumber, cardNumberExpireMonth + cardNumberExpireYear, transaction.Amount, string.Empty, true);
  request.AddCardCode(cardNumberCvc);
  SetCustomerData(request, transaction);
  
  // Create the gateway using credentials from plugin configuration.
  // Logic for storing and getting data should be implemented.
  ApiData apiData = this.GetApiData();
  Gateway gate = new Gateway(this.Decrypt(apiData.ApiLogin), this.Decrypt(apiData.TransactionKey), apiData.TestMode);
  
  // Send request and set transaction status based on response.
  IGatewayResponse response = gate.Send(request);
  GatewayResponse gatewayResponse = response as GatewayResponse;
  
  transaction.Status = response.Approved ? PaymentTransaction.Ok : PaymentTransaction.Failed;
  transaction.StatusCode = response.ResponseCode;
  transaction.StatusCodeDescription = response.Message;
  transaction.TransactionId = response.TransactionID;
  
  // Save transaction using helper from SDK.
  PaymentMethodsHelper.AddPaymentTransaction(transaction);
  return transaction;
}

/// <summary>
/// Closes the transaction.
/// </summary>
/// <param name="transactionId">The transaction id.</param>
/// <returns>Updated transaction</returns>
/// <remarks>
/// If some payment gateways need some actions taken when transaction is performed, this method does that.
/// It is called by the engine when payment on the payment gateway is successfull.
/// </remarks>
public PaymentTransaction CloseTransaction(string transactionId)
{
  return PaymentMethodsHelper.GetPaymentTransactionById(transactionId);
}

/// <summary>
/// Probes for transaction status. It is called from the outside periodically, to fetch new status of the transaction.
/// Plugin should save this transaction status in the database, too.
/// </summary>
/// <param name="transactionId">The transaction id.</param>
/// <returns>Updated transaction.</returns>
public PaymentTransaction ProbeTransactionStatus(string transactionId)
{
  return PaymentMethodsHelper.GetPaymentTransactionById(transactionId);
}

Provisioning plug-in

Overview

The provisioning plug-in is a common name for a family of plug-ins that represent connectors between Atomia Billing and the Atomia Automation Server. Their basic purpose is to convert billing objects into corresponding provisioning objects, perform provisioning operations by calling the Atomia Provisioning Core API and return information about the result of the operation.

Provisioning plug-ins allows different provisioning systems to be used with Atomia Billing.

IProvisioningPlugin interface

The provisioning plug-in must implement the IProvisioningPlugin interface. Here is a list of all methods of this interface with a short explanation of their purpose.

Example

/// Gets the packages available in the provisioning service.
List<ProvisioningUnit> GetPackages();

/// Adds the package for the specified subscription.
void AddPackage(Subscription subscription, Dictionary<string, object> additionalData);

/// Renews the package for the specified subscription.
void RenewPackage(Subscription subscription, Dictionary<string, object> additionalData);

/// Prepares the provisioning.
void PrepareProvisioning(Subscription subscription, Dictionary<string, object> additionalData);

/// Deletes the package.
void DeletePackage(Subscription subscription, Dictionary<string, object> additionalData);

/// Change package status.
void ChangePackageStatus(Subscription subscription, string statusName, Dictionary<string, object> additionalData);

/// Changes the package.
void ChangePackage(Subscription fromSubscription, Subscription toSubscription, Dictionary<string, object> additionalData);

/// Gets the service status.
string GetPackageStatus(Subscription subscription, Dictionary<string, object> additionalData);

/// Validates the order.
void ValidateOrder(Order order);

/// Determines whether [is posible to change package] [the specified subscription].
bool IsPossibleToChangePackage(Subscription mainSubscription, Subscription newSubscription, Dictionary<string, object> additionalData);

/// Determines whether [is subscription termination possible] [the specified subscription].
List<bool> IsSubscriptionTerminationPossible(List<Subscription> subscriptions, string accountName);

/// Executes the custom action, and returns results.
object ExecuteCustomAction(string actionName, Dictionary<string, object> properties);

The Subscription object contains all necessary data needed for provisioning. For more details on the Subscription object, please see the API and SDK reference.

GetTax method

Overview

GetTax is used for getting tax details for an order when it is created or when its totals are calculated. It can be used to apply custom tax rules based on a reseller’s or customer’s data, ordered item or legal number.

Example

public class TaxPlugin : ITaxPlugin
{
  public TaxRuleResult GetTax(Guid resellerId, Guid customerId, Guid itemId, string countryCode, string legalNumber)
  {
    TaxRuleResult taxRule = new TaxRuleResult();
    
    // Place here custom code which sets taxRule properties based on arguments.
    
    return taxRule;
  }
}

public class TaxRuleResult
{
  /// <summary>
  /// Gets or sets the percentage.
  /// </summary>
  public decimal Percentage { get; set; }
  
  /// <summary>
  /// Gets or sets the description.
  /// </summary>
  public string Description { get; set; }
  
  /// <summary>
  /// Gets or sets a value indicating whether this tax is cumulative.
  /// </summary>
  public bool IsCumulative { get; set;  }
}

Template plug-in

Overview

IInvoiceTemplating is used for getting custom formatted string representations of invoices. It uses invoice data to fill a template’s placeholders and returns the formatted string. Templates are stored in a database as HTML documents with defined placeholders for each invoice property.

Example

Here is simple example of the Template plug-in used for generating an HTML document of an invoice:

public class InvoiceTemplating : IInvoiceTemplating
{
  /// <summary>
  /// Template name.
  /// </summary>
  private string templateName;
  
  /// <summary>
  /// Initializes a new instance of the <see cref="AtomiaInvoiceTemplating"/> class.
  /// </summary>
  public AtomiaInvoiceTemplating()
  {
    this.templateName = "InvoiceTemplate"; // Load from configuration.
  }
  
  public string FillInvoice(Invoice invoice, CultureInfo culture)
  {
    string dateFormat = culture.DateTimeFormat.ShortDatePattern;
    int totalPages = (int)Math.Ceiling(invoice.InvoiceLines.Count / 5.0f);
    
    // Set parameters of custom TemplateData type.
    //It is used to store data needed for template's placeholders.
    TemplateData data = new TemplateData()
      {
        InvoiceReferenceNumber = invoice.ReferenceNumber,
        InvoiceNumber = invoice.Number,
        DocumentDate = invoice.InvoiceDate.ToString(dateFormat),
        CustomerNumber = invoice.CustomerName,
        CustomerCompanyName = invoice.CustomerCompanyName,
        CustomerFirstName = invoice.CustomerFirstName,
        CustomerLastName = invoice.CustomerLastName,
        CustomerAddress = invoice.CustomerAddress,
        CustomerZip = invoice.CustomerZip,
        CustomerCity = invoice.CustomerCity,
        CustomerCountryName = invoice.CustomerCountry,
        Pages = new List<Page>(),
        Tax1Total = invoice.Tax1Total.ToString(CultureHelper.CURRENCY_FORMAT, culture),
        Subtotal = invoice.Subtotal.ToString(CultureHelper.CURRENCY_FORMAT, culture),
        Total = invoice.Total.ToString(CultureHelper.CURRENCY_FORMAT, culture),
        Currency = invoice.Currency,
        IsCompany = !string.IsNullOrEmpty(invoice.CustomerCompanyName)
      };
    
    int i = 0;
    foreach (InvoiceLine invoiceLine in invoice.InvoiceLines)
    {
      // Check if new page should be added
      if ((i % 5) == 0)
      {
        data.Pages.Add(new Page
        {
        PageNumber = ((i / 5) + 1).ToString(),
        Items = new List<Item>(),
        LastPage = totalPages == ((i / 5) + 1)
        });
      }
      
      // Add Item data here
      // ...
      
      i++;
    }
    
    return this.FillTemplate(new Dictionary<string, object> { { "data", data } });
  }
  
  private string FillTemplate(Dictionary<string, object> values)
  {
    string template = this.GetTemplate(this.templateName);
    
    if (!string.IsNullOrEmpty(template))
    {
      // Here is used StringTemplate template engine.
      StringTemplate templateEngine = new StringTemplate(template);
      
      foreach (string key in values.Keys)
      {
        templateEngine.SetAttribute(key, values[key]);
      }
      
      return templateEngine.ToString();
    }
    else
    {
      return string.Empty;
    }
  }
  
  private string GetTemplate(string templateName)
  {
    // Put logic for getting template by name here
    // ...
  }
}

Here is the template that is used in this example:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
  <meta http-equiv="X-UA-COMPATIBLE" content="IE=8" />
  <title>Invoice</title>
  <meta http-equiv="content-type" content="text/html;charset=utf-8" />
  </head>
  <body>
  $data.Pages:{
  <div>
  <div>
  <div>
  <strong>Invoice reference number:</strong> $data.InvoiceReferenceNumber$
  </div>
  </div>
  <div>
  <div>
  <b>$if(data.IsCompany)$     $data.CustomerCompanyName$     $else$     $data.CustomerFirstName$ $data.CustomerLastName$     $endif$</b>
  <br />$data.CustomerAddress$
  <br />$data.CustomerZip$, $data.CustomerCity$
  </div>
  </div>
  <div>
  <table>
  <tr>
  <th style="width: 34%;">Invoice date</th>
  <th style="width: 34%;">Customer number</th>
  <th>Invoice number:</th>
  </tr>
  <tr>
  <td>$data.DocumentDate$</td>
  <td>$data.CustomerNumber$</td>
  <td>$data.InvoiceNumber$</td>
  </tr>
  </table>
  <table>
  <tr>
  <th>Item</th>
  <th>Price</th>
  </tr>
  $it.Items:{
  <tr>
  <td>$it.ItemName$</td>
  <td>$it.Price$ $data.Currency$</td>
  </tr>
  }$
  $if(it.LastPage)$
  <tr>
  <td><strong>Subtotal</strong></td>
  <td>$data.Subtotal$ $data.Currency$</td>
  </tr>
  <tr>
  <td>Tax</td>
  <td>$data.Tax1Total$ $data.Currency$</td>
  </tr>
  <tr>
  <td><strong>Total</strong></td>
  <td><strong>$data.Total$ $data.Currency$</strong></td>
  </tr>
  $endif$
  </table>
  </div>
  </div>
  <div></div>
  }$
  </body>
</html>

Was this helpful?