tap-clutch 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b98dba7a611e56ae431cb914fb2bd85d57030f6c
4
+ data.tar.gz: aaf74f13fca725477914f512df762bf2f11cc942
5
+ SHA512:
6
+ metadata.gz: fe07a0c3165b12aaddd0922463434d4a90a9145acd0bb9cc4829ad27684ee33511769ae0f0664386b5756b0994425e68fb75f3e98d1254e5c269a44c140fad13
7
+ data.tar.gz: f60047ee073300619cb378b39b6f44d4e8077c8d01db1ae36923b99af092f400123f7223f69c8ae6b18eb131c6dfacfa74e6347956b0bc2eda40c8f1f01986ae
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 follain
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ tap-clutch
2
+ ==========
3
+
4
+ ## A [singer.io](http://singer.io) tap to extract data from [Clutch](http://clutch.com) and load into any Singer target, like Stitch or CSV
5
+
6
+ # Configuration
7
+
8
+ {
9
+ "api_key": "api_key",
10
+ "api_secret": "secret",
11
+ "api_base": "https://api.clutch.com/merchant/",
12
+ "brand": "clutch_brand",
13
+ "location": "clutch_location",
14
+ "card_set_id": "clutch_card_set_id",
15
+ "terminal": "clutch_terminal",
16
+ "username": "clutch_portal_user_id",
17
+ "password": "clutch_portal_password"
18
+ }
19
+
20
+ # Usage (with [Stitch target](https://github.com/singer-io/target-stitch))
21
+
22
+ > bundle exec tap-clutch
23
+ Usage: tap-clutch [options]
24
+ -c, --config config_file Set config file (json)
25
+ -s, --state state_file Set state file (json)
26
+ -h, --help Displays help
27
+ -v, --verbose Enables verbose logging to STDERR
28
+
29
+ > pip install target-stitch
30
+ > gem install tap-clutch
31
+ > bundle exec tap-clutch -c config.clutch.json -s state.json | target-stitch --config config.stitch.json | tail -1 > state.new.json
data/bin/tap-clutch ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler'
4
+
5
+ Bundler.require
6
+
7
+ require_relative '../lib/runner'
8
+
9
+ TapClutch::Runner.new(ARGV, stream: $stdout).perform
data/lib/client.rb ADDED
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'clutch'
4
+ require 'faraday-cookie_jar'
5
+ require 'faraday_middleware'
6
+ require 'active_support/time'
7
+
8
+ require_relative 'models/card'
9
+
10
+ module TapClutch
11
+ LIMIT = 25
12
+ KEYS = %i[
13
+ api_key
14
+ api_secret
15
+ api_base
16
+ brand
17
+ location
18
+ terminal
19
+ verbose
20
+ state
21
+ stream
22
+ username
23
+ password
24
+ ].freeze
25
+
26
+ # rubocop:disable Metrics/BlockLength
27
+ Client = Struct.new(*KEYS) do
28
+ def initialize(**kwargs)
29
+ super(*members.map { |k| kwargs[k] })
30
+ Clutch.configure do |c|
31
+ c.clutch_api_key = api_key
32
+ c.clutch_api_secret = api_secret
33
+ c.clutch_api_base = api_base
34
+ c.clutch_brand = brand
35
+ c.clutch_location = location
36
+ c.clutch_terminal = terminal
37
+ end
38
+ end
39
+
40
+ def faraday
41
+ @faraday ||= build_faraday.tap do |f|
42
+ f.post '/authenticate/userpass', username: username, password: password
43
+ end
44
+ end
45
+
46
+ def build_faraday
47
+ # TODO: Extract clutch portal URL to config.json
48
+ Faraday.new(url: 'https://portal.clutch.com') do |builder|
49
+ builder.use :cookie_jar
50
+ builder.request :url_encoded
51
+ builder.response :json
52
+ builder.adapter Faraday.default_adapter
53
+ end
54
+ end
55
+
56
+ def process(start_date = '2018-01-11')
57
+ start_date = Date.parse(start_date) if start_date.is_a? String
58
+ (start_date..Time.current.getlocal.to_date).each do |date|
59
+ process_date date
60
+ end
61
+ end
62
+
63
+ def output(hash)
64
+ stream.puts JSON.generate(hash)
65
+ end
66
+
67
+ private
68
+
69
+ def process_date(date)
70
+ response = search(date)
71
+ card_numbers = response.body['lookerData']['data'].map { |x| x[2] }.uniq
72
+ cards = card_numbers.map { |n| Models::Card.fetch(n) }
73
+ output_records Models::Card, cards
74
+ output_state date
75
+ end
76
+
77
+ def search(date)
78
+ faraday.post do |req|
79
+ req.url '/transactions/search.json'
80
+ req.body = {
81
+ searchType: 'processed',
82
+ brandId: 7532,
83
+ groupId: 7533,
84
+ transactionTypes: '3,5,9',
85
+ currentRange: 'Custom Range',
86
+ timeFrom: '00:00:00 am',
87
+ timeTo: '00:00:00 am',
88
+ dateFrom: date.strftime('%Y-%m-%d'),
89
+ dateTo: (date + 1).strftime('%Y-%m-%d')
90
+ }
91
+ end
92
+ end
93
+
94
+ def output_records(_model, records)
95
+ records.compact.each do |record|
96
+ record.records.flatten.each do |model_record|
97
+ output model_record
98
+ end
99
+ end
100
+ end
101
+
102
+ def output_state(date)
103
+ output type: :STATE, value: { start_date: date }
104
+ end
105
+
106
+ def get(model, offset)
107
+ model.fetch offset
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../schema'
4
+
5
+ module TapClutch
6
+ module Models
7
+ Base = Struct.new(:data, :client) do # rubocop:disable Metrics/BlockLength
8
+ def self.subclasses
9
+ ObjectSpace.each_object(Class).select { |klass| klass < self }
10
+ end
11
+
12
+ def self.stream
13
+ raise 'Implement #stream in a subclass'
14
+ end
15
+
16
+ def self.key_property
17
+ :id
18
+ end
19
+
20
+ def self.schema(&block)
21
+ @schema ||= ::TapClutch::Schema.new(stream, key_property)
22
+ @schema.instance_eval(&block) if block_given?
23
+ @schema.to_hash
24
+ end
25
+
26
+ def transform
27
+ data.dup
28
+ end
29
+
30
+ def base_record
31
+ {
32
+ type: 'RECORD',
33
+ stream: self.class.stream,
34
+ record: transform
35
+ }
36
+ end
37
+
38
+ def extra_records
39
+ []
40
+ end
41
+
42
+ def records
43
+ [base_record] + extra_records.compact.flat_map(&:records)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'transaction'
5
+
6
+ module TapClutch
7
+ module Models
8
+ # Models a Clutch Card
9
+ class Card < Base
10
+ def self.key_property
11
+ :cardNumber
12
+ end
13
+
14
+ def self.stream
15
+ 'cards'
16
+ end
17
+
18
+ schema do
19
+ string :cardNumber, :not_null
20
+ string :cardSetId
21
+ array :balances
22
+ end
23
+
24
+ def self.fetch(card_number)
25
+ response = Clutch.client.post(
26
+ '/search',
27
+ limit: 1,
28
+ offset: 0,
29
+ filters: {
30
+ cardNumber: card_number
31
+ },
32
+ returnFields: {
33
+ balances: true,
34
+ activationDate: true
35
+ }
36
+ )
37
+
38
+ new(response.cards.first) if response.cards.first
39
+ end
40
+
41
+ def extra_records
42
+ Transaction.history(data.cardNumber)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require 'byebug'
5
+ require 'active_support/time'
6
+
7
+ module TapClutch
8
+ module Models
9
+ # Models a Clutch Transaction
10
+ class Transaction < Base
11
+ def self.key_property
12
+ :transactionId
13
+ end
14
+
15
+ def self.stream
16
+ 'transactions'
17
+ end
18
+
19
+ def self.history(card_number)
20
+ response = Clutch.client.post(
21
+ '/cardHistory',
22
+ limit: 100,
23
+ offset: 0,
24
+ cardNumber: card_number,
25
+ restrictTransactionTypes: %w[ALLOCATE UPDATE_BALANCE]
26
+ )
27
+
28
+ response.transactions.map do |transaction|
29
+ new(transaction.merge(cardNumber: card_number))
30
+ end
31
+ end
32
+
33
+ schema do
34
+ string :transactionId, :not_null
35
+ string :cardNumber, :not_null
36
+ string :requestRef
37
+ string :callType
38
+ boolean :isLegacy
39
+ string :location
40
+ string :transactionTime
41
+ array :balanceUpdates
42
+ end
43
+
44
+ def transform
45
+ Time.zone = Time.now.zone
46
+
47
+ super.tap do |data|
48
+ data.merge! 'transactionTime' =>
49
+ Time.zone.at(data['transactionTime'] / 1000)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
data/lib/runner.rb ADDED
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent'
4
+ require 'optparse'
5
+
6
+ require_relative 'client'
7
+ require_relative 'schema'
8
+ require_relative 'models/base'
9
+ require_relative 'models/transaction'
10
+
11
+ module TapClutch
12
+ # Kicks off tap-clutch process
13
+ class Runner
14
+ attr_reader :config_filename
15
+ attr_reader :state_filename
16
+ attr_reader :stream
17
+ attr_reader :verbose
18
+
19
+ def initialize(argv, stream: $stderr, config: nil, state: nil)
20
+ @stream = stream
21
+ @config = config
22
+ @state = state
23
+ parser.parse! argv
24
+ end
25
+
26
+ def perform
27
+ return stream.puts(parser) if config.keys.empty?
28
+ output_schemata
29
+ client.process state['start_date']
30
+ end
31
+
32
+ def config
33
+ @config ||= read_json(config_filename)
34
+ end
35
+
36
+ def state
37
+ @state ||= read_json(state_filename)
38
+ end
39
+
40
+ private
41
+
42
+ def output_schemata
43
+ TapClutch::Models::Base.subclasses.each do |model|
44
+ client.output model.schema
45
+ end
46
+ end
47
+
48
+ # rubocop: disable Metrics/AbcSize
49
+ def client
50
+ @client ||= TapClutch::Client.new(
51
+ api_key: config['api_key'],
52
+ api_secret: config['api_secret'],
53
+ api_base: config['api_base'],
54
+ brand: config['brand'],
55
+ location: config['location'],
56
+ terminal: config['terminal'],
57
+ username: config['username'],
58
+ password: config['password'],
59
+ verbose: verbose,
60
+ state: Concurrent::Hash.new.merge!(state),
61
+ stream: stream
62
+ )
63
+ end
64
+ # rubocop: enable Metrics/AbcSize
65
+
66
+ def read_json(filename)
67
+ return JSON.parse(File.read(filename)) if filename
68
+ {}
69
+ end
70
+
71
+ def parser
72
+ @parser ||= OptionParser.new do |opts|
73
+ opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
74
+ opts.on('-c', '--config filename', 'Set config file (json)') do |config|
75
+ @config_filename = config
76
+ end
77
+
78
+ opts.on('-s', '--state filename', 'Set state file (json)') do |state|
79
+ @state_filename = state
80
+ end
81
+
82
+ opts.on('-v', '--verbose', 'Enables verbose logging to STDERR') do
83
+ @verbose = true
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
data/lib/schema.rb ADDED
@@ -0,0 +1,66 @@
1
+
2
+ # frozen_string_literal: true
3
+
4
+ module TapClutch
5
+ # Models a JSON Schema required for Singer taps
6
+ class Schema
7
+ attr_reader :stream, :key_property
8
+
9
+ def initialize(stream, key_property)
10
+ @stream = stream
11
+ @key_property = key_property
12
+ end
13
+
14
+ # Models JSON Schema types
15
+ class Types
16
+ def self.method_missing(method, *args)
17
+ return super unless %I[
18
+ array
19
+ number
20
+ object
21
+ string
22
+ boolean
23
+ ].include?(method)
24
+
25
+ types = [method.to_sym]
26
+ types << :null unless args.include?(:not_null)
27
+
28
+ {
29
+ type: types.one? ? types.first : types
30
+ }.tap do |hash|
31
+ hash[:format] = 'date-time' if args.include?(:datetime)
32
+ end
33
+ end
34
+
35
+ def self.respond_to_missing?(_method, *_args)
36
+ true
37
+ end
38
+
39
+ def self.datetime(*args)
40
+ args << :datetime
41
+ string(*args)
42
+ end
43
+ end
44
+
45
+ %i[string number array object datetime boolean].each do |type|
46
+ define_method type do |name, *options|
47
+ properties[name.to_sym] = Types.send(type, *options)
48
+ end
49
+ end
50
+
51
+ def properties
52
+ @properties ||= {}
53
+ end
54
+
55
+ def to_hash
56
+ {
57
+ type: :SCHEMA,
58
+ stream: stream,
59
+ key_properties: [key_property],
60
+ schema: {
61
+ properties: properties
62
+ }
63
+ }
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TapClutch
4
+ VERSION = '0.0.1'
5
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tap-clutch
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Joe Lind
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-06-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: clutch-client
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: concurrent-ruby
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.0'
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: 1.0.2
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - "~>"
56
+ - !ruby/object:Gem::Version
57
+ version: '1.0'
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 1.0.2
61
+ - !ruby/object:Gem::Dependency
62
+ name: faraday-cookie_jar
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: 0.0.6
68
+ type: :runtime
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: 0.0.6
75
+ - !ruby/object:Gem::Dependency
76
+ name: faraday_middleware
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: 0.12.2
82
+ type: :runtime
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: 0.12.2
89
+ description: Stream Clutch records to a Singer target, such as Stitch
90
+ email: joelind@gmail.com
91
+ executables:
92
+ - tap-clutch
93
+ extensions: []
94
+ extra_rdoc_files: []
95
+ files:
96
+ - LICENSE
97
+ - README.md
98
+ - bin/tap-clutch
99
+ - lib/client.rb
100
+ - lib/models/base.rb
101
+ - lib/models/card.rb
102
+ - lib/models/transaction.rb
103
+ - lib/runner.rb
104
+ - lib/schema.rb
105
+ - lib/tap_clutch/version.rb
106
+ homepage: https://github.com/Follain/tap-clutch
107
+ licenses:
108
+ - MIT
109
+ metadata: {}
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubyforge_project:
126
+ rubygems_version: 2.5.2
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: Singer.io tap for Clutch Loyalty
130
+ test_files: []