├── lib ├── recharge-api.rb ├── recharge │ ├── version.rb │ ├── tasks.rb │ ├── http_request.rb │ └── classes.rb └── recharge.rb ├── Gemfile ├── Rakefile ├── Changes ├── .github └── workflows │ └── ci.yml ├── spec ├── webhook_spec.rb ├── persistable_spec.rb ├── spec_helper.rb ├── discount_spec.rb ├── address_spec.rb ├── customer_spec.rb ├── subscription_spec.rb ├── order_spec.rb ├── charge_spec.rb └── http_request_spec.rb ├── LICENSE.txt ├── recharge-api.gemspec ├── .gitignore └── README.md /lib/recharge-api.rb: -------------------------------------------------------------------------------- 1 | require "recharge" 2 | -------------------------------------------------------------------------------- /lib/recharge/version.rb: -------------------------------------------------------------------------------- 1 | module Recharge 2 | VERSION = "0.0.3".freeze 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in recharge-api.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | 2020-11-27 v0.0.3 2 | -------------------- 3 | * Fix HTTP requests so that they're thread-safe 4 | * Fix Discount#value; it's a Float not an Integer 5 | 6 | 2020-05-05 v0.0.2 7 | -------------------- 8 | * Fix for HTTP 204 responses 9 | * Add support for Metafield 10 | * Add limited support for Product and Discount 11 | * Add additional properties to Subscription and Charge 12 | * Add support for setting the API key via the RECHARGE_API_KEY env var 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | ruby: ['3.2', '3.1', '3.0', '2.7', '2.6', '2.5', '2.4' ] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: ruby/setup-ruby@v1 17 | with: 18 | bundler-cache: true 19 | ruby-version: ${{ matrix.ruby }} 20 | 21 | - run: bundle install 22 | - run: bundle exec rake 23 | -------------------------------------------------------------------------------- /spec/webhook_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Recharge::Webhook do 4 | subject { described_class } 5 | 6 | it { is_expected.to be_a(Recharge::HTTPRequest::Create) } 7 | it { is_expected.to be_a(Recharge::HTTPRequest::Delete) } 8 | it { is_expected.to be_a(Recharge::HTTPRequest::Get) } 9 | it { is_expected.to be_a(Recharge::HTTPRequest::List) } 10 | it { is_expected.to be_a(Recharge::HTTPRequest::Update) } 11 | it { is_expected.to define_const("PATH").set_to("/webhooks") } 12 | 13 | describe ".new" do 14 | it "instantiates a webhook object with the given attributes" do 15 | data = { 16 | "id" => 1, 17 | "addresses" => "https://example.com", 18 | "topic" => "order/create" 19 | } 20 | end 21 | end 22 | 23 | context "an instance" do 24 | subject { described_class.new } 25 | it { is_expected.to be_a(Recharge::Persistable) } 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/persistable_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Recharge::Persistable do 4 | # Can't use Struct since we need to_h to return String keys 5 | class2 :foo => %w[id bar] do 6 | include Recharge::Persistable 7 | 8 | # See comment in classes.rb 9 | alias __og_to_h to_h 10 | def to_h 11 | __og_to_h.deep_stringify_keys! 12 | end 13 | end 14 | 15 | describe "#save" do 16 | context "given an instance without an id" do 17 | it "creates the record" do 18 | foo = Foo.new(:bar => "blah") 19 | 20 | expect(Foo).to receive(:create).with("bar" => "blah").and_return(instance_double("Foo", :id => 1)) 21 | foo.save 22 | 23 | expect(foo.id).to eq 1 24 | end 25 | end 26 | 27 | context "given an instance with an id" do 28 | it "updates the record" do 29 | foo = Foo.new(:id => 2, :bar => "blah") 30 | 31 | expect(Foo).to receive(:update).with(2, "bar" => "blah") 32 | foo.save 33 | 34 | expect(foo.id).to eq 2 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 ScreenStaring 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 2 | require "recharge" 3 | require "json" 4 | 5 | require "rspec" 6 | require "time" 7 | require "webmock/rspec" 8 | 9 | Recharge.api_key = "XXX" 10 | TIME_INSTANCES = [Time.now, Date.today, DateTime.now].freeze 11 | 12 | RSpec.configure do |c| 13 | c.include Module.new { 14 | def format_time(t) 15 | t.strftime("%Y-%m-%dT%H:%M:%S") 16 | end 17 | } 18 | end 19 | 20 | RSpec.shared_examples_for "a method that requires an id" do 21 | it "requires an id argument" do 22 | [nil, ""].each do |arg| 23 | expect { described_class.get(arg) }.to raise_error(ArgumentError, "id required") 24 | end 25 | end 26 | end 27 | 28 | RSpec::Matchers.define :define_const do |name| 29 | match do |klass| 30 | begin 31 | const = klass.const_get(name) 32 | value ? const == value : true 33 | rescue NameError 34 | false 35 | end 36 | end 37 | 38 | chain :set_to, :value 39 | 40 | failure_message do |actual| 41 | msg = "expected class #{actual} to define const #{expected}" 42 | msg << " with value '#{value}'" if value 43 | msg 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/recharge.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "recharge/classes" 4 | require "recharge/version" 5 | 6 | module Recharge 7 | ENDPOINT = "api.rechargeapps.com".freeze 8 | PORT = 443 9 | TOKEN_HEADER = "X-Recharge-Access-Token".freeze 10 | VERSION_HEADER = "X-Recharge-Version" 11 | USER_AGENT = "ReCharge API Client v#{VERSION} (Ruby v#{RUBY_VERSION})" 12 | 13 | Error = Class.new(StandardError) 14 | ConnectionError = Class.new(Error) 15 | 16 | # 17 | # Raised when a non-2XX HTTP response is returned or a response with 18 | # an error or warning property 19 | # 20 | class RequestError < Error 21 | attr_accessor :errors 22 | attr_accessor :status 23 | attr_accessor :meta 24 | 25 | def initialize(message, status, meta = nil, errors = nil) 26 | super message 27 | @status = status 28 | @meta = meta || {} 29 | @errors = errors || {} 30 | end 31 | end 32 | 33 | class << self 34 | attr_accessor :api_key 35 | # Defaults to your account's API settings 36 | attr_accessor :api_version 37 | # If +true+ output HTTP request/response to stderr. Can also be an +IO+ instance to output to. 38 | attr_accessor :debug 39 | end 40 | end 41 | 42 | ReCharge = Recharge 43 | -------------------------------------------------------------------------------- /spec/discount_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Recharge::Discount do 4 | subject { described_class } 5 | 6 | it { is_expected.to be_a(Recharge::HTTPRequest::Create) } 7 | it { is_expected.to define_const("PATH").set_to("/discounts") } 8 | it { is_expected.to define_const("SINGLE").set_to("discount") } 9 | 10 | describe ".new" do 11 | it "instantiates an discount object with the given attributes" do 12 | data = { 13 | "id" => 1, 14 | "code" => "X99", 15 | "created_at" => "2018-12-31", 16 | "value" => 99, 17 | "applies_to_id" => 6, 18 | "applies_to_product_type" => "foo", 19 | "discount_type" => "percentage", 20 | "applies_to" => 132, 21 | "applies_to_resource" => "Foo", 22 | "times_used" => 0, 23 | "duration" => "10 days", 24 | "once_per_customer" => false, 25 | "starts_at" => "2019-01-01", 26 | "ends_at" => "2019-01-11", 27 | "duration_usage_limit" => nil, 28 | "status" => nil, 29 | "updated_at" => nil, 30 | "usage_limit" => nil 31 | } 32 | 33 | discount = described_class.new(data) 34 | expect(discount.to_h).to eq data 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/address_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Recharge::Address do 4 | subject { described_class } 5 | 6 | it { is_expected.to be_a(Recharge::HTTPRequest::Get) } 7 | it { is_expected.to be_a(Recharge::HTTPRequest::Update) } 8 | it { is_expected.to define_const("PATH").set_to("/addresses") } 9 | it { is_expected.to define_const("SINGLE").set_to("address") } 10 | it { is_expected.to define_const("COLLECTION").set_to("addresses") } 11 | 12 | describe ".new" do 13 | it "instantiates an address object with the given attributes" do 14 | data = { 15 | "id" => 1, 16 | "customer_id" => 2, 17 | "address_id" => 3, 18 | "charge_id" => 4, 19 | } 20 | end 21 | end 22 | 23 | describe ".validate" do 24 | it "makes a POST request to /addresses/validate with the given params" do 25 | data = { :city => "Terminal Land" } 26 | expect(described_class).to receive(:POST).with("/addresses/validate", data) 27 | described_class.validate(data) 28 | end 29 | end 30 | 31 | describe "#save" do 32 | it "updates the record" do 33 | address = described_class.new(:id => 1, :city => "Terminal Land") 34 | data = address.to_h 35 | data.delete("id") 36 | 37 | expect(described_class).to receive(:update).with(1, data) 38 | address.save 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /recharge-api.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "recharge/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "recharge-api" 8 | spec.version = Recharge::VERSION 9 | spec.authors = ["Skye Shaw"] 10 | spec.email = ["skye.shaw@gmail.com"] 11 | 12 | spec.summary = %q{Client for ReCharge Payments API} 13 | spec.description = %q{Client for ReCharge Payments recurring payments API for Shopify} 14 | spec.homepage = "https://github.com/ScreenStaring/recharge-api" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = "exe" 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ["lib"] 21 | spec.metadata = { 22 | "changelog_uri" => "https://github.com/ScreenStaring/recharge-api/blob/master/Changes", 23 | "bug_tracker_uri" => "https://github.com/ScreenStaring/recharge-api/issues", 24 | "documentation_uri" => "http://rdoc.info/gems/recharge-api", 25 | "source_code_uri" => "https://github.com/ScreenStaring/recharge-api", 26 | } 27 | 28 | spec.add_dependency "class2", "~> 0.5.0" 29 | # Need this temporarily for deep_stringify_keys! until we break to_h String key return value 30 | spec.add_dependency "activesupport", "< 8" 31 | spec.add_development_dependency "webmock", "~> 3.0" 32 | spec.add_development_dependency "bundler" 33 | spec.add_development_dependency "rake" 34 | spec.add_development_dependency "rspec", "~> 3.0" 35 | end 36 | -------------------------------------------------------------------------------- /lib/recharge/tasks.rb: -------------------------------------------------------------------------------- 1 | require "rake" 2 | require "recharge" 3 | 4 | namespace :recharge do 5 | namespace :hooks do 6 | task :_setup_recharge do 7 | abort "RECHARGE_API_KEY required" unless ENV["RECHARGE_API_KEY"] 8 | ReCharge.api_key = ENV["RECHARGE_API_KEY"] 9 | ReCharge.api_version = ENV["RECHARGE_API_VERSION"] if ENV["RECHARGE_API_VERSION"] 10 | end 11 | 12 | desc "List webhooks for RECHARGE_API_KEY" 13 | task :list => :_setup_recharge do 14 | format = "%-5s %-25s %-80s\n" 15 | printf format, "ID", "Hook", "URL" 16 | 17 | Recharge::Webhook.list.each do |hook| 18 | printf format, hook.id, hook.topic, hook.address 19 | end 20 | end 21 | 22 | desc "Delete webhooks for RECHARGE_API_KEY" 23 | task :delete_all => :_setup_recharge do 24 | Recharge::Webhook.list.each { |hook| hook.class.delete(hook.id) } 25 | end 26 | 27 | desc "Delete the webhooks given by the ID(s) in ID for RECHARGE_API_KEY" 28 | task :delete => :_setup_recharge do 29 | ids = ENV["ID"].to_s.strip.split(",") 30 | abort "ID required" unless ids.any? 31 | 32 | ids.each do |id| 33 | puts "Deleting webhook #{id}" 34 | Recharge::Webhook.delete(id) 35 | end 36 | end 37 | 38 | desc "Create webhook HOOK to be sent to CALLBACK for RECHARGE_API_KEY" 39 | task :create => :_setup_recharge do 40 | known_hooks = %w[ 41 | subscription/created 42 | subscription/updated 43 | subscription/activated 44 | subscription/cancelled 45 | customer/created 46 | customer/updated 47 | order/created 48 | charge/created 49 | charge/paid 50 | ] 51 | 52 | abort "CALLBACK required" unless ENV["CALLBACK"] 53 | abort "HOOK required" unless ENV["HOOK"] 54 | abort "unknown hook #{ENV["HOOK"]}" unless known_hooks.include?(ENV["HOOK"]) 55 | 56 | puts "Creating webhook #{ENV["HOOK"]} for #{ENV["CALLBACK"]}" 57 | 58 | hook = Recharge::Webhook.create( 59 | :topic => ENV["HOOK"], 60 | :address => ENV["CALLBACK"] 61 | ) 62 | 63 | puts "Created hook ##{hook.id}" 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/emacs,ruby 3 | 4 | ### Emacs ### 5 | # -*- mode: gitignore; -*- 6 | *~ 7 | \#*\# 8 | /.emacs.desktop 9 | /.emacs.desktop.lock 10 | *.elc 11 | auto-save-list 12 | tramp 13 | .\#* 14 | 15 | # Org-mode 16 | .org-id-locations 17 | *_archive 18 | 19 | # flymake-mode 20 | *_flymake.* 21 | 22 | # eshell files 23 | /eshell/history 24 | /eshell/lastdir 25 | 26 | # elpa packages 27 | /elpa/ 28 | 29 | # reftex files 30 | *.rel 31 | 32 | # AUCTeX auto folder 33 | /auto/ 34 | 35 | # cask packages 36 | .cask/ 37 | dist/ 38 | 39 | # Flycheck 40 | flycheck_*.el 41 | 42 | # server auth directory 43 | /server/ 44 | 45 | # projectiles files 46 | .projectile 47 | projectile-bookmarks.eld 48 | 49 | # directory configuration 50 | .dir-locals.el 51 | 52 | # saveplace 53 | places 54 | 55 | # url cache 56 | url/cache/ 57 | 58 | # cedet 59 | ede-projects.el 60 | 61 | # smex 62 | smex-items 63 | 64 | # company-statistics 65 | company-statistics-cache.el 66 | 67 | # anaconda-mode 68 | anaconda-mode/ 69 | 70 | ### Ruby ### 71 | *.gem 72 | *.rbc 73 | /.config 74 | /coverage/ 75 | /InstalledFiles 76 | /pkg/ 77 | /spec/reports/ 78 | /spec/examples.txt 79 | /test/tmp/ 80 | /test/version_tmp/ 81 | /tmp/ 82 | 83 | # Used by dotenv library to load environment variables. 84 | # .env 85 | 86 | ## Specific to RubyMotion: 87 | .dat* 88 | .repl_history 89 | build/ 90 | *.bridgesupport 91 | build-iPhoneOS/ 92 | build-iPhoneSimulator/ 93 | 94 | ## Specific to RubyMotion (use of CocoaPods): 95 | # 96 | # We recommend against adding the Pods directory to your .gitignore. However 97 | # you should judge for yourself, the pros and cons are mentioned at: 98 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 99 | # 100 | # vendor/Pods/ 101 | 102 | ## Documentation cache and generated files: 103 | /.yardoc/ 104 | /_yardoc/ 105 | /doc/ 106 | /rdoc/ 107 | 108 | ## Environment normalization: 109 | /.bundle/ 110 | /vendor/bundle 111 | /lib/bundler/man/ 112 | 113 | # for a library or gem, you might want to ignore these files since the code is 114 | # intended to run in multiple environments; otherwise, check them in: 115 | Gemfile.lock 116 | # .ruby-version 117 | # .ruby-gemset 118 | 119 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 120 | .rvmrc 121 | 122 | .env* 123 | 124 | # End of https://www.gitignore.io/api/emacs,ruby 125 | -------------------------------------------------------------------------------- /spec/customer_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Recharge::Customer do 4 | subject { described_class } 5 | 6 | it { is_expected.to be_a(Recharge::HTTPRequest::Create) } 7 | it { is_expected.to be_a(Recharge::HTTPRequest::Get) } 8 | it { is_expected.to be_a(Recharge::HTTPRequest::Update) } 9 | it { is_expected.to be_a(Recharge::HTTPRequest::List) } 10 | it { is_expected.to be_a(Recharge::HTTPRequest::Count) } 11 | 12 | it { is_expected.to define_const("PATH").set_to("/customers") } 13 | it { is_expected.to define_const("SINGLE").set_to("customer") } 14 | it { is_expected.to define_const("COLLECTION").set_to("customers") } 15 | 16 | describe ".new" do 17 | it "instantiates a customer object with the given attributes" do 18 | data = { 19 | "id" => 1, 20 | "hash" => "X123", 21 | "shopify_customer_id" => "Y999", 22 | "email" => "sshaw@screenstaring.com", 23 | "created_at" => "2018-01-10T11:00:00", 24 | "updated_at" => "2017-01-11T13:16:19", 25 | "first_name" => "Mike", 26 | "last_name" => "Flynn", 27 | "billing_first_name" => "S", 28 | "billing_last_name" => "SS", 29 | "billing_company" => "Company", 30 | "billing_address1" => "Address", 31 | "billing_address2" => "Address2", 32 | "billing_zip" => "90210", 33 | "billing_city" => "LA", 34 | "billing_province" => "CA", 35 | "billing_country" => "USA", 36 | "billing_phone" => "5555551213", 37 | "processor_type" => "stripe", 38 | "status" => "X", 39 | "stripe_customer_token" => "stripetok", 40 | "paypal_customer_token" => "pptok", 41 | "braintree_customer_token" => "bttok", 42 | "external_customer_id" => { "ecommerce" => "FooFoo" } 43 | } 44 | 45 | sub = described_class.new(data) 46 | expect(sub.to_h).to eq data 47 | end 48 | end 49 | 50 | describe ".create_address" do 51 | it_behaves_like "a method that requires an id" 52 | 53 | it "makes a POST request to the customer's address end endpoint with the given data" do 54 | id = 1 55 | address = Recharge::Address.new("customer_id" => id) 56 | expect(described_class).to receive(:POST) 57 | .with("/customers/#{id}/addresses", address.to_h) 58 | .and_return("address" => { "customer_id" => id }) 59 | 60 | expect(described_class.create_address(id, address.to_h)).to eq address 61 | end 62 | end 63 | 64 | describe ".addresses" do 65 | it_behaves_like "a method that requires an id" 66 | 67 | it "makes a GET request to the customer's address endpoint for the given id" do 68 | id = 1 69 | address = Recharge::Address.new("id" => id) 70 | expect(described_class).to receive(:GET) 71 | .with("/customers/#{id}/addresses") 72 | .and_return("addresses" => [address.to_h]) 73 | 74 | expect(described_class.addresses(id)).to eq [address] 75 | end 76 | end 77 | 78 | context "an instance" do 79 | subject { described_class.new } 80 | it { is_expected.to be_a(Recharge::Persistable) } 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReCharge API Client 2 | 3 | [![Build Status](https://github.com/ScreenStaring/recharge-api/workflows/CI/badge.svg)](https://github.com/ScreenStaring/recharge-api/actions/workflows/ci.yml) 4 | 5 | Ruby client for [ReCharge Payments'](https://rechargepayments.com/developers) 6 | recurring payments API for Shopify. 7 | 8 | ## Installation 9 | 10 | Ruby gems: 11 | 12 | gem install recharge-api 13 | 14 | Bundler: 15 | 16 | gem "recharge-api", :require => "recharge" 17 | 18 | ## Usage 19 | 20 | An API key is required. The key can be set via `ReCharge.api_key` or via the `RECHARGE_API_KEY` 21 | environment variable. 22 | 23 | ```rb 24 | require "recharge" 25 | 26 | ReCharge.api_key = "YOUR_KEY" # Can also use Recharge 27 | data = { 28 | :address_id => 123234321, 29 | :customer_id => 565728, 30 | # ... more stuff 31 | :next_charge_scheduled_at => Time.new, 32 | :properties => { 33 | :name => "size", 34 | :value => "medium" 35 | } 36 | } 37 | 38 | subscription = ReCharge::Subscription.create(data) 39 | subscription.address_id = 454343 40 | subscription.save 41 | 42 | # Or 43 | ReCharge::Subscription.update(id, data) 44 | 45 | subscription = ReCharge::Subscription.new(data) 46 | subscription.save 47 | 48 | order1 = ReCharge::Order.get(123123) 49 | order1.line_items.each do |li| 50 | p li.title 51 | p li.quantity 52 | end 53 | 54 | order2 = ReCharge::Order.get(453321) 55 | p "Different" if order1 != order2 56 | 57 | JSON.dump(order2.to_h) 58 | 59 | customers = ReCharge::Customer.list(:page => 10, :limit => 50) 60 | customers.each do |customer| 61 | addresses = ReCharge::Customer.addresses(customer.id) 62 | # ... 63 | end 64 | ``` 65 | 66 | For complete documentation refer to the API docs: http://rdoc.info/gems/recharge-api 67 | 68 | ### Setting the ReCharge API Version 69 | 70 | Defaults to your account's API settings but can be overridden via: 71 | 72 | ```rb 73 | ReCharge.api_version = "2021-01" 74 | ``` 75 | 76 | ## Rake Tasks for Webhook Management 77 | 78 | Add the following to your `Rakefile`: 79 | 80 | ```rb 81 | require "recharge/tasks" 82 | ``` 83 | 84 | This will add the following tasks: 85 | 86 | * `recharge:hook:create` - create webhook `HOOK` to be sent to `CALLBACK` 87 | * `recharge:hooks:delete` - delete the webhook(s) given by `ID` 88 | * `recharge:hooks:delete_all` - delete all webhooks 89 | * `recharge:hooks:list` - list webhooks 90 | 91 | All tasks require `RECHARGE_API_KEY` be set. 92 | 93 | For example, to create a hook run the following: 94 | 95 | ``` 96 | rake recharge:hooks:create RECHARGE_API_KEY=YOURKEY HOOK=subscription/created CALLBACK=https://example.com/callback 97 | ``` 98 | 99 | You can set the API version via `RECHARGE_API_VERSION`. 100 | 101 | ## See Also 102 | 103 | - [Shopify Development Tools](https://github.com/ScreenStaring/shopify-dev-tools) - Assists with the development and/or maintenance of Shopify apps and stores 104 | - [Shopify ID Export](https://github.com/ScreenStaring/shopify_id_export/) - Dump Shopify product and variant IDs —along with other identifiers— to a CSV or JSON file 105 | - [`ShopifyAPI::GraphQL::Tiny`](https://github.com/ScreenStaring/shopify_api-graphql-tiny) - Lightweight, no-nonsense, Shopify GraphQL Admin API client with built-in retry 106 | - [Shopify API Retry](https://github.com/ScreenStaring/shopify_api_retry) - retry requests if rate-limited or other errors occur. Works with the REST and GraphQL APIs. 107 | 108 | ## License 109 | 110 | Released under the MIT License: www.opensource.org/licenses/MIT 111 | 112 | --- 113 | 114 | Made by [ScreenStaring](http://screenstaring.com) 115 | -------------------------------------------------------------------------------- /spec/subscription_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Recharge::Subscription do 4 | subject { described_class } 5 | 6 | it { is_expected.to be_a(Recharge::HTTPRequest::Create) } 7 | it { is_expected.to be_a(Recharge::HTTPRequest::Get) } 8 | it { is_expected.to be_a(Recharge::HTTPRequest::Update) } 9 | it { is_expected.to be_a(Recharge::HTTPRequest::List) } 10 | 11 | it { is_expected.to define_const("PATH").set_to("/subscriptions") } 12 | it { is_expected.to define_const("SINGLE").set_to("subscription") } 13 | it { is_expected.to define_const("COLLECTION").set_to("subscriptions") } 14 | 15 | describe ".activate" do 16 | it "makes a POST request to activate with the given subscription id" do 17 | sub = described_class.new(:id => 1) 18 | expect(described_class).to receive(:POST) 19 | .with("/subscriptions/#{sub.id}/activate", :status => "active") 20 | .and_return("subscription" => { "id" => sub.id }) 21 | 22 | expect(described_class.activate(sub.id)).to eq sub 23 | end 24 | end 25 | 26 | describe ".cancel" do 27 | it "makes a POST request to cancel with the given subscription id and reason" do 28 | sub = described_class.new(:id => 1) 29 | expect(described_class).to receive(:POST) 30 | .with("/subscriptions/#{sub.id}/cancel", :cancellation_reason => "spite") 31 | .and_return("subscription" => { "id" => sub.id }) 32 | 33 | expect(described_class.cancel(sub.id, "spite")).to eq sub 34 | end 35 | end 36 | 37 | describe ".set_next_charge_date" do 38 | it "makes a POST request to set_next_charge_date with the given subscription id and date" do 39 | sub = described_class.new(:id => 1) 40 | time = "2017-01-01T00:00" 41 | 42 | expect(described_class).to receive(:POST) 43 | .with("/subscriptions/#{sub.id}/set_next_charge_date", :date => time) 44 | .and_return("subscription" => { "id" => sub.id }) 45 | 46 | 47 | expect(described_class.set_next_charge_date(sub.id, time)).to eq sub 48 | end 49 | 50 | it "converts date/time instances" do 51 | id = 1 52 | TIME_INSTANCES.each do |time| 53 | expect(described_class).to receive(:POST) 54 | .with("/subscriptions/#{id}/set_next_charge_date", :date => format_time(time)) 55 | .and_return("subscription" => { "id" => id }) 56 | 57 | described_class.set_next_charge_date(id, time) 58 | end 59 | end 60 | end 61 | 62 | describe ".list" do 63 | %i[created_at created_at_max updated_at updated_at_max].each do |param| 64 | it "filters on #{param}" do 65 | sub = described_class.new(:id => 1) 66 | 67 | TIME_INSTANCES.each do |time| 68 | expect(described_class).to receive(:GET) 69 | .with("/subscriptions", param => format_time(time)) 70 | .and_return("subscriptions" => ["id" => 1]) 71 | 72 | expect(described_class.list(param => time)).to eq [ sub ] 73 | 74 | 75 | expect(described_class).to receive(:GET) 76 | .with("/subscriptions", param => format_time(time)) 77 | .and_return("subscriptions" => ["id" => 1]) 78 | 79 | expect(described_class.list(param => format_time(time))).to eq [ sub ] 80 | end 81 | end 82 | end 83 | end 84 | 85 | context "an instance" do 86 | subject { described_class.new } 87 | it { is_expected.to be_a(Recharge::Persistable) } 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /spec/order_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Recharge::Order do 4 | subject { described_class } 5 | 6 | it { is_expected.to be_a(Recharge::HTTPRequest::Get) } 7 | it { is_expected.to be_a(Recharge::HTTPRequest::Count) } 8 | it { is_expected.to be_a(Recharge::HTTPRequest::List) } 9 | it { is_expected.to define_const("COLLECTION").set_to("orders") } 10 | it { is_expected.to define_const("SINGLE").set_to("order") } 11 | it { is_expected.to define_const("PATH").set_to("/orders") } 12 | 13 | describe ".new" do 14 | it "instantiates an order object with the given attributes" do 15 | data = { 16 | "id" => 1, 17 | "customer_id" => 2, 18 | "address_id" => 3, 19 | "charge_id" => 4, 20 | "transaction_id" => "TID", 21 | "shopify_order_id" => "5", 22 | "shopify_order_number" => 6, 23 | "created_at" => "2017-01-01", 24 | "updated_at" => "2017-01-02", 25 | "scheduled_at" => "2017-01-02", 26 | "processed_at" => "2017-01-03", 27 | "status" => "SUCCESS", 28 | "charge_status" => "SUCCESS", 29 | "type" => "CHECKOUT", 30 | "first_name" => "sshaw", 31 | "last_name" => "X", 32 | "email" => "s@example.com", 33 | "payment_processor" => "4 much", 34 | "address_is_active" => 1, 35 | "is_prepaid" => false, 36 | "line_items" => [], 37 | "total_price" => 100.0, 38 | "shipping_address" => [], 39 | "billing_address" => [], 40 | # 2021-11 API 41 | "note" => "Noted", 42 | "customer" => {} 43 | } 44 | 45 | order = described_class.new(data) 46 | expect(order.to_h).to eq data 47 | end 48 | end 49 | 50 | describe ".get" do 51 | it_behaves_like "a method that requires an id" 52 | 53 | it "makes a GET request to the orders endpoint for the given id" do 54 | id = 123 55 | order = described_class.new(:id => id) 56 | expect(described_class).to receive(:GET) 57 | .with("/orders/#{id}") 58 | .and_return("order" => { "id" => id }) 59 | 60 | expect(described_class.get(id)).to eq order 61 | end 62 | end 63 | 64 | describe ".update_shopify_variant" do 65 | it "makes a POST request to update_shopify_variant" do 66 | old_variant_id = 999 67 | new_variant_id = 123 68 | order = described_class.new(:id => 123) 69 | 70 | expect(described_class).to receive(:POST) 71 | .with("/orders/#{order.id}/update_shopify_variant/#{old_variant_id}", :new_shopify_variant_id => 123) 72 | .and_return("order" => { "id" => new_variant_id }) 73 | 74 | 75 | expect(described_class.update_shopify_variant(order.id, old_variant_id, new_variant_id)).to eq described_class.new(:id => 123) 76 | end 77 | end 78 | 79 | describe ".change_date" do 80 | it "makes a POST request to change_date with the given order id" do 81 | order = described_class.new(:id => 1) 82 | time = Time.new 83 | 84 | expect(described_class).to receive(:POST) 85 | .with("/orders/#{order.id}/change_date", :shipping_date => format_time(time)) 86 | .and_return("order" => { "id" => 1 }) 87 | 88 | 89 | expect(described_class.change_date(order.id, time)).to eq order 90 | end 91 | end 92 | 93 | describe ".count" do 94 | before do 95 | @path = "/orders/count" 96 | @retval = { "count" => 1 } 97 | end 98 | 99 | it "makes a GET request to count and returns the count" do 100 | expect(described_class).to receive(:GET) 101 | .with(@path, nil) 102 | .and_return(@retval) 103 | 104 | expect(described_class.count).to eq 1 105 | end 106 | 107 | %i[created_at_max created_at_min date_min date_max].each do |param| 108 | it "filters the count on #{param}" do 109 | time = Time.now 110 | 111 | expect(described_class).to receive(:GET) 112 | .with(@path, param => format_time(time)) 113 | .and_return(@retval).twice 114 | 115 | expect(described_class.count(param => time)).to eq 1 116 | expect(described_class.count(param => format_time(time))).to eq 1 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/recharge/http_request.rb: -------------------------------------------------------------------------------- 1 | require "net/http" 2 | require "uri" 3 | require "json" 4 | 5 | # For iso8601 6 | #require "date" 7 | #require "time" 8 | 9 | module Recharge 10 | module HTTPRequest # :nodoc: 11 | 12 | protected 13 | 14 | def DELETE(path, data = {}) 15 | request(Net::HTTP::Delete.new(path), data) 16 | end 17 | 18 | def GET(path, data = {}) 19 | path += sprintf("?%s", URI.encode_www_form(data)) if data && data.any? 20 | request(Net::HTTP::Get.new(path)) 21 | end 22 | 23 | def POST(path, data = {}) 24 | request(Net::HTTP::Post.new(path), data) 25 | end 26 | 27 | def PUT(path, data = {}) 28 | request(Net::HTTP::Put.new(path), data) 29 | end 30 | 31 | def id_required!(id) 32 | raise ArgumentError, "id required" if id.nil? || id.to_s.strip.empty? 33 | end 34 | 35 | def join(*parts) 36 | parts.unshift(self::PATH).join("/") 37 | end 38 | 39 | def convert_date_params(options, *keys) 40 | return unless options 41 | 42 | options = options.dup 43 | keys.each do |key| 44 | options[key] = date_param(options[key]) if options.include?(key) 45 | end 46 | 47 | options 48 | end 49 | 50 | def date_param(date) 51 | # ReCharge doesn't accept 8601, 500s on zone specifier 52 | # date.respond_to?(:iso8601) ? date.iso8601 : date 53 | date.respond_to?(:strftime) ? date.strftime("%Y-%m-%dT%H:%M:%S") : date 54 | end 55 | 56 | private 57 | 58 | def request(req, data = {}) 59 | req[TOKEN_HEADER] = ReCharge.api_key || ENV["RECHARGE_API_KEY"] || "" 60 | req[VERSION_HEADER] = ReCharge.api_version if ReCharge.api_version 61 | req["User-Agent"] = USER_AGENT 62 | 63 | if req.request_body_permitted? && data && data.any? 64 | req.body = data.to_json 65 | req["Content-Type"] = "application/json" 66 | end 67 | 68 | request = Net::HTTP.new(ENDPOINT, PORT) 69 | request.use_ssl = true 70 | 71 | if !Recharge.debug 72 | request.set_debug_output(nil) 73 | else 74 | request.set_debug_output( 75 | Recharge.debug.is_a?(IO) ? Recharge.debug : $stderr 76 | ) 77 | end 78 | 79 | request.start do |http| 80 | res = http.request(req) 81 | # API returns 204 but content-type header is set to application/json so check body 82 | data = res.body && res["Content-Type"] == "application/json" ? parse_json(res.body) : {} 83 | data["meta"] = { "id" => res["X-Request-Id"], "limit" => res["X-Recharge-Limit"] } 84 | 85 | return data if res.code[0] == "2" && !data["warning"] && !data["error"] 86 | 87 | message = data["warning"] || data["error"] || "#{res.code} - #{res.message}" 88 | raise RequestError.new(message, res.code, data["meta"], data["errors"]) 89 | end 90 | rescue Net::ReadTimeout, IOError, SocketError, SystemCallError => e 91 | raise ConnectionError, "connection failure: #{e}" 92 | end 93 | 94 | def parse_json(s) 95 | JSON.parse(s) 96 | rescue JSON::ParserError => e 97 | raise Error, "failed to parse JSON response: #{e}" 98 | end 99 | 100 | # 101 | # Make a count request to the included/extended class' endpoint 102 | # 103 | # === Arguments 104 | # 105 | # [options (Hash)] Optional arguments to filter the count on. 106 | # 107 | # See the appropriate count call in {ReCharge's documentation}[https://developer.rechargepayments.com/] for valid options. 108 | # 109 | # === Returns 110 | # 111 | # +Fixnum+ of the resulting count 112 | # 113 | # === Errors 114 | # 115 | # Recharge::ConnectionError, Recharge::RequestError 116 | # 117 | module Count 118 | include HTTPRequest 119 | 120 | def count(options = nil) 121 | GET(join("count"), options)["count"] 122 | end 123 | end 124 | 125 | # 126 | # Create an new record for the included/extended entity 127 | # 128 | # === Arguments 129 | # 130 | # [data (Hash)] New record's attributes 131 | # 132 | # See the appropriate create call in {ReCharge's documentation}[https://developer.rechargepayments.com/] for valid attributes 133 | # 134 | # === Returns 135 | # 136 | # An instance of the created entity 137 | # 138 | # === Errors 139 | # 140 | # Recharge::ConnectionError, Recharge::RequestError 141 | # 142 | module Create 143 | include HTTPRequest 144 | 145 | def create(data) 146 | new(POST(self::PATH, data)[self::SINGLE]) 147 | end 148 | end 149 | 150 | # 151 | # Delete a record for included/extended entity 152 | # 153 | # === Arguments 154 | # 155 | # [id (Fixnum)] ID of the record to delete 156 | # 157 | # === Returns 158 | # 159 | # An instance of the deleted entity 160 | # 161 | # === Errors 162 | # 163 | # Recharge::ConnectionError, Recharge::RequestError 164 | # 165 | module Delete 166 | include HTTPRequest 167 | 168 | def delete(id) 169 | id_required!(id) 170 | DELETE(join(id)) 171 | end 172 | end 173 | 174 | module Get 175 | include HTTPRequest 176 | 177 | def get(id) 178 | id_required!(id) 179 | new(GET(join(id))[self::SINGLE]) 180 | end 181 | end 182 | 183 | # 184 | # Retrieve a page of records for the included/extended class' endpoint 185 | # 186 | # === Arguments 187 | # 188 | # [options (Hash)] Optional arguments to filter the result set on. 189 | # 190 | # In most cases +:page+ and +:limit+ are accepted but see 191 | # the appropriate call in {ReCharge's documentation}[https://developer.rechargepayments.com/] for valid options. 192 | # 193 | # === Returns 194 | # 195 | # +Array+ of record instances 196 | # 197 | # === Errors 198 | # 199 | # Recharge::ConnectionError, Recharge::RequestError 200 | # 201 | module List 202 | include HTTPRequest 203 | 204 | def list(options = nil) 205 | data = GET(self::PATH, options) 206 | (data[self::COLLECTION] || []).map { |d| new(d.merge("meta" => data["meta"])) } 207 | end 208 | end 209 | 210 | module Update 211 | include HTTPRequest 212 | 213 | def update(id, data) 214 | id_required!(id) 215 | new(PUT(join(id), data)[self::SINGLE]) 216 | end 217 | end 218 | end 219 | end 220 | -------------------------------------------------------------------------------- /spec/charge_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Recharge::Charge do 4 | subject { described_class } 5 | 6 | it { is_expected.to be_a(Recharge::HTTPRequest::Get) } 7 | it { is_expected.to be_a(Recharge::HTTPRequest::Count) } 8 | it { is_expected.to be_a(Recharge::HTTPRequest::List) } 9 | 10 | it { is_expected.to define_const("PATH").set_to("/charges") } 11 | it { is_expected.to define_const("SINGLE").set_to("charge") } 12 | it { is_expected.to define_const("COLLECTION").set_to("charges") } 13 | 14 | describe ".skip" do 15 | it "makes a POST request to the skip endpoint with the given id and subscription id" do 16 | sub_id = 999 17 | charge = described_class.new(:id => 123) 18 | 19 | expect(described_class).to receive(:POST) 20 | .with("/charges/#{charge.id}/skip", :subscription_id => sub_id) 21 | .and_return("charge" => { "id" => charge.id }) 22 | 23 | expect(described_class.skip(charge.id, sub_id)).to eq charge 24 | end 25 | end 26 | 27 | describe ".change_next_charge_date" do 28 | it "makes a POST request to the change_next_charge_date endpoint with the given id and date" do 29 | date = "2017-01-01" 30 | charge = described_class.new(:id => 123) 31 | 32 | expect(described_class).to receive(:POST) 33 | .with("/charges/#{charge.id}/change_next_charge_date", :next_charge_date => date) 34 | .and_return("charge" => { "id" => charge.id }) 35 | 36 | expect(described_class.change_next_charge_date(charge.id, date)).to eq charge 37 | end 38 | 39 | it "converts date/time instances" do 40 | TIME_INSTANCES.each do |time| 41 | expect(described_class).to receive(:POST) 42 | .with("/charges/1/change_next_charge_date", :next_charge_date => format_time(time)) 43 | .and_return("charge" => { "id" => 1 }) 44 | 45 | described_class.change_next_charge_date(1, time) 46 | end 47 | end 48 | end 49 | 50 | describe ".list" do 51 | %i[date_min date_max].each do |param| 52 | it "filters on #{param}" do 53 | # set has_uncommited_changes as charge.to_h => { "has_uncommited_changes" => nil } 54 | # and Charge.new("has_uncommited_changes" => nil).has_uncommited_changes == false 55 | charge = described_class.new(:id => 1, :has_uncommited_changes => false) 56 | 57 | TIME_INSTANCES.each do |time| 58 | expect(described_class).to receive(:GET) 59 | .with("/charges", param => format_time(time)) 60 | .and_return("charges" => [charge.to_h]) 61 | 62 | expect(described_class.list(param => time)).to eq [charge] 63 | 64 | expect(described_class).to receive(:GET) 65 | .with("/charges", param => format_time(time)) 66 | .and_return("charges" => [charge.to_h]) 67 | 68 | expect(described_class.list(param => format_time(time))).to eq [charge] 69 | 70 | end 71 | end 72 | end 73 | end 74 | 75 | describe ".new" do 76 | it "instantiates a charge object with the given attributes" do 77 | data = { 78 | "id" => 1, 79 | "address_id" => 2, 80 | "analytics_data" => { "foo" => 999 }, 81 | "billing_address" => { 82 | "city" => "NYC", 83 | "address1" => "555 5th ave", 84 | "address2" => "10th floor", 85 | "company" => "foo", 86 | "country" => "USA", 87 | "first_name" => "s", 88 | "last_name" => "shaw", 89 | "phone" => "555-555-1212", 90 | "province" => "BC", 91 | "zip" => "10000" 92 | }, 93 | "shipping_address" => { 94 | "city" => "LA", 95 | "address1" => "123 Sepulveda Blvd.", 96 | "address2" => "10th floor", 97 | "company" => "bar", 98 | "country" => "USA", 99 | "first_name" => "s", 100 | "last_name" => "shaw", 101 | "phone" => "555-555-9999", 102 | "province" => "BC", 103 | "zip" => "90210" 104 | }, 105 | "client_details" => { 106 | "browser_ip" => "127.0.0.1", 107 | "user_agent" => "Konquer" 108 | }, 109 | "created_at" => "2017-01-01T00:00:00", 110 | "customer_hash" => "X123", 111 | "customer_id" => 3, 112 | "first_name" => "sshaw", 113 | "last_name" => "xxx", 114 | "has_uncommited_changes" => false, 115 | "line_items" => [ 116 | "images" => { "small" => "http://example.com/foo.webp" }, 117 | "subscription_id" => 9999, 118 | "quantity" => 10, 119 | "shopify_product_id" => "90", 120 | "shopify_variant_id" => "91", 121 | "sku" => "SKU111", 122 | "title" => "Foo Product", 123 | "variant_title" => "Foo Variant", 124 | "vendor" => "Plug Depot", 125 | "grams" => 453, 126 | "price" => 100.0, 127 | "properties" => [ 128 | { 129 | "name" => "x", 130 | "value" => "y" 131 | } 132 | ] 133 | ], 134 | "note" => "noted", 135 | "note_attributes" => [:a => 123], 136 | "processed_at" => "2017-01-01T00:00:00", 137 | "processor_name" => "sshaw", 138 | "scheduled_at" => "2014-01-05T00:00:00", 139 | "shipments_count" => 4, 140 | "shopify_order_id" => "5", 141 | "shipping_lines" => [ 142 | "price" => "0.00", 143 | "code" => "Standard Shipping", 144 | "title" => "Standard Shipping" 145 | ], 146 | "status" => "SUCCESS", 147 | "total_price" => 1.0, 148 | "tax_lines" => 0, 149 | "total_discounts" => "0.0", 150 | "total_line_items_price" => "12.00", 151 | "total_price" => "12.00", 152 | "total_refunds" => nil, 153 | "total_tax" => 0, 154 | "total_weight" => 4536, 155 | "transaction_id" => "XX_XX", 156 | "type" => "RECURRING", 157 | "updated_at" => "2017-01-03T00:00:00", 158 | "external_order_id" => { "ecommerce" => "Charge-It-2-da-Game" }, 159 | "external_transaction_id" => { "payment_processor" => 123 } 160 | } 161 | 162 | charge = described_class.new(data) 163 | expect(charge.to_h).to eq data 164 | end 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /spec/http_request_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Recharge::HTTPRequest do 4 | before do 5 | @meta = { "id" => "X123000", "limit" => "1/50" } 6 | @response = { :a => { :b => 1 } } 7 | @endpoint = "https://api.rechargeapps.com" 8 | @headers = { "X-Recharge-Access-Token" => "XXX" } 9 | 10 | stub_request(:any, %r{#@endpoint/.*}) 11 | .and_return( 12 | :headers => { 13 | "Content-Type" => "application/json", 14 | "X-Request-Id" => @meta["id"], 15 | "X-Recharge-Limit" => @meta["limit"] 16 | }, 17 | :body => @response.to_json 18 | ) 19 | end 20 | 21 | describe ".GET" do 22 | before do 23 | @o = Object.new.extend described_class 24 | def @o.get(path, data = {}) 25 | GET(path, data) 26 | end 27 | end 28 | 29 | after { ReCharge.api_version = nil } 30 | 31 | # FIXME: shared example 32 | it "uses the configured specified API version" do 33 | # Not a real version, FYI 34 | ReCharge.api_version = "2022-01-01" 35 | 36 | @o.get("/foo") 37 | expect(WebMock).to have_requested(:get, "#@endpoint/foo").with(:headers => @headers.merge("X-Recharge-Version" => "2022-01-01")) 38 | end 39 | 40 | it "generates a GET request to the given path" do 41 | @o.get("/foo") 42 | expect(WebMock).to have_requested(:get, "#@endpoint/foo").with(:headers => @headers) 43 | end 44 | 45 | it "generates a GET request to the given path with the given parameters" do 46 | @o.get("/bar", :a => 123, :b => 456) 47 | expect(WebMock).to have_requested(:get, "#@endpoint/bar") 48 | .with(:query => { :a => 123, :b => 456 }, 49 | :headers => @headers) 50 | end 51 | 52 | context "given an application/json response" do 53 | it "parses the JSON and returns a Hash" do 54 | expect(@o.get("/")).to eq "a" => { "b" => 1 }, "meta" => @meta 55 | end 56 | end 57 | end 58 | 59 | describe ".POST" do 60 | before do 61 | @o = Object.new.extend described_class 62 | def @o.post(path, data = {}) 63 | POST(path, data) 64 | end 65 | end 66 | 67 | it "generates a POST request to the given path" do 68 | @o.post("/foo") 69 | expect(WebMock).to have_requested(:post, "#@endpoint/foo").with(:headers => @headers) 70 | end 71 | 72 | it "generates a POST request to the given path with the given parameters" do 73 | @o.post("/bar", :a => 123, :b => 456) 74 | expect(WebMock).to have_requested(:post, "#@endpoint/bar") 75 | .with(:body => { :a => 123, :b => 456 }, 76 | :headers => @headers) 77 | end 78 | 79 | context "given an application/json response" do 80 | it "parses the JSON and returns a Hash" do 81 | expect(@o.post("/")).to eq "a" => { "b" => 1 }, "meta" => @meta 82 | end 83 | end 84 | end 85 | 86 | describe ".PUT" do 87 | before do 88 | @o = Object.new.extend described_class 89 | def @o.put(path, data = {}) 90 | PUT(path, data) 91 | end 92 | end 93 | 94 | it "generates a PUT request to the given path" do 95 | @o.put("/foo") 96 | expect(WebMock).to have_requested(:put, "#@endpoint/foo").with(:headers => @headers) 97 | end 98 | 99 | it "generates a PUT request to the given path with the given parameters" do 100 | @o.put("/bar", :a => 123, :b => 456) 101 | expect(WebMock).to have_requested(:put, "#@endpoint/bar") 102 | .with(:body => { :a => 123, :b => 456 }, 103 | :headers => @headers) 104 | end 105 | end 106 | end 107 | 108 | RSpec.describe Recharge::HTTPRequest::Count do 109 | FooCount = Struct.new(:args) do 110 | extend Recharge::HTTPRequest::Count 111 | end 112 | 113 | FooCount::PATH = "/foo" 114 | 115 | describe ".count" do 116 | it "makes a GET request to PATH's count endpoint and returns the count" do 117 | expect(FooCount).to receive(:GET) 118 | .with(FooCount::PATH + "/count", nil) 119 | .and_return("count" => 1) 120 | 121 | expect(FooCount.count).to eq 1 122 | end 123 | 124 | it "makes a GET request to PATH's count endpoint with the given options" do 125 | args = { :foo => 1, :bar => 2 } 126 | expect(FooCount).to receive(:GET) 127 | .with(FooCount::PATH + "/count", args) 128 | .and_return("count" => 1) 129 | 130 | expect(FooCount.count(args)).to eq 1 131 | end 132 | end 133 | end 134 | 135 | RSpec.describe Recharge::HTTPRequest::Create do 136 | FooCreate = Struct.new(:args) do 137 | extend Recharge::HTTPRequest::Create 138 | end 139 | 140 | FooCreate::PATH = "/create" 141 | FooCreate::SINGLE = "create" 142 | 143 | describe ".create" do 144 | it "makes a POST request to PATH endpoint for the given data" do 145 | data = { :a => 1, :b => 2 } 146 | expect(FooCreate).to receive(:POST) 147 | .with(FooCreate::PATH, data) 148 | .and_return({}) 149 | 150 | expect(FooCreate.create(data)).to eq FooCreate.new 151 | end 152 | 153 | it "returns an instance of the receiving class from the response" do 154 | data = { :a => 1, :b => 2 } 155 | expect(FooCreate).to receive(:POST) 156 | .with(FooCreate::PATH, data) 157 | .and_return(FooCreate::SINGLE => data) 158 | 159 | expect(FooCreate.create(data)).to eq FooCreate.new(data) 160 | end 161 | end 162 | end 163 | 164 | RSpec.describe Recharge::HTTPRequest::Delete do 165 | FooDelete = Struct.new(:args) do 166 | extend Recharge::HTTPRequest::Delete 167 | end 168 | 169 | FooDelete::PATH = "/delete" 170 | 171 | describe ".delete" do 172 | it "makes a DELETE request to PATH endpoint for the given id" do 173 | id = 123 174 | expect(FooDelete).to receive(:DELETE) 175 | .with(FooDelete::PATH + "/#{id}") 176 | .and_return({}) 177 | 178 | expect(FooDelete.delete(id)).to eq({}) 179 | end 180 | end 181 | end 182 | 183 | 184 | RSpec.describe Recharge::HTTPRequest::Get do 185 | FooGet = Struct.new(:args) do 186 | extend Recharge::HTTPRequest::Get 187 | end 188 | 189 | FooGet::PATH = "/get" 190 | FooGet::SINGLE = "get" 191 | 192 | describe ".get" do 193 | it "requires an id argument" do 194 | [nil, ""].each do |arg| 195 | expect { FooGet.get(arg) }.to raise_error(ArgumentError, "id required") 196 | end 197 | end 198 | 199 | it "makes a GET request to PATH endpoint for the given id" do 200 | id = 123 201 | expect(FooGet).to receive(:GET) 202 | .with(FooGet::PATH + "/#{id}") 203 | .and_return({}) 204 | 205 | expect(FooGet.get(id)).to eq FooGet.new 206 | end 207 | 208 | it "returns an instance of the receiving class from the response" do 209 | id = 123 210 | args = { "id" => id, "name" => "sshaw" } 211 | expect(FooGet).to receive(:GET) 212 | .with(FooGet::PATH + "/#{id}") 213 | .and_return(FooGet::SINGLE => args) 214 | 215 | expect(FooGet.get(id)).to eq FooGet.new(args) 216 | end 217 | end 218 | end 219 | 220 | RSpec.describe Recharge::HTTPRequest::List do 221 | FooList = Struct.new(:args) do 222 | extend Recharge::HTTPRequest::List 223 | end 224 | 225 | FooList::PATH = "/list" 226 | FooList::COLLECTION = "foos" 227 | 228 | describe ".list" do 229 | it "makes a GET request to PATH endpoint with the given options" do 230 | args = { "page" => 1, "size" => 30 } 231 | expect(FooList).to receive(:GET) 232 | .with(FooList::PATH, args) 233 | .and_return({}) 234 | 235 | expect(FooList.list(args)).to eq [] 236 | end 237 | 238 | it "returns an array of instances of the receiving class" do 239 | meta = { "id" => "X123", "limit" => "0/40" } 240 | result = [ 241 | FooList.new("name" => "sshaw", "meta" => meta), 242 | FooList.new("name" => "fofinha", "meta" => meta) 243 | ] 244 | 245 | expect(FooList).to receive(:GET) 246 | .with(FooList::PATH, nil) 247 | .and_return( 248 | "meta" => meta, 249 | FooList::COLLECTION => [ 250 | result[0].args, 251 | result[1].args 252 | ]) 253 | 254 | expect(FooList.list).to eq result 255 | end 256 | end 257 | end 258 | 259 | 260 | RSpec.describe Recharge::HTTPRequest::Update do 261 | FooUpdate = Struct.new(:args) do 262 | extend Recharge::HTTPRequest::Update 263 | end 264 | 265 | FooUpdate::PATH = "/update" 266 | FooUpdate::SINGLE = "update" 267 | 268 | describe ".update" do 269 | it "makes a PUT request to PATH endpoint for the given id and data" do 270 | id = 123 271 | data = { :a => 123, :b => 456 } 272 | 273 | expect(FooUpdate).to receive(:PUT) 274 | .with(FooUpdate::PATH + "/#{id}", data) 275 | .and_return({}) 276 | 277 | expect(FooUpdate.update(id, data)).to eq FooUpdate.new 278 | end 279 | end 280 | end 281 | -------------------------------------------------------------------------------- /lib/recharge/classes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "json" 4 | require "class2" 5 | require "recharge/http_request" 6 | require "active_support/core_ext/hash/keys" 7 | 8 | 9 | # For better or worse, this contains properties for 2021-11 and 2021-01 APIs 10 | class2 "Recharge", JSON.parse(<<-END) do 11 | { 12 | "subscription": 13 | { 14 | "id":10101, 15 | "address_id":178918, 16 | "analytics_data": {}, 17 | "customer_id":1438, 18 | "cancellation_reason": "$$$$", 19 | "cancellation_reason_comments": "", 20 | "cancelled_at":"2017-02-28T20:31:29", 21 | "charge_interval_frequency": "3", 22 | "created_at":"2017-02-28T20:31:29", 23 | "updated_at":"2017-02-28 20:31:29", 24 | "next_charge_scheduled_at":"2017-04-01T00:00:00", 25 | "cancelled_at":null, 26 | "has_queued_charges": 1, 27 | "is_skippable": true, 28 | "is_swappable": true, 29 | "is_prepaid": false, 30 | "max_retries_reached": 0, 31 | "product_title":"Sumatra Coffee", 32 | "price":12.0, 33 | "quantity":1, 34 | "recharge_product_id": 518676, 35 | "status":"ACTIVE", 36 | "shopify_product_id":1255183683, 37 | "shopify_variant_id":3844924611, 38 | "sku_override": false, 39 | "sku":null, 40 | "order_interval_unit":"day", 41 | "order_interval_frequency":"30", 42 | "charge_interval_frequency":"30", 43 | "order_day_of_month":null, 44 | "order_day_of_week":null, 45 | "properties": [], 46 | "variant_title": "Milk - a / b" 47 | }, 48 | "charge": 49 | { 50 | "address_id":178918, 51 | "analytics_data": {}, 52 | "billing_address":{ 53 | "address1":"3030 Nebraska Avenue", 54 | "address2":"", 55 | "city":"Los Angeles", 56 | "company":"", 57 | "country":"United States", 58 | "first_name":"Mike", 59 | "last_name":"Flynn", 60 | "phone":"", 61 | "province":"California", 62 | "zip":"90404" 63 | }, 64 | "client_details": { 65 | "browser_ip": null, 66 | "user_agent": null 67 | }, 68 | "created_at":"2017-03-01T19:52:11", 69 | "customer_hash":null, 70 | "customer_id":10101, 71 | "first_name":"Mike", 72 | "has_uncommited_changes": false, 73 | "id":1843, 74 | "last_name":"Flynn", 75 | "line_items":[ 76 | { 77 | "grams":0, 78 | "images": {}, 79 | "price":100.0, 80 | "properties":[], 81 | "quantity":1, 82 | "shopify_product_id": "1255183683", 83 | "shopify_variant_id":"3844924611", 84 | "sku":"", 85 | "title": "Milk 10% Off Auto renew", 86 | "variant_title": "a / b", 87 | "vendor": "Example Storeeeeeee", 88 | "subscription_id":14562 89 | } 90 | ], 91 | "note": "blah blah", 92 | "note_attributes": [], 93 | "processor_name": "foo", 94 | "processed_at":"2014-11-20T00:00:00", 95 | "scheduled_at":"2014-11-20T00:00:01", 96 | "shipments_count":null, 97 | "shipping_address":{ 98 | "address1":"3030 Nebraska Avenue", 99 | "address2":"", 100 | "city":"Los Angeles", 101 | "company":"", 102 | "country":"United States", 103 | "first_name":"Mike", 104 | "last_name":"Flynn", 105 | "phone":"3103843698", 106 | "province":"California", 107 | "zip":"90404" 108 | }, 109 | "shipping_lines": [], 110 | "shopify_order_id":"281223307", 111 | "status":"SUCCESS", 112 | "total_price":446.00, 113 | "updated_at":"2016-09-05T09:19:29", 114 | "tax_lines": 0, 115 | "total_discounts": "0.0", 116 | "total_line_items_price": "12.00", 117 | "total_price": "12.00", 118 | "total_refunds": null, 119 | "total_tax": 0, 120 | "total_weight": 4536, 121 | "transaction_id": "cch_1Du2QpJ2iqHvZRd18RyqoPvc", 122 | "type": "RECURRING", 123 | "external_order_id": { 124 | "ecommerce": "Charge-It-2-da-Game" 125 | }, 126 | "external_transaction_id": { 127 | "payment_processor": 999999 128 | } 129 | }, 130 | "customer": 131 | { 132 | "id": 1438, 133 | "hash": "143806234a9ff87a8d9e", 134 | "shopify_customer_id": null, 135 | "email": "mike@gmail.com", 136 | "created_at": "2018-01-10T11:00:00", 137 | "updated_at": "2017-01-11T13:16:19", 138 | "first_name": "Mike", 139 | "last_name": "Flynn", 140 | "billing_first_name": "Mike", 141 | "billing_last_name": "Flynn", 142 | "billing_company": null, 143 | "billing_address1": "3030 Nebraska Avenue", 144 | "billing_address2": null, 145 | "billing_zip": "90404", 146 | "billing_city": "Los Angeles", 147 | "billing_province": "California", 148 | "billing_country": "United States", 149 | "billing_phone": "3103843698", 150 | "processor_type": null, 151 | "status": "FOO", 152 | "stripe_customer_token": "123123", 153 | "paypal_customer_token": "123123", 154 | "braintree_customer_token": "123123", 155 | "external_customer_id": { 156 | "ecommerce": "382028302" 157 | }, 158 | "hash": "18819267b1f9095be98f13a8" 159 | }, 160 | "order": { 161 | "id":7271806, 162 | "customer_id":10101, 163 | "address_id":178918, 164 | "charge_id":9519316, 165 | "transaction_id":"ch_19sdP2J2zqHvZRd1hqkeGANO", 166 | "shopify_order_id":"5180645510", 167 | "shopify_order_number":5913, 168 | "created_at":"2017-03-01T14:46:26", 169 | "updated_at":"2017-03-01T14:46:26", 170 | "scheduled_at":"2017-03-01T00:00:00", 171 | "processed_at":"2017-03-01T14:46:26", 172 | "status":"SUCCESS", 173 | "charge_status":"SUCCESS", 174 | "type":"CHECKOUT", 175 | "first_name":"Mike", 176 | "last_name":"Flynn", 177 | "email":"mike@gmail.com", 178 | "payment_processor":"stripe", 179 | "address_is_active":1, 180 | "is_prepaid":false, 181 | "line_items":[ 182 | { 183 | "subscription_id":10101, 184 | "shopify_product_id":"1255183683", 185 | "shopify_variant_id":"3844924611", 186 | "variant_title":"Sumatra", 187 | "title":"Sumatra Latte", 188 | "quantity":1, 189 | "properties":[], 190 | "purchase_item_id": 365974856, 191 | "external_inventory_policy": "decrement_obeying_policy", 192 | "external_product_id": {}, 193 | "external_variant_id": {}, 194 | "grams": 454, 195 | "images": {}, 196 | "original_price": "10.00", 197 | "properties": [], 198 | "purchase_item_type": "subscription", 199 | "sku": "TOM0001", 200 | "tax_due": "3.80", 201 | "tax_lines": [ 202 | { 203 | "price": "0.993", 204 | "rate": "0.0725", 205 | "unit_price": "0.331", 206 | "title": "Highest taxed state in the nation: California!" 207 | } 208 | ], 209 | "taxable": true, 210 | "taxable_amount": "10.00", 211 | "title": "Shirt bundle", 212 | "total_price": "43.80", 213 | "unit_price": "10.00", 214 | "unit_price_includes_tax": false, 215 | "variant_title": "Blue t-shirts" 216 | } 217 | ], 218 | "total_price":18.00, 219 | "shipping_address":{ 220 | "address1":"1933 Manning", 221 | "address2":"204", 222 | "city":"los angeles", 223 | "province":"California", 224 | "first_name":"mike", 225 | "last_name":"flynn", 226 | "zip":"90025", 227 | "company":"bootstrap", 228 | "phone":"3103103101", 229 | "country":"United States" 230 | }, 231 | "billing_address":{ 232 | "address1":"1933 Manning", 233 | "address2":"204", 234 | "city":"los angeles", 235 | "province":"California", 236 | "first_name":"mike", 237 | "last_name":"flynn", 238 | "zip":"90025", 239 | "company":"bootstrap", 240 | "phone":"3103103101", 241 | "country":"United States" 242 | }, 243 | "customer": {}, 244 | "note": "Noted!" 245 | }, 246 | "metafield": { 247 | "created_at": "2018-11-05T12:59:30", 248 | "description": "desc lorem ipsum", 249 | "id": 15, 250 | "key": "marjan", 251 | "namespace": "nmsp2c", 252 | "owner_id": 17868054, 253 | "owner_resource": "customer", 254 | "updated_at": "2018-11-05T15:48:42", 255 | "value": "5", 256 | "value_type": "integer" 257 | }, 258 | "product": { 259 | "collection_id": null, 260 | "created_at": "2019-11-07T11:36:19", 261 | "discount_amount": 15.0, 262 | "discount_type": "percentage", 263 | "handle": null, 264 | "id": 1327844, 265 | "images": {}, 266 | "product_id": 4354268856408, 267 | "shopify_product_id": 4354268856408, 268 | "subscription_defaults": { 269 | "charge_interval_frequency": 4, 270 | "cutoff_day_of_month": null, 271 | "cutoff_day_of_week": null, 272 | "expire_after_specific_number_of_charges": null, 273 | "modifiable_properties": [], 274 | "number_charges_until_expiration": null, 275 | "order_day_of_month": 0, 276 | "order_day_of_week": null, 277 | "order_interval_frequency": 4, 278 | "order_interval_frequency_options": [], 279 | "order_interval_unit": "month", 280 | "storefront_purchase_options": "subscription_only" 281 | }, 282 | "title": "T-shirt", 283 | "updated_at": "2019-11-07T14:04:52", 284 | "description": "ScreenStaring, that's what we do!", 285 | "external_product_id": "123123", 286 | "external_created_at": "2019-11-07T14:04:52", 287 | "external_updated_at": "2019-11-07T14:04:52", 288 | "brand": "ScreenStaring", 289 | "options": [ 290 | { 291 | "name": "Size", 292 | "position": 0, 293 | "values": [] 294 | } 295 | ] 296 | }, 297 | "webhook": { 298 | "id":6, 299 | "address":"https://request.in/foo", 300 | "topic":"order/create" 301 | }, 302 | "address":{ 303 | "id":3411137, 304 | "address1":"1933 Manning", 305 | "address2":"204", 306 | "city":"los angeles", 307 | "province":"California", 308 | "first_name":"mike", 309 | "last_name":"flynn", 310 | "zip":"90025", 311 | "company":"bootstrap", 312 | "phone":"3103103101", 313 | "original_shipping_lines": [ 314 | { 315 | "code": "Standard Shipping", 316 | "price": "0.00", 317 | "title": "Standard Shipping" 318 | } 319 | ], 320 | "shipping_lines_override": [ 321 | { 322 | "code": "Free Shipping", 323 | "price": "0.00", 324 | "title": "Free Shipping" 325 | } 326 | ], 327 | "country":"United States" 328 | }, 329 | "discount":{ 330 | "id": 3748296, 331 | "code": "Discount1", 332 | "value": 12.5, 333 | "ends_at": "2019-12-15T00:00:00", 334 | "starts_at": "2018-05-16T00:00:00", 335 | "status": "enabled", 336 | "usage_limit": 10, 337 | "applies_to_id": null, 338 | "discount_type": "percentage", 339 | "applies_to": null, 340 | "applies_to_resource": null, 341 | "times_used": 0, 342 | "duration": "usage_limit", 343 | "duration_usage_limit": 10, 344 | "applies_to_product_type": "ALL", 345 | "created_at": "2018-04-25T14:32:39", 346 | "updated_at": "2018-05-04T13:33:53", 347 | "once_per_customer": false 348 | } 349 | } 350 | END 351 | def meta=(meta) 352 | @meta = meta 353 | end 354 | 355 | def meta 356 | @meta ||= {} 357 | end 358 | 359 | # Class2 >= 0.5 uses Symbol keys and we don't (yet) want to break #to_h's signature 360 | # Class2 is also not setup so that we can call super 361 | alias __og_to_h to_h 362 | def to_h 363 | __og_to_h.deep_stringify_keys! 364 | end 365 | 366 | private 367 | 368 | def self.instance(response) 369 | args = response[self::SINGLE] 370 | args["meta"] = response["meta"] 371 | new(args) 372 | end 373 | end 374 | 375 | module Recharge 376 | module Persistable # :nodoc: 377 | def save 378 | data = to_h 379 | data.delete("id") 380 | 381 | if id 382 | self.class.update(id, data) 383 | else 384 | self.id = self.class.create(data).id 385 | end 386 | end 387 | end 388 | 389 | class Address 390 | PATH = "/addresses".freeze 391 | SINGLE = "address".freeze 392 | COLLECTION = "addresses".freeze 393 | 394 | extend HTTPRequest::Get 395 | extend HTTPRequest::Update 396 | 397 | # 398 | # Persist the updated address 399 | # 400 | # === Errors 401 | # 402 | # Recharge::ConnectionError, Recharge::RequestError 403 | # 404 | def save 405 | data = to_h 406 | data.delete("id") 407 | self.class.update(id, data) 408 | end 409 | 410 | # Validate an address 411 | # 412 | # === Arguments 413 | # 414 | # [data (Hash)] Address to validate, see: https://developer.rechargepayments.com/?shell#validate-address 415 | # 416 | # === Returns 417 | # 418 | # [Hash] Validated and sometimes updated address 419 | # 420 | # === Errors 421 | # 422 | # Recharge::ConnectionError, Recharge::RequestError 423 | # 424 | # If the address is invalid a Recharge::RequestError is raised. The validation 425 | # errors can be retrieved via Recharge::RequestError#errors 426 | # 427 | def self.validate(data) 428 | POST(join("validate"), data) 429 | end 430 | end 431 | 432 | class Customer 433 | PATH = "/customers".freeze 434 | SINGLE = "customer".freeze 435 | COLLECTION = "customers".freeze 436 | 437 | extend HTTPRequest::Create 438 | extend HTTPRequest::Get 439 | extend HTTPRequest::Update 440 | extend HTTPRequest::List 441 | extend HTTPRequest::Count 442 | 443 | include Persistable 444 | 445 | # Retrieve all of a customer's addresses 446 | # 447 | # === Arguments 448 | # 449 | # [id (Fixnum)] Customer ID 450 | # 451 | # === Errors 452 | # 453 | # ConnectionError, RequestError 454 | # 455 | # === Returns 456 | # 457 | # [Array[Recharge::Address]] The customer's addresses 458 | # 459 | def self.addresses(id) 460 | id_required!(id) 461 | data = GET(join(id, Address::COLLECTION)) 462 | (data[Address::COLLECTION] || []).map do |d| 463 | address = Address.new(d) 464 | address.meta = data["meta"] 465 | address 466 | end 467 | end 468 | 469 | # Create a new address 470 | # 471 | # === Arguments 472 | # 473 | # [id (Fixnum)] Customer ID 474 | # [address (Hash)] Address attributes, see: https://developer.rechargepayments.com/?shell#create-address 475 | # 476 | # === Returns 477 | # 478 | # [Recharge::Address] The created address 479 | # 480 | # === Errors 481 | # 482 | # Recharge::ConnectionError, Recharge::RequestError 483 | # 484 | def self.create_address(id, address) 485 | id_required!(id) 486 | data = POST(join(id, Address::COLLECTION), address) 487 | address = Address.new(data[Address::SINGLE]) 488 | address.meta = data["meta"] 489 | address 490 | end 491 | end 492 | 493 | class Charge 494 | PATH = "/charges".freeze 495 | SINGLE = "charge".freeze 496 | COLLECTION = "charges".freeze 497 | 498 | extend HTTPRequest::Count 499 | extend HTTPRequest::Get 500 | extend HTTPRequest::List 501 | 502 | def self.list(options = nil) 503 | super(convert_date_params(options, :date_min, :date_max)) 504 | end 505 | 506 | def self.change_next_charge_date(id, date) 507 | path = join(id, "change_next_charge_date") 508 | instance(POST(path, :next_charge_date => date_param(date))) 509 | end 510 | 511 | def self.skip(id, subscription_id) 512 | path = join(id, "skip") 513 | instance(POST(path, :subscription_id => subscription_id)) 514 | end 515 | end 516 | 517 | class Discount 518 | PATH = "/discounts".freeze 519 | SINGLE = "discount".freeze 520 | COLLECTION = "discounts" 521 | 522 | extend HTTPRequest::Count 523 | extend HTTPRequest::Create 524 | extend HTTPRequest::Delete 525 | extend HTTPRequest::Get 526 | extend HTTPRequest::List 527 | extend HTTPRequest::Update 528 | 529 | include Persistable 530 | 531 | def self.count(options = nil) 532 | super(convert_date_params(options, :created_at_max, :created_at_min, :date_min, :date_max)) 533 | end 534 | 535 | def self.list(options = nil) 536 | super(convert_date_params(options, :created_at, :created_at_max, :updated_at, :updated_at_max)) 537 | end 538 | 539 | def delete 540 | self.class.delete(id) 541 | true 542 | end 543 | end 544 | 545 | class Metafield 546 | PATH = "/metafields" 547 | SINGLE = "metafield" 548 | COLLECTION = "metafields" 549 | 550 | extend HTTPRequest::Count 551 | extend HTTPRequest::Create 552 | extend HTTPRequest::Delete 553 | extend HTTPRequest::Get 554 | extend HTTPRequest::List 555 | 556 | include Persistable 557 | 558 | def self.list(owner, options = nil) 559 | raise ArgumentError, "owner resource required" if owner.nil? || owner.to_s.strip.empty? 560 | super (options||{}).merge(:owner_resource => owner) 561 | end 562 | 563 | def delete 564 | self.class.delete(id) 565 | true 566 | end 567 | end 568 | 569 | class Order 570 | PATH = "/orders".freeze 571 | SINGLE = "order".freeze 572 | COLLECTION = "orders".freeze 573 | 574 | extend HTTPRequest::Count 575 | extend HTTPRequest::Get 576 | extend HTTPRequest::List 577 | 578 | def self.count(options = nil) 579 | super(convert_date_params(options, :created_at_max, :created_at_min, :date_min, :date_max)) 580 | end 581 | 582 | def self.change_date(id, date) 583 | id_required!(id) 584 | instance(POST(join(id, "change_date"), :shipping_date => date_param(date))) 585 | end 586 | 587 | def self.update_shopify_variant(id, old_variant_id, new_varient_id) 588 | id_required!(id) 589 | path = join(id, "update_shopify_variant", old_variant_id) 590 | instance(POST(path, :new_shopify_variant_id => new_varient_id)) 591 | end 592 | end 593 | 594 | class Product 595 | PATH = "/products".freeze 596 | SINGLE = "product".freeze 597 | COLLECTION = "products".freeze 598 | 599 | extend HTTPRequest::Count 600 | extend HTTPRequest::Get 601 | extend HTTPRequest::List 602 | end 603 | 604 | class Subscription 605 | PATH = "/subscriptions".freeze 606 | SINGLE = "subscription".freeze 607 | COLLECTION = "subscriptions".freeze 608 | 609 | extend HTTPRequest::Create 610 | extend HTTPRequest::Get 611 | extend HTTPRequest::Update 612 | extend HTTPRequest::List 613 | 614 | include Persistable 615 | 616 | # 617 | # Activate a subscription 618 | # 619 | # === Arguments 620 | # 621 | # [id (Integer)] ID of subscription to cancel 622 | # 623 | # === Returns 624 | # 625 | # [Recharge::Subscription] The activated subscription 626 | # 627 | # === Errors 628 | # 629 | # Recharge::ConnectionError, Recharge::RequestError 630 | # 631 | # If the subscription was already activated a Recharge::RequestError will be raised 632 | # 633 | def self.activate(id) 634 | id_required!(id) 635 | instance(POST(join(id, "activate"), :status => "active")) 636 | end 637 | 638 | # 639 | # Cancel a subscription 640 | # 641 | # === Arguments 642 | # 643 | # [id (Integer)] ID of subscription to cancel 644 | # [reason (String)] Reason for the cancellation 645 | # 646 | # === Returns 647 | # 648 | # [Recharge::Subscription] The canceled subscription 649 | # 650 | # === Errors 651 | # 652 | # Recharge::ConnectionError, Recharge::RequestError 653 | # 654 | # If the subscription was already canceled a Recharge::RequestError will be raised 655 | # 656 | def self.cancel(id, reason) 657 | id_required!(id) 658 | instance(POST(join(id, "cancel"), :cancellation_reason => reason)) 659 | end 660 | 661 | def self.set_next_charge_date(id, date) 662 | id_required!(id) 663 | instance(POST(join(id, "set_next_charge_date"), :date => date_param(date))) 664 | end 665 | 666 | def self.list(options = nil) 667 | #options[:status] = options[:status].upcase if options[:status] 668 | super(convert_date_params(options, :created_at, :created_at_max, :updated_at, :updated_at_max)) 669 | end 670 | end 671 | 672 | class Webhook 673 | PATH = "/webhooks".freeze 674 | COLLECTION = "webhooks".freeze 675 | SINGLE = "webhook".freeze 676 | 677 | extend HTTPRequest::Create 678 | extend HTTPRequest::Delete 679 | extend HTTPRequest::Get 680 | extend HTTPRequest::List 681 | extend HTTPRequest::Update 682 | 683 | include Persistable 684 | 685 | def delete 686 | self.class.delete(id) 687 | true 688 | end 689 | end 690 | end 691 | --------------------------------------------------------------------------------