Skip to content

Commit

Permalink
Implement YearlyInvoiceJob for Abo Magazin Roles (#1561)
Browse files Browse the repository at this point in the history
  • Loading branch information
njaeggi committed Feb 12, 2025
1 parent 89ed86b commit 0589cef
Show file tree
Hide file tree
Showing 3 changed files with 265 additions and 0 deletions.
41 changes: 41 additions & 0 deletions app/domain/invoices/abacus/abo_magazin_invoice.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

# Copyright (c) 2024, 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.

module Invoices
module Abacus
class AboMagazinInvoice
attr_reader :abonnent

def initialize(abonnent)
@abonnent = abonnent
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
)]
end

def total
positions.sum(&:amount)
end

private

def position_description_and_amount
description = participation.price_category? ? Event::Course.human_attribute_name(participation.price_category) : nil
[description, participation.price]
end
end
end
end
179 changes: 179 additions & 0 deletions app/jobs/invoices/abacus/create_yearly_abo_alpen_invoices_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# frozen_string_literal: true

# Copyright (c) 2024, 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.

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)
super
create_error_log_entry("Mitgliedschaftsrechnungen konnten nicht an Abacus übermittelt werden. " \
"Es erfolgt ein weiterer Versuch.", exception.message)
end

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
end

def self.job_running?
Delayed::Job.where("handler LIKE ?", "%#{name}%")
.where(failed_at: nil).exists?
end

private

def process_invoices
# TODO: start_progress
active_abonnenten do |person|
check_terminated!
create_invoice(person)
end
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)
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
end

def clear_external_invoices(sales_orders)
sales_orders.each do |so|
so.entity.destroy
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 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_external_invoice(membership_invoice)
ExternalInvoice::SacMembership.create!(
person: membership_invoice.member.person,
year: @invoice_year,
state: :draft,
total: membership_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}%"
)
end

def create_error_log_entry(message, payload)
HitobitoLogEntry.create!(
category: "stapelverarbeitung",
level: :error,
message: message,
payload: payload
)
end

def log_error_parts(parts)
parts.reject(&:success?).each do |part|
create_log_entry(part)
end
end

def create_log_entry(part)
part.context_object.entity.update!(state: :error)
HitobitoLogEntry.create!(
subject: part.context_object.entity,
category: "rechnungen",
level: :error,
message: "Mitgliedschaftsrechnung konnte nicht in Abacus erstellt werden",
payload: part.error_payload
)
end

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
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

# Copyright (c) 2024, 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::CreateYearlyAboAlpenInvoicesJob do
let(:subject) { described_class.new }
let(:abonnent) { people(:abonnent) }
let(:abonnent_role) { roles(:abonnent_alpen) }

before do
abonnent.update_column(:abacus_subject_key, "123")
abonnent_role.update_column(:end_on, 50.days.from_now)
end

describe "#active_abonnenten" do
it "includes every abonnent role where role end in next 62 days" do
expect(subject.active_abonnenten).to eq [abonnent]
end

it "does no unclude abonnent where role ends in more than 62 days" do
abonnent_role.update_column(:end_on, 70.days.from_now)
expect(subject.active_abonnenten).to be_empty
end

it "does not include people where abonnent role is terminated" do
abonnent_role.update_column(:terminated, true)
expect(subject.active_abonnenten).to be_empty
end

it "does not include people who already have external invoice abo magazin in this year" do
ExternalInvoice::AboMagazin.create!(person: abonnent, year: (abonnent_role.end_on + 1.day).year)
expect(subject.active_abonnenten).to be_empty
end

it "does include people who already have external invoice but not in this year" do
ExternalInvoice::AboMagazin.create!(person: abonnent, year: 3.years.ago.year)
expect(subject.active_abonnenten).to eq [abonnent]
end
end
end

0 comments on commit 0589cef

Please sign in to comment.