pact_broker 1.3.0 → 1.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -13
- data/.travis.yml +6 -0
- data/CHANGELOG.md +6 -0
- data/README.md +3 -1
- data/lib/pact_broker/api/contracts/consumer_version_number_validation.rb +32 -0
- data/lib/pact_broker/api/contracts/pacticipant_name_contract.rb +27 -0
- data/lib/pact_broker/api/contracts/pacticipant_name_validation.rb +30 -0
- data/lib/pact_broker/api/contracts/put_pact_params_contract.rb +55 -0
- data/lib/pact_broker/api/contracts/request_validations.rb +40 -0
- data/lib/pact_broker/api/contracts/webhook_contract.rb +32 -0
- data/lib/pact_broker/api/pact_broker_urls.rb +7 -0
- data/lib/pact_broker/api/resources/base_resource.rb +9 -0
- data/lib/pact_broker/api/resources/pact.rb +10 -3
- data/lib/pact_broker/api/resources/pact_webhooks.rb +9 -0
- data/lib/pact_broker/constants.rb +5 -0
- data/lib/pact_broker/doc/views/pacticipants.markdown +2 -2
- data/lib/pact_broker/locale/en.yml +6 -0
- data/lib/pact_broker/messages.rb +4 -0
- data/lib/pact_broker/models/webhook.rb +10 -8
- data/lib/pact_broker/models/webhook_request.rb +5 -17
- data/lib/pact_broker/pacts/pact_params.rb +79 -0
- data/lib/pact_broker/services/webhook_service.rb +6 -0
- data/lib/pact_broker/version.rb +1 -1
- data/pact_broker.gemspec +4 -3
- data/spec/fixtures/webhook_valid.json +14 -0
- data/spec/integration/endpoints/pact_put_spec.rb +16 -0
- data/spec/integration/endpoints/pact_webhooks_spec.rb +96 -0
- data/spec/lib/pact_broker/api/contracts/put_pact_params_contract_spec.rb +122 -0
- data/spec/lib/pact_broker/api/contracts/webhook_contract_spec.rb +106 -0
- data/spec/lib/pact_broker/api/resources/pact_spec.rb +15 -1
- data/spec/lib/pact_broker/api/resources/pact_webhooks_spec.rb +11 -8
- data/spec/lib/pact_broker/models/webhook_request_spec.rb +0 -31
- data/spec/lib/pact_broker/models/webhook_spec.rb +0 -21
- data/spec/lib/pact_broker/pacts/pact_params_spec.rb +69 -0
- data/spec/support/shared_examples_for_responses.rb +14 -0
- metadata +88 -55
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
N2QwYjkzYzk0YWQ1NjFkODVkZjIyOGJiYThhMzEwYzRlNGIxM2RkNw==
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c2efe6a0f09d53fcd3eda935e760ffdddf06c39c
|
4
|
+
data.tar.gz: 35000029a9b755d9c4f14be9ef900864dcb50a51
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
10
|
-
MmE3Yzc1NGVmZDA0YmQ2YjJlYjZhODIwODAxOGRhYTgxZGI1MTZmODRlOGY5
|
11
|
-
ZGYyMDRiNzBkN2MzMmIxOWI4ZTI4MDM1MDIwNmFiNTU5N2I0ZTk=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
MDNkNzJkNjgwYzI2ZjdiOGM2NTNhZmJiMDBmNWZjNWVjZTVmM2VlYTQyOTI4
|
14
|
-
NzEwN2JjYzUxYWViMzJiZGI1NzQwYTkyNTJjZTM2NGI1Y2FmNjE4OWVhYTUx
|
15
|
-
NTA1NDgyYjYxZDQyNDE4NGEyYWY3Yzc5M2VlYWJiYjBjNjRiYmY=
|
6
|
+
metadata.gz: f5852a4d2cd9bedd28fc5dff28da6d6464d9936a1cb9e693956364d7c6a0c421b364af5be6d3ff229dd24ac36462b8af530ae01209f433dd971574cd87691e8a
|
7
|
+
data.tar.gz: a2224dbd381b2313175752bc002c2d118363c90fdd9a08b54f20d6a868dd1599c334c3574fdeac5fc1c6cf3fff99b46c226695ee4bb7a4d2f6a3e24f29430b57
|
data/.travis.yml
ADDED
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,12 @@ Do this to generate your change history
|
|
2
2
|
|
3
3
|
$ git log --pretty=format:' * %h - %s (%an, %ad)'
|
4
4
|
|
5
|
+
#### 1.3.1 (2014-10-23)
|
6
|
+
|
7
|
+
* e61b40e - Added Travis configuration. (Beth, Fri Oct 17 16:32:26 2014 +1100)
|
8
|
+
* b320fe4 - Fixed pact publish validation for ruby 1.9.3 (Beth, Fri Oct 17 16:31:41 2014 +1100)
|
9
|
+
* b9b4d2b - Added validation to ensure that the participant names in the path match the participant names in the pact. (Beth, Thu Oct 16 20:21:10 2014 +1100)
|
10
|
+
|
5
11
|
#### 1.3.0 (2014-10-14)
|
6
12
|
|
7
13
|
* ed08811 - Converted raw SQL create view statements to Sequel so they will run on Postgres (Beth, Sat Oct 11 22:07:37 2014 +1100)
|
data/README.md
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
The Pact Broker provides a repository for pacts created using the pact gem. It solves the problem of how to share pacts between consumer and provider projects.
|
4
4
|
|
5
|
+
Travis CI Status: [![Build Status](https://travis-ci.org/bethesque/pact_broker.svg?branch=master)](https://travis-ci.org/bethesque/pact_broker)
|
6
|
+
|
5
7
|
The Pact Broker:
|
6
8
|
|
7
9
|
* Enables pacts to be shared between consumer and provider projects.
|
@@ -18,7 +20,7 @@ See the [wiki](https://github.com/bethesque/pact_broker/wiki) for documentation.
|
|
18
20
|
|
19
21
|
## Usage
|
20
22
|
|
21
|
-
* Create a database using a product that is supported by the Sequel gem (listed on this page http://sequel.jeremyevans.net/rdoc/files/README_rdoc.html). At time of writing, Sequel has adapters for: ADO, Amalgalite, CUBRID, DataObjects, DB2, DBI, Firebird, IBM_DB, Informix, JDBC, MySQL, Mysql2, ODBC, OpenBase, Oracle, PostgreSQL, SQLAnywhere, SQLite3, Swift, and TinyTDS.
|
23
|
+
* Create a database using a product that is supported by the Sequel gem (listed on this page http://sequel.jeremyevans.net/rdoc/files/README_rdoc.html). At time of writing, Sequel has adapters for: ADO, Amalgalite, CUBRID, DataObjects, DB2, DBI, Firebird, IBM_DB, Informix, JDBC, MySQL, Mysql2, ODBC, OpenBase, Oracle, PostgreSQL, SQLAnywhere, SQLite3, Swift, and TinyTDS.
|
22
24
|
* __Note:__ It is recommended to use __PostgreSQL__ as it will support JSON search features that are planned in a future release.
|
23
25
|
* Install ruby 1.9.3 or later
|
24
26
|
* Copy the [example](/example) directory to your workstation.
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module PactBroker
|
2
|
+
module Api
|
3
|
+
module Contracts
|
4
|
+
module ConsumerVersionNumberValidation
|
5
|
+
|
6
|
+
include PactBroker::Messages
|
7
|
+
|
8
|
+
def consumer_version_number_present
|
9
|
+
unless consumer_version_number
|
10
|
+
errors.add(:base, validation_message('consumer_version_number_missing'))
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def consumer_version_number_valid
|
15
|
+
if consumer_version_number && invalid_consumer_version_number?
|
16
|
+
errors.add(:base, consumer_version_number_validation_message)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def invalid_consumer_version_number?
|
21
|
+
begin
|
22
|
+
Versionomy.parse(consumer_version_number)
|
23
|
+
false
|
24
|
+
rescue Versionomy::Errors::ParseError => e
|
25
|
+
true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'reform'
|
2
|
+
require 'reform/contract'
|
3
|
+
|
4
|
+
module PactBroker
|
5
|
+
module Api
|
6
|
+
module Contracts
|
7
|
+
|
8
|
+
class PacticipantNameContract < Reform::Contract
|
9
|
+
property :name
|
10
|
+
property :name_in_pact
|
11
|
+
property :pacticipant
|
12
|
+
property :message_args
|
13
|
+
|
14
|
+
|
15
|
+
include PactBroker::Messages
|
16
|
+
|
17
|
+
def blank? string
|
18
|
+
string && string.strip.empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
def empty? string
|
22
|
+
string.nil? || blank?(string)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module PactBroker
|
2
|
+
module Api
|
3
|
+
module Contracts
|
4
|
+
module PacticipantNameValidation
|
5
|
+
|
6
|
+
include PactBroker::Messages
|
7
|
+
|
8
|
+
def name_in_pact_present
|
9
|
+
unless name_in_pact
|
10
|
+
errors.add(:'name', validation_message('pact_missing_pacticipant_name', pacticipant: pacticipant))
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def name_not_blank
|
15
|
+
if blank? name
|
16
|
+
errors.add(:'name', validation_message('blank'))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def blank? string
|
21
|
+
string && string.strip.empty?
|
22
|
+
end
|
23
|
+
|
24
|
+
def empty? string
|
25
|
+
string.nil? || blank?(string)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'reform'
|
2
|
+
require 'reform/contract'
|
3
|
+
require 'versionomy'
|
4
|
+
require 'pact_broker/messages'
|
5
|
+
require 'pact_broker/constants'
|
6
|
+
require 'pact_broker/api/contracts/pacticipant_name_contract'
|
7
|
+
require 'pact_broker/api/contracts/consumer_version_number_validation'
|
8
|
+
|
9
|
+
module PactBroker
|
10
|
+
module Api
|
11
|
+
module Contracts
|
12
|
+
|
13
|
+
class PutPacticipantNameContract < PacticipantNameContract
|
14
|
+
|
15
|
+
validates :name, presence: true, blank: false
|
16
|
+
validate :name_in_path_matches_name_in_pact
|
17
|
+
|
18
|
+
def name_in_path_matches_name_in_pact
|
19
|
+
if present?(name) && present?(name_in_pact)
|
20
|
+
if name != name_in_pact
|
21
|
+
errors.add(:name, validation_message('pacticipant_name_mismatch', message_args))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def present? string
|
27
|
+
string && !blank?(string)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
class PutPactParamsContract < Reform::Contract
|
33
|
+
|
34
|
+
include PactBroker::Messages
|
35
|
+
|
36
|
+
property :consumer_version_number
|
37
|
+
property :consumer, form: PutPacticipantNameContract
|
38
|
+
property :provider, form: PutPacticipantNameContract
|
39
|
+
|
40
|
+
validates :consumer_version_number, presence: true
|
41
|
+
validate :consumer_version_number_valid
|
42
|
+
|
43
|
+
|
44
|
+
include ConsumerVersionNumberValidation
|
45
|
+
|
46
|
+
def consumer_version_number_validation_message
|
47
|
+
validation_message('consumer_version_number_invalid', consumer_version_number: consumer_version_number)
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'reform'
|
2
|
+
require 'reform/contract'
|
3
|
+
require 'net/http'
|
4
|
+
|
5
|
+
module PactBroker
|
6
|
+
module Api
|
7
|
+
module Contracts
|
8
|
+
|
9
|
+
module RequestValidations
|
10
|
+
def method_is_valid
|
11
|
+
if http_method && !valid_method?
|
12
|
+
errors.add(:method, "is not a recognised HTTP method")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def valid_method?
|
17
|
+
Net::HTTP.const_defined?(http_method.capitalize)
|
18
|
+
end
|
19
|
+
|
20
|
+
def url_is_valid
|
21
|
+
if url && !url_valid?
|
22
|
+
errors.add(:url, "is not a valid URL eg. http://example.org")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def url_valid?
|
27
|
+
uri && uri.scheme && uri.host
|
28
|
+
end
|
29
|
+
|
30
|
+
def uri
|
31
|
+
begin
|
32
|
+
URI(url)
|
33
|
+
rescue URI::InvalidURIError
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'reform'
|
2
|
+
require 'reform/contract'
|
3
|
+
require 'pact_broker/api/contracts/request_validations'
|
4
|
+
|
5
|
+
module PactBroker
|
6
|
+
module Api
|
7
|
+
module Contracts
|
8
|
+
|
9
|
+
class WebhookContract < Reform::Contract
|
10
|
+
|
11
|
+
property :request
|
12
|
+
validates :request, presence: true
|
13
|
+
|
14
|
+
property :request do
|
15
|
+
|
16
|
+
include RequestValidations
|
17
|
+
|
18
|
+
property :url
|
19
|
+
property :http_method
|
20
|
+
|
21
|
+
validates :url, presence: true
|
22
|
+
validates :http_method, presence: true
|
23
|
+
|
24
|
+
validate :method_is_valid
|
25
|
+
validate :url_is_valid
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -29,6 +29,13 @@ module PactBroker
|
|
29
29
|
"#{pactigration_base_url(base_url, representable_pact)}/version/#{representable_pact.consumer.version.number}"
|
30
30
|
end
|
31
31
|
|
32
|
+
def pact_url_from_params base_url, params
|
33
|
+
[ base_url, 'pacts',
|
34
|
+
'provider', url_encode(params[:provider_name]),
|
35
|
+
'consumer', url_encode(params[:consumer_name]),
|
36
|
+
'version', url_encode(params[:consumer_version_number]) ].join('/')
|
37
|
+
end
|
38
|
+
|
32
39
|
def latest_pact_url base_url, pact
|
33
40
|
"#{pactigration_base_url(base_url, pact)}/latest"
|
34
41
|
end
|
@@ -36,6 +36,8 @@ module PactBroker
|
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
39
|
+
alias_method :path_info, :identifier_from_path
|
40
|
+
|
39
41
|
def base_url
|
40
42
|
request.uri.to_s.gsub(/#{request.uri.path}$/,'')
|
41
43
|
end
|
@@ -99,6 +101,13 @@ module PactBroker
|
|
99
101
|
end
|
100
102
|
end
|
101
103
|
|
104
|
+
def contract_validation_errors? contract
|
105
|
+
if (invalid = !contract.validate)
|
106
|
+
set_json_validation_error_messages contract.errors.full_messages
|
107
|
+
end
|
108
|
+
invalid
|
109
|
+
end
|
110
|
+
|
102
111
|
end
|
103
112
|
end
|
104
113
|
end
|
@@ -3,6 +3,8 @@ require 'pact_broker/api/resources/base_resource'
|
|
3
3
|
require 'pact_broker/api/resources/pacticipant_resource_methods'
|
4
4
|
require 'pact_broker/api/decorators/pact_decorator'
|
5
5
|
require 'pact_broker/json'
|
6
|
+
require 'pact_broker/pacts/pact_params'
|
7
|
+
require 'pact_broker/api/contracts/put_pact_params_contract'
|
6
8
|
|
7
9
|
module PactBroker
|
8
10
|
|
@@ -28,7 +30,8 @@ module PactBroker
|
|
28
30
|
def malformed_request?
|
29
31
|
if request.put?
|
30
32
|
return invalid_json? ||
|
31
|
-
|
33
|
+
contract_validation_errors?(Contracts::PutPactParamsContract.new(pact_params)) ||
|
34
|
+
potential_duplicate_pacticipants?(pact_params.pacticipant_names)
|
32
35
|
else
|
33
36
|
false
|
34
37
|
end
|
@@ -40,7 +43,7 @@ module PactBroker
|
|
40
43
|
|
41
44
|
def from_json
|
42
45
|
response_code = pact ? 200 : 201
|
43
|
-
@pact = pact_service.create_or_update_pact(
|
46
|
+
@pact = pact_service.create_or_update_pact(pact_params)
|
44
47
|
response.body = to_json
|
45
48
|
response_code
|
46
49
|
end
|
@@ -50,7 +53,11 @@ module PactBroker
|
|
50
53
|
end
|
51
54
|
|
52
55
|
def pact
|
53
|
-
@pact ||= pact_service.find_pact(
|
56
|
+
@pact ||= pact_service.find_pact(pact_params)
|
57
|
+
end
|
58
|
+
|
59
|
+
def pact_params
|
60
|
+
@pact_params ||= PactBroker::Pacts::PactParams.from_request request, path_info
|
54
61
|
end
|
55
62
|
|
56
63
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
require 'pact_broker/api/resources/base_resource'
|
3
3
|
require 'pact_broker/api/decorators/webhook_decorator'
|
4
4
|
require 'pact_broker/api/decorators/webhooks_decorator'
|
5
|
+
require 'pact_broker/api/contracts/webhook_contract'
|
5
6
|
|
6
7
|
module PactBroker
|
7
8
|
|
@@ -33,6 +34,14 @@ module PactBroker
|
|
33
34
|
false
|
34
35
|
end
|
35
36
|
|
37
|
+
def validation_errors? webhook
|
38
|
+
if (errors = webhook_service.errors(webhook)).any?
|
39
|
+
response.headers['Content-Type'] = 'application/json'
|
40
|
+
response.body = {errors: errors.full_messages }.to_json
|
41
|
+
end
|
42
|
+
errors.any?
|
43
|
+
end
|
44
|
+
|
36
45
|
def create_path
|
37
46
|
webhook_url next_uuid, base_url
|
38
47
|
end
|
@@ -2,9 +2,9 @@
|
|
2
2
|
|
3
3
|
"Pacticipant" - a party that participates in a pact (ie. a Consumer or a Provider).
|
4
4
|
|
5
|
-
### Creating
|
5
|
+
### Creating pacticipants
|
6
6
|
Participants are created automatically when a pact is published to the pact broker. The name is based on the URL compontents used to publish the pact (ie. /pacts/provider/$PROVIDER\_NAME/consumer/$CONSUMER\_NAME/version/$CONSUMER\_VERSION), not on the contents of the pact, as the Pact Broker is designed to be agnostic of the actual pact format as much as possible.
|
7
7
|
|
8
8
|
|
9
9
|
### Deleting pacticipants
|
10
|
-
Deleting a pacticipant will delete all associated pacts, versions, tags and webhooks. To delete a pacticipant, send a DELETE request to the relevant pacticipant URL via the HAL browser.
|
10
|
+
Deleting a pacticipant will delete all associated pacts, versions, tags and webhooks. To delete a pacticipant, send a DELETE request to the relevant pacticipant URL via the HAL browser.
|
@@ -2,9 +2,15 @@ en:
|
|
2
2
|
pact_broker:
|
3
3
|
errors:
|
4
4
|
validation:
|
5
|
+
blank: "cannot be blank."
|
5
6
|
attribute_missing: "Missing required attribute '%{attribute}'"
|
6
7
|
invalid_http_method: "Invalid HTTP method '%{method}'"
|
7
8
|
invalid_url: "Invalid URL '%{url}'. Expected format: http://example.org"
|
9
|
+
pact_missing_pacticipant_name: "was not found at expected path $.%{pacticipant}.name in the submitted pact file."
|
10
|
+
consumer_version_number_missing: "Please specify the consumer version number by setting the X-Pact-Consumer-Version header."
|
11
|
+
consumer_version_number_header_invalid: "X-Pact-Consumer-Version '%{consumer_version_number}' is not recognised as a standard semantic version. eg. 1.3.0 or 2.0.4.rc1"
|
12
|
+
consumer_version_number_invalid: "Consumer version number '%{consumer_version_number}' is not recognised as a standard semantic version. eg. 1.3.0 or 2.0.4.rc1"
|
13
|
+
pacticipant_name_mismatch: "in pact ('%{name_in_pact}') does not match %{pacticipant} name in path ('%{name}')."
|
8
14
|
duplicate_pacticipant: |
|
9
15
|
This is the first time a pact has been published for "%{new_name}".
|
10
16
|
The name "%{new_name}" is very similar to the following existing consumers/providers:
|
data/lib/pact_broker/messages.rb
CHANGED
@@ -19,6 +19,10 @@ module PactBroker
|
|
19
19
|
::I18n.t(key, options.merge(:scope => :pact_broker))
|
20
20
|
end
|
21
21
|
|
22
|
+
def validation_message key, options = {}
|
23
|
+
message('errors.validation.' + key, options)
|
24
|
+
end
|
25
|
+
|
22
26
|
def potential_duplicate_pacticipant_message new_name, potential_duplicate_pacticipants, base_url
|
23
27
|
existing_names = potential_duplicate_pacticipants.
|
24
28
|
collect{ | p | "* #{p.name}" }.join("\n")
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'pact_broker/models/webhook_request'
|
2
2
|
require 'pact_broker/messages'
|
3
3
|
require 'pact_broker/logging'
|
4
|
+
require 'pact_broker/api/contracts/webhook_contract'
|
4
5
|
|
5
6
|
module PactBroker
|
6
7
|
|
@@ -22,13 +23,6 @@ module PactBroker
|
|
22
23
|
@updated_at = attributes[:updated_at]
|
23
24
|
end
|
24
25
|
|
25
|
-
def validate
|
26
|
-
messages = []
|
27
|
-
messages << message('errors.validation.attribute_missing', attribute: 'request') unless request
|
28
|
-
messages.concat request.validate if request
|
29
|
-
messages
|
30
|
-
end
|
31
|
-
|
32
26
|
def description
|
33
27
|
"A webhook for the pact between #{consumer.name} and #{provider.name}"
|
34
28
|
end
|
@@ -44,7 +38,15 @@ module PactBroker
|
|
44
38
|
end
|
45
39
|
|
46
40
|
def to_s
|
47
|
-
"webhook for consumer=#{
|
41
|
+
"webhook for consumer=#{consumer_name} provider=#{provider_name} uuid=#{uuid} request=#{request}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def consumer_name
|
45
|
+
consumer && consumer.name
|
46
|
+
end
|
47
|
+
|
48
|
+
def provider_name
|
49
|
+
provider && provider.name
|
48
50
|
end
|
49
51
|
end
|
50
52
|
|