Skip to content

Instantly share code, notes, and snippets.

@sj26
Created December 14, 2011 13:39
Show Gist options
  • Save sj26/1476603 to your computer and use it in GitHub Desktop.
Save sj26/1476603 to your computer and use it in GitHub Desktop.
Yet Another Chargify HTTParty-based API gem for Ruby
require 'httparty'
# This may go away:
require 'active_support/all'
# (doesn't *require* Rails, but adds some niceties)
module Chargify
VERSION = "0.0.1".freeze
# Create a default `Client`, attempting to use Rails application config.
def self.client options=nil
@client ||= begin
options ||= Rails.application.config.chargify if defined? Rails
Client.new options
end
end
# Delegate direct calls to the default `Client`, `.client`
def self.method_missing method, *args, &block
client.send method, *args, &block
end
def self.respond_to? method, include_private=false
super or client.respond_to? method, include_private
end
# Raise an exception for responses with 4xx and 5xx status codes.
# Use PresentParser so parsing an error response doesn't raise.
module HTTPExceptions
def perform_request http_method, path, options
super.tap do |response|
response.error! if response.response.is_a?(Net::HTTPClientError) or response.response.is_a?(Net::HTTPServerError)
end
end
end
# Only parses body if present.
module PresentParser
protected
def xml
super unless body.blank?
end
def json
super unless body.blank?
end
def yaml
super unless body.blank?
end
end
# Turns XML, JSON and YAML bodies into `Hashie::Mash`s.
module MashedParser
protected
def xml
mashed super
end
def json
mashed super
end
def yaml
mashed super
end
private
def mashed thing
if thing.is_a? Hash
Hashie::Mash.new thing
elsif thing.is_a? Array
thing.map &method(:mashed)
else
thing
end
end
end
# Chargify
class Client
# v1 configuration
attr_accessor :subdomain, :api_key, :shared_key
# v2 configuration
attr_accessor :api_id, :api_password, :api_secret
def initialize options={}
options.each do |key, value|
send :"#{key}=", value
end
end
# APIs
#
# HTTParty uses class attribtues to set base_uri and some other friends
# so we make a client-specific subclass for each API-version which can
# have some defaults set.
# Subclassed for each `Client` in `#v1`
class V1
include HTTParty
# extend HTTPExceptions
class_attribute :client
delegate :subdomain, :api_key, :shared_key, :to => :client
# Injected by `Client#v1`
# base_uri "https://#{subdomain}.chargify.com"
headers "Content-Type" => "application/json"
headers "User-Agent" => "Ruby sj26-chargify #{Chargify::VERSION}"
format :json
parser Class.new(HTTParty::Parser) { include PresentParser, MashedParser }
end
# Subclassed for each `Client` in `#v2`
class V2 < V1
delegate :api_id, :api_password, :api_secret, :to => :client
base_uri "https://api.chargify.com/api/v2"
end
# A sub-class of `V1` with this `Client`'s configuration.
def v1
@v1 ||= Class.new(V1).tap do |v1|
v1.client = self
v1.base_uri "https://#{subdomain}.chargify.com"
v1.basic_auth api_key, "x"
end
end
# A sub-class of `V2` with this `Client`'s configuration.
def v2
@v2 ||= Class.new(V2).tap do |v2|
v2.client = self
v2.basic_auth api_id, api_password
end
end
# Customers
def customers
v1.get("/customers")
end
def customer id
v1.get("/customers/#{id}")
end
def customer_lookup customer_attributes
v1.get("/customers/lookup", :query => customer_attributes)
end
def customer_by_reference reference
customer_lookup :reference => reference
end
def create_customer customer_attributes
v1.post("/customers", :body => {:customer => customer_attributes}.to_json)
end
def update_customer id, customer_attributes
v1.put("/customers/#{id}", :body => {:customer => customer_attributes}.to_json)
end
# Products
def products
v1.get("/products")
end
def product id
v1.get("/products/#{id}")
end
def product_by_handle handle
v1.get("/products/handle/#{handle}")
end
def create_product product_attributes
v1.post("/products", :body => {:product => product_attributes}.to_json)
end
def update_product id, product_attributes
v1.put("/products/#{id}", :body => {:product => product_attributes}.to_json)
end
# Subscriptions
def subscriptions
v1.get("/subscriptions")
end
def subscription id
v1.get("/subscription/#{id}")
end
def create_subscription subscription_attributes
v1.post("/subscriptions", :body => {:subscription => subscription_attributes}.to_json)
end
def update_subscription id, subscription_attributes
v1.put("/subscriptions/#{id}", :body => {:subscription => subscription_attributes}.to_json)
end
def cancel_subscription id, subscription_attributes
v1.delete("/subscriptions/#{id}", :body => {:subscription => subscription_attributes}.to_json)
end
def reactivate_subscription
v1.put("/subscriptions/#{id}/reactivate")
end
def charge_subscription id, charge_attributes={}
v1.put("/subscriptions/#{id}/charge", :body => {:charge => charge_attributes}.to_json)
end
# Pass an individual product id or a `Hash` (like `{:product_handle => "something"}`)
def migrate_subscription id, product_id_or_attributes
product_id_or_attributes = {:product_id => product_id_or_attributes} unless product_id_or_attributes.is_a? Hash
v1.put("/subscriptions/#{id}/migrations", :body => product_id_attributes.to_json)
end
def adjust_subscription id, adjustment_attributes
v1.put("/subscriptions/#{id}/adjustments", :body => {:adjustment => adjustment_attributes}.to_json)
end
# Direct
# Post URL for a Chargify Direct signup
def direct_signup_uri
"#{v2.base_uri}/signups"
end
# Post URL for a Chargify Direct card update
def direct_card_update_uri subscription_id
"#{v2.base_uri}/subscriptions/#{subscription_id}/card_update"
end
# Helper to calculate signature for Chargify Direct secure parameters.
# Adds `#to_hidden_fields` to the resulting hash for convenience in
# views when used from Rails, maintaining output safety.
def direct_params data={}
{
"secure[api_id]" => api_id,
"secure[nonce]" => nonce = rand(10 ** 30).to_s.rjust(30,'0'),
"secure[data]" => data.to_query,
"secure[signature]" => OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new('sha1'), api_secret, "#{api_id}#{nonce}#{data}"),
}.tap { |options| options.extend(HashToHiddenFields) if defined? Rails }
end
# Calls
# Fetches and verifies a response from a returned Chargify Direct request
def call id
Hashie::Mash.new(v2.get("calls/#{id}")).tap do |call|
raise "unverified response" unless response.signature == OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new('sha1'), api_secret, "#{api_id}#{response.timestamp}#{response.nonce}#{response.status_code}#{response.result_code}#{response.call_id}")
end
end
end
# +nodoc+
module HashToHiddenFields
def to_hidden_fields
Rails.application.send(:helper).safe_join(map do |name, value|
Rails.application.send(:helper).hidden_field_tag name, value
end)
end
end
end
@sj26
Copy link
Author

sj26 commented Dec 14, 2011

Created mainly out of frustration with existing gems being outdated, missing little bits of functionality, or using too many other gems. Mainly because chargify2 is too immature and I don't want to use multiple gems for talking to the same service but need to use chargify direct.

@warmwaffles
Copy link

do you have any example usages?

@sj26
Copy link
Author

sj26 commented Mar 31, 2012

In each rails environment configuration file (config/environments/development.rb for instance) I have some config:

MyRails::Application.configure do
  # Chargify for billing
  config.chargify = {
    # v1
    :subdomain => "chargify-subdomain",
    :api_key => "abc123",
    :shared_key => "abc123",

    # v2
    :api_id => "abc123",
    :api_password => "abc123",
    :api_secret => "abc123",
  }
end

Then it works just like any other HTTParty-based API:

customer = Chargify.create_customer reference: "mycustomer", first_name: "John", last_name: "Smith", email: "[email protected]"
Chargify.create_subscription product_handle: "myproduct", customer_id: customer.id

You could also create several instances of Chargify::Client instead of the default Chargify.client. I wrote a test harness for this as well, but haven't published that yet. Will probably release as a gem eventually.

@warmwaffles
Copy link

Will probably release as a gem eventually.

I would love to see this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment