instant_quote 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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