From 9aaa1e491ea08680a68b812809c10628f2da8ed3 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Tue, 8 Jun 2021 14:34:54 -0500 Subject: [PATCH] Add start to RecurrenceRule --- app/models/recurrence.rb | 20 +- docs/event_definitions/common.ts | 11 +- spec/factories/recurrences.rb | 32 ++- .../insert/insert_recurring_donation_spec.rb | 3 +- .../update/update_recurring_donations_spec.rb | 3 +- spec/models/recurrence_spec.rb | 198 +++++++++++++++++- 6 files changed, 257 insertions(+), 10 deletions(-) diff --git a/app/models/recurrence.rb b/app/models/recurrence.rb index 2f08efe6..525bd892 100644 --- a/app/models/recurrence.rb +++ b/app/models/recurrence.rb @@ -33,7 +33,8 @@ class Recurrence < ApplicationRecord # rubocop:disable Metrics/ClassLength [ { interval: recurring_donation.interval, - type: from_recurring_time_unit_to_recurrence(recurring_donation.time_unit) + type: from_recurring_time_unit_to_recurrence(recurring_donation.time_unit), + start: recurrence_start_date } ] end @@ -58,10 +59,9 @@ class Recurrence < ApplicationRecord # rubocop:disable Metrics/ClassLength json.add_builder_expansion :nonprofit, :supporter - json.start_date start_date - json.recurrences recurrences do |rec| json.(rec, :interval, :type) + json.start rec[:start].to_i end json.invoice_template do # rubocop:disable Metrics/BlockLength @@ -134,4 +134,18 @@ class Recurrence < ApplicationRecord # rubocop:disable Metrics/ClassLength 'year' => 'yearly' }[time_unit] end + + def recurrence_start_date # rubocop:disable Metrics/AbcSize,Metrics/MethodLength + paydate = recurring_donation.paydate + paydate = if paydate.nil? + (1..28).cover?(start_date.day) ? start_date.day : 28 + else + paydate + end + if paydate < start_date.day + (start_date + 1.month).beginning_of_month + (paydate - 1).days + else + start_date.beginning_of_month + (paydate - 1).days + end + end end diff --git a/docs/event_definitions/common.ts b/docs/event_definitions/common.ts index 1fcd5b1f..c3a22657 100644 --- a/docs/event_definitions/common.ts +++ b/docs/event_definitions/common.ts @@ -45,14 +45,21 @@ export type RecurrenceRule = { * Interval of `type` for the event to recur */ interval: number; + + /** + * The instant when the recurrence should be calculated from + */ + start: number; + /** * The scale of the recurrence */ type: 'monthly' | 'year'; /** - * The the point after which the rule should not recur anymore. + * The point after which the rule should not recur anymore. */ - until?: Date; + until?: number; + }; diff --git a/spec/factories/recurrences.rb b/spec/factories/recurrences.rb index cad710ca..4e9c01ed 100644 --- a/spec/factories/recurrences.rb +++ b/spec/factories/recurrences.rb @@ -2,8 +2,8 @@ # License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later # Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE -FactoryBot.define do - factory :recurrence do +FactoryBot.define do # rubocop:disable Metrics/BlockLength + factory :recurrence do # rubocop:disable Metrics/BlockLength supporter { association :supporter_with_fv_poverty } recurring_donation do association(:rd_with_dedication_designation, @@ -13,5 +13,33 @@ FactoryBot.define do supporter: supporter)) end amount { 500 } + + factory :recurrence_with_paydate_earlier_in_month do + recurring_donation do + association(:rd_with_dedication_designation, + nonprofit: supporter.nonprofit, + supporter: supporter, + donation: association( + :donation_with_dedication_designation, + nonprofit: supporter.nonprofit, + supporter: supporter + ), + paydate: 3) + end + end + + factory :recurrence_with_paydate_later_in_month do + recurring_donation do + association(:rd_with_dedication_designation, + nonprofit: supporter.nonprofit, + supporter: supporter, + donation: association( + :donation_with_dedication_designation, + nonprofit: supporter.nonprofit, + supporter: supporter + ), + paydate: 5) + end + end end end diff --git a/spec/lib/insert/insert_recurring_donation_spec.rb b/spec/lib/insert/insert_recurring_donation_spec.rb index dfd215d1..e1fd7f80 100644 --- a/spec/lib/insert/insert_recurring_donation_spec.rb +++ b/spec/lib/insert/insert_recurring_donation_spec.rb @@ -487,9 +487,10 @@ describe InsertRecurringDonation do 'object' => common_builder_expanded.merge({ 'object' => 'recurrence', 'id' => match_houid('recur'), - 'start_date' => Time.current, + 'start_date' => Time.new(2020, 5, 4).utc.to_i, 'recurrences' => [ { + 'start' => Time.new(2020, 5, 4).utc.to_i, 'interval' => 1, 'type' => 'monthly' }], diff --git a/spec/lib/update/update_recurring_donations_spec.rb b/spec/lib/update/update_recurring_donations_spec.rb index 9f3b37b8..3ead1a80 100644 --- a/spec/lib/update/update_recurring_donations_spec.rb +++ b/spec/lib/update/update_recurring_donations_spec.rb @@ -119,9 +119,10 @@ describe UpdateRecurringDonations do 'object' => common_builder_expanded.merge({ 'object' => 'recurrence', 'id' => orig_recurrence.id, - 'start_date' => orig_recurrence.start_date, + 'start_date' => orig_recurrence.start_date.to_i, 'recurrences' => [ { + 'start' => orig_recurrence.start_date.beginning_of_day.to_i, 'interval' => 1, 'type' => 'monthly' }], diff --git a/spec/models/recurrence_spec.rb b/spec/models/recurrence_spec.rb index c4d7db0e..f6475147 100644 --- a/spec/models/recurrence_spec.rb +++ b/spec/models/recurrence_spec.rb @@ -62,6 +62,7 @@ RSpec.describe Recurrence, type: :model do it { is_expected.to have_attributes(recurrences: contain_exactly( { + start: Time.current.beginning_of_day, interval: 1, type: 'monthly' } @@ -82,15 +83,210 @@ RSpec.describe Recurrence, type: :model do let(:recurrence) { create(:recurrence) } let(:invoice_template) { subject['invoice_template'] } + it do # rubocop:disable RSpec/ExampleLength + is_expected.to match_json( + { + object: 'recurrence', + nonprofit: kind_of(Numeric), + supporter: kind_of(Numeric), + id: match_houid('recur'), + start_date: Time.current.to_i, + recurrences: [ + { + start: Time.new(2020, 5, 4).to_i, + interval: 1, + type: 'monthly' + } + ], + invoice_template: { + supporter: kind_of(Numeric), + amount: { 'cents' => 500, 'currency' => 'usd' }, + payment_method: { 'type' => 'stripe' }, + trx_assignments: [trx_assignment_json] + } + } + ) + end + end + end + + describe 'recurrence_with_paydate_later_in_month' do + subject { create(:recurrence_with_paydate_later_in_month) } + + def trx_assignment_match # rubocop:disable Metrics/MethodLength + { + assignment_object: 'donation', + amount: 500, + dedication: { + contact: { + email: 'email@ema.com' + }, + name: 'our loved one', + note: 'we miss them dearly', + type: 'memory' + }, + designation: 'designated for soup kitchen' + } + end + + def trx_assignment_json + trx_assignment_match.merge({ amount: { cents: 500, currency: 'usd' } }) + end + + def invoice_template_match + { + + amount: 500, + supporter: an_instance_of(Supporter), + payment_method: { + type: 'stripe' + }, + trx_assignments: [trx_assignment_match] + } + end + + def invoice_template_json + invoice_template_match.merge(trx_assignments: [trx_assignment_json]).deep_stringify_keys + end + + it { + is_expected.to have_attributes( + supporter: an_instance_of(Supporter), + nonprofit: an_instance_of(Nonprofit), + start_date: Time.current, + id: match_houid('recur') + ) + } + + it { + is_expected.to have_attributes(recurrences: contain_exactly( + { + start: Time.new(2020, 5, 5).utc, + interval: 1, + type: 'monthly' + } + )) + } + + it { is_expected.to be_persisted } + + it do + is_expected.to have_attributes( + invoice_template: invoice_template_match + ) + end + + describe '.to_builder' do + subject { JSON.parse(recurrence.to_builder.target!) } + + let(:recurrence) { create(:recurrence_with_paydate_later_in_month) } + let(:invoice_template) { subject['invoice_template'] } + it do is_expected.to match_json({ object: 'recurrence', nonprofit: kind_of(Numeric), supporter: kind_of(Numeric), id: match_houid('recur'), - start_date: Time.current, + start_date: Time.current.to_i, recurrences: [ { + start: Time.new(2020, 5, 5).to_i, + interval: 1, + type: 'monthly' + } + ], + invoice_template: { supporter: kind_of(Numeric), + amount: { 'cents' => 500, 'currency' => 'usd' }, + payment_method: { 'type' => 'stripe' }, + trx_assignments: [trx_assignment_json] } + }) + end + end + end + + describe 'recurrence_with_paydate_earlier_in_month' do + subject { create(:recurrence_with_paydate_earlier_in_month) } + + def trx_assignment_match # rubocop:disable Metrics/MethodLength + { + assignment_object: 'donation', + amount: 500, + dedication: { + contact: { + email: 'email@ema.com' + }, + name: 'our loved one', + note: 'we miss them dearly', + type: 'memory' + }, + designation: 'designated for soup kitchen' + } + end + + def trx_assignment_json + trx_assignment_match.merge({ amount: { cents: 500, currency: 'usd' } }) + end + + def invoice_template_match + { + + amount: 500, + supporter: an_instance_of(Supporter), + payment_method: { + type: 'stripe' + }, + trx_assignments: [trx_assignment_match] + } + end + + def invoice_template_json + invoice_template_match.merge(trx_assignments: [trx_assignment_json]).deep_stringify_keys + end + + it { + is_expected.to have_attributes( + supporter: an_instance_of(Supporter), + nonprofit: an_instance_of(Nonprofit), + start_date: Time.current, + id: match_houid('recur') + ) + } + + it { + is_expected.to have_attributes(recurrences: contain_exactly( + { + start: Time.new(2020, 6, 3).utc, + interval: 1, + type: 'monthly' + } + )) + } + + it { is_expected.to be_persisted } + + it do + is_expected.to have_attributes( + invoice_template: invoice_template_match + ) + end + + describe '.to_builder' do + subject { JSON.parse(recurrence.to_builder.target!) } + + let(:recurrence) { create(:recurrence_with_paydate_earlier_in_month) } + let(:invoice_template) { subject['invoice_template'] } + + it do + is_expected.to match_json({ + object: 'recurrence', + nonprofit: kind_of(Numeric), + supporter: kind_of(Numeric), + id: match_houid('recur'), + start_date: Time.current.to_i, + recurrences: [ + { + start: Time.new(2020, 6, 3).to_i, interval: 1, type: 'monthly' }