synapse_pay_rest 0.0.15 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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