module Osm class OnlinePayment class Schedule < Osm::Model class Payment < Osm::Model; end # Ensure the constant exists for the validators class PaymentStatus < Osm::Model; end # Ensure the constant exists for validators SORT_BY = [:section_id, :name, :id] PAY_NOW_OPTIONS = { -1 => 'Allowed at all times', 0 => 'Permanently disabled', 7 => 'Allowed within 1 week of due day', 14 => 'Allowed within 2 weeks of due day', 21 => 'Allowed within 3 weeks of due day', 28 => 'Allowed within 4 weeks of due day', 42 => 'Allowed within 6 weeks of due day', 56 => 'Allowed within 8 weeks of due day', } # @!attribute [rw] id # @return [FixNum] the schedule's ID # @!attribute [rw] section_id # @return [FixNum] the ID of the section the schedule belongs to # @!attribute [rw] account_id # @return [FixNum] the ID of the bank account this schedule is tied to # @!attribute [rw] name # @return [String] the name of the schedule # @!attribute [rw] description # @return [String] the description of what the schedule is for # @!attribute [rw] archived # @return [Boolean] whether the schedule has been archived # @!attribute [rw] gift_aid # @return [Boolean] whether payments made using this schedule are eligable for gift aid # @!attribute [rw] require_all # @return [Boolean] whether to require all payments within the schedule by default # @!attribute [rw] pay_now # @return [FixNum] controls the use of the pay now feature in OSM, see the PAY_NOW_OPTIONS hash # @!attribute [rw] annual_limit # @return [String] the maximum amount you'll be able to collect in a rolling 12 month period using this schedule # @!attribute [rw] payments # @return [Array] the payments which make up this schedule attribute :id, type: Integer attribute :section_id, type: Integer attribute :account_id, type: Integer attribute :name, type: String attribute :description, type: String, default: '' attribute :archived, type: Boolean attribute :gift_aid, type: Boolean attribute :require_all, type: Boolean attribute :pay_now, type: Integer attribute :annual_limit, type: String attribute :payments, type: Object, default: [] if ActiveModel::VERSION::MAJOR < 4 attr_accessible :id, :section_id, :account_id, :name, :description, :archived, :gift_aid, :require_all, :pay_now, :annual_limit, :payments end validates_numericality_of :id, only_integer: true, greater_than: 0 validates_numericality_of :section_id, only_integer: true, greater_than: 0 validates_numericality_of :account_id, only_integer: true, greater_than: 0 validates_presence_of :annual_limit validates_presence_of :name validates_inclusion_of :pay_now, in: PAY_NOW_OPTIONS.keys validates_inclusion_of :archived, in: [true, false] validates_inclusion_of :gift_aid, in: [true, false] validates_inclusion_of :require_all, in: [true, false] validates :payments, array_of: {item_type: Osm::OnlinePayment::Schedule::Payment, item_valid: true} # @!method initialize # Initialize a new Schedule # @param [Hash] attributes The hash of attributes (see attributes for descriptions, use Symbol of attribute name as the key) # Get a simple list of schedules for a section # @param [Osm::Api] api The api to use to make the request # @param [Osm::Section, Fixnum, #to_i] section The section (or its ID) to get the due badges for # @!macro options_get # @return [Array] def self.get_list_for_section(api, section, options={}) require_ability_to(api, :read, :finance, section, options) section_id = section.to_i cache_key = ['online_payments', 'schedule_list', section_id] if !options[:no_cache] && Osm::Model.cache_exist?(api, cache_key) return cache_read(api, cache_key) end data = api.perform_query("ext/finances/onlinepayments/?action=getSchemes§ionid=#{section_id}") data = data.is_a?(Hash) ? data['items'] : nil data ||= [] data.map!{ |i| {id: Osm::to_i_or_nil(i['schemeid']), name: i['name'].to_s } } cache_write(api, cache_key, data) return data end # Get all payment schedules for a section # @param [Osm::Api] api The api to use to make the request # @param [Osm::Section, Fixnum, #to_i] section The section (or its ID) to get the due badges for # @!macro options_get # @return [Array] def self.get_for_section(api, section, options={}) require_ability_to(api, :read, :finance, section, options) get_list_for_section(api, section, options).map do |schedule| get(api, section, schedule[:id], options) end end # Get a payment schedules for a section # @param [Osm::Api] api The api to use to make the request # @param [Osm::Section, Fixnum, #to_i] section The section (or its ID) to get the due badges for # @param [Fixnum, #to_i] schedule The ID of the payment schedule to get # @!macro options_get # @return [Array] def self.get(api, section, schedule, options={}) require_ability_to(api, :read, :finance, section, options) section_id = section.to_i schedule_id = schedule.to_i cache_key = ['online_payments', 'schedule', schedule_id] if !options[:no_cache] && cache_exist?(api, cache_key) data = cache_read(api, cache_key) return data if data.section_id.eql?(section_id) end data = api.perform_query("ext/finances/onlinepayments/?action=getPaymentSchedule§ionid=#{section_id}&schemeid=#{schedule_id}&allpayments=true") schedule = new( id: Osm::to_i_or_nil(data['schemeid']), section_id: section_id, account_id: Osm::to_i_or_nil(data['accountid']), name: data['name'], description: data['description'], archived: data['archived'].eql?('1'), gift_aid: data['giftaid'].eql?('1'), require_all: data['defaulton'].eql?('1'), pay_now: data['paynow'], annual_limit: data['preauth_amount'], ) (data['payments'] || []).each do |payment_data| payment = Payment.new( amount: payment_data['amount'], archived: payment_data['archived'].eql?('1'), due_date: Osm::parse_date(payment_data['date']), name: payment_data['name'].to_s, id: Osm::to_i_or_nil(payment_data['paymentid']), schedule: schedule, ) schedule.payments.push payment end cache_write(api, cache_key, schedule) return schedule end # Get payments made by members for the schedule # @param [Osm::Api] api The api to use to make the request # @param [Osm::Term, Fixnum, #to_i] term The term (or it's id) to get details for (defaults to current term) # @!macro options_get # @return [Array] def get_payments_for_members(api, term=nil, options={}) require_ability_to(api, :read, :finance, section_id, options) if term.nil? section = Osm::Section.get(api, section_id, options) term = section.waiting? ? -1 : Osm::Term.get_current_term_for_section(api, section) end cache_key = ['online_payments', 'for_members', id, term.to_i] if !options[:no_cache] && cache_exist?(api, cache_key) return cache_read(api, cache_key) end data = api.perform_query("ext/finances/onlinepayments/?action=getPaymentStatus§ionid=#{section_id}&schemeid=#{id}&termid=#{term.to_i}") data = data['items'] || [] data.map! do |item| payments_data = {} payments.each do |payment| unless item[payment.id.to_s].nil? payments_data[payment.id] = PaymentStatus.build_from_json(item[payment.id.to_s], payment) end end PaymentsForMember.new( member_id: Osm::to_i_or_nil(item['scoutid']), section_id: section_id, grouping_id: Osm::to_i_or_nil(item['patrolid']), first_name: item['firstname'], last_name: item['lastname'], start_date: require_all ? Osm::parse_date(item['startdate']) : nil, direct_debit: item['directdebit'].downcase.to_sym, payments: payments_data, schedule: self, ) end cache_write(api, cache_key, data) return data end # Get unarchived payments for the schedule # @return [Array] def current_payments payments.select{ |p| !p.archived? } end # Check if there are any unarchived payments for the schedule # @return [Boolean] def current_payments? payments.any?{ |p| !p.archived? } end # Get archived payments for the schedule # @return [Array] def archived_payments payments.select{ |p| p.archived? } end # Check if there are any archived payments for the schedule # @return [Boolean] def archived_payments? payments.any?{ |p| p.archived? } end def to_s "#{id} -> #{name}" end class Payment < Osm::Model # @!attribute [rw] id # @return [FixNum] the payment's ID # @!attribute [rw] amount # @return [Sreing] the amount of the payment # @!attribute [rw] name # @return [String] the name given to the payment # @!attribute [rw] archived # @return [Boolean] whether the payment has been archived # @!attribute [rw] date # @return [Date] the payment's due date # @!attribute [rw] schedule # @return [Osm::OnlnePayment::Schedule] the schedule the payment belongs to attribute :id, type: Integer attribute :amount, type: String attribute :name, type: String attribute :archived, type: Boolean attribute :due_date, type: Object attribute :schedule, type: Object if ActiveModel::VERSION::MAJOR < 4 attr_accessible :id, :amount, :name, :archived, :due_date, :schedule end validates_numericality_of :id, only_integer: true, greater_than: 0 validates_presence_of :amount validates_presence_of :name validates_presence_of :due_date validates_presence_of :schedule validates_inclusion_of :archived, in: [true, false] # @!method initialize # Initialize a new Payment # @param [Hash] attributes The hash of attributes (see attributes for descriptions, use Symbol of attribute name as the key) # Check if the payment is past due (or will be past due on the passed date) # @param [Date] date The date to check for (defaults to today) # @return [Boolean] def past_due?(date=Date.today) date > due_date end def inspect Osm.inspect_instance(self, {:replace_with => {'schedule' => :to_s}}) end end # Schedule::Payment class class PaymentsForMember < Osm::Model attribute :first_name, type: String attribute :last_name, type: String attribute :member_id, type: Integer attribute :direct_debit, type: Object attribute :start_date, type: Object attribute :payments, type: Object, default: {} # payment_id -> Array of statuses attribute :schedule, type: Object if ActiveModel::VERSION::MAJOR < 4 attr_accessible :first_name, :last_name, :member_id, :direct_debit, :start_date, :payments, :schedule end validates_numericality_of :member_id, only_integer: true, greater_than: 0 validates_presence_of :first_name validates_presence_of :last_name validates_presence_of :schedule validates_inclusion_of :direct_debit, in: [:active, :inactive, :cancelled] validates :payments, hash: {key_type: Fixnum, value_type: Array} # @!method initialize # Initialize a new Schedule # @param [Hash] attributes The hash of attributes (see attributes for descriptions, use Symbol of attribute name as the key) # Get the most recent status for a member's payment # @param [Osm::OnlinePayment::Schedule::Payment, Fixnum, #to_i] payment The payment (or it's ID) to check # @return [Boolean] def latest_status_for(payment) @latest_status ||= Hash[ payments.map{ |k,v| [k, v.sort[0]] } ] @latest_status[payment.to_i] end # Check if the status of a member's payment is considered paid # @param [Osm::OnlinePayment::Schedule::Payment, Fixnum, #to_i] payment The payment (or it's ID) to check # @return [Boolean, nil] def paid?(payment) status = latest_status_for(payment.to_i) return nil if status.nil? [:paid, :paid_manually, :received, :initiated].include?(status.status) end # Check if the status of a member's payment is considered unpaid # @param [Osm::OnlinePayment::Schedule::Payment, Fixnum, #to_i] payment The payment (or it's ID) to check # @return [Boolean, nil] def unpaid?(payment) status = latest_status_for(payment.to_i) return nil if status.nil? [:required].include?(status.status) end # Check if a payment is over due (or will be over due on the passed date) # @param [Osm::OnlinePayment::Schedule::Payment, Fixnum, #to_i] payment The payment (or it's ID) to check # @param [Date] date The date to check for (defaults to today) # @return [Boolean] whether the member's payment is unpaid and the payment's due date has passed def over_due?(payment, date=nil) unpaid?(payment) && payment.past_due?(date) end # Check if the member has an active direct debit for this schedule # @return [Boolean] def active_direct_debit? direct_debit.eql?(:active) end # Update the status of a payment for the member in OSM # @param [Osm::Api] api The api to use to make the request # @param [Osm::OnlinePayment::Schedule::Payment, Fixnum, #to_i] payment The payment (or it's ID) to update # @param [Symbol] status What to update the status to (:required, :not_required or :paid_manually) # @param [Boolean] gift_aid Whether to update the gift aid record too (only relevant when setting to :paid_manually) # @return [Boolean] whether the update was made in OSM def update_payment_status(api, payment, status, gift_aid=false) payment_id = payment.to_i fail ArgumentError, "#{payment_id} is not a valid payment for the schedule." unless schedule.payments.map(&:id).include?(payment_id) fail ArgumentError, "status must be either :required, :not_required or :paid_manually. You passed in #{status.inspect}" unless [:required, :not_required, :paid_manually].include?(status) gift_aid = false unless payment.schedule.gift_aid? api_status = { required: 'Payment required', not_required: 'Payment not required', paid_manually: 'Paid manually', }[status] data = api.perform_query("ext/finances/onlinepayments/?action=updatePaymentStatus", { 'sectionid' => schedule.section_id, 'schemeid' => schedule.id, 'scoutid' => member_id, 'paymentid' => payment_id, 'giftaid' => gift_aid, 'value' => api_status, }) data = data[payment_id.to_s] return false if data.nil? # No data (at all) for this payment data = PaymentStatus.build_from_json(data) return false if data.nil? # No history for payment so it didn't get updated data = data.sort[0] return false if data.nil? # No history for payment so it didn't get updated return false unless data.status.eql?(status) # Latest status is not what we set return true end # Mark a payment as required by the member # @param [Osm::Api] api The api to use to make the request # @param [Osm::OnlinePayment::Schedule::Payment, Fixnum, #to_i] payment The payment (or it's ID) to update # @return [Boolean] whether the update was made in OSM def mark_payment_required(api, payment) update_payment_status(api, payment, :required) end # Mark a payment as not required by the member # @param [Osm::Api] api The api to use to make the request # @param [Osm::OnlinePayment::Schedule::Payment, Fixnum, #to_i] payment The payment (or it's ID) to update # @return [Boolean] whether the update was made in OSM def mark_payment_not_required(api, payment) update_payment_status(api, payment, :not_required) end # Mark a payment as paid by the member # @param [Osm::Api] api The api to use to make the request # @param [Osm::OnlinePayment::Schedule::Payment, Fixnum, #to_i] payment The payment (or it's ID) to update # @param [Boolean] gift_aid Whether to update the gift aid record too # @return [Boolean] whether the update was made in OSM def mark_payment_paid_manually(api, payment, gift_aid=false) update_payment_status(api, payment, :paid_manually, gift_aid) end end # Schedule::PaymentsForMember class class PaymentStatus < Osm::Model VALID_STATUSES = [:required, :not_required, :initiated, :paid, :received, :paid_manually] attribute :id, type: Integer attribute :payment, type: Object attribute :timestamp, type: Object attribute :status, type: Object attribute :details, type: String attribute :updated_by, type: String attribute :updated_by_id, type: Integer if ActiveModel::VERSION::MAJOR < 4 attr_accessible :id, :payment, :timestamp, :status, :details, :updated_by, :updated_by_id end validates_numericality_of :id, only_integer: true, greater_than: 0 validates_numericality_of :updated_by_id, only_integer: true, greater_than_or_equal_to: -2 validates_presence_of :payment validates_presence_of :timestamp validates_presence_of :updated_by validates_inclusion_of :status, in: VALID_STATUSES # @!method initialize # Initialize a new PaymentStatus # @param [Hash] attributes The hash of attributes (see attributes for descriptions, use Symbol of attribute name as the key) # @!method required? # Whether the status is :required # @return (Boolean) # @!method not_required? # Whether the status is :not_required # @return (Boolean) # @!method initiated? # Whether the status is :initiated # @return (Boolean) # @!method paid? # Whether the status is :paid # @return (Boolean) # @!method received? # Whether the status is :received # @return (Boolean) # @!method paid_manually? # Whether the status is :paid_manually # @return (Boolean) VALID_STATUSES.each do |attribute| define_method "#{attribute}?" do status.eql?(attribute) end end def <=>(another) result = -(self.timestamp <=> another.try(:timestamp)) result = self.payment <=> another.try(:payment) if result.eql?(0) result = self.id <=> another.try(:id) if result.eql?(0) return result end def inspect Osm.inspect_instance(self, {:replace_with => {'payment' => :id}}) end protected def self.build_from_json(json, payment=nil) data = ActiveSupport::JSON.decode(json) return [] unless data.is_a?(Hash) data = data['status'] return [] unless data.is_a?(Array) status_map = { 'Payment required' => :required, 'Payment not required' => :not_required, 'Initiated' => :initiated, 'Paid' => :paid, 'Received' => :received, 'Paid manually' => :paid_manually, } data.map! do |item| new( id: Osm::to_i_or_nil(item['statusid']), payment: payment, timestamp: Time.strptime(item['statustimestamp'], '%d/%m/%Y %H:%M'), status: status_map[item['status']], details: item['details'], updated_by: item['firstname'], updated_by_id: item['who'].to_i, ) end end end # Schedule::PaymentStatus class end # Schedule class end end