require "date" require "bigdecimal" require "securerandom" class PracticeTransferExport class Invalid < StandardError; end MAX_ENTRIES = 500 def initialize(user:, from:, to:, descriptions: false) @user, @from, @to, @descriptions = user, Date.iso8601(from.to_s), Date.iso8601(to.to_s), descriptions raise Invalid, "Der Zeitraum ist ungültig." if @from > @to || !(1900..2200).cover?(@from.year) || !(1900..2200).cover?(@to.year) rescue Date::Error raise Invalid, "Bitte Beginn und Ende des Zeitraums angeben." end def call entries = @user.entries.where(date: @from..@to).order(:id).limit(MAX_ENTRIES + 1).to_a raise Invalid, "Höchstens 500 Einträge je Export. Bitte den Zeitraum verkleinern." if entries.length > MAX_ENTRIES namespace = @user.with_lock do @user.update!(practice_transfer_id: SecureRandom.uuid) if @user.practice_transfer_id.blank? @user.practice_transfer_id end rates = MileageRate.where(year: (@from.year..@to.year)).pluck(:year, :rate_per_km).to_h skipped = 0 rows = entries.flat_map do |entry| if entry.start_time.present? && entry.end_time.nil? skipped += 1 next [] end hours, minutes = entry.hours_and_minutes total_minutes = hours * 60 + minutes common = { source_id: entry.id.to_s, date: entry.date.iso8601, education: entry.praktikums_typ, activity: entry.entry_art, duration_minutes: total_minutes, description: @descriptions ? entry.beschreibung.to_s.first(4_000) : nil, cost_cents: entry.kosten ? (entry.kosten.to_d * 100).round.to_i : nil } result = [] if entry.distance_km.to_i.positive? rate = rates[entry.date.year] || BigDecimal("0.42") # Existing source application fallback, not a tax determination. result << common.merge(type: "mileage", kilometers: entry.distance_km, rate_cents: (rate.to_d * 100).round.to_i, rate_origin: rates.key?(entry.date.year) ? "configured" : "source_default") end result << common.merge(type: "development") if total_minutes.positive? result end { format: "pcm-professional-transfer-v1", source_namespace: namespace, exported_at: Time.current.iso8601, from: @from.iso8601, to: @to.iso8601, skipped_running: skipped, records: rows } end end