instant_quote 1.0.5
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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +17 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +110 -0
- data/README.md +31 -0
- data/Rakefile +8 -0
- data/bin/console +30 -0
- data/bin/setup +9 -0
- data/instant_quote.gemspec +41 -0
- data/lib/instant_quote/adapter.rb +43 -0
- data/lib/instant_quote/adapter_finder.rb +75 -0
- data/lib/instant_quote/adapters/capital_on_tap.rb +80 -0
- data/lib/instant_quote/adapters/iwoca.rb +104 -0
- data/lib/instant_quote/api_error.rb +18 -0
- data/lib/instant_quote/connection_translator.rb +20 -0
- data/lib/instant_quote/connection_translators/capital_on_tap.rb +79 -0
- data/lib/instant_quote/connection_translators/iwoca.rb +151 -0
- data/lib/instant_quote/decision_parser.rb +44 -0
- data/lib/instant_quote/decision_parsers/capital_on_tap.rb +65 -0
- data/lib/instant_quote/decision_parsers/iwoca.rb +112 -0
- data/lib/instant_quote/version.rb +5 -0
- data/lib/instant_quote/webhooks/iwoca.rb +77 -0
- data/lib/instant_quote.rb +37 -0
- metadata +223 -0
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'iwoca'
|
4
|
+
require 'instant_quote/decision_parsers/iwoca'
|
5
|
+
|
6
|
+
module InstantQuote
|
7
|
+
module Adapters
|
8
|
+
class Iwoca < Adapter
|
9
|
+
additional_fields [
|
10
|
+
{
|
11
|
+
name: 'last_full_year_turnover',
|
12
|
+
label: 'Last full year turnover',
|
13
|
+
required: true,
|
14
|
+
type: 'money'
|
15
|
+
},
|
16
|
+
{
|
17
|
+
name: 'applicant_date_of_birth',
|
18
|
+
label: 'Applicant Date of Birth',
|
19
|
+
required: true,
|
20
|
+
type: 'date'
|
21
|
+
},
|
22
|
+
{
|
23
|
+
name: 'applicant_house_number',
|
24
|
+
label: 'Applicant house number',
|
25
|
+
required: true
|
26
|
+
},
|
27
|
+
{
|
28
|
+
name: 'applicant_address_line_1',
|
29
|
+
label: 'Applicant home address',
|
30
|
+
required: true
|
31
|
+
},
|
32
|
+
{
|
33
|
+
name: 'applicant_town',
|
34
|
+
label: 'Town',
|
35
|
+
required: true
|
36
|
+
},
|
37
|
+
{
|
38
|
+
name: 'applicant_postcode',
|
39
|
+
label: 'Postcode',
|
40
|
+
required: true
|
41
|
+
},
|
42
|
+
{
|
43
|
+
name: 'applicant_country',
|
44
|
+
label: 'Country',
|
45
|
+
type: 'country'
|
46
|
+
},
|
47
|
+
{
|
48
|
+
name: 'applicant_phone_number',
|
49
|
+
label: 'Applicant mobile number',
|
50
|
+
required: true
|
51
|
+
},
|
52
|
+
{
|
53
|
+
name: 'applicant_consent',
|
54
|
+
label: "I agree to iwoca running soft credit checks on my personal information with \
|
55
|
+
credit reference agencies. <b>This will not affect my credit score</b> - I understand \
|
56
|
+
that iwoca will do a full credit search if I take funding. I also agree to iwoca’s \
|
57
|
+
<a href='https://www.iwoca.co.uk/terms-of-use/' target='_blank'> \
|
58
|
+
Terms and Conditions</a> and \
|
59
|
+
<a href='https://www.iwoca.co.uk/privacy-policy/' target='_blank'>Privacy Policy</a>.",
|
60
|
+
required: true,
|
61
|
+
type: 'checkbox'
|
62
|
+
}
|
63
|
+
]
|
64
|
+
|
65
|
+
# Creates the iwoca customer and returns the state key from the response.
|
66
|
+
def get_quote(params)
|
67
|
+
response = ::Iwoca::Quote.create_customer(params)
|
68
|
+
|
69
|
+
return response.data[:state_key] if response.success?
|
70
|
+
|
71
|
+
raise_error(response)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Get the current status for the created customer.
|
75
|
+
def get_status(state_key)
|
76
|
+
response = ::Iwoca::Quote.credit_facility_status(state_key)
|
77
|
+
|
78
|
+
return response.data if response.success?
|
79
|
+
|
80
|
+
raise_error(response)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Gets the URL for the given state key.
|
84
|
+
def get_link(state_key)
|
85
|
+
response = ::Iwoca::Quote.login_link(state_key)
|
86
|
+
|
87
|
+
return response.data[:login_link] if response.success?
|
88
|
+
|
89
|
+
raise_error(response)
|
90
|
+
end
|
91
|
+
|
92
|
+
def get_approval(state_key)
|
93
|
+
response = ::Iwoca::Quote.approval(state_key)
|
94
|
+
response.success?
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def raise_error(response)
|
100
|
+
raise ApiError.new(data: response.data, error: response.errors.first[:detail])
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module InstantQuote
|
4
|
+
class ApiError < StandardError
|
5
|
+
attr_accessor :error, :data
|
6
|
+
|
7
|
+
alias message error
|
8
|
+
alias to_s error
|
9
|
+
|
10
|
+
def initialize(params = {})
|
11
|
+
@error = params[:error]
|
12
|
+
@data = params[:data] || {}
|
13
|
+
|
14
|
+
# Delete the metaData key, because it's irrelevant and has long strings
|
15
|
+
@data.delete(:metaData) if @data.key?(:metaData)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module InstantQuote
|
4
|
+
class ConnectionTranslator
|
5
|
+
attr_accessor :application, :extra_info
|
6
|
+
|
7
|
+
def self.translate(connection)
|
8
|
+
new(connection).translate
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(connection)
|
12
|
+
@application = connection.finance_application
|
13
|
+
@extra_info = connection.extra_info
|
14
|
+
end
|
15
|
+
|
16
|
+
def translate
|
17
|
+
raise NotImplementedError, "You must implement '#{self.class.name}#translate'"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module InstantQuote
|
4
|
+
module ConnectionTranslators
|
5
|
+
class CapitalOnTap < ConnectionTranslator
|
6
|
+
DEFAULT_SALUTATION = 'Mr'
|
7
|
+
|
8
|
+
# rubocop:disable Metrics/AbcSize
|
9
|
+
def translate
|
10
|
+
{
|
11
|
+
Salutation: app_user.salutation || DEFAULT_SALUTATION,
|
12
|
+
FirstName: app_user.first_name,
|
13
|
+
LastName: app_user.last_name,
|
14
|
+
DateOfBirth: extra_info['date_of_birth'],
|
15
|
+
MobilePhone: extra_info['applicant_phone_number'],
|
16
|
+
EmailAddress: app_user.email,
|
17
|
+
PersonalAddress: translate_address,
|
18
|
+
TradingName: app_org.name,
|
19
|
+
BusinessLegalName: app_org.name,
|
20
|
+
BusinessLandline: extra_info['business_landline'],
|
21
|
+
YearsTrading: app_org.years_in_business,
|
22
|
+
MonthlyTurnOver: translate_turnover,
|
23
|
+
BusinessType: translate_business_type,
|
24
|
+
BusinessAddress: translate_address,
|
25
|
+
RegistrationNumber: app_org.company_number
|
26
|
+
}
|
27
|
+
end
|
28
|
+
# rubocop:enable Metrics/AbcSize
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def app_user
|
33
|
+
application.primary_user
|
34
|
+
end
|
35
|
+
|
36
|
+
def app_org
|
37
|
+
application.borrower_organisation
|
38
|
+
end
|
39
|
+
|
40
|
+
def translate_address
|
41
|
+
addr = app_org.address
|
42
|
+
|
43
|
+
return '' unless addr.present?
|
44
|
+
|
45
|
+
street_parts = [addr.line_1, addr.line_2, addr.line_3, addr.line_4]
|
46
|
+
|
47
|
+
{
|
48
|
+
CountryCode: 'UK',
|
49
|
+
Street: street_parts.compact.reject(&:empty?).join(', '),
|
50
|
+
PostCode: addr.postcode,
|
51
|
+
City: addr.town
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
# Since in Finpoint the user selects an intervel (i.e. "500,000 - 999,999"). We get the avg
|
56
|
+
# on this interval and use that number as the monthly turnover.
|
57
|
+
def translate_turnover
|
58
|
+
turnover = app_org.turnover_range
|
59
|
+
|
60
|
+
((turnover.from + turnover.to) / 2 / 100).ceil
|
61
|
+
end
|
62
|
+
|
63
|
+
def translate_business_type
|
64
|
+
name = app_org.company_type.name
|
65
|
+
|
66
|
+
case name
|
67
|
+
when /Sole/
|
68
|
+
'SoleTrader'
|
69
|
+
when /(LLP)/
|
70
|
+
'LimitedLiabilityPartnership'
|
71
|
+
when /(LTD)/
|
72
|
+
'LimitedCompany'
|
73
|
+
else
|
74
|
+
'Partnership'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module InstantQuote
|
4
|
+
module ConnectionTranslators
|
5
|
+
class Iwoca < ConnectionTranslator
|
6
|
+
def translate
|
7
|
+
{
|
8
|
+
data: {
|
9
|
+
application: {
|
10
|
+
company: company_information,
|
11
|
+
people: [person_information],
|
12
|
+
requested_products: {
|
13
|
+
credit_facility: {
|
14
|
+
approval: {
|
15
|
+
amount: (application.amount_pennies / 100),
|
16
|
+
duration: 12,
|
17
|
+
detailed_purpose: application&.proceeds_purpose&.name
|
18
|
+
}
|
19
|
+
}
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# rubocop:disable Metrics/AbcSize
|
29
|
+
def company_information
|
30
|
+
information = {
|
31
|
+
registered_company_name: company.name,
|
32
|
+
industry: company.industry.name,
|
33
|
+
company_number: company.company_number.to_s,
|
34
|
+
type: company_type_translated,
|
35
|
+
trading_from_date: company.business_starting_date,
|
36
|
+
registered_address: company_address,
|
37
|
+
vat_status: {
|
38
|
+
is_vat_registered: company.vat_registered
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
if company_type_translated == 'sole_trader'
|
43
|
+
information.merge(
|
44
|
+
last_12_months_profit: {
|
45
|
+
amount: extra_info['last_full_year_turnover'].to_f
|
46
|
+
}
|
47
|
+
)
|
48
|
+
else
|
49
|
+
information.merge(
|
50
|
+
last_12_months_turnover: {
|
51
|
+
amount: extra_info['last_full_year_turnover'].to_f
|
52
|
+
}
|
53
|
+
)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
# rubocop:enable Metrics/AbcSize
|
57
|
+
|
58
|
+
def company_type_translated
|
59
|
+
case company.company_type.name
|
60
|
+
when 'Limited Company (LTD)'
|
61
|
+
'limited_liability_company'
|
62
|
+
when 'Limited Partnership and Limited Liability Partnership (LLP)'
|
63
|
+
'limited_liability_partnership'
|
64
|
+
when 'Public Limited Company (PLC)'
|
65
|
+
'public_limited_company'
|
66
|
+
when 'Sole Trader'
|
67
|
+
'sole_trader'
|
68
|
+
else
|
69
|
+
'other'
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def company
|
74
|
+
application.borrower_organisation
|
75
|
+
end
|
76
|
+
|
77
|
+
# rubocop:disable Metrics/AbcSize
|
78
|
+
def company_address
|
79
|
+
{
|
80
|
+
postcode: company.address.postcode,
|
81
|
+
street_line_1: company.address.line_1,
|
82
|
+
street_line_2: company.address.line_2 || '',
|
83
|
+
town: company.address.town,
|
84
|
+
country: company.address.country
|
85
|
+
}
|
86
|
+
end
|
87
|
+
# rubocop:enable Metrics/AbcSize
|
88
|
+
|
89
|
+
def person_information
|
90
|
+
{
|
91
|
+
uid: SecureRandom.uuid,
|
92
|
+
first_name: application.primary_user.first_name,
|
93
|
+
last_name: application.primary_user.last_name,
|
94
|
+
date_of_birth: extra_info['applicant_date_of_birth'],
|
95
|
+
roles: %w[applicant shareholder guarantor director],
|
96
|
+
phones: [person_phone],
|
97
|
+
emails: [person_email],
|
98
|
+
residential_addresses: [person_residential_address],
|
99
|
+
privacy_policy: {
|
100
|
+
agreed: true,
|
101
|
+
datetime: DateTime.current
|
102
|
+
}
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
def person_phone
|
107
|
+
{
|
108
|
+
uid: SecureRandom.uuid,
|
109
|
+
number: extra_info['applicant_phone_number'],
|
110
|
+
type: 'primary'
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
def person_email
|
115
|
+
{
|
116
|
+
uid: SecureRandom.uuid,
|
117
|
+
email: application&.primary_user&.email,
|
118
|
+
type: 'primary'
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
122
|
+
def person_residential_address
|
123
|
+
{
|
124
|
+
uid: SecureRandom.uuid,
|
125
|
+
town: extra_info['applicant_town'],
|
126
|
+
street_line_1: extra_info['applicant_address_line_1'],
|
127
|
+
street_line_2: extra_info['applicant_address_line_2'] || '',
|
128
|
+
country: extra_info['applicant_country'],
|
129
|
+
postcode: extra_info['applicant_postcode'],
|
130
|
+
house_number: extra_info['applicant_house_number'],
|
131
|
+
residential_status: home_owning_translated
|
132
|
+
}
|
133
|
+
end
|
134
|
+
|
135
|
+
def home_owning_translated
|
136
|
+
case company.directors_home_owning.name
|
137
|
+
when 'Home owner without mortgage'
|
138
|
+
'owner_no_mortgage'
|
139
|
+
when 'Tenant'
|
140
|
+
'tenant'
|
141
|
+
when 'Home owner with mortgage'
|
142
|
+
'owner_with_mortgage'
|
143
|
+
when 'Living rent free with family'
|
144
|
+
'rent_free'
|
145
|
+
else
|
146
|
+
''
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
4
|
+
|
5
|
+
module InstantQuote
|
6
|
+
# This is the class responsible for parsing the decision that is provided by every different
|
7
|
+
# provider. You should inherit from this class in every specific parser (for the different
|
8
|
+
# providers).
|
9
|
+
class DecisionParser
|
10
|
+
attr_reader :data
|
11
|
+
|
12
|
+
def initialize(data = {})
|
13
|
+
@data = ActiveSupport::HashWithIndifferentAccess.new(data)
|
14
|
+
end
|
15
|
+
|
16
|
+
def pending?
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
def approved?
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
def declined?
|
25
|
+
false
|
26
|
+
end
|
27
|
+
|
28
|
+
def manual_review?
|
29
|
+
false
|
30
|
+
end
|
31
|
+
|
32
|
+
def no_decision_possible?
|
33
|
+
false
|
34
|
+
end
|
35
|
+
|
36
|
+
def loan_started?
|
37
|
+
false
|
38
|
+
end
|
39
|
+
|
40
|
+
def status
|
41
|
+
raise NotImplementedError, "You must implement '#{self.class}.status'"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'money'
|
4
|
+
|
5
|
+
module InstantQuote
|
6
|
+
module DecisionParsers
|
7
|
+
class CapitalOnTap < DecisionParser
|
8
|
+
STATUSES = {
|
9
|
+
pending: 'Pending',
|
10
|
+
approved: 'Approved',
|
11
|
+
declined: 'Declined',
|
12
|
+
could_not_submit: 'CouldNotSubmit' # this is our own status
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
def initialize(data = {})
|
16
|
+
@data = super
|
17
|
+
@data.delete(:metaData) # remove useless information
|
18
|
+
end
|
19
|
+
|
20
|
+
def pending?
|
21
|
+
status == STATUSES[:pending]
|
22
|
+
end
|
23
|
+
|
24
|
+
def approved?
|
25
|
+
status == STATUSES[:approved]
|
26
|
+
end
|
27
|
+
|
28
|
+
def declined?
|
29
|
+
status == STATUSES[:declined]
|
30
|
+
end
|
31
|
+
|
32
|
+
def amount
|
33
|
+
Money.new(credit_decision[:approvalAmount].to_f * 1000).format
|
34
|
+
end
|
35
|
+
|
36
|
+
def monthly_interest_rate
|
37
|
+
credit_decision[:monthlyInterestRate] || 0.0
|
38
|
+
end
|
39
|
+
|
40
|
+
def monthly_card_interest_rate
|
41
|
+
credit_decision[:monthlyCardInterestRate] || 0.0
|
42
|
+
end
|
43
|
+
|
44
|
+
def status
|
45
|
+
credit_decision.dig(:status) || STATUSES[:could_not_submit]
|
46
|
+
end
|
47
|
+
|
48
|
+
# The applicationStage can be one of the following:
|
49
|
+
#
|
50
|
+
# Undetermined, AppSubmitted, Referred, KybSoleTrader, KybPartner, IncompleteInfo,
|
51
|
+
# FinancialsOverdue, Declined, Duplicate, IdVerificationRequired, ReferredAfterUpdate,
|
52
|
+
# ApprovedCredit, ApprovedPrepaid, DdSetup, AddOnSelection, DoneHavFailed,
|
53
|
+
# DoneIdVerificationRequired, Done, Portal
|
54
|
+
def stage
|
55
|
+
data.dig(:applicationStage) || {}
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def credit_decision
|
61
|
+
data.dig(:creditDecision) || {}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|