From 9df2067a103b13c8ce8333a33b6ac5b0a3a1d499 Mon Sep 17 00:00:00 2001 From: Andrew Taber Date: Wed, 1 Oct 2014 13:05:19 -0700 Subject: [PATCH] First draft of file structure, port classes from Compete. --- .rspec | 2 + Gemfile | 2 + lib/rubill.rb | 26 ++++++++++ lib/rubill/base.rb | 63 ++++++++++++++++++++++++ lib/rubill/bill.rb | 15 ++++++ lib/rubill/customer.rb | 36 ++++++++++++++ lib/rubill/invoice.rb | 32 ++++++++++++ lib/rubill/session.rb | 108 +++++++++++++++++++++++++++++++++++++++++ rubill.gemspec | 16 ++++++ spec/spec_helper.rb | 8 +++ 10 files changed, 308 insertions(+) create mode 100644 .rspec create mode 100644 Gemfile create mode 100644 lib/rubill.rb create mode 100644 lib/rubill/base.rb create mode 100644 lib/rubill/bill.rb create mode 100644 lib/rubill/customer.rb create mode 100644 lib/rubill/invoice.rb create mode 100644 lib/rubill/session.rb create mode 100644 rubill.gemspec create mode 100644 spec/spec_helper.rb diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..16f9cdb --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--color +--format documentation diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..3be9c3c --- /dev/null +++ b/Gemfile @@ -0,0 +1,2 @@ +source "https://rubygems.org" +gemspec diff --git a/lib/rubill.rb b/lib/rubill.rb new file mode 100644 index 0000000..1376f45 --- /dev/null +++ b/lib/rubill.rb @@ -0,0 +1,26 @@ +module Rubill + class << self + attr_accessor :configuration + end + + def self.configure + self.configuration ||= Configuration.new + yield(configuration) + end + + class Configuration + attr_accessor :user_name + attr_accessor :password + attr_accessor :dev_key + attr_accessor :org_id + + def to_hash + { + "user_name" => user_name, + "password" => password, + "dev_key" => dev_key, + "org_id" => org_id, + } + end + end +end diff --git a/lib/rubill/base.rb b/lib/rubill/base.rb new file mode 100644 index 0000000..f823375 --- /dev/null +++ b/lib/rubill/base.rb @@ -0,0 +1,63 @@ +module Rubill + class Base + attr_accessor :remote_record + + delegate :[], to: :remote_record + + class NotFound < StandardError; end + + def initialize(remote) + self.remote_record = remote + end + + def self.active + # There is also a way to list only active via the API but it's opaque + # and unlikely to be much faster than doing it in Ruby + all_remote.select do |record| + record[:isActive] == "1" + end + end + + def id + remote_record[:id] + end + + def update + self.class.update(remote_record) + end + + def delete + self.class.delete(remote_record[:id]) + end + + def self.find(id) + new(session.read(remote_class_name, id)) + end + + def self.create(data) + new(session.create(remote_class_name, data.merge({entity: remote_class_name}))) + end + + def self.update(data) + session.update(remote_class_name, data) + end + + def self.delete(id) + session.delete(remote_class_name, id) + end + + def self.all + session.list(remote_class_name) + end + + def self.session + Session.instance + end + + private + + def self.remote_class_name + raise NotImplementedError + end + end +end diff --git a/lib/rubill/bill.rb b/lib/rubill/bill.rb new file mode 100644 index 0000000..3db91e9 --- /dev/null +++ b/lib/rubill/bill.rb @@ -0,0 +1,15 @@ +module Rubill + class Bill < Base + def self.send_payment(opts) + session.send_payment(opts) + end + + def self.void_sent_payment(id) + session.void_sent_payment(id) + end + + def self.remote_class_name + "Bill" + end + end +end diff --git a/lib/rubill/customer.rb b/lib/rubill/customer.rb new file mode 100644 index 0000000..de45b7d --- /dev/null +++ b/lib/rubill/customer.rb @@ -0,0 +1,36 @@ +module Rubill + class Customer < Base + def self.find_by_name(name) + record = active_remote.detect do |d| + d[:name] == name + end + + raise NotFound unless record + new(record) + end + + def self.receive_payment(opts) + session.receive_payment(opts) + end + + def self.void_received_payment(id) + session.void_received_payment(id) + end + + def create_credit(amount, description="") + data = { + customerId: remote_record.id, + amount: amount.to_f, + description: description, + paymentType: "5", + paymentDate: Date.today, + } + + self.class.receive_payment(data) + end + + def self.remote_class_name + "Customer" + end + end +end diff --git a/lib/rubill/invoice.rb b/lib/rubill/invoice.rb new file mode 100644 index 0000000..f31427a --- /dev/null +++ b/lib/rubill/invoice.rb @@ -0,0 +1,32 @@ +module Rubill + class Invoice < Base + def amount_paid + amount - amount_due + end + + def amount + # to_s then to_d because it returns a float + remote_record[:amount].to_s.to_d + end + + def amount_due + # to_s then to_d because it returns a float + remote_record[:amountDue].to_s.to_d + end + + def self.invoice_line_item(amount, description, item_id) + { + entity: "InvoiceLineItem", + quantity: 1, + itemId: item_id, + # must to_f amount otherwise decimal will be converted to string in JSON + price: amount.to_f, + description: description, + } + end + + def self.remote_class_name + "Invoice" + end + end +end diff --git a/lib/rubill/session.rb b/lib/rubill/session.rb new file mode 100644 index 0000000..c559408 --- /dev/null +++ b/lib/rubill/session.rb @@ -0,0 +1,108 @@ +module Rubill + class Session + include HTTParty + include Singleton + + base_uri "https://api.bill.com/api/v2" + + CREDENTIALS = Rubill.configuration.to_hash + + delegate :_post, to: self + + attr_accessor :session_id + + def initialize + login + end + + def read(entity, id) + _post("/Crud/Read/#{entity}.json", options(id: id)) + end + + def create(entity, object={}) + _post("/Crud/Create/#{entity}.json", options(obj: object)) + end + + def update(entity, object={}) + _post("/Crud/Update/#{entity}.json", options(obj: object)) + end + + def delete(entity, id) + _post("/Crud/Delete/#{entity}.json", options(id: id)) + end + + def receive_payment(opts={}) + _post("/RecordARPayment.json", options(opts)) + end + + def send_payment(opts={}) + _post("/RecordAPPayment.json", options(opts)) + end + + def void_sent_payment(id) + _post("/VoidAPPayment.json", options(sentPayId: id)) + end + + def void_received_payment(id) + _post("/VoidARPayment.json", options(id: id)) + end + + def list(entity) + # Note: this method returns ALL of desired entity, including inactive + result = [] + start = 0 + step = 999 + loop do + chunk = _post("/List/#{entity}.json", options(start: start, max: step)).presence + + if chunk + result += chunk + start += step + else + break + end + end + + result + end + + def login + self.session_id = self.class.login + end + + def self.login + login_options = { + headers: default_headers, + query: { + password: CREDENTIALS["password"], + userName: CREDENTIALS["user_name"], + devKey: CREDENTIALS["dev_key"], + orgId: CREDENTIALS["org_id"], + } + } + login = _post("/Login.json", login_options) + login[:sessionId] + end + + def options(data={}) + { + headers: self.class.default_headers, + query: { + sessionId: session_id, + devKey: CREDENTIALS["dev_key"], + data: data.to_json, + }, + } + end + + def self.default_headers + {"Content-Type" => "application/x-www-form-urlencoded"} + end + + def self._post(url, options) + result = JSON.parse(post(url, options).body).with_indifferent_access + raise result[:response_data][:error_message] unless result[:response_status] == 0 + result[:response_data] + end + end +end diff --git a/rubill.gemspec b/rubill.gemspec new file mode 100644 index 0000000..59b406e --- /dev/null +++ b/rubill.gemspec @@ -0,0 +1,16 @@ +Gem::Specification.new do |s| + s.name = 'rubill' + s.version = '0.0.1' + s.date = '2014-09-31' + s.summary = "Interface with Bill.com" + s.description = "A Ruby interface to Bill.com's API" + s.authors = ["Andrew Taber"] + s.email = 'andrew.e.taber@gmail.com ' + s.files = ["lib/rubill.rb"] + s.homepage = 'http://rubygems.org/gems/rubill' + s.license = 'MIT' + + s.add_dependency "httparty" + + s.add_development_dependency "rspec" +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..f8014a7 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,8 @@ +require "bundler/setup" +Bundler.setup + +require "rubill" + +RSpec.configure do |c| +end +