synapse_pay_rest 0.0.15 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +7 -2
- data/Gemfile.lock +10 -6
- data/LICENSE +20 -0
- data/README.md +80 -22
- data/lib/synapse_pay_rest.rb +65 -21
- data/lib/synapse_pay_rest/api/nodes.rb +93 -19
- data/lib/synapse_pay_rest/api/transactions.rb +103 -0
- data/lib/synapse_pay_rest/api/users.rb +101 -41
- data/lib/synapse_pay_rest/client.rb +49 -0
- data/lib/synapse_pay_rest/error.rb +8 -2
- data/lib/synapse_pay_rest/http_client.rb +94 -27
- data/lib/synapse_pay_rest/models/node/ach_us_node.rb +111 -0
- data/lib/synapse_pay_rest/models/node/base_node.rb +192 -0
- data/lib/synapse_pay_rest/models/node/eft_ind_node.rb +19 -0
- data/lib/synapse_pay_rest/models/node/eft_node.rb +27 -0
- data/lib/synapse_pay_rest/models/node/eft_np_node.rb +19 -0
- data/lib/synapse_pay_rest/models/node/iou_node.rb +27 -0
- data/lib/synapse_pay_rest/models/node/node.rb +99 -0
- data/lib/synapse_pay_rest/models/node/reserve_us_node.rb +23 -0
- data/lib/synapse_pay_rest/models/node/synapse_ind_node.rb +22 -0
- data/lib/synapse_pay_rest/models/node/synapse_node.rb +25 -0
- data/lib/synapse_pay_rest/models/node/synapse_np_node.rb +22 -0
- data/lib/synapse_pay_rest/models/node/synapse_us_node.rb +23 -0
- data/lib/synapse_pay_rest/models/node/unverified_node.rb +73 -0
- data/lib/synapse_pay_rest/models/node/wire_int_node.rb +23 -0
- data/lib/synapse_pay_rest/models/node/wire_node.rb +38 -0
- data/lib/synapse_pay_rest/models/node/wire_us_node.rb +23 -0
- data/lib/synapse_pay_rest/models/transaction/transaction.rb +212 -0
- data/lib/synapse_pay_rest/models/user/base_document.rb +346 -0
- data/lib/synapse_pay_rest/models/user/document.rb +71 -0
- data/lib/synapse_pay_rest/models/user/physical_document.rb +29 -0
- data/lib/synapse_pay_rest/models/user/question.rb +45 -0
- data/lib/synapse_pay_rest/models/user/social_document.rb +7 -0
- data/lib/synapse_pay_rest/models/user/user.rb +593 -0
- data/lib/synapse_pay_rest/models/user/virtual_document.rb +77 -0
- data/lib/synapse_pay_rest/version.rb +2 -1
- data/samples.md +391 -219
- data/synapse_pay_rest.gemspec +13 -12
- metadata +78 -24
- data/lib/synapse_pay_rest/api/trans.rb +0 -38
@@ -0,0 +1,22 @@
|
|
1
|
+
module SynapsePayRest
|
2
|
+
# Represents a Synapse node allowing any user to hold Nepali Rupees.
|
3
|
+
class SynapseNpNode < SynapseNode
|
4
|
+
class << self
|
5
|
+
private
|
6
|
+
|
7
|
+
def payload_for_create(nickname:, **options)
|
8
|
+
args = {
|
9
|
+
type: 'SYNAPSE-NP',
|
10
|
+
nickname: nickname
|
11
|
+
}.merge(options)
|
12
|
+
payload = super(args)
|
13
|
+
# optional payload fields
|
14
|
+
extra = {}
|
15
|
+
extra['supp_id'] = options[:supp_id] if options[:supp_id]
|
16
|
+
extra['gateway_restricted'] = options[:gateway_restricted] if options[:gateway_restricted]
|
17
|
+
payload['extra'] = extra if extra.any?
|
18
|
+
payload
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module SynapsePayRest
|
2
|
+
# Represents a Synapse node allowing any user to hold funds. You can use this
|
3
|
+
# node as a wallet, an escrow account or something else along those lines.
|
4
|
+
class SynapseUsNode < SynapseNode
|
5
|
+
class << self
|
6
|
+
private
|
7
|
+
|
8
|
+
def payload_for_create(nickname:, **options)
|
9
|
+
args = {
|
10
|
+
type: 'SYNAPSE-US',
|
11
|
+
nickname: nickname
|
12
|
+
}.merge(options)
|
13
|
+
payload = super(args)
|
14
|
+
# optional payload fields
|
15
|
+
extra = {}
|
16
|
+
extra['supp_id'] = options[:supp_id] if options[:supp_id]
|
17
|
+
extra['gateway_restricted'] = options[:gateway_restricted] if options[:gateway_restricted]
|
18
|
+
payload['extra'] = extra if extra.any?
|
19
|
+
payload
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module SynapsePayRest
|
2
|
+
# Represents a node that has not yet been created due to pending bank login
|
3
|
+
# MFA questions.
|
4
|
+
class UnverifiedNode
|
5
|
+
# @!attribute [r] user
|
6
|
+
# @return [SynapsePayRest::User] the user to whom the node belongs
|
7
|
+
# @!attribute [r] mfa_access_token
|
8
|
+
# @return [String] access token that must be included in the response (handled automatically)
|
9
|
+
# @!attribute [r] mfa_message
|
10
|
+
# @return [String] question or MFA prompt from bank that must be answered
|
11
|
+
# @!attribute [r] mfa_verified
|
12
|
+
# @return [Boolean] whether the node is verified yet
|
13
|
+
# @todo should be mfa_verified? in Ruby idiom
|
14
|
+
attr_reader :user, :mfa_access_token, :mfa_message, :mfa_verified
|
15
|
+
|
16
|
+
def initialize(user:, mfa_access_token:, mfa_message:, mfa_verified:)
|
17
|
+
@user = user
|
18
|
+
@mfa_access_token = mfa_access_token
|
19
|
+
@mfa_message = mfa_message
|
20
|
+
@mfa_verified = mfa_verified
|
21
|
+
end
|
22
|
+
|
23
|
+
# Allows the user to submit an answer to the bank in response to mfa_message.
|
24
|
+
#
|
25
|
+
# @param answer [String] the user's answer to the mfa_message asked by the bank
|
26
|
+
#
|
27
|
+
# @raise [SynapsePayRest::Error] if incorrect answer
|
28
|
+
#
|
29
|
+
# @return [Array<SynapsePayRest::AchUsNode>,SynapsePayRest::UnverifiedNode] may contain multiple nodes if successful, else self if new MFA question to answer
|
30
|
+
#
|
31
|
+
# @todo make a new Error subclass for incorrect MFA
|
32
|
+
def answer_mfa(answer)
|
33
|
+
payload = payload_for_answer_mfa(answer: answer)
|
34
|
+
response = user.client.nodes.post(payload: payload)
|
35
|
+
|
36
|
+
handle_answer_mfa_response(response)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def payload_for_answer_mfa(answer:)
|
42
|
+
{
|
43
|
+
'access_token' => mfa_access_token,
|
44
|
+
'mfa_answer' => answer
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
# Determines whether the response is successful in verifying the node, has
|
49
|
+
# follow-up MFA questions, or failed with an incorrect answer.
|
50
|
+
#
|
51
|
+
# @todo Use Error#code instead of parsing the response for the code.
|
52
|
+
def handle_answer_mfa_response(response)
|
53
|
+
if response['error_code'] == '0'
|
54
|
+
# correct answer
|
55
|
+
@mfa_verified = true
|
56
|
+
AchUsNode.create_multiple_from_response(user, response['nodes'])
|
57
|
+
elsif response['error_code'] == '10' && response['mfa']['message'] == mfa_message
|
58
|
+
# wrong answer (mfa message the same), retry if allowed
|
59
|
+
args = {
|
60
|
+
message: 'incorrect bank login mfa answer',
|
61
|
+
code: response['http_code'],
|
62
|
+
response: response
|
63
|
+
}
|
64
|
+
raise SynapsePayRest::Error, args
|
65
|
+
elsif response['error_code'] == '10'
|
66
|
+
# new additional MFA question. need to call #answer_mfa with new answer
|
67
|
+
@mfa_access_token = response['mfa']['access_token']
|
68
|
+
@mfa_message = response['mfa']['message']
|
69
|
+
self
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module SynapsePayRest
|
2
|
+
# Represents a non-US account for wire payments.
|
3
|
+
class WireIntNode < WireNode
|
4
|
+
class << self
|
5
|
+
private
|
6
|
+
|
7
|
+
def payload_for_create(nickname:, bank_name:, account_number:, swift:,
|
8
|
+
name_on_account:, address:, **options)
|
9
|
+
args = {
|
10
|
+
type: 'WIRE-INT',
|
11
|
+
nickname: nickname,
|
12
|
+
bank_name: bank_name,
|
13
|
+
account_number: account_number,
|
14
|
+
name_on_account: name_on_account,
|
15
|
+
address: address
|
16
|
+
}.merge(options)
|
17
|
+
payload = super(args)
|
18
|
+
payload['info']['swift'] = swift
|
19
|
+
payload
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module SynapsePayRest
|
2
|
+
# Parent of all Wire nodes. Should not be instantiated directly.
|
3
|
+
#
|
4
|
+
# @todo Make this a module instead.
|
5
|
+
class WireNode < BaseNode
|
6
|
+
class << self
|
7
|
+
private
|
8
|
+
|
9
|
+
def payload_for_create(type:, nickname:, bank_name:, account_number:, address:,
|
10
|
+
name_on_account:, **options)
|
11
|
+
payload = {
|
12
|
+
'type' => type,
|
13
|
+
'info' => {
|
14
|
+
'nickname' => nickname,
|
15
|
+
'name_on_account' => name_on_account,
|
16
|
+
'account_num' => account_number,
|
17
|
+
'bank_name' => bank_name,
|
18
|
+
'address' => address,
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
# optional payload fields
|
23
|
+
payload['info']['routing_num'] = options[:routing_number] if options[:routing_number]
|
24
|
+
correspondent_info = {}
|
25
|
+
correspondent_info['routing_num'] = options[:correspondent_routing_number] if options[:correspondent_routing_number]
|
26
|
+
correspondent_info['bank_name'] = options[:correspondent_bank_name] if options[:correspondent_bank_name]
|
27
|
+
correspondent_info['address'] = options[:correspondent_address] if options[:correspondent_address]
|
28
|
+
correspondent_info['swift'] = options[:correspondent_swift] if options[:correspondent_swift]
|
29
|
+
payload['info']['correspondent_info'] = correspondent_info if correspondent_info.any?
|
30
|
+
extra = {}
|
31
|
+
extra['supp_id'] = options[:supp_id] if options[:supp_id]
|
32
|
+
extra['gateway_restricted'] = options[:gateway_restricted] if options[:gateway_restricted]
|
33
|
+
payload['extra'] = extra if extra.any?
|
34
|
+
payload
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module SynapsePayRest
|
2
|
+
# Represents a US bank account for processing wire payments.
|
3
|
+
class WireUsNode < WireNode
|
4
|
+
class << self
|
5
|
+
private
|
6
|
+
|
7
|
+
def payload_for_create(nickname:, bank_name:, account_number:, routing_number:,
|
8
|
+
name_on_account:, address:, **options)
|
9
|
+
args = {
|
10
|
+
type: 'WIRE-US',
|
11
|
+
nickname: nickname,
|
12
|
+
bank_name: bank_name,
|
13
|
+
account_number: account_number,
|
14
|
+
routing_number: routing_number,
|
15
|
+
name_on_account: name_on_account,
|
16
|
+
address: address
|
17
|
+
}.merge(options)
|
18
|
+
|
19
|
+
super(args)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
module SynapsePayRest
|
2
|
+
# Represents a transaction record and holds methods for constructing transaction instances
|
3
|
+
# from API calls. This is built on top of the SynapsePayRest::Transactions class and
|
4
|
+
# is intended to make it easier to use the API without knowing payload formats
|
5
|
+
# or knowledge of REST.
|
6
|
+
#
|
7
|
+
# @todo use mixins to remove duplication between Node and BaseNode. May be
|
8
|
+
# better to refactor this into a mixin altogether since this shouldn't be instantiated.
|
9
|
+
# @todo reduce duplicated logic between User/BaseNode/Transaction
|
10
|
+
class Transaction
|
11
|
+
|
12
|
+
# @!attribute [rw] node
|
13
|
+
# @return [SynapsePayRest::Node] the node to which the transaction belongs
|
14
|
+
attr_reader :node, :id, :amount, :currency, :client_id, :client_name, :created_on,
|
15
|
+
:ip, :latlon, :note, :process_on, :supp_id, :webhook, :fees,
|
16
|
+
:recent_status, :timeline, :from, :to, :to_type, :to_id,
|
17
|
+
:fee_amount, :fee_note, :fee_to_id
|
18
|
+
|
19
|
+
class << self
|
20
|
+
# Creates a new transaction in the API belonging to the provided node and
|
21
|
+
# returns a transaction instance from the response data.
|
22
|
+
#
|
23
|
+
# @param node [SynapsePayRest::BaseNode] node to which the transaction belongs
|
24
|
+
# @param to_id [String] node id of the receiving node
|
25
|
+
# @param to_type [String] node type of the receiving node
|
26
|
+
# @see https://docs.synapsepay.com/docs/node-resources valid node types
|
27
|
+
# @param amount [Float] 100.0 = $100.00 for example
|
28
|
+
# @param currency [String] e.g. 'USD'
|
29
|
+
# @param ip [String]
|
30
|
+
# @param note [String] (optional)
|
31
|
+
# @param process_in [Integer] (optional) days until processed (default/minimum 1)
|
32
|
+
# @param fee_amount [Float] (optional) fee amount to add to the transaction
|
33
|
+
# @param fee_to_id [String] (optional) node id to which to send the fee (must be SYNAPSE-US)
|
34
|
+
# @param fee_note [String] (optional)
|
35
|
+
# @param supp_id [String] (optional)
|
36
|
+
#
|
37
|
+
# @raise [SynapsePayRest::Error] if HTTP error or invalid argument format
|
38
|
+
#
|
39
|
+
# @return [SynapsePayRest::Transaction]
|
40
|
+
#
|
41
|
+
# @todo allow either to_node or to_type/to_id
|
42
|
+
# @todo allow node to be entered as alternative to fee_to node
|
43
|
+
# @todo validate if fee_to node is synapse-us
|
44
|
+
# @todo allow multiple fees
|
45
|
+
def create(node:, to_type:, to_id:, amount:, currency:, ip:, **options)
|
46
|
+
raise ArgumentError, 'cannot create a transaction with an UnverifiedNode' if node.is_a?(UnverifiedNode)
|
47
|
+
raise ArgumentError, 'node must be a type of BaseNode object' unless node.is_a?(BaseNode)
|
48
|
+
raise ArgumentError, 'amount must be a Numeric (Integer or Float)' unless amount.is_a?(Numeric)
|
49
|
+
[to_type, to_id, currency, ip].each do |arg|
|
50
|
+
raise ArgumentError, "#{arg} must be a String" unless arg.is_a?(String)
|
51
|
+
end
|
52
|
+
|
53
|
+
payload = payload_for_create(node: node, to_type: to_type, to_id: to_id,
|
54
|
+
amount: amount, currency: currency, ip: ip, **options)
|
55
|
+
node.user.authenticate
|
56
|
+
response = node.user.client.trans.create(node_id: node.id, payload: payload)
|
57
|
+
create_from_response(node, response)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Queries the API for a transaction belonging to the supplied node by transaction id
|
61
|
+
# and returns a Transaction instance if found.
|
62
|
+
#
|
63
|
+
# @param node [SynapsePayRest::BaseNode] node to which the transaction belongs
|
64
|
+
# @param id [String] id of the transaction to find
|
65
|
+
#
|
66
|
+
# @raise [SynapsePayRest::Error] if not found or other HTTP error
|
67
|
+
#
|
68
|
+
# @return [SynapsePayRest::Transaction]
|
69
|
+
def find(node:, id:)
|
70
|
+
raise ArgumentError, 'node must be a type of BaseNode object' unless node.is_a?(BaseNode)
|
71
|
+
raise ArgumentError, 'id must be a String' unless id.is_a?(String)
|
72
|
+
|
73
|
+
node.user.authenticate
|
74
|
+
response = node.user.client.trans.get(node_id: node.id, trans_id: id)
|
75
|
+
create_from_response(node, response)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Queries the API for all transactions belonging to the supplied node and returns
|
79
|
+
# them as Transaction instances.
|
80
|
+
#
|
81
|
+
# @param node [SynapsePayRest::BaseNode] node to which the transaction belongs
|
82
|
+
# @param page [String,Integer] (optional) response will default to 1
|
83
|
+
# @param per_page [String,Integer] (optional) response will default to 20
|
84
|
+
#
|
85
|
+
# @raise [SynapsePayRest::Error]
|
86
|
+
#
|
87
|
+
# @return [Array<SynapsePayRest::Transaction>]
|
88
|
+
def all(node:, page: nil, per_page: nil)
|
89
|
+
raise ArgumentError, 'node must be a type of BaseNode object' unless node.is_a?(BaseNode)
|
90
|
+
[page, per_page].each do |arg|
|
91
|
+
if arg && (!arg.is_a?(Integer) || arg < 1)
|
92
|
+
raise ArgumentError, "#{arg} must be nil or an Integer >= 1"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
node.user.authenticate
|
97
|
+
response = node.user.client.trans.get(node_id: node.id, page: page, per_page: per_page)
|
98
|
+
create_multiple_from_response(node, response['trans'])
|
99
|
+
end
|
100
|
+
|
101
|
+
# Creates a Transaction from a response hash.
|
102
|
+
#
|
103
|
+
# @note Shouldn't need to call this directly.
|
104
|
+
#
|
105
|
+
# @todo convert the nodes and users in response into User/Node objects
|
106
|
+
# @todo rework to handle multiple fees
|
107
|
+
def create_from_response(node, response)
|
108
|
+
args = {
|
109
|
+
node: node,
|
110
|
+
id: response['_id'],
|
111
|
+
amount: response['amount']['amount'],
|
112
|
+
currency: response['amount']['currency'],
|
113
|
+
client_id: response['client']['id'],
|
114
|
+
client_name: response['client']['name'],
|
115
|
+
created_on: response['extra']['created_on'],
|
116
|
+
ip: response['extra']['ip'],
|
117
|
+
latlon: response['extra']['latlon'],
|
118
|
+
note: response['extra']['note'],
|
119
|
+
process_on: response['extra']['process_on'],
|
120
|
+
supp_id: response['extra']['supp_id'],
|
121
|
+
webhook: response['extra']['webhook'],
|
122
|
+
fees: response['fees'],
|
123
|
+
recent_status: response['recent_status'],
|
124
|
+
timeline: response['timeline'],
|
125
|
+
from: response['from'],
|
126
|
+
to: response['to'],
|
127
|
+
to_type: response['to']['type'],
|
128
|
+
to_id: response['to']['id'],
|
129
|
+
fee_amount: response['fees'].last['fee'],
|
130
|
+
fee_note: response['fees'].last['note'],
|
131
|
+
fee_to_id: response['fees'].last['to']['id'],
|
132
|
+
}
|
133
|
+
self.new(args)
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def payload_for_create(node:, to_type:, to_id:, amount:, currency:, ip:,
|
139
|
+
**options)
|
140
|
+
payload = {
|
141
|
+
'to' => {
|
142
|
+
'type' => to_type,
|
143
|
+
'id' => to_id
|
144
|
+
},
|
145
|
+
'amount' => {
|
146
|
+
'amount' => amount,
|
147
|
+
'currency' => currency
|
148
|
+
},
|
149
|
+
'extra' => {
|
150
|
+
'ip' => ip
|
151
|
+
}
|
152
|
+
}
|
153
|
+
# optional payload fields
|
154
|
+
payload['extra']['supp_id'] = options[:supp_id] if options[:supp_id]
|
155
|
+
payload['extra']['note'] = options[:note] if options[:note]
|
156
|
+
payload['extra']['process_on'] = options[:process_in] if options[:process_in]
|
157
|
+
other = {}
|
158
|
+
other['attachments'] = options[:attachments] if options[:attachments]
|
159
|
+
payload['other'] = other if other.any?
|
160
|
+
fees = []
|
161
|
+
fee = {}
|
162
|
+
fee['fee'] = options[:fee_amount] if options[:fee_amount]
|
163
|
+
fee['note'] = options[:fee_note] if options[:fee_note]
|
164
|
+
fee_to = {}
|
165
|
+
fee_to['id'] = options[:fee_to_id] if options[:fee_to_id]
|
166
|
+
fee['to'] = fee_to if fee_to.any?
|
167
|
+
fees << fee if fee.any?
|
168
|
+
payload['fees'] = fees if fees.any?
|
169
|
+
payload
|
170
|
+
end
|
171
|
+
|
172
|
+
def create_multiple_from_response(node, response)
|
173
|
+
return [] if response.empty?
|
174
|
+
response.map { |trans_data| create_from_response(node, trans_data) }
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# @note Do not call directly. Use Transaction.create or other class
|
179
|
+
# method to instantiate via API action.
|
180
|
+
def initialize(**options)
|
181
|
+
options.each { |key, value| instance_variable_set("@#{key}", value) }
|
182
|
+
end
|
183
|
+
|
184
|
+
# Adds a comment to the transaction's timeline/recent_status fields.
|
185
|
+
#
|
186
|
+
# @param comment [String]
|
187
|
+
#
|
188
|
+
# @raise [SynapsePayRest::Error]
|
189
|
+
#
|
190
|
+
# @return [Array<SynapsePayRest::Transaction>] (self)
|
191
|
+
def add_comment(comment)
|
192
|
+
payload = {'comment': comment}
|
193
|
+
response = node.user.client.trans.update(node_id: node.id, trans_id: id, payload: payload)
|
194
|
+
self.class.create_from_response(node, response['trans'])
|
195
|
+
end
|
196
|
+
|
197
|
+
# Cancels this transaction if it has not already settled.
|
198
|
+
#
|
199
|
+
# @raise [SynapsePayRest::Error]
|
200
|
+
#
|
201
|
+
# @return [Array<SynapsePayRest::Transaction>] (self)
|
202
|
+
def cancel
|
203
|
+
response = node.user.client.trans.delete(node_id: node.id, trans_id: id)
|
204
|
+
self.class.create_from_response(node, response)
|
205
|
+
end
|
206
|
+
|
207
|
+
# Checks if two Transaction instances have same id (different instances of same record).
|
208
|
+
def ==(other)
|
209
|
+
other.instance_of?(self.class) && !id.nil? && id == other.id
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,346 @@
|
|
1
|
+
module SynapsePayRest
|
2
|
+
# Stores info on the base document portion (personal/business info) of the CIP
|
3
|
+
# document and also manages physical/social/virtual documents.
|
4
|
+
class BaseDocument
|
5
|
+
# @!attribute [rw] user
|
6
|
+
# @return [SynapsePayRest::User] the user to whom the transaction belongs
|
7
|
+
# @!attribute [r] permission_scope
|
8
|
+
# @return [String] https://docs.synapsepay.com/docs/user-resources#section-document-permission-scope
|
9
|
+
attr_accessor :user, :email, :phone_number, :ip, :name, :aka, :entity_type,
|
10
|
+
:entity_scope, :birth_day, :birth_month, :birth_year,
|
11
|
+
:address_street, :address_city, :address_subdivision,
|
12
|
+
:address_postal_code, :address_country_code,
|
13
|
+
:physical_documents, :social_documents, :virtual_documents
|
14
|
+
attr_reader :id, :permission_scope
|
15
|
+
|
16
|
+
class << self
|
17
|
+
# Creates a new base document in the API belonging to the provided user and
|
18
|
+
# returns a base document instance from the response data.
|
19
|
+
#
|
20
|
+
# @param user [SynapsePayRest::User] the user to whom the base document belongs
|
21
|
+
# @param email [String]
|
22
|
+
# @param phone_number [String]
|
23
|
+
# @param ip [String]
|
24
|
+
# @param name [String]
|
25
|
+
# @param aka [String] corresponds to 'alias' in docs, use name if no alias
|
26
|
+
# @param entity_type [String] consult your organization's CIP for valid options
|
27
|
+
# @see https://docs.synapsepay.com/docs/user-resources#section-supported-entity-types all supported entity_type values
|
28
|
+
# @param entity_scope [String] consult your organization's CIP for valid options
|
29
|
+
# @see https://docs.synapsepay.com/docs/user-resources#section-supported-entity-scope all entity_scope options
|
30
|
+
# @param birth_day [Integer]
|
31
|
+
# @param birth_month [Integer]
|
32
|
+
# @param birth_year [Integer]
|
33
|
+
# @param address_street [String]
|
34
|
+
# @param address_city [String]
|
35
|
+
# @param address_subdivision [String]
|
36
|
+
# @param address_postal_code [String]
|
37
|
+
# @param address_country_code [String]
|
38
|
+
# @param physical_documents [Array<SynapsePayRest::PhysicalDocument>] (optional)
|
39
|
+
# @param social_documents [Array<SynapsePayRest::SocialDocument>] (optional)
|
40
|
+
# @param virtual_documents [Array<SynapsePayRest::VirtualDocument>] (optional)
|
41
|
+
#
|
42
|
+
# @raise [SynapsePayRest::Error]
|
43
|
+
#
|
44
|
+
# @return [SynapsePayRest::BaseDocument] new instance with updated info
|
45
|
+
def create(user:, email:, phone_number:, ip:, name:,
|
46
|
+
aka:, entity_type:, entity_scope:, birth_day:, birth_month:, birth_year:,
|
47
|
+
address_street:, address_city:, address_subdivision:, address_postal_code:,
|
48
|
+
address_country_code:, physical_documents: [], social_documents: [],
|
49
|
+
virtual_documents: [])
|
50
|
+
raise ArgumentError, 'user must be a User object' unless user.is_a?(User)
|
51
|
+
[email, phone_number, ip, name, aka, entity_type, entity_scope,
|
52
|
+
address_street, address_city, address_subdivision, address_postal_code,
|
53
|
+
address_country_code].each do |arg|
|
54
|
+
raise ArgumentError, "#{arg} must be a String" unless arg.is_a?(String)
|
55
|
+
end
|
56
|
+
[physical_documents, social_documents, virtual_documents].each do |arg|
|
57
|
+
raise ArgumentError, "#{arg} must be an Array" unless arg.is_a?(Array)
|
58
|
+
end
|
59
|
+
unless physical_documents.empty? || physical_documents.first.is_a?(PhysicalDocument)
|
60
|
+
raise ArgumentError, 'physical_documents be empty or contain PhysicalDocument(s)'
|
61
|
+
end
|
62
|
+
unless social_documents.empty? || social_documents.first.is_a?(SocialDocument)
|
63
|
+
raise ArgumentError, 'social_documents be empty or contain SocialDocument(s)'
|
64
|
+
end
|
65
|
+
unless virtual_documents.empty? || virtual_documents.first.is_a?(VirtualDocument)
|
66
|
+
raise ArgumentError, 'virtual_documents be empty or contain VirtualDocument(s)'
|
67
|
+
end
|
68
|
+
|
69
|
+
base_document = BaseDocument.new(
|
70
|
+
user: user,
|
71
|
+
email: email,
|
72
|
+
phone_number: phone_number,
|
73
|
+
ip: ip,
|
74
|
+
name: name,
|
75
|
+
aka: aka,
|
76
|
+
entity_type: entity_type,
|
77
|
+
entity_scope: entity_scope,
|
78
|
+
birth_day: birth_day,
|
79
|
+
birth_month: birth_month,
|
80
|
+
birth_year: birth_year,
|
81
|
+
address_street: address_street,
|
82
|
+
address_city: address_city,
|
83
|
+
address_subdivision: address_subdivision,
|
84
|
+
address_postal_code: address_postal_code,
|
85
|
+
address_country_code: address_country_code,
|
86
|
+
physical_documents: physical_documents,
|
87
|
+
social_documents: social_documents,
|
88
|
+
virtual_documents: virtual_documents
|
89
|
+
)
|
90
|
+
|
91
|
+
base_document.submit
|
92
|
+
end
|
93
|
+
|
94
|
+
# Parses multiple base_documents from response
|
95
|
+
# @note Do not call directly (it's automatic).
|
96
|
+
def create_from_response(user, response)
|
97
|
+
base_documents_data = response['documents']
|
98
|
+
base_documents_data.map do |base_document_data|
|
99
|
+
physical_docs = base_document_data['physical_docs'].map do |data|
|
100
|
+
doc = PhysicalDocument.create_from_response(data)
|
101
|
+
doc.base_document = self
|
102
|
+
doc
|
103
|
+
end
|
104
|
+
social_docs = base_document_data['social_docs'].map do |data|
|
105
|
+
doc = SocialDocument.create_from_response(data)
|
106
|
+
doc.base_document = self
|
107
|
+
doc
|
108
|
+
end
|
109
|
+
virtual_docs = base_document_data['virtual_docs'].map do |data|
|
110
|
+
doc = VirtualDocument.create_from_response(data)
|
111
|
+
doc.base_document = self
|
112
|
+
doc
|
113
|
+
end
|
114
|
+
|
115
|
+
args = {
|
116
|
+
user: user,
|
117
|
+
id: base_document_data['id'],
|
118
|
+
name: base_document_data['name'],
|
119
|
+
permission_scope: base_document_data['permission_scope'],
|
120
|
+
physical_documents: physical_docs,
|
121
|
+
social_documents: social_docs,
|
122
|
+
virtual_documents: virtual_docs
|
123
|
+
}
|
124
|
+
|
125
|
+
base_doc = self.new(args)
|
126
|
+
[physical_docs, social_docs, virtual_docs].flatten.each do |doc|
|
127
|
+
doc.base_document = base_doc
|
128
|
+
end
|
129
|
+
|
130
|
+
base_doc
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# @note It should not be necessary to call this method directly.
|
136
|
+
def initialize(**args)
|
137
|
+
@id = args[:id]
|
138
|
+
@permission_scope = args[:permission_scope]
|
139
|
+
@user = args[:user]
|
140
|
+
@email = args[:email]
|
141
|
+
@phone_number = args[:phone_number]
|
142
|
+
@ip = args[:ip]
|
143
|
+
@name = args[:name]
|
144
|
+
@aka = args[:aka]
|
145
|
+
@entity_type = args[:entity_type]
|
146
|
+
@entity_scope = args[:entity_scope]
|
147
|
+
@birth_day = args[:birth_day]
|
148
|
+
@birth_month = args[:birth_month]
|
149
|
+
@birth_year = args[:birth_year]
|
150
|
+
@address_street = args[:address_street]
|
151
|
+
@address_city = args[:address_city]
|
152
|
+
@address_subdivision = args[:address_subdivision]
|
153
|
+
@address_postal_code = args[:address_postal_code]
|
154
|
+
@address_country_code = args[:address_country_code]
|
155
|
+
@physical_documents = args[:physical_documents]
|
156
|
+
@social_documents = args[:social_documents]
|
157
|
+
@virtual_documents = args[:virtual_documents]
|
158
|
+
end
|
159
|
+
|
160
|
+
# Submits the base document to the API.
|
161
|
+
# @note It should not be necessary to call this method directly.
|
162
|
+
#
|
163
|
+
# @raise [SynapsePayRest::Error]
|
164
|
+
#
|
165
|
+
# @return [SynapsePayRest::BaseDocument] new instance with updated info (id will be different if email or phone changed)
|
166
|
+
def submit
|
167
|
+
user.authenticate
|
168
|
+
response = user.client.users.update(payload: payload_for_submit)
|
169
|
+
@user = User.create_from_response(user.client, response)
|
170
|
+
|
171
|
+
if id
|
172
|
+
# return updated version of self
|
173
|
+
user.base_documents.find { |doc| doc.id == id }
|
174
|
+
else
|
175
|
+
# first time submission, assume last doc is updated version of self
|
176
|
+
require 'pry';
|
177
|
+
user.base_documents.last
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Updates the supplied fields in the base document. See #create for valid
|
182
|
+
#
|
183
|
+
# @param email [String] (optional)
|
184
|
+
# @param phone_number [String] (optional)
|
185
|
+
# @param ip [String] (optional)
|
186
|
+
# @param name [String] (optional)
|
187
|
+
# @param aka [String] (optional) corresponds to 'alias' in docs, use name if no alias
|
188
|
+
# @param entity_type [String] (optional) consult your organization's CIP for valid options
|
189
|
+
# @see https://docs.synapsepay.com/docs/user-resources#section-supported-entity-types all supported entity_type values
|
190
|
+
# @param entity_scope [String] (optional) consult your organization's CIP for valid options
|
191
|
+
# @see https://docs.synapsepay.com/docs/user-resources#section-supported-entity-scope all entity_scope options
|
192
|
+
# @param birth_day [Integer] (optional)
|
193
|
+
# @param birth_month [Integer] (optional)
|
194
|
+
# @param birth_year [Integer] (optional)
|
195
|
+
# @param address_street [String] (optional)
|
196
|
+
# @param address_city [String] (optional)
|
197
|
+
# @param address_subdivision [String] (optional)
|
198
|
+
# @param address_postal_code [String] (optional)
|
199
|
+
# @param address_country_code [String] (optional)
|
200
|
+
# @param physical_documents [Array<SynapsePayRest::PhysicalDocument>] (optional)
|
201
|
+
# @param social_documents [Array<SynapsePayRest::SocialDocument>] (optional)
|
202
|
+
# @param virtual_documents [Array<SynapsePayRest::VirtualDocument>] (optional)
|
203
|
+
#
|
204
|
+
# @raise [SynapsePayRest::Error]
|
205
|
+
#
|
206
|
+
# @return [SynapsePayRest::BaseDocument] new instance with updated info
|
207
|
+
#
|
208
|
+
# @todo validate changes are valid fields in base_document
|
209
|
+
# (or make other methods more like this)
|
210
|
+
def update(**changes)
|
211
|
+
if changes.empty?
|
212
|
+
raise ArgumentError, 'must provide some key-value pairs to update'
|
213
|
+
end
|
214
|
+
user.authenticate
|
215
|
+
payload = payload_for_update(changes)
|
216
|
+
response = user.client.users.update(payload: payload)
|
217
|
+
@user = User.create_from_response(user.client, response)
|
218
|
+
|
219
|
+
if id
|
220
|
+
# return updated version of self
|
221
|
+
return user.base_documents.find { |doc| doc.id == id }
|
222
|
+
else
|
223
|
+
# first time submission, assume last doc is updated version of self
|
224
|
+
return user.base_documents.last
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Adds one or more physical documents to the base document and submits
|
229
|
+
# them to the API using KYC 2.0 endpoints.
|
230
|
+
#
|
231
|
+
# @param documents [Array<SynapsePayRest::PhysicalDocument>]
|
232
|
+
#
|
233
|
+
# @raise [SynapsePayRest::Error]
|
234
|
+
#
|
235
|
+
# @return [SynapsePayRest::BaseDocument] new instance with updated info
|
236
|
+
def add_physical_documents(documents)
|
237
|
+
raise ArgumentError, 'must be an Array' unless documents.is_a?(Array)
|
238
|
+
unless documents.first.is_a?(PhysicalDocument)
|
239
|
+
raise ArgumentError, 'must contain a PhysicalDocument'
|
240
|
+
end
|
241
|
+
|
242
|
+
update(physical_documents: documents)
|
243
|
+
end
|
244
|
+
|
245
|
+
# Adds one or more social documents to the base document and submits
|
246
|
+
# them to the API using KYC 2.0 endpoints.
|
247
|
+
#
|
248
|
+
# @param documents [Array<SynapsePayRest::SocialDocument>]
|
249
|
+
#
|
250
|
+
# @raise [SynapsePayRest::Error]
|
251
|
+
#
|
252
|
+
# @return [SynapsePayRest::BaseDocument] new instance with updated info
|
253
|
+
def add_social_documents(documents)
|
254
|
+
raise ArgumentError, 'must be an Array' unless documents.is_a?(Array)
|
255
|
+
unless documents.first.is_a?(SocialDocument)
|
256
|
+
raise ArgumentError, 'must contain a SocialDocument'
|
257
|
+
end
|
258
|
+
|
259
|
+
update(social_documents: documents)
|
260
|
+
end
|
261
|
+
|
262
|
+
# Adds one or more virtual documents to the base document and submits
|
263
|
+
# them to the API using KYC 2.0 endpoints.
|
264
|
+
#
|
265
|
+
# @param documents [Array<SynapsePayRest::VirtualDocument>]
|
266
|
+
#
|
267
|
+
# @raise [SynapsePayRest::Error]
|
268
|
+
#
|
269
|
+
# @return [SynapsePayRest::BaseDocument] new instance with updated info
|
270
|
+
def add_virtual_documents(documents)
|
271
|
+
raise ArgumentError, 'must be an Array' unless documents.is_a?(Array)
|
272
|
+
unless documents.first.is_a?(VirtualDocument)
|
273
|
+
raise ArgumentError, 'must contain a VirtualDocument'
|
274
|
+
end
|
275
|
+
|
276
|
+
update(virtual_documents: documents)
|
277
|
+
end
|
278
|
+
|
279
|
+
# Checks if two BaseDocument instances have same id (different instances of same record).
|
280
|
+
def ==(other)
|
281
|
+
other.instance_of?(self.class) && !id.nil? && id == other.id
|
282
|
+
end
|
283
|
+
|
284
|
+
private
|
285
|
+
|
286
|
+
def payload_for_submit
|
287
|
+
payload = {
|
288
|
+
'documents' => [{
|
289
|
+
'email' => email,
|
290
|
+
'phone_number' => phone_number,
|
291
|
+
'ip' => ip,
|
292
|
+
'name' => name,
|
293
|
+
'alias' => aka,
|
294
|
+
'entity_type' => entity_type,
|
295
|
+
'entity_scope' => entity_scope,
|
296
|
+
'day' => birth_day,
|
297
|
+
'month' => birth_month,
|
298
|
+
'year' => birth_year,
|
299
|
+
'address_street' => address_street,
|
300
|
+
'address_city' => address_city,
|
301
|
+
'address_subdivision' => address_subdivision,
|
302
|
+
'address_postal_code' => address_postal_code,
|
303
|
+
'address_country_code' => address_country_code
|
304
|
+
}]
|
305
|
+
}
|
306
|
+
|
307
|
+
unless physical_documents.empty?
|
308
|
+
payload['documents'].first['physical_docs'] = physical_documents.map(&:to_hash)
|
309
|
+
end
|
310
|
+
|
311
|
+
unless social_documents.empty?
|
312
|
+
payload['documents'].first['social_docs'] = social_documents.map(&:to_hash)
|
313
|
+
end
|
314
|
+
|
315
|
+
unless virtual_documents.empty?
|
316
|
+
payload['documents'].first['virtual_docs'] = virtual_documents.map(&:to_hash)
|
317
|
+
end
|
318
|
+
|
319
|
+
payload
|
320
|
+
end
|
321
|
+
|
322
|
+
def payload_for_update(changes)
|
323
|
+
payload = {
|
324
|
+
'documents' => [{
|
325
|
+
'id' => id
|
326
|
+
}]
|
327
|
+
}
|
328
|
+
|
329
|
+
changes.each do |field, new_value|
|
330
|
+
# convert docs to their hash format for payload
|
331
|
+
if field == :physical_documents
|
332
|
+
payload['documents'].first['physical_docs'] = new_value.map(&:to_hash)
|
333
|
+
elsif field == :social_documents
|
334
|
+
payload['documents'].first['social_docs'] = new_value.map(&:to_hash)
|
335
|
+
elsif field == :virtual_documents
|
336
|
+
payload['documents'].first['virtual_docs'] = new_value.map(&:to_hash)
|
337
|
+
else
|
338
|
+
# insert non-document fields into payload
|
339
|
+
payload['documents'].first[field.to_s] = new_value
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
payload
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|