instant_quote 1.7.33 → 1.7.34

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 961b0814c5895349859b111b9154e56582aade793a7e7623c68e53b142742b86
4
- data.tar.gz: 7e88f40122d9bc7442618b3fc4aa3994f6b8f988887cfb240de853d47094bd95
3
+ metadata.gz: 97b97adb2fb250be1f7b7a46b44cbb082500cb0dae53cb350e31123d506a1efe
4
+ data.tar.gz: 66ca99708c03bfbea6587abf097196998d366c1842026f43ce02b04705a779ae
5
5
  SHA512:
6
- metadata.gz: ccf3b9a9eb9d9e38c3c9ad2a9341432f683dd0dc6ca14e59b31b80b701563560c4796869f8202a84c45461798387419dbf90b1db943635ad6b513e417b6971f8
7
- data.tar.gz: aaa6b9dcd4fc6dfebf247e443911af337f77a0cd0f1f713547fc2aa51840e73d6069f68b194b8504ae997598f26c7d9556dff0a665b72457f41d6723629b7947
6
+ metadata.gz: 9f94bf675fa6e919ece437296cad1684401ee435f9e2e54c1d86be65b94469f00d4e155187644c1e93ba75cc745a09554a9e988e45738d28d3b3ced25fa3b5a4
7
+ data.tar.gz: d095be67aa6e9b7f0d07d3bdebef1e89e95140b7435445b90287b8275814b071cceff5c657d8dd74bb97f7ac84c01cec156844fc8a6fb0a878daac1638ef279f
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- instant_quote (1.7.33)
4
+ instant_quote (1.7.34)
5
5
  activesupport
6
6
  capital_on_tap (~> 1.0.1)
7
7
  iwoca (~> 1.1.0)
@@ -22,6 +22,7 @@ GEM
22
22
  addressable (2.8.5)
23
23
  public_suffix (>= 2.0.2, < 6.0)
24
24
  ast (2.4.2)
25
+ base64 (0.1.1)
25
26
  byebug (11.1.3)
26
27
  capital_on_tap (1.0.1)
27
28
  addressable
@@ -35,7 +36,8 @@ GEM
35
36
  diff-lcs (1.5.0)
36
37
  docile (1.4.0)
37
38
  dotenv (2.8.1)
38
- faraday (2.7.10)
39
+ faraday (2.7.11)
40
+ base64
39
41
  faraday-net_http (>= 2.0, < 3.1)
40
42
  ruby2_keywords (>= 0.0.4)
41
43
  faraday-net_http (3.0.2)
@@ -51,7 +53,7 @@ GEM
51
53
  json-schema (4.0.0)
52
54
  addressable (>= 2.8)
53
55
  method_source (1.0.0)
54
- minitest (5.19.0)
56
+ minitest (5.20.0)
55
57
  money (6.16.0)
56
58
  i18n (>= 0.6.4, <= 2)
57
59
  multi_json (1.15.0)
@@ -123,7 +125,7 @@ GEM
123
125
  addressable (>= 2.8.0)
124
126
  crack (>= 0.3.2)
125
127
  hashdiff (>= 0.4.0, < 2.0.0)
126
- youlend (1.1.4)
128
+ youlend (1.2.0)
127
129
  addressable
128
130
  faraday (~> 2.7)
129
131
  rainbow
@@ -146,4 +148,4 @@ DEPENDENCIES
146
148
  webmock
147
149
 
148
150
  BUNDLED WITH
149
- 2.4.7
151
+ 2.3.10
@@ -40,6 +40,10 @@ module InstantQuote
40
40
  new.get_approval(application_id, connection)
41
41
  end
42
42
 
43
+ def self.accept_offer(application_id, connection, offer_id)
44
+ new.accept_offer(application_id, connection, offer_id)
45
+ end
46
+
43
47
  def get_link(_connection)
44
48
  raise NotImplementedError, "You must implement '#{self.class.name}#get_link'"
45
49
  end
@@ -61,5 +65,9 @@ module InstantQuote
61
65
  def get_approval(_application_id, _connection)
62
66
  OpenStruct.new(success?: true)
63
67
  end
68
+
69
+ def accept_offer(_application_id, _connection, _offer_id)
70
+ true
71
+ end
64
72
  end
65
73
  end
@@ -59,6 +59,10 @@ module InstantQuote
59
59
  adapter.get_approval(connection.external_id, connection)
60
60
  end
61
61
 
62
+ def accept_offer(offer_id)
63
+ adapter.accept_offer(connection.external_id, connection, offer_id)
64
+ end
65
+
62
66
  def additional_fields
63
67
  adapter.additional_fields
64
68
  end
@@ -2,24 +2,12 @@
2
2
 
3
3
  require 'youlend'
4
4
  require 'instant_quote/decision_parsers/youlend'
5
+ require 'instant_quote/connection_translators/youlend_persons'
5
6
 
6
7
  module InstantQuote
7
8
  module Adapters
8
9
  class Youlend < Adapter
9
10
  additional_fields [
10
- {
11
- name: 'monthly_card_revenue',
12
- label: 'Avg. monthly card revenue',
13
- required: true,
14
- type: 'money',
15
- minimum: 1_000
16
- },
17
- {
18
- name: 'monthly_revenue_from_invoices',
19
- label: 'Avg. monthly revenue from invoices',
20
- required: true,
21
- type: 'money'
22
- },
23
11
  {
24
12
  name: 'applicant_consent',
25
13
  label: 'Yes, I give my consent for my data to be shared with YouLend',
@@ -28,48 +16,44 @@ module InstantQuote
28
16
  }
29
17
  ]
30
18
 
31
- # For Youlend we first need to do the pre_qualification and then, if we get a positive
32
- # response we can create the lead. We need to call both endpoints here, so it might take a
33
- # while.
19
+ # For Youlend we first need to create a lead and then update lead
20
+ # since some of the fields are not available in create
34
21
  def get_quote(params, connection)
35
- response = ::Youlend::Lead.create(params)
22
+ data = create_lead(params)
36
23
 
37
- if response.success?
38
- connection.update(link: response.data[:signUpURL])
24
+ update_lead(data[:leadId], params)
25
+ submit_persons(data[:leadId], connection)
39
26
 
40
- return response.data[:leadId]
41
- end
27
+ connection.update(link: data[:signUpURL])
42
28
 
43
- raise_error(response)
29
+ return data[:leadId]
44
30
  end
45
31
 
46
- def get_status(_lead_id, connection)
32
+ def get_status(lead_id, connection)
33
+ details = ::Youlend::Lead.details(lead_id)
34
+ raise_error(details) unless details.success?
35
+
36
+ offers = ::Youlend::Lead.offers(lead_id) if details.body[:onboardingState] == 'offersProvided'
37
+ raise_error(offers) if offers && !offers.success?
38
+
39
+ decision = connection.approval_decision_info.merge(details: details.body)
40
+ decision.merge!(offers: offers&.body) if offers
41
+
42
+ connection.update(approval_decision_info: decision)
47
43
  DecisionParsers::Youlend.new(connection.approval_decision_info)
48
44
  end
49
45
 
46
+ def accept_offer(application_id, _connection, offer_id)
47
+ response = ::Youlend::Lead.accept_offer(application_id, offer_id)
48
+
49
+ response.success?
50
+ end
51
+
50
52
  # Gets the URL for the given state key.
51
53
  def get_link(connection)
52
54
  connection.link
53
55
  end
54
56
 
55
- # Checks if the application is eligible for a loan. If it is we need to update the
56
- # connection's approval_decision_info right away, since it's the only place where we get it.
57
- def get_preapproval(params, connection)
58
- # The preapproval endpoint doesn't work with all params, we need to remove some
59
- params.delete(:companyName)
60
- params.delete(:monthsTrading)
61
- params.delete(:keyContactName)
62
- params.delete(:registeredAddress)
63
- params.delete(:thirdPartyLeadId)
64
- params.delete(:contactPhoneNumber)
65
-
66
- response = ::Youlend::Prequalification.verify(params)
67
-
68
- connection.update(approval_decision_info: response.data) if response.success?
69
-
70
- response
71
- end
72
-
73
57
  private
74
58
 
75
59
  def raise_error(response)
@@ -79,6 +63,26 @@ module InstantQuote
79
63
  error: response.errors.first || '[Youlend] API error'
80
64
  )
81
65
  end
66
+
67
+ def create_lead(params)
68
+ response = ::Youlend::Lead.create(params)
69
+ raise_error(response) unless response.success?
70
+
71
+ response.data
72
+ end
73
+
74
+ def update_lead(lead_id, params)
75
+ response = ::Youlend::Lead.update(lead_id, params)
76
+ raise_error(response) unless response.success?
77
+ end
78
+
79
+ def submit_persons(lead_id, connection)
80
+ people_translator = ConnectionTranslators::YoulendPersons.new(connection)
81
+ people_params = people_translator.translate
82
+
83
+ response = ::Youlend::SignificantPersons.create(lead_id, people_params)
84
+ raise_error(response) unless response.success?
85
+ end
82
86
  end
83
87
  end
84
88
  end
@@ -5,58 +5,31 @@ module InstantQuote
5
5
  CURRENCY = 'GBP'
6
6
 
7
7
  class Youlend < ConnectionTranslator
8
- # rubocop:disable Metrics/AbcSize
9
- def translate
8
+ def translate # rubocop:disable Metrics/AbcSize
10
9
  {
11
10
  companyName: company.name,
12
11
  companyType: company_type_translated,
12
+ companyNumber: company.company_number,
13
13
  monthsTrading: company.total_months_in_business,
14
14
  keyContactName: primary_user_name,
15
15
  thirdPartyLeadId: application.code,
16
- financialData: financial_data,
17
16
  countryISOCode: 'GBR',
18
17
  loanCurrencyISOCode: CURRENCY,
19
- thirdPartyMerchantId: company.id,
20
- thirdPartyCustomerId: application&.primary_user&.id,
18
+ loanAmount: application.amount.to_f,
19
+ thirdPartyCustomerId: application&.primary_user&.id.to_s,
21
20
  registeredAddress: company_address,
22
21
  contactPhoneNumber: extra_info[:applicant_phone_number],
23
- monthlyCardRevenue: extra_info[:monthly_card_revenue].to_i,
24
- contactEmailAddress: application&.primary_user&.email # optional
22
+ contactEmailAddress: application&.primary_user&.email,
23
+ # we need country and description on update
24
+ # but they are ignored in create
25
+ country: 'GBR',
26
+ description: application.description,
27
+ confirmedCreditSearch: true
25
28
  }
26
29
  end
27
- # rubocop:enable Metrics/AbcSize
28
30
 
29
31
  private
30
32
 
31
- def financial_data
32
- {
33
- monthlyCardRevenue: extra_info[:monthly_card_revenue].to_i,
34
- monthlyRevenueFromInvoices: extra_info[:monthly_revenue_from_invoices].to_i,
35
- paymentData: payment_data
36
- }
37
- end
38
-
39
- def payment_data
40
- (1..12).to_a.map do |month|
41
- formatted_month = format('%02<month>d', month: month)
42
-
43
- {
44
- paymentDate: "#{Date.today.year - 1}-#{formatted_month}-01",
45
- amount: total_amount,
46
- currencyISOCode: CURRENCY
47
- }
48
- end
49
- end
50
-
51
- def total_amount
52
- @total_amount ||= begin
53
- card = extra_info[:monthly_card_revenue].to_i || 0
54
- invoices = extra_info[:monthly_revenue_from_invoices].to_i || 0
55
-
56
- card + invoices
57
- end
58
- end
59
-
60
33
  def company_address
61
34
  address = company.address
62
35
 
@@ -81,13 +54,13 @@ module InstantQuote
81
54
  def company_type_translated
82
55
  case company.company_type.name
83
56
  when /LTD/
84
- 'ltd'
57
+ 'Ltd'
85
58
  when /LLP/
86
- 'llp'
59
+ 'Llp'
87
60
  when /PLC/
88
- 'plc'
61
+ 'Plc'
89
62
  when 'Sole Trader'
90
- 'soleTrader'
63
+ 'SoleTrader'
91
64
  else
92
65
  'other'
93
66
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InstantQuote
4
+ module ConnectionTranslators
5
+ class YoulendPersons < ConnectionTranslator
6
+ def translate
7
+ people = application.borrower_organisation.company_people.map do |person|
8
+ translate_person(person)
9
+ end
10
+
11
+ {
12
+ significantPersons: people.select { |p| valid?(p) }
13
+ }
14
+ end
15
+
16
+ private
17
+
18
+ def valid?(person_hash)
19
+ person_hash.values_at(:address, :typeOfPerson, :dateOfBirth, :firstName, :surname)
20
+ .none?(&:nil?)
21
+ end
22
+
23
+ def translate_person(person)
24
+ {
25
+ address: address(person),
26
+ dateOfBirth: date_of_birth(person),
27
+ firstName: person.first_name || person.name,
28
+ surname: person.last_name || person.name,
29
+ typeOfPerson: type_of_person(person),
30
+ emailAddress: application.primary_user.email,
31
+ mobilePhoneNumber: extra_info[:applicant_phone_number],
32
+ percentageOwned: person.shares_percent || 0,
33
+ nationality: person.nationality
34
+ }
35
+ end
36
+
37
+ def address(person)
38
+ address = person.address
39
+ return unless address
40
+
41
+ {
42
+ line1: address.line_1,
43
+ line2: address.line_2,
44
+ city: address.town,
45
+ region: address.town,
46
+ areaCode: address.postcode,
47
+ country: address.country
48
+ }
49
+ end
50
+
51
+ def type_of_person(person)
52
+ if person.director? && person.shareholder?
53
+ 'DirectorAndBeneficialOwner'
54
+ elsif person.director?
55
+ 'Director'
56
+ elsif person.shareholder?
57
+ 'BeneficialOwner'
58
+ end
59
+ end
60
+
61
+ def date_of_birth(person)
62
+ dob = person.birthdate || application.primary_user.birthdate
63
+ return unless dob
64
+
65
+ {
66
+ year: dob.year,
67
+ month: dob.month,
68
+ day: dob.day
69
+ }
70
+ end
71
+ end
72
+ end
73
+ end
@@ -6,71 +6,145 @@ module InstantQuote
6
6
  module DecisionParsers
7
7
  # Example response:
8
8
  #
9
+ # Details response merged with optional offers
9
10
  # {
10
- # thirdPartyMerchantId: "b97b0834-74e5-4da6-a3df-c8dbe017fe09",
11
- # mid: nil,
12
- # companyName: "Test Company",
13
- # overallCreditRiskScore: 3.5,
14
- # overrideCreditRiskScore: 1.0,
15
- # loanOptions: [
16
- # { fundedAmount: 0.0, fee: 0.0, loanAmount: 0.0, sweep: 0.0, currencyISOCode: "GBP" },
17
- # { fundedAmount: 0.0, fee: 0.0, loanAmount: 0.0, sweep: 0.0, currencyISOCode: "GBP" },
18
- # { fundedAmount: 0.0, fee: 0.0, loanAmount: 0.0, sweep: 0.0, currencyISOCode: "GBP" }
19
- # ]
11
+ # "details": {
12
+ # "leadSourceLendingPartnerId": "2487faad-24c3-40ed-8e63-f4551555116a",
13
+ # "onboardingState": "offersProvided",
14
+ # "onboardingStateReason": "",
15
+ # "organisationDetailsStepComplete": true,
16
+ # "paymentDataStepComplete": true,
17
+ # "offerStepComplete": false,
18
+ # "kycDocumentsStepComplete": true,
19
+ # "companyName": "SATURDAY LTD",
20
+ # "leadType": "Api",
21
+ # "signingDetails": {
22
+ # "eligibleForInstantSigning": false,
23
+ # "instantSigningDetails": null
24
+ # }
25
+ # },
26
+ # "offers": {
27
+ # "leadId": "5c35a9f4-436e-47b9-a31c-b55f5881f8be",
28
+ # "previousAcceptedOffer": null,
29
+ # "providedOffers": [
30
+ # {
31
+ # "offerId": "12cfda2b-cfb6-4d79-bd16-2979ed6a069a",
32
+ # "youWillGet": 6000,
33
+ # "youWillRepay": 7020,
34
+ # "currencyISOCode": "GBP",
35
+ # "sweep": 20,
36
+ # "minimumWeeklyRepayment": 150.28,
37
+ # "daysUntilRepayment": 218,
38
+ # "maturityDate": "2024-06-26T16:42:15.038Z"
39
+ # },
40
+ # {
41
+ # "offerId": "2d93cb8b-7e3b-4c87-87cf-40e2f7e226e3",
42
+ # "youWillGet": 6500,
43
+ # "youWillRepay": 7735,
44
+ # "currencyISOCode": "GBP",
45
+ # "sweep": 20,
46
+ # "minimumWeeklyRepayment": 150.4,
47
+ # "daysUntilRepayment": 240,
48
+ # "maturityDate": "2024-07-29T16:42:15.038Z"
49
+ # },
50
+ # {
51
+ # "offerId": "f2ef06ef-0c29-418c-ad93-d7abdee5994e",
52
+ # "youWillGet": 7000,
53
+ # "youWillRepay": 8470,
54
+ # "currencyISOCode": "GBP",
55
+ # "sweep": 20,
56
+ # "minimumWeeklyRepayment": 150.1,
57
+ # "daysUntilRepayment": 263,
58
+ # "maturityDate": "2024-09-02T16:42:15.038Z"
59
+ # }
60
+ # ],
61
+ # "acceptedOffer": null,
62
+ # "unsignedLoanDocument": null,
63
+ # "unsignedLoanDocumentDownloaded": false,
64
+ # "documentSigningId": "00000000-0000-0000-0000-000000000000",
65
+ # "sortCode": null,
66
+ # "accountNumber": null,
67
+ # "swift": null,
68
+ # "iban": null,
69
+ # "bankName": null,
70
+ # "confirmedCompanyAccount": false,
71
+ # "minFundingAmount": 0,
72
+ # "maxFundingAmount": 0
73
+ # }
20
74
  # }
75
+ #
21
76
  class Youlend < DecisionParser
77
+ APPROVED_STATES = %w[offersProvided offerAccepted contractgenerated contractsigned]
78
+
22
79
  def approved?
23
- status_hash[:loanOptions].any? && !funded_option.empty?
80
+ APPROVED_STATES.include?(status) && loan_offer.present?
24
81
  end
25
82
 
26
83
  def declined?
27
- !approved?
84
+ !(approved? || incomplete? || submitted? || pending?)
85
+ end
86
+
87
+ def pending?
88
+ data.nil? || data.empty?
89
+ end
90
+
91
+ def submitted?
92
+ status == 'stage1submitted'
93
+ end
94
+
95
+ def incomplete?
96
+ status == 'stage1incomplete'
28
97
  end
29
98
 
30
99
  def sweep
31
100
  return '-' unless approved?
32
101
 
33
- funded_option[:sweep]
102
+ loan_offer[:sweep]
34
103
  end
35
104
 
36
105
  def status
37
106
  approved? ? 'approved' : 'declined'
38
107
  end
39
108
 
40
- def fee
41
- fee = funded_option[:fee].to_f || 0
42
-
43
- Money.new(fee * 100).format
44
- end
45
-
46
109
  def loan_amount
47
- amount = funded_option[:loanAmount].to_f || 0
48
-
49
- Money.new(amount * 100).format
110
+ Money.new(raw_loan_amount * 100).format
50
111
  end
51
112
 
52
113
  def funded_amount
53
- amount = funded_option[:fundedAmount].to_f || 0
114
+ Money.new(raw_funded_amount * 100).format
115
+ end
116
+
117
+ def fee
118
+ Money.new(raw_fee_amount * 100).format
119
+ end
54
120
 
55
- Money.new(amount * 100).format
121
+ def offer_id
122
+ loan_offer[:offerId]
56
123
  end
57
124
 
58
125
  alias amount funded_amount
59
126
 
60
127
  private
61
128
 
62
- def funded_option
63
- @funded_option ||= status_hash[:loanOptions].find(-> { {} }) do |opt|
64
- opt[:fundedAmount].positive?
65
- end
129
+ # Ideally we want to give the borrower the highest amount on offer.
130
+ def loan_offer
131
+ @loan_offer ||= (data.dig(:offers, :providedOffers) || []).max_by { |offer| offer[:youWillGet].to_f }
66
132
  end
67
133
 
68
- def status_hash
69
- default = ActiveSupport::HashWithIndifferentAccess.new({ loanOptions: [] })
134
+ def raw_fee_amount
135
+ raw_loan_amount - raw_funded_amount
136
+ end
70
137
 
71
- return default unless data && !data.empty? && !data[:error]
138
+ def raw_funded_amount
139
+ loan_offer&.dig(:youWillGet)&.to_f || 0
140
+ end
72
141
 
73
- default.merge(data)
142
+ def raw_loan_amount
143
+ loan_offer&.dig(:youWillRepay)&.to_f || 0
144
+ end
145
+
146
+ def status
147
+ data.dig(:details, :onboardingState)
74
148
  end
75
149
  end
76
150
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module InstantQuote
4
- VERSION = '1.7.33'
4
+ VERSION = '1.7.34'
5
5
  end
data/lib/instant_quote.rb CHANGED
@@ -37,6 +37,10 @@ module InstantQuote
37
37
  end
38
38
  end
39
39
 
40
+ def accept_offer(connection, offer_id)
41
+ AdapterFinder.new(connection).accept_offer(offer_id)
42
+ end
43
+
40
44
  def available_adapters
41
45
  AdapterFinder.available_adapters
42
46
  end
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.7.33
4
+ version: 1.7.34
5
5
  platform: ruby
6
6
  authors:
7
7
  - rikas
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-09-13 00:00:00.000000000 Z
11
+ date: 2023-09-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -150,6 +150,7 @@ files:
150
150
  - lib/instant_quote/connection_translators/iwoca_revenue_based_loans.rb
151
151
  - lib/instant_quote/connection_translators/optimum.rb
152
152
  - lib/instant_quote/connection_translators/youlend.rb
153
+ - lib/instant_quote/connection_translators/youlend_persons.rb
153
154
  - lib/instant_quote/decision_parser.rb
154
155
  - lib/instant_quote/decision_parsers/capital_on_tap.rb
155
156
  - lib/instant_quote/decision_parsers/fake.rb
@@ -183,7 +184,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
183
184
  - !ruby/object:Gem::Version
184
185
  version: '0'
185
186
  requirements: []
186
- rubygems_version: 3.2.3
187
+ rubygems_version: 3.0.8
187
188
  signing_key:
188
189
  specification_version: 4
189
190
  summary: Instant quote providers gem.