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.
@@ -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