Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 118 additions & 3 deletions app/controllers/payments_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class PaymentsController < ApplicationController
before_action :authenticate_user!, only: %i[index create add]
after_action :verify_authorized, only: %i[index create add]
class PaymentsController < ApplicationController # rubocop:disable Metrics/ClassLength
before_action :authenticate_user!, only: %i[index create add setup_mandate mandate_callback toggle_auto_charge]
after_action :verify_authorized, only: %i[index create add setup_mandate toggle_auto_charge]

def index
@payments = Payment.order(created_at: :desc)
Expand Down Expand Up @@ -37,6 +37,90 @@ def add # rubocop:disable Metrics/AbcSize
@payment.amount = params[:resulting_credit].to_f - @user.credit if params[:resulting_credit]
end

def setup_mandate # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
authorize :payment

if Rails.application.config.x.mollie_api_key.blank?
flash[:error] = 'iDEAL is niet beschikbaar'
redirect_to user_path(current_user)
return
end

begin
# Create or retrieve Mollie customer
mollie_customer = if current_user.mollie_customer_id.present?
Mollie::Customer.get(current_user.mollie_customer_id)
else
Mollie::Customer.create(
name: current_user.name,
email: current_user.email
)
end

current_user.update(mollie_customer_id: mollie_customer.id)

# Create payment for 1 cent to set up mandate
payment = Payment.create_with_mollie(
'Automatische opwaardering setup (1 cent)',
user: current_user,
amount: 0.01,
first_payment: true
)

if payment.valid?
checkout_url = payment.mollie_payment.checkout_url
redirect_to URI.parse(checkout_url).to_s, allow_other_host: true
else
flash[:error] = "Kon betaling niet aanmaken: #{payment.errors.full_messages.join(', ')}"
redirect_to user_path(current_user)
end
rescue Mollie::ResponseError => e
flash[:error] = "Mollie fout: #{e.message}"
redirect_to user_path(current_user)
end
end

def mandate_callback # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
payment = Payment.find(params[:id])
unless payment
redirect_to users_path
return
end

if payment.completed?
flash[:error] = 'Deze betaling is al verwerkt'
else
tries = 3
begin
payment.update(status: payment.mollie_payment.status)

if payment.mollie_payment.paid?
# Extract mandate from payment
if payment.mollie_payment.mandate_id
current_user.update(mollie_mandate_id: payment.mollie_payment.mandate_id)
flash[:success] = 'Automatische opwaardering ingesteld! Je kan nu automatische opwaardering inschakelen.'
else
flash[:success] = 'Betaling gelukt, maar mandate kon niet worden opgeslagen.'
end
else
flash[:error] = 'iDEAL betaling is mislukt. Mandate kon niet worden ingesteld.'
end
rescue ActiveRecord::StaleObjectError => e
raise e unless (tries -= 1).positive?

payment = Payment.find(params[:id])
retry
end
end

redirect_to user_path(current_user)
end

def toggle_auto_charge
authorize :payment
toggle_auto_charge_handler
end

def callback # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
payment = Payment.find(params[:id])
unless payment
Expand Down Expand Up @@ -73,4 +157,35 @@ def callback # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/Pe
redirect_to invoice_path(payment.invoice.token)
end
end

private

def toggle_auto_charge_handler
return handle_invalid_mandate unless valid_mandate?

toggle_user_auto_charge
redirect_to user_path(current_user)
end

def valid_mandate?
current_user.mollie_mandate_id.present? && current_user.mandate_valid?
end

def handle_invalid_mandate
flash[:error] = 'Je hebt geen geldige mandate ingesteld'
redirect_to user_path(current_user)
end

def toggle_user_auto_charge
current_user.update(auto_charge_enabled: !current_user.auto_charge_enabled)
set_auto_charge_flash_message
end

def set_auto_charge_flash_message
if current_user.auto_charge_enabled
flash[:success] = 'Automatische opwaardering ingeschakeld'
else
flash[:warning] = 'Automatische opwaardering uitgeschakeld'
end
end
end
72 changes: 72 additions & 0 deletions app/jobs/auto_charge_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
class AutoChargeJob < ApplicationJob
queue_as :default

def perform
# Find all users with auto_charge_enabled and valid mandates with negative credit
User.where(auto_charge_enabled: true).find_each do |user|
process_user_charge(user)
end

perform_health_check
end

private

def process_user_charge(user)
return unless user.mandate_valid?
return unless user.credit.negative?

charge_user(user)
rescue StandardError => e
Rails.logger.error("AutoChargeJob failed for user #{user.id}: #{e.message}")
end

def perform_health_check
HealthCheckJob.perform_later('auto_charge') if production_or_staging?
end

def production_or_staging?
Rails.env.production? || Rails.env.staging? || Rails.env.luxproduction? || Rails.env.euros?
end

def charge_user(user) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
# Calculate amount needed to bring balance back to 0 or add buffer
amount_needed = (user.credit.abs + 1.00).round(2) # Add 1 euro buffer

# Cap at reasonable amount to prevent accidental large charges
amount_needed = 100.00 if amount_needed > 100.00

return if amount_needed < 0.01

# Create payment with mandate using Mollie API
mollie_customer = Mollie::Customer.get(user.mollie_customer_id)
mollie_mandate = mollie_customer.mandates.get(user.mollie_mandate_id)

return unless mollie_mandate.status == 'valid'

# Create a recurring payment (charge)
mollie_payment = mollie_customer.payments.create(
amount: { value: format('%.2f', amount_needed), currency: 'EUR' },
description: 'Automatische opwaardering (onderschrijving)',
mandateId: user.mollie_mandate_id,
sequenceType: 'recurring',
redirectUrl: "http://#{Rails.application.config.x.sofia_host}/users/#{user.id}"
)

# Create payment record in database
payment = Payment.create(
user:,
amount: amount_needed,
mollie_id: mollie_payment.id,
status: mollie_payment.status
)

Rails.logger.info("AutoChargeJob created payment #{payment.id} for user #{user.id} with amount #{amount_needed}")

# If payment is already paid (for recurring), process it immediately
payment.update(status: 'paid') if mollie_payment.paid?
rescue Mollie::ResponseError => e
Rails.logger.error("Mollie API error in AutoChargeJob for user #{user.id}: #{e.message}")
raise
end
end
6 changes: 3 additions & 3 deletions app/models/activity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ def revenue_total
def count_per_product(**args)
records = orders.where(args)

@count_per_product = OrderRow.where(order: records).group(:product_id, :name).joins(:product)
.pluck(:name, Arel.sql('SUM(product_count)'), Arel.sql('SUM(product_count * price_per_product)'))
@count_per_product.map { |name, amount, price| { name:, amount: amount.to_i, price: price.to_f } }
rows = OrderRow.where(order: records).group(:product_id, :name).joins(:product)
.pluck(:name, Arel.sql('SUM(product_count)'), Arel.sql('SUM(product_count * price_per_product)'))
rows.map { |name, amount, price| { name:, amount: amount.to_i, price: price.to_f } }
end

def revenue_by_category
Expand Down
46 changes: 40 additions & 6 deletions app/models/payment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,30 +27,62 @@ def completed?
end

def self.create_with_mollie(description, attributes = nil)
is_mandate_setup = attributes&.delete(:first_payment)
obj = create(attributes)
return obj unless obj.valid?

mollie_payment = Mollie::Payment.create(
amount: { value: format('%<amount>.2f', amount: attributes[:amount]), currency: 'EUR' },
description:,
redirect_url: "http://#{Rails.application.config.x.sofia_host}/payments/#{obj.id}/callback"
)

mollie_payment = create_mollie_payment(description, attributes, obj, is_mandate_setup)
obj.update(mollie_id: mollie_payment.id)
obj
end

def self.create_mollie_payment(description, attributes, obj, is_mandate_setup)
mollie_payment_attrs = build_mollie_attrs(description, attributes, obj, is_mandate_setup)
Mollie::Payment.create(mollie_payment_attrs)
end

def self.build_mollie_attrs(description, attributes, obj, is_mandate_setup)
attrs = build_base_mollie_attrs(description, attributes)
add_redirect_url(attrs, obj, is_mandate_setup)
attrs
end

def self.build_base_mollie_attrs(description, attributes)
{
amount: { value: format('%.2f', attributes[:amount]), currency: 'EUR' },
description:
}
end

def self.add_redirect_url(attrs, obj, is_mandate_setup)
if is_mandate_setup
attrs[:sequenceType] = 'first'
attrs[:redirectUrl] = "http://#{Rails.application.config.x.sofia_host}/payments/#{obj.id}/mandate_callback"
else
attrs[:redirectUrl] = "http://#{Rails.application.config.x.sofia_host}/payments/#{obj.id}/callback"
end
end

private_class_method :create_mollie_payment, :build_mollie_attrs, :build_base_mollie_attrs, :add_redirect_url

def mollie_payment
Mollie::Payment.get(mollie_id)
end

def process_complete_payment!
return unless status_previously_was != 'paid' && status == 'paid'

# Skip credit mutation for mandate setup payments (1 cent)
return if setup_payment?

process_user! if user
process_invoice! if invoice
end

def setup_payment?
amount.to_d == 0.01.to_d
end

def process_user!
mutation = CreditMutation.create(user:,
amount:,
Expand All @@ -76,6 +108,8 @@ def user_xor_invoice

def user_amount
return unless user
# Allow 1 cent payments for mandate setup
return if setup_payment?

min_amount = Rails.application.config.x.min_payment_amount
errors.add(:amount, "must be bigger than or equal to €#{format('%.2f', min_amount)}") unless amount && (amount >= min_amount)
Expand Down
32 changes: 31 additions & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,17 @@ def minor
end

def insufficient_credit
# Users with auto-charge enabled are always allowed to order
return false if auto_charge_available?

provider.in?(%w[amber_oauth2 sofia_account]) && credit.negative?
end

def can_order(activity = nil)
activity ||= current_activity
# Users with auto-charge enabled can always order
return true if auto_charge_available?

if activity.nil?
!insufficient_credit
else
Expand Down Expand Up @@ -105,9 +111,33 @@ def update_role(groups)
roles_users_not_to_have.map(&:destroy)
end

def mollie_customer
return if mollie_customer_id.blank?

@mollie_customer ||= Mollie::Customer.get(mollie_customer_id)
rescue Mollie::ResponseError
nil
end

def mollie_mandate
return nil unless mollie_mandate_id.present? && mollie_customer.present?

mollie_customer.mandates.get(mollie_mandate_id)
rescue Mollie::ResponseError
nil
end

def mandate_valid?
mollie_mandate&.status == 'valid'
end

def auto_charge_available?
auto_charge_enabled && mandate_valid?
end

def archive!
attributes.each_key do |attribute|
self[attribute] = nil unless %w[deleted_at updated_at created_at provider id uid].include? attribute
self[attribute] = nil unless %w[deleted_at updated_at created_at provider id uid auto_charge_enabled].include? attribute
end
self.name = "Gearchiveerde gebruiker #{id}"
self.deactivated = true
Expand Down
12 changes: 12 additions & 0 deletions app/policies/payment_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ def add?
mollie_enabled? && user
end

def setup_mandate?
mollie_enabled? && user
end

def mandate_callback?
mollie_enabled? && user
end

def toggle_auto_charge?
mollie_enabled? && user
end

def invoice_callback?
mollie_enabled? && record && !record.completed?
end
Expand Down
Loading