instant_quote 1.1.5 → 1.3.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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/Gemfile +1 -0
- data/Gemfile.lock +19 -12
- data/instant_quote.gemspec +1 -0
- data/lib/instant_quote/adapter_finder.rb +11 -8
- data/lib/instant_quote/adapters/capital_on_tap.rb +43 -11
- data/lib/instant_quote/adapters/iwoca.rb +11 -2
- data/lib/instant_quote/adapters/iwoca_cbils.rb +19 -0
- data/lib/instant_quote/adapters/optimum.rb +144 -0
- data/lib/instant_quote/api_error.rb +4 -2
- data/lib/instant_quote/connection_translators/capital_on_tap.rb +99 -3
- data/lib/instant_quote/connection_translators/iwoca.rb +5 -1
- data/lib/instant_quote/connection_translators/iwoca_cbils.rb +15 -0
- data/lib/instant_quote/connection_translators/optimum.rb +39 -0
- data/lib/instant_quote/decision_parsers/iwoca_cbils.rb +10 -0
- data/lib/instant_quote/decision_parsers/optimum.rb +88 -0
- data/lib/instant_quote/version.rb +1 -1
- data/lib/instant_quote/webhooks/iwoca.rb +4 -20
- metadata +22 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c0e4d676043f18051bcb1f2b7e19d1717f8358b0b91f5045904836e02b298f41
|
|
4
|
+
data.tar.gz: 685fbafd3e28e54cfda323046c128fcb030dcb3ee7f63d43b57883a550f3e1a9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fa8b5523e543f875eeb2bd9c35290b362f1be0b68a253d4711bd375f13cbf808781109dd1c283c1c686a5bc398cab7185f519fb2cb97a33d842a9ed705e71a66
|
|
7
|
+
data.tar.gz: 80552793f341720a2030a8fe02a84392c4823daaaf96fa550fe081ece670382f218ed459950547e2157cd6a8bdd08f3a61b7c3c6cf02b84cdf8f676f65c33eeb
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.6.
|
|
1
|
+
2.6.6
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
instant_quote (1.
|
|
4
|
+
instant_quote (1.3.0)
|
|
5
5
|
activesupport
|
|
6
6
|
capital_on_tap
|
|
7
7
|
iwoca
|
|
8
8
|
money
|
|
9
|
+
optimum
|
|
9
10
|
phonelib
|
|
10
11
|
|
|
11
12
|
GEM
|
|
12
13
|
remote: https://rubygems.org/
|
|
13
14
|
specs:
|
|
14
|
-
activesupport (6.0.
|
|
15
|
+
activesupport (6.0.3.3)
|
|
15
16
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
16
17
|
i18n (>= 0.7, < 2)
|
|
17
18
|
minitest (~> 5.1)
|
|
18
19
|
tzinfo (~> 1.1)
|
|
19
|
-
zeitwerk (~> 2.2)
|
|
20
|
+
zeitwerk (~> 2.2, >= 2.2.2)
|
|
20
21
|
addressable (2.7.0)
|
|
21
22
|
public_suffix (>= 2.0.2, < 5.0)
|
|
22
23
|
ast (2.4.0)
|
|
@@ -28,7 +29,7 @@ GEM
|
|
|
28
29
|
multi_json
|
|
29
30
|
rainbow
|
|
30
31
|
coderay (1.1.2)
|
|
31
|
-
concurrent-ruby (1.1.
|
|
32
|
+
concurrent-ruby (1.1.7)
|
|
32
33
|
crack (0.4.3)
|
|
33
34
|
safe_yaml (~> 1.0.0)
|
|
34
35
|
diff-lcs (1.3)
|
|
@@ -39,7 +40,7 @@ GEM
|
|
|
39
40
|
faraday_middleware (1.0.0)
|
|
40
41
|
faraday (~> 1.0)
|
|
41
42
|
hashdiff (1.0.1)
|
|
42
|
-
i18n (1.8.
|
|
43
|
+
i18n (1.8.5)
|
|
43
44
|
concurrent-ruby (~> 1.0)
|
|
44
45
|
iwoca (1.0.0)
|
|
45
46
|
addressable
|
|
@@ -49,15 +50,21 @@ GEM
|
|
|
49
50
|
rainbow
|
|
50
51
|
jaro_winkler (1.5.4)
|
|
51
52
|
method_source (1.0.0)
|
|
52
|
-
minitest (5.14.
|
|
53
|
-
money (6.13.
|
|
53
|
+
minitest (5.14.2)
|
|
54
|
+
money (6.13.8)
|
|
54
55
|
i18n (>= 0.6.4, <= 2)
|
|
55
|
-
multi_json (1.
|
|
56
|
+
multi_json (1.15.0)
|
|
56
57
|
multipart-post (2.1.1)
|
|
58
|
+
optimum (1.0.0)
|
|
59
|
+
addressable
|
|
60
|
+
faraday
|
|
61
|
+
faraday_middleware
|
|
62
|
+
multi_json
|
|
63
|
+
rainbow
|
|
57
64
|
parallel (1.19.1)
|
|
58
65
|
parser (2.7.0.5)
|
|
59
66
|
ast (~> 2.4.0)
|
|
60
|
-
phonelib (0.6.
|
|
67
|
+
phonelib (0.6.45)
|
|
61
68
|
pry (0.13.0)
|
|
62
69
|
coderay (~> 1.1)
|
|
63
70
|
method_source (~> 1.0)
|
|
@@ -96,14 +103,14 @@ GEM
|
|
|
96
103
|
simplecov-html (~> 0.11)
|
|
97
104
|
simplecov-html (0.12.2)
|
|
98
105
|
thread_safe (0.3.6)
|
|
99
|
-
tzinfo (1.2.
|
|
106
|
+
tzinfo (1.2.7)
|
|
100
107
|
thread_safe (~> 0.1)
|
|
101
108
|
unicode-display_width (1.6.1)
|
|
102
109
|
webmock (3.8.3)
|
|
103
110
|
addressable (>= 2.3.6)
|
|
104
111
|
crack (>= 0.3.2)
|
|
105
112
|
hashdiff (>= 0.4.0, < 2.0.0)
|
|
106
|
-
zeitwerk (2.
|
|
113
|
+
zeitwerk (2.4.0)
|
|
107
114
|
|
|
108
115
|
PLATFORMS
|
|
109
116
|
ruby
|
|
@@ -120,4 +127,4 @@ DEPENDENCIES
|
|
|
120
127
|
webmock
|
|
121
128
|
|
|
122
129
|
BUNDLED WITH
|
|
123
|
-
2.
|
|
130
|
+
2.1.4
|
data/instant_quote.gemspec
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Require all the connection translators
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Dir.glob('*.rb', base: File.expand_path('connection_translators', __dir__)) do |translator|
|
|
5
|
+
require "instant_quote/connection_translators/#{translator}"
|
|
6
|
+
end
|
|
6
7
|
|
|
7
8
|
# Require all the decision parsers
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
Dir.glob('*.rb', base: File.expand_path('decision_parsers', __dir__)) do |parser|
|
|
10
|
+
require "instant_quote/decision_parsers/#{parser}"
|
|
11
|
+
end
|
|
10
12
|
|
|
11
13
|
# Require all the adapters
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
Dir.glob('*.rb', base: File.expand_path('adapters', __dir__)) do |adapter|
|
|
15
|
+
require "instant_quote/adapters/#{adapter}"
|
|
16
|
+
end
|
|
14
17
|
|
|
15
18
|
module InstantQuote
|
|
16
19
|
class AdapterFinder
|
|
@@ -28,8 +31,8 @@ module InstantQuote
|
|
|
28
31
|
end
|
|
29
32
|
|
|
30
33
|
def self.available_adapters
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
Dir.glob('*.rb', base: File.expand_path('adapters', __dir__))
|
|
35
|
+
.map { |adapter| adapter.sub('.rb', '') }
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
def get_link
|
|
@@ -25,7 +25,7 @@ module InstantQuote
|
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
name: 'monthly_income',
|
|
28
|
-
label: '
|
|
28
|
+
label: 'Applicant monthly income',
|
|
29
29
|
type: 'number',
|
|
30
30
|
required: true,
|
|
31
31
|
if: ->(app) { app.borrower_organisation.sole_trader? }
|
|
@@ -37,6 +37,30 @@ module InstantQuote
|
|
|
37
37
|
required: true,
|
|
38
38
|
if: ->(app) { app.borrower_organisation.sole_trader? }
|
|
39
39
|
},
|
|
40
|
+
{
|
|
41
|
+
name: 'existing_cbils_loan',
|
|
42
|
+
label: 'Do you already have a CBILS or BBLS loan?',
|
|
43
|
+
required: true,
|
|
44
|
+
choices: {
|
|
45
|
+
'Yes' => 'true',
|
|
46
|
+
'No' => 'false'
|
|
47
|
+
},
|
|
48
|
+
type: 'radio'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'turnover_percentage_after_feb_2020',
|
|
52
|
+
label: 'When compared to before COVID-19, what is your current turnover level?',
|
|
53
|
+
required: true,
|
|
54
|
+
choices: {
|
|
55
|
+
'Greater than before (>100%)' => '>100%',
|
|
56
|
+
'Marginally reduced (80-100%)' => '80-100%',
|
|
57
|
+
'Reduced (60-79%)' => '60-79%',
|
|
58
|
+
'Halved (40-59%)' => '40-59%',
|
|
59
|
+
'Less than half (20-39%)' => '20-39%',
|
|
60
|
+
'Severely reduced or no turnover (<20%)' => '<20%'
|
|
61
|
+
},
|
|
62
|
+
type: 'select'
|
|
63
|
+
},
|
|
40
64
|
{
|
|
41
65
|
name: 'applicant_consent',
|
|
42
66
|
label: 'Yes, I give my consent for my data to be shared',
|
|
@@ -50,32 +74,30 @@ module InstantQuote
|
|
|
50
74
|
response = ::CapitalOnTap::Application.create(params)
|
|
51
75
|
|
|
52
76
|
# CoT responses can be 200 OK but still have an error!
|
|
53
|
-
unless response.success? && !response.error
|
|
54
|
-
raise ApiError.new(data: response.body, error: response.error || 'Unknown API error')
|
|
55
|
-
end
|
|
77
|
+
raise_error(response) unless response.success? && !response.error
|
|
56
78
|
|
|
57
79
|
response.body.dig(:result)
|
|
58
80
|
end
|
|
59
81
|
|
|
60
82
|
# Get the status for a given application ID
|
|
61
83
|
# {:applicationStatus=>
|
|
62
|
-
# {:locatorId=>
|
|
63
|
-
# :applicationStage=>
|
|
84
|
+
# {:locatorId=>'B247T62',
|
|
85
|
+
# :applicationStage=>'AppSubmitted',
|
|
64
86
|
# :creditDecision=>
|
|
65
|
-
# {:status=>
|
|
87
|
+
# {:status=>'Pending',
|
|
66
88
|
# :starRating=>0.0,
|
|
67
89
|
# :approvalAmount=>0.0,
|
|
68
90
|
# :monthlyInterestRate=>0.0,
|
|
69
91
|
# :monthlyCardInterestRate=>0.0,
|
|
70
|
-
# :decisionDate=>
|
|
71
|
-
# :creditDecisionReason=>
|
|
72
|
-
# :links=>{:self=>
|
|
92
|
+
# :decisionDate=>'2020-01-28T01:44:29.241',
|
|
93
|
+
# :creditDecisionReason=>'None'}},
|
|
94
|
+
# :links=>{:self=>'/api/v1/Applications/B247T62', :parent=>nil},
|
|
73
95
|
# :metaData=>{}
|
|
74
96
|
# }
|
|
75
97
|
def get_status(application_id)
|
|
76
98
|
response = ::CapitalOnTap::Application.status(application_id)
|
|
77
99
|
|
|
78
|
-
|
|
100
|
+
raise_error(response) unless response.success?
|
|
79
101
|
|
|
80
102
|
DecisionParsers::CapitalOnTap.new(response.body[:applicationStatus])
|
|
81
103
|
end
|
|
@@ -89,6 +111,16 @@ module InstantQuote
|
|
|
89
111
|
def get_approval(_application_id)
|
|
90
112
|
true
|
|
91
113
|
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def raise_error(response)
|
|
118
|
+
raise ApiError.new(
|
|
119
|
+
adapter: 'capital_on_tap',
|
|
120
|
+
data: response.body,
|
|
121
|
+
error: response.error || 'Unknown API error'
|
|
122
|
+
)
|
|
123
|
+
end
|
|
92
124
|
end
|
|
93
125
|
end
|
|
94
126
|
end
|
|
@@ -77,7 +77,7 @@ module InstantQuote
|
|
|
77
77
|
|
|
78
78
|
raise_error(response) unless response.success?
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
decision_parser.new(response.body)
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
# Gets the URL for the given state key.
|
|
@@ -96,8 +96,17 @@ module InstantQuote
|
|
|
96
96
|
|
|
97
97
|
private
|
|
98
98
|
|
|
99
|
+
def decision_parser
|
|
100
|
+
DecisionParsers::Iwoca
|
|
101
|
+
end
|
|
102
|
+
|
|
99
103
|
def raise_error(response)
|
|
100
|
-
raise ApiError.new(
|
|
104
|
+
raise ApiError.new(
|
|
105
|
+
adapter: 'iwoca',
|
|
106
|
+
data: response.data,
|
|
107
|
+
code: response.errors.first[:code],
|
|
108
|
+
error: response.errors.first[:detail]
|
|
109
|
+
)
|
|
101
110
|
end
|
|
102
111
|
end
|
|
103
112
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'iwoca'
|
|
4
|
+
require 'instant_quote/adapters/iwoca'
|
|
5
|
+
require 'instant_quote/decision_parsers/iwoca_cbils'
|
|
6
|
+
|
|
7
|
+
module InstantQuote
|
|
8
|
+
module Adapters
|
|
9
|
+
class IwocaCbils < Iwoca
|
|
10
|
+
additional_fields Iwoca.additional_fields
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def decision_parser
|
|
15
|
+
DecisionParsers::IwocaCbils
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optimum'
|
|
4
|
+
require 'instant_quote/decision_parsers/optimum'
|
|
5
|
+
|
|
6
|
+
module InstantQuote
|
|
7
|
+
module Adapters
|
|
8
|
+
class Optimum < Adapter
|
|
9
|
+
additional_fields [
|
|
10
|
+
{
|
|
11
|
+
name: 'applicant_phone_number',
|
|
12
|
+
label: 'Applicant mobile phone',
|
|
13
|
+
required: true
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'expected_turnover_next_12_months',
|
|
17
|
+
label: 'Expected turnover next 12 months',
|
|
18
|
+
required: true,
|
|
19
|
+
type: 'money'
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'expected_customers',
|
|
23
|
+
label: 'Number of expected customers with a balance in 6 months',
|
|
24
|
+
required: true,
|
|
25
|
+
type: 'number'
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'industry_sector',
|
|
29
|
+
label: 'Industry sector',
|
|
30
|
+
required: true,
|
|
31
|
+
type: 'select',
|
|
32
|
+
choices: [
|
|
33
|
+
'Advertising / Marketing',
|
|
34
|
+
'Beer / Wine Merchant',
|
|
35
|
+
'Builders Merchant',
|
|
36
|
+
'Clothing & Fashion',
|
|
37
|
+
'Computer Hardware',
|
|
38
|
+
'Computer Software',
|
|
39
|
+
'Construction',
|
|
40
|
+
'Consultancy',
|
|
41
|
+
'Contract Cleaning',
|
|
42
|
+
'Courier',
|
|
43
|
+
'Distribution',
|
|
44
|
+
'Driving Agency',
|
|
45
|
+
'Electrical Contracting',
|
|
46
|
+
'Electrical Wholesaler',
|
|
47
|
+
'Engineering - Heavy',
|
|
48
|
+
'Engineering - Light',
|
|
49
|
+
'Engineering - Precision',
|
|
50
|
+
'Fabrication',
|
|
51
|
+
'Flooring Contractor',
|
|
52
|
+
'Food Manufacture',
|
|
53
|
+
'Franchise',
|
|
54
|
+
'Free Newspaper',
|
|
55
|
+
'Freight Forwarding',
|
|
56
|
+
'Fruit Importer',
|
|
57
|
+
'Furniture Manufacture',
|
|
58
|
+
'Garden Equipment',
|
|
59
|
+
'Haulage',
|
|
60
|
+
'IT Contracting',
|
|
61
|
+
'Jewellery',
|
|
62
|
+
'Maintenance',
|
|
63
|
+
'Manufacturing',
|
|
64
|
+
'Meat Trade',
|
|
65
|
+
'Other Services',
|
|
66
|
+
'Permanent Recruitment',
|
|
67
|
+
'Plant Hire',
|
|
68
|
+
'Plastic Injection Moulding',
|
|
69
|
+
'Plumbers Merchant',
|
|
70
|
+
'Printing',
|
|
71
|
+
'Publishing',
|
|
72
|
+
'Recycling',
|
|
73
|
+
'Security Guards',
|
|
74
|
+
'Shopfitting',
|
|
75
|
+
'Stationery',
|
|
76
|
+
'Steel Stockholding',
|
|
77
|
+
'Telecoms',
|
|
78
|
+
'Temporary Recruitment',
|
|
79
|
+
'Timber Merchant',
|
|
80
|
+
'Tooling Manufacture',
|
|
81
|
+
'Toys & Fancy Goods',
|
|
82
|
+
'Warehousing & Storage',
|
|
83
|
+
'Wholesale'
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'credit_control',
|
|
88
|
+
label: 'Do you require credit control?',
|
|
89
|
+
required: false,
|
|
90
|
+
choices: {
|
|
91
|
+
'Yes' => '1',
|
|
92
|
+
'No' => '0'
|
|
93
|
+
},
|
|
94
|
+
type: 'radio'
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'applicant_consent',
|
|
98
|
+
label: 'Yes, I give my consent for my data to be shared',
|
|
99
|
+
required: true,
|
|
100
|
+
type: 'checkbox'
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
def get_quote(params)
|
|
105
|
+
response = ::Optimum::Decision.create(params)
|
|
106
|
+
|
|
107
|
+
return response.data[:opportunityId] if response.success?
|
|
108
|
+
|
|
109
|
+
raise_error(response)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def get_status(application_id)
|
|
113
|
+
response = ::Optimum::Decision.update(application_id)
|
|
114
|
+
|
|
115
|
+
raise_error(response) unless response.success?
|
|
116
|
+
|
|
117
|
+
decision_parser.new(response.body)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Gets the URL for the given state key.
|
|
121
|
+
def get_link(_)
|
|
122
|
+
''
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def get_approval(_)
|
|
126
|
+
true
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
def decision_parser
|
|
132
|
+
DecisionParsers::Optimum
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def raise_error(response)
|
|
136
|
+
raise ApiError.new(
|
|
137
|
+
adapter: 'optimum',
|
|
138
|
+
data: response.body,
|
|
139
|
+
error: response.error || 'Unknown API error'
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -2,17 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
module InstantQuote
|
|
4
4
|
class ApiError < StandardError
|
|
5
|
-
attr_accessor :error, :data
|
|
5
|
+
attr_accessor :error, :data, :code, :adapter
|
|
6
6
|
|
|
7
7
|
alias message error
|
|
8
8
|
alias to_s error
|
|
9
9
|
|
|
10
10
|
def initialize(params = {})
|
|
11
|
+
@adapter = params[:adapter]
|
|
11
12
|
@error = params[:error]
|
|
12
13
|
@data = params[:data] || {}
|
|
14
|
+
@code = params[:code]
|
|
13
15
|
|
|
14
16
|
# Delete the metaData key, because it's irrelevant and has long strings
|
|
15
|
-
@data.delete(:metaData) if @data.key?(:metaData)
|
|
17
|
+
@data.delete(:metaData) if @data.respond_to?(:key?) && @data.key?(:metaData)
|
|
16
18
|
end
|
|
17
19
|
end
|
|
18
20
|
end
|
|
@@ -6,6 +6,60 @@ module InstantQuote
|
|
|
6
6
|
module ConnectionTranslators
|
|
7
7
|
class CapitalOnTap < ConnectionTranslator
|
|
8
8
|
DEFAULT_SALUTATION = 'Mr'
|
|
9
|
+
LOAN_PURPOSE_MAPPING = {
|
|
10
|
+
'Apply for COVID-19 support/for other reasons' => 'General working capital requirements',
|
|
11
|
+
'Apply for COVID-19 support/to pay existing loans' => 'Repay Finances with another lender',
|
|
12
|
+
'Apply for COVID-19 support/to pay suppliers' => 'General working capital requirements',
|
|
13
|
+
'Apply for COVID-19 support/to protect jobs' => 'Payment of wages or rent',
|
|
14
|
+
'Apply for COVID-19 support/to refinance existing loans' => 'Repay Finances with another lender',
|
|
15
|
+
'Buy or develop property/buy the property my business already occupies' => '',
|
|
16
|
+
'Buy or develop property/buying a business that includes premises' => 'Acquisition of another business in the UK',
|
|
17
|
+
'Buy or develop property/buying a property for my business to move into' => '',
|
|
18
|
+
'Buy or develop property/develop residential or commercial premises' => 'Bridging loan',
|
|
19
|
+
'Buy or develop property/invest in a foreign property to rent it out' => '',
|
|
20
|
+
'Buy or develop property/invest in a holiday let' => '',
|
|
21
|
+
'Buy or develop property/invest in UK property' => 'Bridging loan',
|
|
22
|
+
'Buy or develop property/other property finance needs' => 'Bridging loan',
|
|
23
|
+
'Buy or refinance vehicle(s)/agricultural vehicle(s)' => 'General working capital requirements',
|
|
24
|
+
'Buy or refinance vehicle(s)/car(s)' => 'General working capital requirements',
|
|
25
|
+
'Buy or refinance vehicle(s)/caravan(s) & mobile home(s)' => 'General working capital requirements',
|
|
26
|
+
'Buy or refinance vehicle(s)/commercial vehicle(s)' => 'General working capital requirements',
|
|
27
|
+
'Buy or refinance vehicle(s)/fork lift truck(s)' => 'General working capital requirements',
|
|
28
|
+
'Buy or refinance vehicle(s)/other vehicle finance requirements' => 'General working capital requirements',
|
|
29
|
+
'Help with grants, tax credits and pension finance/for a research & development project' => 'Research and development activities',
|
|
30
|
+
'Help with grants, tax credits and pension finance/for green energy projects' => 'General working capital requirements',
|
|
31
|
+
'Help with grants, tax credits and pension finance/for projects with social impact' => 'General working capital requirements',
|
|
32
|
+
'Help with grants, tax credits and pension finance/other grant funding needs' => '',
|
|
33
|
+
'Help with grants, tax credits and pension finance/starting a business, incl. accelerator and incubator programs' => 'Start up costs',
|
|
34
|
+
'Help with grants, tax credits and pension finance/to fund skills and training initiatives' => 'General working capital requirements',
|
|
35
|
+
'Help with grants, tax credits and pension finance/utilise pensions to lend to a business' => '',
|
|
36
|
+
'Improve cashflow/borrow against monthly income from a credit card terminal' => '',
|
|
37
|
+
'Improve cashflow/finance overseas trading and exporting' => 'General working capital requirements',
|
|
38
|
+
'Improve cashflow/other cashflow requirements' => 'General working capital requirements',
|
|
39
|
+
'Improve cashflow/raise credit against agreed purchase orders' => 'General working capital requirements',
|
|
40
|
+
'Improve cashflow/release capital from pension fund(s)' => '',
|
|
41
|
+
'Improve cashflow/release cash from one unpaid invoice to another business' => 'General working capital requirements',
|
|
42
|
+
'Improve cashflow/release cash from several invoices to other businesses' => 'General working capital requirements',
|
|
43
|
+
'Improve cashflow/spread the cost of a large bill' => 'General working capital requirements',
|
|
44
|
+
'Invest in equipment or machinery/computers (hardware, software)' => 'Purchase of equipment/machinery/stock',
|
|
45
|
+
'Invest in equipment or machinery/equipment (office, printing, medical, etc.)' => 'Purchase of equipment/machinery/stock',
|
|
46
|
+
'Invest in equipment or machinery/furniture & furnishings' => 'Purchase of equipment/machinery/stock',
|
|
47
|
+
'Invest in equipment or machinery/machinery & tools' => 'Purchase of equipment/machinery/stock',
|
|
48
|
+
'Invest in equipment or machinery/other investment needs' => 'Purchase of licences,quotas or other entitlements to trade',
|
|
49
|
+
'Invest in equipment or machinery/phone & telecom systems' => 'Purchase of equipment/machinery/stock',
|
|
50
|
+
'Refinance an existing.../asset' => 'Repay Finances with another lender',
|
|
51
|
+
'Refinance an existing.../business loan' => 'Repay Finances with another lender',
|
|
52
|
+
'Refinance an existing.../commercial mortgage' => '',
|
|
53
|
+
'Refinance an existing.../other refinancing needs' => 'Repay Finances with another lender',
|
|
54
|
+
'Seeking investment/for a management buy-out' => 'Acquisition of another business in the UK',
|
|
55
|
+
'Seeking investment/other funding requirements' => 'General working capital requirements',
|
|
56
|
+
'Seeking investment/to launch a new B2B product or service' => 'Start up costs',
|
|
57
|
+
'Seeking investment/to launch a new consumer product or service' => 'Start up costs',
|
|
58
|
+
'Seeking investment/to purchase another business' => 'Acquisition of another business in the UK',
|
|
59
|
+
'Seeking investment/to reach more consumers with a proven product or service' => 'Expansion Activities in the UK',
|
|
60
|
+
'Seeking investment/to win more business clients with a proven product or service' => 'Expansion Activities in the UK',
|
|
61
|
+
'Seeking investment/venture builder, accelerator and incubator programs' => ''
|
|
62
|
+
}.freeze
|
|
9
63
|
|
|
10
64
|
# rubocop:disable Metrics/AbcSize
|
|
11
65
|
def translate
|
|
@@ -24,7 +78,8 @@ module InstantQuote
|
|
|
24
78
|
MonthlyTurnOver: translate_turnover,
|
|
25
79
|
BusinessType: translate_business_type,
|
|
26
80
|
BusinessAddress: translate_address,
|
|
27
|
-
RegistrationNumber: app_org.company_number
|
|
81
|
+
RegistrationNumber: app_org.company_number,
|
|
82
|
+
additionalQuestions: additional_questions
|
|
28
83
|
}
|
|
29
84
|
|
|
30
85
|
# If the borrower organisation is Sole Trader we need to merge two additional fields.
|
|
@@ -61,7 +116,7 @@ module InstantQuote
|
|
|
61
116
|
|
|
62
117
|
return '' unless addr && addr != ''
|
|
63
118
|
|
|
64
|
-
street_parts = [addr.line_1, addr.line_2
|
|
119
|
+
street_parts = [addr.line_1, addr.line_2]
|
|
65
120
|
|
|
66
121
|
{
|
|
67
122
|
CountryCode: 'UK',
|
|
@@ -71,7 +126,7 @@ module InstantQuote
|
|
|
71
126
|
}
|
|
72
127
|
end
|
|
73
128
|
|
|
74
|
-
# Since in Finpoint the user selects an intervel (i.e.
|
|
129
|
+
# Since in Finpoint the user selects an intervel (i.e. '500,000 - 999,999'). We get the avg
|
|
75
130
|
# on this interval and use that number as the monthly turnover.
|
|
76
131
|
def translate_turnover
|
|
77
132
|
turnover = app_org.turnover_range
|
|
@@ -93,6 +148,47 @@ module InstantQuote
|
|
|
93
148
|
'Partnership'
|
|
94
149
|
end
|
|
95
150
|
end
|
|
151
|
+
|
|
152
|
+
def additional_questions
|
|
153
|
+
[
|
|
154
|
+
loan_purpose_question,
|
|
155
|
+
cbils_loan_question,
|
|
156
|
+
turnover_percentage_question
|
|
157
|
+
]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def loan_purpose_question
|
|
161
|
+
{
|
|
162
|
+
question: 'What is the purpose of the loan?',
|
|
163
|
+
answer: translate_loan_purpose,
|
|
164
|
+
questionType: '16'
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def cbils_loan_question
|
|
169
|
+
question_from_extra_info('existing_cbils_loan', '17')
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def turnover_percentage_question
|
|
173
|
+
question_from_extra_info('turnover_percentage_after_feb_2020', '20')
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def question_from_extra_info(name, question_type)
|
|
177
|
+
field = Adapters::CapitalOnTap.additional_fields.find do |f|
|
|
178
|
+
f[:name] == name
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
{ question: field[:label], answer: extra_info[field[:name]], questionType: question_type }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def translate_loan_purpose
|
|
185
|
+
proceeds_purpose = [
|
|
186
|
+
application.proceeds_purpose&.overview&.name,
|
|
187
|
+
application.proceeds_purpose&.name
|
|
188
|
+
].join('/')
|
|
189
|
+
|
|
190
|
+
LOAN_PURPOSE_MAPPING[proceeds_purpose]
|
|
191
|
+
end
|
|
96
192
|
end
|
|
97
193
|
end
|
|
98
194
|
end
|
|
@@ -10,7 +10,7 @@ module InstantQuote
|
|
|
10
10
|
company: company_information,
|
|
11
11
|
people: [person_information],
|
|
12
12
|
requested_products: {
|
|
13
|
-
|
|
13
|
+
product_name => {
|
|
14
14
|
approval: {
|
|
15
15
|
amount: (application.amount_pennies / 100),
|
|
16
16
|
duration: 12,
|
|
@@ -25,6 +25,10 @@ module InstantQuote
|
|
|
25
25
|
|
|
26
26
|
private
|
|
27
27
|
|
|
28
|
+
def product_name
|
|
29
|
+
:credit_facility
|
|
30
|
+
end
|
|
31
|
+
|
|
28
32
|
# rubocop:disable Metrics/AbcSize
|
|
29
33
|
def company_information
|
|
30
34
|
information = {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module InstantQuote
|
|
4
|
+
module ConnectionTranslators
|
|
5
|
+
class Optimum < ConnectionTranslator
|
|
6
|
+
def translate
|
|
7
|
+
{
|
|
8
|
+
companyNumber: company&.company_number.to_s,
|
|
9
|
+
sector: extra_info['industry_sector'],
|
|
10
|
+
turnover: extra_info['expected_turnover_next_12_months'].to_f,
|
|
11
|
+
homeowners: homeowners?,
|
|
12
|
+
yearsTrading: company&.years_in_business,
|
|
13
|
+
expectedCustomers: extra_info['expected_customers'].to_i,
|
|
14
|
+
contactName: application&.primary_user&.name,
|
|
15
|
+
contactNumber: extra_info['applicant_phone_number'],
|
|
16
|
+
contactEmail: application&.primary_user&.email,
|
|
17
|
+
creditControl: extra_info['credit_control'] == '1',
|
|
18
|
+
phoenix: false,
|
|
19
|
+
quotationOnly: false
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def company
|
|
26
|
+
application.borrower_organisation
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def homeowners?
|
|
30
|
+
case company&.directors_home_owning&.name
|
|
31
|
+
when 'Home owner without mortgage', 'Home owner with mortgage'
|
|
32
|
+
true
|
|
33
|
+
else
|
|
34
|
+
false
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module InstantQuote
|
|
4
|
+
module DecisionParsers
|
|
5
|
+
# Example response:
|
|
6
|
+
#
|
|
7
|
+
# {
|
|
8
|
+
# decision: {
|
|
9
|
+
# decision: "approved",
|
|
10
|
+
# trialPeriodMonths: 4, # trial period length
|
|
11
|
+
# fundingLimit: 745000,
|
|
12
|
+
# annualRenewalFee: 0,
|
|
13
|
+
# badDebtProtection: 0.85,
|
|
14
|
+
# cashAvailableWithinHours: 48,
|
|
15
|
+
# discountFee: 3.35,
|
|
16
|
+
# facility: "Factoring", # Factoring or Invoice Discounting or Confidential Invoice Discounting
|
|
17
|
+
# prepayment: 80,
|
|
18
|
+
# serviceFee: 1.14
|
|
19
|
+
# },
|
|
20
|
+
# opportunityId: "01234567ABCDEFGH", # the Optimum Finance opportunity ID (only supplied for successful applications that are not 'quotation only')
|
|
21
|
+
# companyName: "COMPANY NAME LTD" # company name from the given company number - use this to verify correct company
|
|
22
|
+
# }
|
|
23
|
+
|
|
24
|
+
class Optimum < DecisionParser
|
|
25
|
+
STATUSES = {
|
|
26
|
+
approved: 'approved',
|
|
27
|
+
declined: 'declined',
|
|
28
|
+
referred: 'referred',
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
LOAN_STATES = {
|
|
32
|
+
started: 'Closed Won'
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
def initialize(data = {})
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def pending?
|
|
40
|
+
!STATUSES.value?(status)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def approved?
|
|
44
|
+
status == STATUSES[:approved]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def declined?
|
|
48
|
+
status == STATUSES[:declined]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def manual_review?
|
|
52
|
+
status == STATUSES[:referred]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def loan_started?
|
|
56
|
+
data[:status] == LOAN_STATES[:started]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def amount
|
|
60
|
+
amount_approved = decision[:fundingLimit] || 0
|
|
61
|
+
|
|
62
|
+
Money.new(amount_approved * 100).format
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def credit_duration
|
|
66
|
+
0
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def credit_interval
|
|
70
|
+
decision[:trialPeriodMonths]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def monthly_interest_rate
|
|
74
|
+
0
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def status
|
|
78
|
+
decision[:decision]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def decision
|
|
84
|
+
data[:decision] || data[:originalDecision] || {}
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -16,31 +16,15 @@ module InstantQuote
|
|
|
16
16
|
class SignatureVerifyError < StandardError
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def iwoca_event
|
|
20
|
-
# Dynamically calls the following methods:
|
|
21
|
-
# * handle_approval_status_changed
|
|
22
|
-
# * handle_instruct_payments
|
|
23
|
-
# * handle_customer_funded
|
|
24
|
-
if iwoca_verify_signature
|
|
25
|
-
return send("handle_#{iwoca_event_type}") if respond_to?("handle_#{iwoca_event_type}")
|
|
26
|
-
|
|
27
|
-
render json: iwoca_payload, status: 200
|
|
28
|
-
else
|
|
29
|
-
iwoca_verify_signature_error
|
|
30
|
-
end
|
|
31
|
-
rescue SignatureVerifyError => e
|
|
32
|
-
iwoca_verify_signature_error(e.message)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
19
|
private
|
|
36
20
|
|
|
37
21
|
def iwoca_verify_signature
|
|
38
|
-
unless webhook_signature &&
|
|
22
|
+
unless webhook_signature && payload && iwoca_signature
|
|
39
23
|
raise SignatureVerifyError, "#{SIGNATURE_HEADER} header missing"
|
|
40
24
|
end
|
|
41
25
|
|
|
42
26
|
digest = OpenSSL::Digest.new('sha1')
|
|
43
|
-
hmac_digest = OpenSSL::HMAC.digest(digest, webhook_signature,
|
|
27
|
+
hmac_digest = OpenSSL::HMAC.digest(digest, webhook_signature, payload)
|
|
44
28
|
signature = "sha1=#{Base64.strict_encode64(hmac_digest)}"
|
|
45
29
|
|
|
46
30
|
Rack::Utils.secure_compare(signature, iwoca_signature)
|
|
@@ -50,8 +34,8 @@ module InstantQuote
|
|
|
50
34
|
@webhook_signature ||= ::Iwoca.configuration.webhook_signature
|
|
51
35
|
end
|
|
52
36
|
|
|
53
|
-
def
|
|
54
|
-
@
|
|
37
|
+
def payload
|
|
38
|
+
@payload ||= request.body.read
|
|
55
39
|
end
|
|
56
40
|
|
|
57
41
|
def iwoca_signature
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: instant_quote
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rikas
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2020-
|
|
11
|
+
date: 2020-09-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -164,6 +164,20 @@ dependencies:
|
|
|
164
164
|
- - ">="
|
|
165
165
|
- !ruby/object:Gem::Version
|
|
166
166
|
version: '0'
|
|
167
|
+
- !ruby/object:Gem::Dependency
|
|
168
|
+
name: optimum
|
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
|
170
|
+
requirements:
|
|
171
|
+
- - ">="
|
|
172
|
+
- !ruby/object:Gem::Version
|
|
173
|
+
version: '0'
|
|
174
|
+
type: :runtime
|
|
175
|
+
prerelease: false
|
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
177
|
+
requirements:
|
|
178
|
+
- - ">="
|
|
179
|
+
- !ruby/object:Gem::Version
|
|
180
|
+
version: '0'
|
|
167
181
|
- !ruby/object:Gem::Dependency
|
|
168
182
|
name: money
|
|
169
183
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -216,13 +230,19 @@ files:
|
|
|
216
230
|
- lib/instant_quote/adapter_finder.rb
|
|
217
231
|
- lib/instant_quote/adapters/capital_on_tap.rb
|
|
218
232
|
- lib/instant_quote/adapters/iwoca.rb
|
|
233
|
+
- lib/instant_quote/adapters/iwoca_cbils.rb
|
|
234
|
+
- lib/instant_quote/adapters/optimum.rb
|
|
219
235
|
- lib/instant_quote/api_error.rb
|
|
220
236
|
- lib/instant_quote/connection_translator.rb
|
|
221
237
|
- lib/instant_quote/connection_translators/capital_on_tap.rb
|
|
222
238
|
- lib/instant_quote/connection_translators/iwoca.rb
|
|
239
|
+
- lib/instant_quote/connection_translators/iwoca_cbils.rb
|
|
240
|
+
- lib/instant_quote/connection_translators/optimum.rb
|
|
223
241
|
- lib/instant_quote/decision_parser.rb
|
|
224
242
|
- lib/instant_quote/decision_parsers/capital_on_tap.rb
|
|
225
243
|
- lib/instant_quote/decision_parsers/iwoca.rb
|
|
244
|
+
- lib/instant_quote/decision_parsers/iwoca_cbils.rb
|
|
245
|
+
- lib/instant_quote/decision_parsers/optimum.rb
|
|
226
246
|
- lib/instant_quote/version.rb
|
|
227
247
|
- lib/instant_quote/webhooks/iwoca.rb
|
|
228
248
|
homepage: https://finpoint.co.uk
|