Hugo Peixoto
76df4d41b7
Previously, saucy generated each member's future notification every day (marked as scheduled). And every day saucy would deliver every unsent notification scheduled for that day. This means that each member with expiration date in the future had ~6 notifications scheduled for them in the database. This was troublesome because if the cron job that delivers notification was down or the server was down for more than 24h, notifications were silently skipped and we had no easy way of detecting it: we had to check every member for missing sent notifications. It also had the disadvantage that we were deleting and recreating hundreds of database entries for no good reason. To fix this, the new system no longer stores future scheduled notifications. Instead, it now only stores sent notifications and notifications currently being delivered. Every day, the deliver task checks every member if there's a notification that needs to be sent in that day, by checking if we're past the date of sending a particular notification type and checking if in that window of time no notifications of that type have been sent. Let's suppose we send 6 notifications, one per month starting 60 days before the membership expires until 90 days after: -60d -30d t=0 30d 60d 90d ----|------|------|------|------|------|------ A B C D E F If we're on day t=-60d, we need to deliver a notification of type A. But first we check if no notifications of type A have been sent to that member on that day. From days t=-59d to t=-31d, we check if in the time range t=[-60d, -31d] any notification of type A was sent to that member. If not, we need to deliver it. If we're on days t=[-30d, -1d], we do the same but for notification of type B.
126 lines
4.1 KiB
Ruby
126 lines
4.1 KiB
Ruby
desc "Application specific tasks"
|
|
namespace :saucy do
|
|
desc "Background sync operations"
|
|
task sync: :environment do
|
|
Member.reset_all_status!
|
|
end
|
|
|
|
desc "Send daily email notifications"
|
|
task notify: :environment do
|
|
Notification.send_scheduled_for_today
|
|
end
|
|
|
|
desc "Import data from CiviCRM XML files"
|
|
task import: :environment do
|
|
require 'nokogiri'
|
|
require 'date'
|
|
|
|
Notification.delete_all
|
|
Contribution.delete_all
|
|
Member.delete_all
|
|
|
|
def sql_resultset_to_hashes(resultset_filename)
|
|
Nokogiri::XML(File.read(resultset_filename))
|
|
.xpath('resultset/row')
|
|
.map do |row|
|
|
row
|
|
.xpath('field')
|
|
.map { |field| [field["name"], field.children.size > 0 ? field.text : nil] }
|
|
.to_h
|
|
end
|
|
end
|
|
|
|
def ensure_one(list)
|
|
raise if list.size > 1
|
|
|
|
list[0]
|
|
end
|
|
|
|
dir = ENV.fetch('CIVICRM_DUMP_DIRECTORY')
|
|
addresses = sql_resultset_to_hashes("#{dir}/address.xml").select { |a| a["is_primary"] == "1" }
|
|
contacts = sql_resultset_to_hashes("#{dir}/contact.xml")
|
|
contributions = sql_resultset_to_hashes("#{dir}/contribution.xml")
|
|
emails = sql_resultset_to_hashes("#{dir}/email.xml").select { |a| a["is_primary"] == "1" }
|
|
memberships = sql_resultset_to_hashes("#{dir}/membership.xml").select { |m| Date.parse(m['end_date']).year >= 2020 }
|
|
payment_instruments = sql_resultset_to_hashes("#{dir}/payment_instrument.xml")
|
|
statuses = sql_resultset_to_hashes("#{dir}/status.xml")
|
|
types = sql_resultset_to_hashes("#{dir}/type.xml")
|
|
|
|
|
|
members = contacts.map do |contact|
|
|
contact.merge(
|
|
"membership" => memberships.select { |m| m['contact_id'] == contact['id'] }.then { |ms| ensure_one(ms) },
|
|
"address" => addresses.select { |a| a['contact_id'] == contact['id'] }.then { |ms| ensure_one(ms) },
|
|
"email" => emails.select { |a| a['contact_id'] == contact['id'] }.then { |ms| ensure_one(ms) },
|
|
"contributions" => contributions.select { |c| c['contact_id'] == contact['id'] },
|
|
)
|
|
end
|
|
|
|
members = members.select { |m| !m['membership'].nil? }
|
|
|
|
raise "we have duplicates" unless members.map { |m| m['display_name'] }.count == members.map { |m| m['display_name'] }.uniq.count
|
|
|
|
members.each do |member|
|
|
address = member['address']
|
|
|
|
address = if address
|
|
postal_code = [
|
|
address['postal_code'],
|
|
address['postal_code_suffix']
|
|
].reject(&:nil?).reject(&:empty?).join('-')
|
|
|
|
[
|
|
address['street_address'],
|
|
address['supplemental_address_1'],
|
|
address['supplemental_address_2'],
|
|
address['supplemental_address_3'],
|
|
"#{postal_code} #{address['city']}",
|
|
].reject(&:nil?).reject(&:empty?).join("\n")
|
|
else
|
|
nil
|
|
end
|
|
|
|
category = case member['membership']['membership_type_id']
|
|
when "1" then 'student'
|
|
when "2" then 'employed'
|
|
when "3" then 'retired'
|
|
when "4" then 'unemployed'
|
|
else raise "derp membership type"
|
|
end
|
|
|
|
m = Member.new(
|
|
number: member['membership']['id'],
|
|
email: member['email']['email'],
|
|
display_name: member['display_name'],
|
|
identification_number: member['legal_identifier'],
|
|
status: "", # must be reset
|
|
category: category, # membership type id
|
|
address: address,
|
|
joined_on: member['membership']['join_date'],
|
|
expires_on: member['membership']['end_date'],
|
|
#t.string "regular_ifthenpay_link"
|
|
#t.string "reduced_ifthenpay_link"
|
|
excluded: false,
|
|
wants_mailing_list: true,
|
|
prefers_postal: false,
|
|
)
|
|
|
|
m.save!
|
|
|
|
m.reset_status!
|
|
|
|
member['contributions'].each do |contribution|
|
|
c = Contribution.new(
|
|
member: m,
|
|
eurocents: contribution['total_amount'],
|
|
payment_on: contribution['receive_date'] || m.joined_on,
|
|
payment_method: payment_instruments.find { |i| i["value"] == contribution['payment_instrument_id'] }["name"],
|
|
payment_reference: "",
|
|
)
|
|
|
|
c.save!
|
|
end
|
|
end
|
|
end
|
|
end
|