Last active
December 26, 2015 23:59
-
-
Save dpmccabe/7234858 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # == Schema Information | |
| # | |
| # Table name: billings | |
| # | |
| # id :integer not null, primary key | |
| # user_id :integer | |
| # bt_cc_token :string(255) | |
| # bt_cust_id :string(255) | |
| # bt_cc_expiration :string(255) | |
| # bt_cc_last4 :string(255) | |
| # bt_card_type :string(255) | |
| # bt_cardholder_name :string(255) | |
| # created_at :datetime not null | |
| # updated_at :datetime not null | |
| # | |
| class Billing < ActiveRecord::Base | |
| belongs_to :user | |
| has_many :transactions, as: :transactable, dependent: :destroy | |
| def name | |
| "#{self.user.name} - Billing" | |
| end | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Gem: braintree (2.16.0) | |
| class Brainforest | |
| extend ActiveModel::Naming | |
| include ActiveModel::Conversion | |
| include ActiveModel::Validations | |
| attr_reader :transaction, :success, :result, :billing | |
| attr_accessor :fname, :lname, :email, :address, :address_2, :country, :city, :state_province, :postal_code, :number, :number_encrypted, :expiration_month, :expiration_year, :cvv, :cvv_encrypted, :save_my_billing | |
| def initialize(attributes={}) | |
| attributes.each { |key, val| send("#{key}=", val) if respond_to?("#{key}=") } | |
| @attributes = attributes | |
| end | |
| def persisted? | |
| false | |
| end | |
| def valid? | |
| self.errors.empty? | |
| end | |
| def charge(item, price, amount_to_charge, store_in_vault=false, billing=nil) | |
| if amount_to_charge == 0 | |
| @transaction = Transaction.new(transactable: item) | |
| @success = true | |
| log_zero_charge(price) | |
| else | |
| validate_billing_fields | |
| if self.valid? | |
| @transaction = Transaction.new(transactable: item) | |
| begin | |
| @result = Braintree::Transaction.sale( | |
| amount: amount_to_charge, | |
| order_id: @transaction.id, | |
| merchant_account_id: Braintree::Configuration.verification_merchant_account_id, | |
| customer: { | |
| first_name: self.fname, | |
| last_name: self.lname, | |
| email: self.email | |
| }, | |
| credit_card: { | |
| cardholder_name: "#{self.fname} #{self.lname}", | |
| number: self.number_encrypted || self.number, | |
| expiration_month: self.expiration_month, | |
| expiration_year: self.expiration_year, | |
| cvv: self.cvv_encrypted || self.cvv | |
| }, | |
| billing: { | |
| street_address: self.address, | |
| extended_address: self.address_2, | |
| locality: self.city, | |
| region: self.state_province, | |
| postal_code: self.postal_code, | |
| country_code_alpha2: self.country | |
| }, | |
| options: { | |
| store_in_vault_on_success: store_in_vault, | |
| submit_for_settlement: true, | |
| add_billing_address_to_payment_method: true | |
| } | |
| ) | |
| @success = @result.success? | |
| rescue => e | |
| Airbrake.notify(e) | |
| @result = :error | |
| @success = false | |
| end | |
| log_charge(price, amount_to_charge) | |
| if @success | |
| store_result_in_vault(billing) if store_in_vault | |
| else | |
| set_errors | |
| end | |
| else | |
| @success = false | |
| end | |
| end | |
| end | |
| def validate_billing_fields | |
| [:fname, :lname, :address, :country, :city, :state_province, :postal_code, :number, :expiration_month, :expiration_year, :cvv].each do |attribute| | |
| self.errors.add(attribute, 'is required') if self.send(attribute).blank? | |
| end | |
| end | |
| def log_zero_charge(price) | |
| @transaction.attributes = { | |
| ttype: :charge, | |
| charged: 0, | |
| price: price, | |
| status: 'credited' | |
| } | |
| @transaction.save! | |
| end | |
| def log_charge(price, amount_to_charge) | |
| if @result == :error | |
| @transaction.attributes = { | |
| ttype: :charge, | |
| status: 'braintree_error' | |
| } | |
| elsif @result.transaction | |
| @transaction.attributes = { | |
| ttype: :charge, | |
| approved: @result.success?, | |
| charged: amount_to_charge, | |
| price: price, | |
| bt_transaction_id: @result.transaction.id, | |
| avs_error_response_code: @result.transaction.avs_error_response_code, | |
| avs_postal_code_response_code: @result.transaction.avs_postal_code_response_code, | |
| avs_street_address_response_code: @result.transaction.avs_street_address_response_code, | |
| cvv_response_code: @result.transaction.cvv_response_code, | |
| gateway_rejection_reason: @result.transaction.gateway_rejection_reason, | |
| processor_response_code: @result.transaction.processor_response_code, | |
| processor_response_text: @result.transaction.processor_response_text, | |
| status: @result.transaction.status | |
| } | |
| else | |
| @transaction.attributes = { | |
| ttype: :charge, | |
| approved: @result.success?, | |
| charged: amount_to_charge, | |
| price: price, | |
| validation_errors: @result.errors.collect { |error| "(#{error.code}) #{error.message}" }.join("\n"), | |
| status: 'validation_error' | |
| } | |
| end | |
| @transaction.save! | |
| end | |
| def store_result_in_vault(billing) | |
| billing.update_attributes( | |
| bt_cust_id: @result.transaction.customer_details.id, | |
| bt_cc_token: @result.transaction.credit_card_details.token, | |
| bt_cc_expiration: @result.transaction.credit_card_details.expiration_date, | |
| bt_cc_last4: @result.transaction.credit_card_details.last_4, | |
| bt_card_type: @result.transaction.credit_card_details.card_type, | |
| bt_cardholder_name: @result.transaction.credit_card_details.cardholder_name | |
| ) | |
| end | |
| def set_errors | |
| if @result == :error | |
| self.errors.add(:base, 'Credit card could not be processed right now. Please try again later (or right now).') | |
| elsif @result.transaction || @result.credit_card_verification | |
| self.errors.add(:base, 'Credit card could not be processed. Please check that your billing information is correct and try again.') | |
| else | |
| @result.errors.each do |error| | |
| self.errors.add(:base, error.message) | |
| end | |
| end | |
| self.errors[:base].uniq! | |
| end | |
| def charge_from_vault(item, price, amount_to_charge, billing) | |
| if amount_to_charge == 0 | |
| @transaction = Transaction.new(transactable: item) | |
| @success = true | |
| log_zero_charge(price) | |
| else | |
| @transaction = Transaction.new(transactable: item) | |
| begin | |
| @result = Braintree::CreditCard.sale( | |
| billing.bt_cc_token, | |
| merchant_account_id: Braintree::Configuration.verification_merchant_account_id, | |
| amount: amount_to_charge, | |
| options: { | |
| submit_for_settlement: true | |
| } | |
| ) | |
| @success = @result.success? | |
| rescue | |
| @result = :error | |
| @success = false | |
| end | |
| log_charge(price, amount_to_charge) | |
| set_errors unless @success | |
| end | |
| end | |
| def update_billing(billing) | |
| validate_billing_fields | |
| if self.valid? | |
| @transaction = Transaction.new(transactable: billing) | |
| begin | |
| @result = Braintree::Customer.update(billing.bt_cust_id, | |
| first_name: self.fname, | |
| last_name: self.lname, | |
| email: self.email, | |
| credit_card: { | |
| options: { | |
| update_existing_token: billing.bt_cc_token, | |
| verify_card: true, | |
| verification_merchant_account_id: Braintree::Configuration.verification_merchant_account_id | |
| }, | |
| cardholder_name: "#{self.fname} #{self.lname}", | |
| number: self.number_encrypted || self.number, | |
| expiration_month: self.expiration_month, | |
| expiration_year: self.expiration_year, | |
| cvv: self.cvv_encrypted || self.cvv, | |
| billing_address: { | |
| street_address: address, | |
| locality: self.city, | |
| region: self.state_province, | |
| postal_code: self.postal_code, | |
| country_code_alpha2: self.country, | |
| options: { | |
| update_existing: true | |
| } | |
| } | |
| } | |
| ) | |
| @success = @result.success? | |
| rescue => e | |
| Airbrake.notify(e) | |
| @result = :error | |
| @success = false | |
| end | |
| log_billing_verification(billing) | |
| if @success | |
| billing.update_attributes( | |
| bt_cust_id: result.customer.id.to_s, | |
| bt_cc_token: result.customer.credit_cards[0].token, | |
| bt_cc_expiration: result.customer.credit_cards[0].expiration_date, | |
| bt_cc_last4: result.customer.credit_cards[0].last_4, | |
| bt_card_type: result.customer.credit_cards[0].card_type, | |
| bt_cardholder_name: result.customer.credit_cards[0].cardholder_name | |
| ) | |
| else | |
| set_errors | |
| end | |
| else | |
| @success = false | |
| end | |
| end | |
| def log_billing_verification(billing) | |
| if @result == :error | |
| @transaction.attributes = { | |
| transactable: billing, | |
| ttype: :billing_verification, | |
| } | |
| elsif @result.success? | |
| @transaction.attributes = { | |
| transactable: billing, | |
| ttype: :billing_verification, | |
| status: 'verified' | |
| } | |
| elsif @result.credit_card_verification | |
| @transaction.attributes = { | |
| transactable: billing, | |
| ttype: :billing_verification, | |
| avs_error_response_code: @result.credit_card_verification.avs_error_response_code, | |
| avs_postal_code_response_code: @result.credit_card_verification.avs_postal_code_response_code, | |
| avs_street_address_response_code: @result.credit_card_verification.avs_street_address_response_code, | |
| cvv_response_code: @result.credit_card_verification.cvv_response_code, | |
| gateway_rejection_reason: @result.credit_card_verification.gateway_rejection_reason, | |
| processor_response_code: @result.credit_card_verification.processor_response_code, | |
| processor_response_text: @result.credit_card_verification.processor_response_text, | |
| status: @result.credit_card_verification.status, | |
| validation_errors: @result.errors.collect { |error| "(#{error.code}) #{error.message}" }.join("\n") | |
| } | |
| else | |
| @transaction.attributes = { | |
| transactable: billing, | |
| ttype: :billing_verification, | |
| validation_errors: @result.errors.collect { |error| "(#{error.code}) #{error.message}" }.join("\n"), | |
| status: 'validation_error' | |
| } | |
| end | |
| @transaction.save! | |
| end | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| FactoryGirl.define do | |
| factory :brainforest do | |
| trait :verifiable do | |
| fname { Faker::Name.first_name } | |
| lname { Faker::Name.last_name } | |
| email { Faker::Internet.email } | |
| address { Faker::Address.street_address } | |
| address_2 { Faker::Address.secondary_address } | |
| country { 'US' } | |
| city { Faker::Address.city } | |
| state_province { Faker::AddressUS.state_abbr } | |
| postal_code { Faker::AddressUS.zip_code } | |
| expiration_month '12' | |
| expiration_year '15' | |
| number '4111111111111111' | |
| cvv '123' | |
| end | |
| end | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| require 'spec_helper' | |
| describe Brainforest do | |
| let(:user) { FactoryGirl.create(:user) } | |
| let(:enrollment) { FactoryGirl.create(:enrollment, user: user) } | |
| let(:other_enrollment) { FactoryGirl.create(:enrollment, user: user) } | |
| let(:invalid_billing_error) { 'Credit card could not be processed. Please check that your billing information is correct and try again.' } | |
| context 'validation errors' do | |
| it 'should validate required fields' do | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable, city: '') | |
| brainforest.charge(enrollment, 75, 75) | |
| brainforest.success.should be_false | |
| brainforest.errors[:city].should include('is required') | |
| end | |
| end | |
| context 'verification errors' do | |
| it 'should require a valid number', :vcr do | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable, number: '1234123412341234') | |
| brainforest.charge(enrollment, 75, 75) | |
| brainforest.success.should be_false | |
| brainforest.errors[:base].should include('Credit card number is invalid.') | |
| brainforest.errors[:base].should include('Credit card type is not accepted by this merchant account.') | |
| brainforest.transaction.transactable.should eq(enrollment) | |
| brainforest.transaction.ttype.should eq(:charge) | |
| brainforest.transaction.status.should eq('validation_error') | |
| end | |
| it 'should require a valid cvv', :vcr do | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable, cvv: '200') | |
| brainforest.charge(enrollment, 75, 75) | |
| brainforest.success.should be_false | |
| brainforest.errors[:base].should include(invalid_billing_error) | |
| brainforest.transaction.status.should eq('gateway_rejected') | |
| brainforest.transaction.cvv_response_code.should eq('N') | |
| end | |
| it 'should require a valid postal postal code', :vcr do | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable, postal_code: '20000') | |
| brainforest.charge(enrollment, 75, 75) | |
| brainforest.success.should be_false | |
| brainforest.errors[:base].should include(invalid_billing_error) | |
| brainforest.transaction.status.should eq('gateway_rejected') | |
| brainforest.transaction.avs_postal_code_response_code.should eq('N') | |
| end | |
| end | |
| context 'gateway/processor declines' do | |
| it 'should decline a charge', :vcr do | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable) | |
| brainforest.charge(enrollment, 2001, 2001) | |
| brainforest.success.should be_false | |
| brainforest.errors[:base].should include(invalid_billing_error) | |
| brainforest.transaction.status.should eq('processor_declined') | |
| brainforest.transaction.processor_response_code.should eq('2001') | |
| end | |
| it 'should decline a charge from the processor', :vcr do | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable) | |
| brainforest.charge(enrollment, 2070, 2070) | |
| brainforest.success.should be_false | |
| brainforest.errors[:base].should include(invalid_billing_error) | |
| brainforest.transaction.status.should eq('processor_declined') | |
| brainforest.transaction.processor_response_code.should eq('2070') | |
| end | |
| it 'should decline when the network is unavailable', :vcr do | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable) | |
| brainforest.charge(enrollment, 3000, 3000) | |
| brainforest.success.should be_false | |
| brainforest.errors[:base].should include(invalid_billing_error) | |
| brainforest.transaction.status.should eq('failed') | |
| brainforest.transaction.processor_response_code.should eq('3000') | |
| end | |
| end | |
| context 'zero dollar charge' do | |
| it 'should successfully charge zero dollars' do | |
| brainforest = FactoryGirl.build(:brainforest) | |
| brainforest.charge(enrollment, 75, 0) | |
| brainforest.success.should be_true | |
| brainforest.errors[:base].should be_empty | |
| brainforest.transaction.status.should eq('credited') | |
| brainforest.transaction.price.should eq(75) | |
| brainforest.transaction.charged.should eq(0) | |
| end | |
| it 'should successfully charge zero dollars using saved billing' do | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable) | |
| brainforest.charge(enrollment, 75, 75, true, user.build_billing) | |
| other_brainforest = FactoryGirl.build(:brainforest, :verifiable) | |
| other_brainforest.charge_from_vault(other_enrollment, 75, 0, user.billing) | |
| other_brainforest.success.should be_true | |
| other_brainforest.transaction.status.should eq('credited') | |
| other_brainforest.transaction.price.should eq(75) | |
| other_brainforest.transaction.charged.should eq(0) | |
| end | |
| end | |
| context 'successful charge' do | |
| it 'should charge for an item', :vcr do | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable) | |
| brainforest.charge(enrollment, 75, 75) | |
| brainforest.success.should be_true | |
| brainforest.errors[:base].should be_empty | |
| brainforest.transaction.status.should eq('submitted_for_settlement') | |
| brainforest.transaction.price.should eq(75) | |
| brainforest.transaction.charged.should eq(75) | |
| end | |
| it 'should charge for a discounted item', :vcr do | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable) | |
| brainforest.charge(enrollment, 75, 65) | |
| brainforest.success.should be_true | |
| brainforest.errors[:base].should be_empty | |
| brainforest.transaction.status.should eq('submitted_for_settlement') | |
| brainforest.transaction.price.should eq(75) | |
| brainforest.transaction.charged.should eq(65) | |
| end | |
| it 'should charge for an item and store in the vault', :vcr do | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable) | |
| brainforest.charge(enrollment, 75, 75, true, user.build_billing) | |
| brainforest.success.should be_true | |
| brainforest.errors[:base].should be_empty | |
| brainforest.transaction.status.should eq('submitted_for_settlement') | |
| brainforest.transaction.price.should eq(75) | |
| brainforest.transaction.charged.should eq(75) | |
| user.billing.bt_card_type.should eq('Visa') | |
| user.billing.bt_cc_last4.should eq('1111') | |
| end | |
| end | |
| context 'updating vault via transaction' do | |
| it 'should not update a user billing when declined', :vcr do | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable) | |
| brainforest.charge(enrollment, 75, 75, true, user.build_billing) | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable, number: '4012000077777777') | |
| brainforest.charge(other_enrollment, 2001, 2001, true, user.billing) | |
| user.billing.bt_cc_last4.should eq('1111') | |
| end | |
| it 'should update a user billing when approved', :vcr do | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable) | |
| brainforest.charge(enrollment, 75, 75, true, user.build_billing) | |
| other_brainforest = FactoryGirl.build(:brainforest, :verifiable, number: '4012000077777777') | |
| other_brainforest.charge(other_enrollment, 75, 75, true, user.billing) | |
| user.billing.bt_cc_last4.should eq('7777') | |
| end | |
| end | |
| context 'charging from vault' do | |
| it 'should decline a transaction from the vault', :vcr do | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable) | |
| brainforest.charge(enrollment, 75, 75, true, user.build_billing) | |
| other_brainforest = FactoryGirl.build(:brainforest, :verifiable) | |
| other_brainforest.charge_from_vault(other_enrollment, 2001, 2001, user.billing) | |
| other_brainforest.success.should be_false | |
| other_brainforest.transaction.transactable.should eq(other_enrollment) | |
| other_brainforest.transaction.ttype.should eq(:charge) | |
| other_brainforest.transaction.status.should eq('processor_declined') | |
| end | |
| it 'should approve a transaction from the vault', :vcr do | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable) | |
| brainforest.charge(enrollment, 75, 75, true, user.build_billing) | |
| other_brainforest = FactoryGirl.build(:brainforest, :verifiable) | |
| other_brainforest.charge_from_vault(other_enrollment, 75, 75, user.billing) | |
| other_brainforest.success.should be_true | |
| other_brainforest.transaction.transactable.should eq(other_enrollment) | |
| other_brainforest.transaction.ttype.should eq(:charge) | |
| other_brainforest.transaction.status.should eq('submitted_for_settlement') | |
| end | |
| end | |
| context 'update billing' do | |
| it 'should not save a card with an invalid number', :vcr do | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable) | |
| brainforest.charge(enrollment, 75, 75, true, user.build_billing) | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable, number: '1234123412341234') | |
| brainforest.update_billing(user.billing) | |
| brainforest.success.should be_false | |
| brainforest.errors[:base].should include('Credit card number is invalid.') | |
| brainforest.transaction.transactable.should eq(user.billing) | |
| brainforest.transaction.status.should eq('validation_error') | |
| end | |
| it 'should not save a card with an unverifiable number', :vcr do | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable) | |
| brainforest.charge(enrollment, 75, 75, true, user.build_billing) | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable, number: '5105105105105100') | |
| brainforest.update_billing(user.billing) | |
| brainforest.success.should be_false | |
| brainforest.errors[:base].should include(invalid_billing_error) | |
| brainforest.transaction.transactable.should eq(user.billing) | |
| brainforest.transaction.status.should eq('processor_declined') | |
| end | |
| it 'should not save a card with an unmatched CVV', :vcr do | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable) | |
| brainforest.charge(enrollment, 75, 75, true, user.build_billing) | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable, cvv: '200') | |
| brainforest.update_billing(user.billing) | |
| brainforest.success.should be_false | |
| brainforest.errors[:base].should include(invalid_billing_error) | |
| brainforest.transaction.transactable.should eq(user.billing) | |
| brainforest.transaction.status.should eq('gateway_rejected') | |
| brainforest.transaction.cvv_response_code.should eq('N') | |
| end | |
| it 'should not save a card with an invalid zip code', :vcr do | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable) | |
| brainforest.charge(enrollment, 75, 75, true, user.build_billing) | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable, postal_code: '20000') | |
| brainforest.update_billing(user.billing) | |
| brainforest.success.should be_false | |
| brainforest.errors[:base].should include(invalid_billing_error) | |
| brainforest.transaction.transactable.should eq(user.billing) | |
| brainforest.transaction.status.should eq('gateway_rejected') | |
| brainforest.transaction.avs_postal_code_response_code.should eq('N') | |
| end | |
| it 'should update billing', :vcr do | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable) | |
| brainforest.charge(enrollment, 75, 75, true, user.build_billing) | |
| brainforest = FactoryGirl.build(:brainforest, :verifiable, number: '4012000077777777') | |
| brainforest.update_billing(user.billing) | |
| brainforest.success.should be_true | |
| brainforest.transaction.transactable.should eq(user.billing) | |
| brainforest.transaction.status.should eq('verified') | |
| user.billing.bt_cc_last4.should eq('7777') | |
| end | |
| end | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # == Schema Information | |
| # | |
| # Table name: transactions | |
| # | |
| # id :integer not null, primary key | |
| # transactable_id :integer | |
| # transactable_type :string(255) | |
| # ttype_cd :integer | |
| # approved :boolean | |
| # charged :decimal(8, 2) | |
| # status :string(255) | |
| # bt_transaction_id :string(255) | |
| # avs_error_response_code :string(255) | |
| # avs_postal_code_response_code :string(255) | |
| # avs_street_address_response_code :string(255) | |
| # cvv_response_code :string(255) | |
| # gateway_rejection_reason :string(255) | |
| # processor_authorization_code :string(255) | |
| # processor_response_code :string(255) | |
| # processor_response_text :string(255) | |
| # validation_errors :text | |
| # created_at :datetime not null | |
| # updated_at :datetime not null | |
| # price :decimal(8, 2) | |
| # | |
| class Transaction < ActiveRecord::Base | |
| belongs_to :transactable, polymorphic: true | |
| as_enum :ttype, billing_verification: 1, charge: 2 | |
| def name | |
| self.created_at.strftime("%b %d, %Y %I:%M %p") | |
| end | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment