accountability 0.1.1 → 0.2.1
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/README.md +86 -9
- data/app/assets/stylesheets/accountability/application.scss +0 -0
- data/app/controllers/accountability/accounts_controller.rb +2 -4
- data/app/controllers/accountability/billing_configurations_controller.rb +80 -0
- data/app/controllers/accountability/order_groups_controller.rb +2 -4
- data/app/controllers/accountability/payments_controller.rb +25 -0
- data/app/controllers/accountability/products_controller.rb +7 -5
- data/app/controllers/accountability/statements_controller.rb +19 -0
- data/app/controllers/accountability_controller.rb +47 -0
- data/app/helpers/accountability/{application_helper.rb → accountability_helper.rb} +1 -1
- data/app/helpers/accountability/billing_configurations_helper.rb +47 -0
- data/app/helpers/accountability/products_helper.rb +21 -0
- data/app/models/accountability/account.rb +31 -1
- data/app/models/accountability/application_record.rb +24 -0
- data/app/models/accountability/billing_configuration.rb +41 -0
- data/app/models/accountability/coupon.rb +2 -0
- data/app/models/accountability/credit.rb +53 -29
- data/app/models/accountability/debit.rb +10 -0
- data/app/models/accountability/inventory.rb +61 -0
- data/app/models/accountability/offerable.rb +28 -1
- data/app/models/accountability/offerable/scope.rb +48 -0
- data/app/models/accountability/order_item.rb +3 -7
- data/app/models/accountability/payment.rb +22 -3
- data/app/models/accountability/product.rb +45 -5
- data/app/models/accountability/statement.rb +46 -0
- data/app/models/accountability/{account/transactions.rb → transactions.rb} +14 -2
- data/app/models/concerns/accountability/active_merchant_interface.rb +15 -0
- data/app/models/concerns/accountability/active_merchant_interface/stripe_interface.rb +134 -0
- data/app/pdfs/statement_pdf.rb +91 -0
- data/app/views/accountability/accounts/show.html.haml +21 -9
- data/app/views/accountability/products/index.html.haml +17 -34
- data/app/views/accountability/products/new.html.haml +73 -0
- data/app/views/accountability/shared/_session_info.html.haml +24 -0
- data/config/locales/en.yml +19 -0
- data/db/migrate/20190814000455_create_accountability_tables.rb +43 -1
- data/lib/accountability.rb +2 -1
- data/lib/accountability/configuration.rb +22 -2
- data/lib/accountability/engine.rb +6 -1
- data/lib/accountability/extensions/acts_as_billable.rb +11 -0
- data/lib/accountability/rails/routes.rb +72 -0
- data/lib/accountability/types.rb +9 -0
- data/lib/accountability/types/billing_configuration_types.rb +57 -0
- data/lib/accountability/version.rb +1 -1
- data/lib/generators/accountability/install_generator.rb +47 -0
- data/lib/generators/accountability/templates/migration.rb.tt +155 -0
- data/lib/generators/accountability/templates/price_overrides_migration.rb.tt +13 -0
- metadata +73 -12
- data/app/assets/stylesheets/accountability/application.css +0 -15
- data/app/controllers/accountability/application_controller.rb +0 -45
- data/app/views/accountability/products/new.html.erb +0 -40
- data/lib/accountability/cartographer.rb +0 -28
@@ -5,15 +5,39 @@
|
|
5
5
|
# Expired - No longer available, but still usable by prior adopters
|
6
6
|
# Terminated - No longer available - even by prior adopters
|
7
7
|
|
8
|
+
# rubocop:disable Rails/HasAndBelongsToMany
|
9
|
+
|
8
10
|
module Accountability
|
9
11
|
class Product < ApplicationRecord
|
12
|
+
RECURRING_SCHEDULES = %i[weekly monthly annually].freeze
|
13
|
+
|
10
14
|
has_and_belongs_to_many :coupons
|
11
15
|
has_many :order_items, dependent: :restrict_with_error
|
12
16
|
has_many :credits, through: :order_items, inverse_of: :product
|
13
17
|
|
14
18
|
serialize :source_scope, Hash
|
15
19
|
|
16
|
-
enum schedule:
|
20
|
+
enum schedule: [:one_time, *RECURRING_SCHEDULES], _prefix: :accrues
|
21
|
+
|
22
|
+
def active?
|
23
|
+
return false unless activation_date.present?
|
24
|
+
|
25
|
+
activation_date.past?
|
26
|
+
end
|
27
|
+
|
28
|
+
def billing_cycle_length
|
29
|
+
case schedule
|
30
|
+
when 'weekly' then 1.week
|
31
|
+
when 'monthly' then 1.month
|
32
|
+
when 'annually' then 1.year
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns an Inventory object containing each record within the product's scope.
|
37
|
+
# This method will eventually phase out the `inventory` and `available_inventory` methods.
|
38
|
+
def inventory_items
|
39
|
+
Inventory.new(self)
|
40
|
+
end
|
17
41
|
|
18
42
|
def inventory
|
19
43
|
return [] if source_class.nil?
|
@@ -21,6 +45,22 @@ module Accountability
|
|
21
45
|
source_class.where(**source_scope)
|
22
46
|
end
|
23
47
|
|
48
|
+
def available_inventory
|
49
|
+
return [] if source_class.nil?
|
50
|
+
|
51
|
+
source_class.where(**source_scope).public_send(offerable_template.whitelist)
|
52
|
+
end
|
53
|
+
|
54
|
+
# TODO: Update offerable_template.scopes to return an array of Scope objects and delegate to that instead
|
55
|
+
def scopes
|
56
|
+
return @scopes if @scopes.present?
|
57
|
+
|
58
|
+
@scopes = offerable_template.scopes.map do |attribute, params|
|
59
|
+
params.merge! source_class: source_class, attribute: attribute
|
60
|
+
Offerable::Scope.new(**params)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
24
64
|
def offerable_template
|
25
65
|
return if offerable_category.nil?
|
26
66
|
return @offerable if @offerable.present?
|
@@ -43,11 +83,11 @@ module Accountability
|
|
43
83
|
end
|
44
84
|
|
45
85
|
delegate :callbacks, to: :offerable_template
|
46
|
-
end
|
47
86
|
|
48
|
-
|
87
|
+
private
|
49
88
|
|
50
|
-
|
51
|
-
|
89
|
+
def raise_offerable_not_found
|
90
|
+
raise 'Offerable not found'
|
91
|
+
end
|
52
92
|
end
|
53
93
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# New Credit records are automatically assigned to the latest Statement on their associated Account.
|
2
|
+
# If the latest statement is past its :end_date, the credit will build a new one.
|
3
|
+
# The new :end_date is determined by the account's :statement_schedule enum.
|
4
|
+
|
5
|
+
module Accountability
|
6
|
+
class Statement < ApplicationRecord
|
7
|
+
self.implicit_order_column = 'end_date'
|
8
|
+
|
9
|
+
before_validation :set_end_date
|
10
|
+
|
11
|
+
belongs_to :account
|
12
|
+
has_many :credits, dependent: :destroy
|
13
|
+
|
14
|
+
validates :end_date, presence: true
|
15
|
+
|
16
|
+
delegate :past?, :future?, to: :end_date
|
17
|
+
|
18
|
+
def paid?
|
19
|
+
return false if account.last_balanced_at.nil?
|
20
|
+
|
21
|
+
end_date.before? account.last_balanced_at
|
22
|
+
end
|
23
|
+
|
24
|
+
def total_accrued
|
25
|
+
credits.sum(:amount)
|
26
|
+
end
|
27
|
+
|
28
|
+
def transactions
|
29
|
+
associated_credits = credits.includes(:product, deductions: :discount).references(:order_item)
|
30
|
+
|
31
|
+
Transactions.new(credits: associated_credits)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def set_end_date
|
37
|
+
return if account.nil?
|
38
|
+
return if end_date.present?
|
39
|
+
|
40
|
+
self.end_date = case account.statement_schedule
|
41
|
+
when 'end_of_month' then Time.current.end_of_month
|
42
|
+
when 'bi_weekly' then 2.weeks.from_now
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -1,12 +1,12 @@
|
|
1
1
|
require 'forwardable'
|
2
2
|
|
3
3
|
module Accountability
|
4
|
-
class
|
4
|
+
class Transactions
|
5
5
|
extend Forwardable
|
6
6
|
|
7
7
|
attr_accessor :transactions
|
8
8
|
|
9
|
-
def_delegators :transactions, :each, :sort_by
|
9
|
+
def_delegators :transactions, :each, :sort_by, :any?, :none?
|
10
10
|
|
11
11
|
def initialize(debits: [], credits: [])
|
12
12
|
debit_transactions = debits.map do |debit|
|
@@ -38,6 +38,18 @@ module Accountability
|
|
38
38
|
def credit?
|
39
39
|
type == :credit
|
40
40
|
end
|
41
|
+
|
42
|
+
def base_amount
|
43
|
+
debit? ? base_amount : record.base_price
|
44
|
+
end
|
45
|
+
|
46
|
+
def deductions
|
47
|
+
debit? ? [] : record.deductions
|
48
|
+
end
|
49
|
+
|
50
|
+
def taxes
|
51
|
+
debit? ? 0 : record.taxes
|
52
|
+
end
|
41
53
|
end
|
42
54
|
end
|
43
55
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Accountability
|
2
|
+
module ActiveMerchantInterface
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
provider = Accountability::Configuration.payment_gateway[:provider]
|
7
|
+
case provider
|
8
|
+
when :stripe
|
9
|
+
include StripeInterface
|
10
|
+
else
|
11
|
+
raise NotImplementedError, "No ActiveMerchantInterface defined for #{provider}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# TODO: Implement #unstore_active_merchant_data
|
2
|
+
|
3
|
+
module Accountability
|
4
|
+
module ActiveMerchantInterface::StripeInterface
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
attribute :stripe_api_errors, default: []
|
9
|
+
validate :validate_no_stripe_errors
|
10
|
+
|
11
|
+
# It is important to clear these errors after validation
|
12
|
+
# so that store_active_merchant_data will run again.
|
13
|
+
after_validation :clear_stripe_api_errors, on: :create
|
14
|
+
after_save_commit :save_customer_info_to_stripe
|
15
|
+
end
|
16
|
+
|
17
|
+
def charge(amount)
|
18
|
+
gateway = initialize_payment_gateway
|
19
|
+
amount_in_cents = (amount * 100).round
|
20
|
+
card_id = active_merchant_data['authorization']
|
21
|
+
|
22
|
+
response = gateway.purchase(amount_in_cents, card_id)
|
23
|
+
|
24
|
+
return true if response.success?
|
25
|
+
|
26
|
+
error_code = response.error_code || I18n.t('accountability.gateway.errors.unknown_gateway_error')
|
27
|
+
stripe_api_errors.append(error_code)
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
31
|
+
def store_active_merchant_data(**options)
|
32
|
+
response = store_card_in_gateway
|
33
|
+
|
34
|
+
unless response.success?
|
35
|
+
error_code = response.error_code || I18n.t('accountability.gateway.errors.unknown_gateway_error')
|
36
|
+
stripe_api_errors.append(error_code)
|
37
|
+
return
|
38
|
+
end
|
39
|
+
|
40
|
+
active_merchant_data = extract_active_merchant_data(response)
|
41
|
+
validate_chargeable(**active_merchant_data) if options.fetch(:verify_card)
|
42
|
+
|
43
|
+
self.active_merchant_data = active_merchant_data
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def store_card_in_gateway(gateway = initialize_payment_gateway)
|
49
|
+
gateway.store(token, description: configuration_name, email: contact_email, set_default: true)
|
50
|
+
end
|
51
|
+
|
52
|
+
def validate_chargeable(gateway = initialize_payment_gateway, **active_merchant_data)
|
53
|
+
authorization = active_merchant_data[:authorization]
|
54
|
+
|
55
|
+
response = gateway.verify(authorization, verification_params)
|
56
|
+
return if response.success?
|
57
|
+
|
58
|
+
error_code = response.error_code || I18n.t('accountability.gateway.errors.unknown_gateway_error')
|
59
|
+
stripe_api_errors.append(error_code)
|
60
|
+
end
|
61
|
+
|
62
|
+
def extract_active_merchant_data(response)
|
63
|
+
customer_id = response.params['id']
|
64
|
+
card_id = response.params['sources']['data'].first['id']
|
65
|
+
authorization = response.authorization
|
66
|
+
|
67
|
+
data = { authorization: authorization, customer_id: customer_id, card_id: card_id }
|
68
|
+
data.symbolize_keys
|
69
|
+
end
|
70
|
+
|
71
|
+
def initialize_payment_gateway
|
72
|
+
secret_key = Accountability::Configuration.payment_gateway.dig(:authentication, :secret_key)
|
73
|
+
ActiveMerchant::Billing::StripeGateway.new(login: secret_key)
|
74
|
+
end
|
75
|
+
|
76
|
+
def add_customer_info(customer_id, gateway = initialize_payment_gateway)
|
77
|
+
response = gateway.update_customer(customer_id, customer_params)
|
78
|
+
return unless response.error_code
|
79
|
+
|
80
|
+
Rails.logger.warn %(Warning: add_customer_info failed for #{self.class}: #{id}; card_id: #{card_id};
|
81
|
+
customer_id: #{customer_id}; Stripe Error: #{add_customer_info_response.error_code}).squish
|
82
|
+
end
|
83
|
+
|
84
|
+
def add_card_info(customer_id, card_id, gateway = initialize_payment_gateway)
|
85
|
+
response = gateway.update(customer_id, card_id, card_params)
|
86
|
+
return unless response.error_code
|
87
|
+
|
88
|
+
Rails.logger.warn %(Warning: add_card_info failed for #{self.class}: #{id}; card_id: #{card_id};
|
89
|
+
customer_id: #{customer_id}; Stripe Error: #{add_customer_info_response.error_code}).squish
|
90
|
+
end
|
91
|
+
|
92
|
+
def save_customer_info_to_stripe(gateway = initialize_payment_gateway)
|
93
|
+
customer_id, card_id = active_merchant_data.with_indifferent_access.values_at(:customer_id, :card_id)
|
94
|
+
return unless customer_id && card_id
|
95
|
+
|
96
|
+
add_customer_info(customer_id, gateway)
|
97
|
+
add_card_info(customer_id, card_id, gateway)
|
98
|
+
end
|
99
|
+
|
100
|
+
def verification_params
|
101
|
+
{ description: 'Accountability Verification Charge', statement_description: 'Card Verification' }
|
102
|
+
end
|
103
|
+
|
104
|
+
def card_params
|
105
|
+
{ metadata: { billing_configuration_id: id, account_id: account.id },
|
106
|
+
name: contact_name,
|
107
|
+
address_line1: billing_address.address_1,
|
108
|
+
address_line2: billing_address.address_2,
|
109
|
+
address_city: billing_address.city,
|
110
|
+
address_zip: billing_address.zip,
|
111
|
+
address_state: billing_address.state,
|
112
|
+
address_country: billing_address.country }
|
113
|
+
end
|
114
|
+
|
115
|
+
def customer_params
|
116
|
+
{ metadata: { billing_configuration_id: id, account_id: account.id },
|
117
|
+
name: contact_name, email: contact_email,
|
118
|
+
address: { line1: billing_address.address_1,
|
119
|
+
line2: billing_address.address_2,
|
120
|
+
city: billing_address.city,
|
121
|
+
postal_code: billing_address.zip,
|
122
|
+
state: billing_address.state,
|
123
|
+
country: billing_address.country } }
|
124
|
+
end
|
125
|
+
|
126
|
+
def validate_no_stripe_errors
|
127
|
+
stripe_api_errors.each { |error| errors.add(:base, error) }
|
128
|
+
end
|
129
|
+
|
130
|
+
def clear_stripe_api_errors
|
131
|
+
stripe_api_errors.clear
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'prawn'
|
2
|
+
require 'prawn/table'
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
class StatementPdf
|
6
|
+
include ActiveSupport::NumberHelper
|
7
|
+
include Prawn::View
|
8
|
+
|
9
|
+
attr_accessor :statement
|
10
|
+
|
11
|
+
def initialize(statement, debug_mode: false)
|
12
|
+
@statement = statement
|
13
|
+
@debug_mode = debug_mode
|
14
|
+
|
15
|
+
draw_document
|
16
|
+
end
|
17
|
+
|
18
|
+
def draw_document
|
19
|
+
font 'Helvetica'
|
20
|
+
|
21
|
+
stroke_axis if debug_mode?
|
22
|
+
draw_header
|
23
|
+
move_down 40
|
24
|
+
draw_transactions_section
|
25
|
+
number_pages '<page>', align: :center, at: [0, -6]
|
26
|
+
end
|
27
|
+
|
28
|
+
def draw_header
|
29
|
+
# Draw upper-left text
|
30
|
+
bounding_box [0, 720], width: 250, height: 70 do
|
31
|
+
move_down 10
|
32
|
+
text 'STATEMENT', size: 18
|
33
|
+
move_down 5
|
34
|
+
text "Ending: #{statement.end_date.strftime('%B %-d, %Y')}", align: :left
|
35
|
+
|
36
|
+
transparent(0.5) { stroke_bounds } if debug_mode?
|
37
|
+
end
|
38
|
+
|
39
|
+
# Draw upper-right logo
|
40
|
+
bounding_box [290, 720], width: 250, height: 70 do
|
41
|
+
logo_path = Accountability::Configuration.logo_path
|
42
|
+
image logo_path, fit: [250, 70], position: :right
|
43
|
+
transparent(0.5) { stroke_bounds } if debug_mode?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def draw_transactions_section
|
48
|
+
text 'Details', style: :bold_italic
|
49
|
+
stroke_horizontal_rule
|
50
|
+
move_down 15
|
51
|
+
|
52
|
+
# Draw transactions table
|
53
|
+
transactions = Accountability::Transactions.new(credits: statement.credits.includes(:product, :deductions))
|
54
|
+
transaction_data = transactions.sort_by(&:date).reverse.map do |transaction|
|
55
|
+
[transaction.date.strftime('%b %-d, %Y'), make_transaction_subtable(transaction)]
|
56
|
+
end
|
57
|
+
|
58
|
+
table transaction_data, column_widths: [100, 440], row_colors: %w[d2e3ed ffffff]
|
59
|
+
|
60
|
+
# Draw statement total in separate table
|
61
|
+
total_data = [[nil, 'Total ', decorate_currency(statement.total_accrued)]]
|
62
|
+
table total_data, cell_style: { borders: [], font_style: :bold }, column_widths: [100, 340, 100] do
|
63
|
+
row(-1).columns(1).style align: :right
|
64
|
+
row(-1).columns(2).borders = %i[bottom left right]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def make_transaction_subtable(transaction)
|
69
|
+
rows = [[transaction.description, decorate_currency(transaction.base_amount)]]
|
70
|
+
|
71
|
+
# Define deduction rows if applicable
|
72
|
+
transaction.deductions.each do |deduction|
|
73
|
+
rows.append([deduction.coupon_name, decorate_currency(-deduction.amount)])
|
74
|
+
end
|
75
|
+
|
76
|
+
# Define tax row if applicable
|
77
|
+
rows.append(['Tax', decorate_currency(transaction.taxes)]) if transaction.taxes.nonzero?
|
78
|
+
|
79
|
+
make_table rows, column_widths: [340, 100]
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def debug_mode?
|
85
|
+
@debug_mode
|
86
|
+
end
|
87
|
+
|
88
|
+
def decorate_currency(value)
|
89
|
+
number_to_currency value, negative_format: '(%u%n)'
|
90
|
+
end
|
91
|
+
end
|
@@ -1,12 +1,24 @@
|
|
1
|
-
|
1
|
+
.container
|
2
|
+
- if Accountability::Configuration.dev_tools_enabled?
|
3
|
+
= render partial: 'accountability/shared/session_info'
|
2
4
|
|
3
|
-
|
5
|
+
- if admin_session?
|
6
|
+
= link_to 'Back', accountability_accounts_path, class: 'btn btn-outline-primary float-right'
|
4
7
|
|
5
|
-
%
|
6
|
-
%
|
7
|
-
= @account.billable.id
|
8
|
-
%p
|
9
|
-
%b Balance:
|
10
|
-
= @account.balance
|
8
|
+
%h1 Billing Info
|
9
|
+
%h4.mb-5= @account.billable_record_name
|
11
10
|
|
12
|
-
|
11
|
+
.row
|
12
|
+
.col-md-6
|
13
|
+
.card
|
14
|
+
.card-header
|
15
|
+
Account Balance
|
16
|
+
.card-body
|
17
|
+
.text-primary= @account.balance
|
18
|
+
|
19
|
+
.col-md-6
|
20
|
+
.card
|
21
|
+
.card-header
|
22
|
+
Payment History
|
23
|
+
.card-body
|
24
|
+
.text-secondary Coming Soon
|
@@ -1,39 +1,22 @@
|
|
1
|
-
|
1
|
+
.container
|
2
|
+
- if Accountability::Configuration.dev_tools_enabled?
|
3
|
+
= render partial: 'accountability/shared/session_info'
|
2
4
|
|
3
|
-
|
4
|
-
|
5
|
-
= link_to 'New Product', new_accountability_product_path
|
5
|
+
- if current_order_group.order_items.any?
|
6
|
+
= link_to 'Shopping Cart', accountability_order_group_path(current_order_group), class: 'btn btn-outline-primary float-right ml-2'
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
= admin_session? ? 'yep' : 'nope'
|
8
|
+
- if current_account.present?
|
9
|
+
= link_to 'Billing', accountability_account_path(current_account), class: 'btn btn-outline-primary float-right ml-2'
|
10
10
|
|
11
|
-
|
12
|
-
%p
|
13
|
-
%b Email:
|
14
|
-
= current_user&.email
|
11
|
+
%h1.mb-5 Products
|
15
12
|
|
16
|
-
|
17
|
-
|
18
|
-
|
13
|
+
- @products.each do |product|
|
14
|
+
.card.mb-4
|
15
|
+
.card-header
|
16
|
+
= product.name
|
17
|
+
.card-body
|
18
|
+
= product.description
|
19
|
+
= button_to 'Add to cart', add_item_accountability_order_group_path(current_order_group, product_id: product.id), class: 'btn btn-outline-success float-right'
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
= current_account
|
23
|
-
|
24
|
-
%p
|
25
|
-
%b OrderGroup ID (db):
|
26
|
-
= current_order_group&.id
|
27
|
-
|
28
|
-
%br
|
29
|
-
%br
|
30
|
-
%br
|
31
|
-
%br
|
32
|
-
%br
|
33
|
-
%hr
|
34
|
-
|
35
|
-
- @products.each do |product|
|
36
|
-
%h2= product.name
|
37
|
-
%p= product.description
|
38
|
-
= button_to 'Add to cart', add_item_accountability_order_group_path(current_order_group, product_id: product.id)
|
39
|
-
%hr
|
21
|
+
- if admin_session?
|
22
|
+
= link_to 'Add New Product', new_accountability_product_path, class: 'btn btn-outline-primary float-right mt-5'
|