diff --git a/app/controllers/payments_controller.rb b/app/controllers/payments_controller.rb
index 8a666a4dd..f91fc92ec 100644
--- a/app/controllers/payments_controller.rb
+++ b/app/controllers/payments_controller.rb
@@ -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)
@@ -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
@@ -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
diff --git a/app/jobs/auto_charge_job.rb b/app/jobs/auto_charge_job.rb
new file mode 100644
index 000000000..32bf02a79
--- /dev/null
+++ b/app/jobs/auto_charge_job.rb
@@ -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
diff --git a/app/models/activity.rb b/app/models/activity.rb
index de04f2785..1bcd6aeb9 100644
--- a/app/models/activity.rb
+++ b/app/models/activity.rb
@@ -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
diff --git a/app/models/payment.rb b/app/models/payment.rb
index 76d897d9b..404d30c42 100644
--- a/app/models/payment.rb
+++ b/app/models/payment.rb
@@ -27,19 +27,44 @@ 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('%
+ Met automatische opwaardering kun je je saldo automatisch aanvullen via SEPA-incasso wanneer je saldo onder de 0 euro komt. +
+ + <% if @user == current_user %> + <% if @user.mollie_mandate_id.blank? %> ++ Je hebt nog geen mandate ingesteld. Klik op onderstaande knop om een mandate in te stellen door 1 cent te betalen via iDEAL. +
+ <%= link_to setup_mandate_payments_path, method: :post, class: 'btn btn-primary' do %> + + Mandate instellen + <% end %> ++ + Je mandate is actief en geldig. +
++ Je mandate is niet meer geldig. Stel alstublieft opnieuw een mandate in. +
+ <%= link_to setup_mandate_payments_path, method: :post, class: 'btn btn-primary' do %> + + Mandate opnieuw instellen + <% end %> ++ Dit gedeelte kan alleen door jezelf worden beheerd. +
+Automatische opwaardering is momenteel niet beschikbaar.
+