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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +7 -2
  3. data/Gemfile.lock +10 -6
  4. data/LICENSE +20 -0
  5. data/README.md +80 -22
  6. data/lib/synapse_pay_rest.rb +65 -21
  7. data/lib/synapse_pay_rest/api/nodes.rb +93 -19
  8. data/lib/synapse_pay_rest/api/transactions.rb +103 -0
  9. data/lib/synapse_pay_rest/api/users.rb +101 -41
  10. data/lib/synapse_pay_rest/client.rb +49 -0
  11. data/lib/synapse_pay_rest/error.rb +8 -2
  12. data/lib/synapse_pay_rest/http_client.rb +94 -27
  13. data/lib/synapse_pay_rest/models/node/ach_us_node.rb +111 -0
  14. data/lib/synapse_pay_rest/models/node/base_node.rb +192 -0
  15. data/lib/synapse_pay_rest/models/node/eft_ind_node.rb +19 -0
  16. data/lib/synapse_pay_rest/models/node/eft_node.rb +27 -0
  17. data/lib/synapse_pay_rest/models/node/eft_np_node.rb +19 -0
  18. data/lib/synapse_pay_rest/models/node/iou_node.rb +27 -0
  19. data/lib/synapse_pay_rest/models/node/node.rb +99 -0
  20. data/lib/synapse_pay_rest/models/node/reserve_us_node.rb +23 -0
  21. data/lib/synapse_pay_rest/models/node/synapse_ind_node.rb +22 -0
  22. data/lib/synapse_pay_rest/models/node/synapse_node.rb +25 -0
  23. data/lib/synapse_pay_rest/models/node/synapse_np_node.rb +22 -0
  24. data/lib/synapse_pay_rest/models/node/synapse_us_node.rb +23 -0
  25. data/lib/synapse_pay_rest/models/node/unverified_node.rb +73 -0
  26. data/lib/synapse_pay_rest/models/node/wire_int_node.rb +23 -0
  27. data/lib/synapse_pay_rest/models/node/wire_node.rb +38 -0
  28. data/lib/synapse_pay_rest/models/node/wire_us_node.rb +23 -0
  29. data/lib/synapse_pay_rest/models/transaction/transaction.rb +212 -0
  30. data/lib/synapse_pay_rest/models/user/base_document.rb +346 -0
  31. data/lib/synapse_pay_rest/models/user/document.rb +71 -0
  32. data/lib/synapse_pay_rest/models/user/physical_document.rb +29 -0
  33. data/lib/synapse_pay_rest/models/user/question.rb +45 -0
  34. data/lib/synapse_pay_rest/models/user/social_document.rb +7 -0
  35. data/lib/synapse_pay_rest/models/user/user.rb +593 -0
  36. data/lib/synapse_pay_rest/models/user/virtual_document.rb +77 -0
  37. data/lib/synapse_pay_rest/version.rb +2 -1
  38. data/samples.md +391 -219
  39. data/synapse_pay_rest.gemspec +13 -12
  40. metadata +78 -24
  41. 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