From e9268af28742c603effd75e59a2098ab5a8b66de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niklas=20J=C3=A4ggi?= Date: Wed, 12 Feb 2025 15:39:50 +0100 Subject: [PATCH] Add abacus abo magazin invoice class --- .../invoices/abacus/abo_magazin_invoice.rb | 65 +++++++--- .../create_yearly_abo_alpen_invoices_job.rb | 122 +++++------------- config/locales/wagon.de.yml | 5 +- config/locales/wagon.fr.yml | 5 + config/locales/wagon.it.yml | 5 + .../abacus/abo_magazin_invoice_spec.rb | 73 +++++++++++ 6 files changed, 167 insertions(+), 108 deletions(-) create mode 100644 spec/domain/invoices/abacus/abo_magazin_invoice_spec.rb diff --git a/app/domain/invoices/abacus/abo_magazin_invoice.rb b/app/domain/invoices/abacus/abo_magazin_invoice.rb index 4fafe9c83..3c484e5b2 100644 --- a/app/domain/invoices/abacus/abo_magazin_invoice.rb +++ b/app/domain/invoices/abacus/abo_magazin_invoice.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) 2024, Schweizer Alpen-Club. This file is part of +# Copyright (c) 2025, Schweizer Alpen-Club. This file is part of # hitobito_sac_cas and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito_sac_cas. @@ -8,22 +8,18 @@ module Invoices module Abacus class AboMagazinInvoice - attr_reader :abonnent + attr_reader :abonnent_role - def initialize(abonnent) - @abonnent = abonnent + delegate :person, to: :abonnent_role + + ARTICLE_NUMBER = "APG" + + def initialize(abonnent_role) + @abonnent_role = abonnent_role end def positions - @positions ||= [Invoices::Abacus::InvoicePosition.new( - name: name, - grouping: name, - amount: , - count: 1, - article_number: article_number, - cost_center: event.kind.cost_center.code, - cost_unit: event.kind.cost_unit.code - )] + @positions ||= build_positions end def total @@ -32,9 +28,46 @@ def total private - def position_description_and_amount - description = participation.price_category? ? Event::Course.human_attribute_name(participation.price_category) : nil - [description, participation.price] + def build_positions + positions = [main_fee_position] + positions << abroad_fee_position if person.country != "CH" # NJTODO: living_abroad? + positions + end + + def main_fee_position + Invoices::Abacus::InvoicePosition.new( + name: fee_position_name, + grouping: fee_position_name, + article_number: ARTICLE_NUMBER, + amount: 60, # NJTODO: model call + count: 1 + ) + end + + def abroad_fee_position + Invoices::Abacus::InvoicePosition.new( + name: abroad_fee_position_name, + amount: 16 # NJTODO: model call + ) + end + + def fee_position_name + I18n.t("invoices.abo_magazin.positions.abo_fee", + group: abonnent_role.group, + time_period: new_role_period, + locale: person.language) + end + + def abroad_fee_position_name + I18n.t("invoices.abo_magazin.positions.abroad_fee", + group: abonnent_role.group, + locale: person.language) + end + + def new_role_period + start_date = abonnent_role.end_on + 1.day + end_date = abonnent_role.end_on + 1.year + "#{I18n.l(start_date)} - #{I18n.l(end_date)}" end end end diff --git a/app/jobs/invoices/abacus/create_yearly_abo_alpen_invoices_job.rb b/app/jobs/invoices/abacus/create_yearly_abo_alpen_invoices_job.rb index b09b6370f..2c9267e91 100644 --- a/app/jobs/invoices/abacus/create_yearly_abo_alpen_invoices_job.rb +++ b/app/jobs/invoices/abacus/create_yearly_abo_alpen_invoices_job.rb @@ -6,20 +6,10 @@ # https://github.com/hitobito/hitobito_sac_cas. class Invoices::Abacus::CreateYearlyAboAlpenInvoicesJob < BaseJob - SLICE_SIZE = 25 # number of people/invoices transmitted per abacus batch request - self.max_run_time = 24.hours - def perform - #log_progress(0) - clear_spurious_draft_invoices! process_invoices - # log_progress(100) if @current_logged_percent < 100 - end - - def enqueue - assert_no_other_job_running! end def error(job, exception) @@ -32,13 +22,14 @@ def failure(job) create_error_log_entry("MV-Jahresinkassolauf abgebrochen", nil) end - def active_abonnenten - Person.joins(:roles_unscoped) - .left_joins(:external_invoices) - .where.not(abacus_subject_key: nil) - .where(roles: { type: Group::AboMagazin::Abonnent.sti_name, terminated: false, end_on: Time.zone.today..62.days.from_now }) - .where("external_invoices.id IS NULL OR external_invoices.year != EXTRACT(YEAR FROM roles.end_on + INTERVAL '1 day')") - .distinct + def active_abonnenten_roles + Role.joins(:person) + .left_joins(:external_invoices) + .where(type: Group::AboMagazin::Abonnent.sti_name, terminated: false) + .where(end_on: Time.zone.today..62.days.from_now) + .where.not(people: {abacus_subject_key: nil}) + .where("external_invoices.id IS NULL OR external_invoices.year != EXTRACT(YEAR FROM roles.end_on + INTERVAL '1 day')") + .distinct end def self.job_running? @@ -49,25 +40,27 @@ def self.job_running? private def process_invoices - # TODO: start_progress - active_abonnenten do |person| - check_terminated! - create_invoice(person) + sales_orders = [] + + active_abonnenten_roles do |abonnent_role| + sales_orders.concat(create_invoice(abonnent_role)) end + submit_sales_orders(sales_orders) unless sales_orders.empty? end - def create_invoice(person) - membership_invoice = membership_invoice(person) - sales_orders = create_sales_orders(membership_invoices) - parts = submit_sales_orders(sales_orders) - log_error_parts(parts) + def create_invoice(abonnent_role) + abo_magazin_invoice = abo_magazin_invoice(abonnent_role) + invoice = create_external_invoice(abo_magazin_invoice) + create_sales_order(invoice, abo_magazin_invoice) end def submit_sales_orders(sales_orders) - sales_order_interface.create_batch(sales_orders) - rescue RestClient::Exception => e - clear_external_invoices(sales_orders) - raise e + sales_orders.each_slice(SLICE_SIZE) do |batch| + sales_order_interface.create_batch(batch) + rescue RestClient::Exception => e + clear_external_invoices(batch) + raise e + end end def clear_external_invoices(sales_orders) @@ -76,66 +69,19 @@ def clear_external_invoices(sales_orders) end end - def membership_invoice(person) - member = Invoices::SacMemberships::Member.new(person, context) - invoice = Invoices::Abacus::MembershipInvoice.new(member, member.active_memberships) - invoice if invoice.invoice? - end + def abo_magazin_invoice(abonnent_role) = Invoices::Abacus::AboMagazinInvoice.new(abonnent_role) - def create_sales_orders(membership_invoices) - membership_invoices.map do |mi| - invoice = create_external_invoice(mi) - Invoices::Abacus::SalesOrder.new(invoice, mi.positions, mi.additional_user_fields) - end - end + def create_sales_order(invoice, abo_magazin_invoice) = Invoices::Abacus::SalesOrder.new(invoice, abo_magazin_invoice.positions) - def create_external_invoice(membership_invoice) - ExternalInvoice::SacMembership.create!( - person: membership_invoice.member.person, + def create_external_invoice(abo_magazin_invoice) + ExternalInvoice::AboMagazin.create!( + person: abo_magazin_invoice.abonnent, year: @invoice_year, state: :draft, - total: membership_invoice.total, + total: abo_magazin_invoice.total, issued_at: @invoice_date, sent_at: @send_date, - # also see comment in ExternalInvoice::SacMembership - link: membership_invoice.member.stammsektion, - invoice_kind: :sac_membership_yearly - ) - end - - def assert_no_other_job_running! - raise "There is already a job running" if self.class.job_running? - end - - # clears invoice models from previously failed job runs - def clear_spurious_draft_invoices! - ExternalInvoice::AboMagazin.where(state: :draft, year: @invoice_year).destroy_all - end - - def start_progress - @current_logged_percent = 0 - @members_count = active_members.count - @processed_members = 0 - end - - def update_progress(people_count) - @processed_members += people_count - @progress_percent = @processed_members * 100 / @members_count - if @progress_percent >= (@current_logged_percent + 10) - @current_logged_percent = @progress_percent / 10 * 10 - log_progress(@current_logged_percent) - end - end - - def load_people(ids) - context.people_with_membership_years.where(id: ids).order(:id) - end - - def log_progress(percent) - HitobitoLogEntry.create!( - category: "stapelverarbeitung", - level: :info, - message: "MV-Jahresinkassolauf: Fortschritt #{percent}%" + link: nil # NJTODO, ) end @@ -169,11 +115,5 @@ def reference_date @reference_date ||= Date.new(@invoice_year) end - def context - @context ||= Invoices::SacMemberships::Context.new(reference_date) - end - - def sales_order_interface - @sales_order_interface ||= Invoices::Abacus::SalesOrderInterface.new - end + def sales_order_interface = @sales_order_interface ||= Invoices::Abacus::SalesOrderInterface.new end diff --git a/config/locales/wagon.de.yml b/config/locales/wagon.de.yml index 015614860..5a793cbe5 100644 --- a/config/locales/wagon.de.yml +++ b/config/locales/wagon.de.yml @@ -1434,7 +1434,10 @@ de: invoices: abo_magazin: - title: Rechnung Die Alpen %{year} + title: NJTODO + positions: + abo_fee: Abonnement %{group} %{time_period} + abroad_fee: Porto %{group} errors: data_quality_error: "Die Person hat Datenqualitätsprobleme, daher wurde keine Rechnung erstellt." create_subject_failed: "Probleme beim Übermitteln der Personendaten" diff --git a/config/locales/wagon.fr.yml b/config/locales/wagon.fr.yml index 65b6eb893..a88214a55 100644 --- a/config/locales/wagon.fr.yml +++ b/config/locales/wagon.fr.yml @@ -1446,6 +1446,11 @@ fr: cannot_set_main_person: Contacte le secrétariat pour modifier le destinataire des factures familiales invoices: + abo_magazin: + title: NJTODO + positions: + abo_fee: Abonnement %{group} %{time_period} + abroad_fee: Porto %{group} errors: data_quality_error: "La personne a des problèmes de qualité des données, c’est pourquoi aucune facture n’a été créée." create_subject_failed: "Problèmes lors de la transmission des données personnelles" diff --git a/config/locales/wagon.it.yml b/config/locales/wagon.it.yml index 04bb8347c..2ebff48b2 100644 --- a/config/locales/wagon.it.yml +++ b/config/locales/wagon.it.yml @@ -1440,6 +1440,11 @@ it: cannot_set_main_person: Contatta il Segretariato centrale per far cambiare il destinatario di fatturazione per la famiglia. invoices: + abo_magazin: + title: NJTODO + positions: + abo_fee: Abbonamento %{group} %{time_period} + abroad_fee: Porto %{group} errors: data_quality_error: "La persona ha dei problemi nella qualità dei dati, pertanto non è stata creata alcuna fattura." create_subject_failed: "Problemi durante il trasferimento dei dati personali" diff --git a/spec/domain/invoices/abacus/abo_magazin_invoice_spec.rb b/spec/domain/invoices/abacus/abo_magazin_invoice_spec.rb new file mode 100644 index 000000000..6fbcec67a --- /dev/null +++ b/spec/domain/invoices/abacus/abo_magazin_invoice_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +# Copyright (c) 2025, Schweizer Alpen-Club. This file is part of +# hitobito_sac_cas and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito_sac_cas. + +require "spec_helper" + +describe Invoices::Abacus::AboMagazinInvoice do + let(:abonnent_alpen) { roles(:abonnent_alpen) } + let(:abonnent) { people(:abonnent) } + + subject { described_class.new(abonnent_alpen) } + + describe "#positions" do + context "for swiss person" do + before do + abonnent.update_column(:country, "CH") + end + + it "creates abo fee position with correct values" do + position = subject.positions.first + expect(subject.positions.count).to eq 1 + expect(position.name).to eq "Abonnement Die Alpen DE 01.01.2026 - 31.12.2026" + expect(position.grouping).to eq "Abonnement Die Alpen DE 01.01.2026 - 31.12.2026" + expect(position.amount).to eq 60 + expect(position.count).to eq 1 + expect(position.article_number).to eq "APG" + end + + it "position uses language of person as locale" do + abonnent.update_column(:language, "it") + expect(subject.positions.first.name).to eq "Abbonamento Die Alpen DE 01.01.2026 - 31.12.2026" + end + end + + context "for person living abroad" do + before do + abonnent.update_column(:country, "BO") + end + + it "has second position for abroad costs" do + position = subject.positions.second + expect(subject.positions.count).to eq 2 + expect(position.name).to eq "Porto Die Alpen DE" + expect(position.amount).to eq 16 + end + end + end + + describe "#total" do + context "for swiss person" do + before do + abonnent.update_column(:country, "CH") + end + + it "total does not include porto cost" do + expect(subject.total).to eq 60 + end + end + + context "for person living abroad" do + before do + abonnent.update_column(:country, "BO") + end + + it "total does include porto cost" do + expect(subject.total).to eq 76 + end + end + end +end