Recurrence#to_builder works and unit tested
This commit is contained in:
parent
b3ce8945c4
commit
157bd20b26
14 changed files with 364 additions and 4 deletions
132
app/models/recurrence.rb
Normal file
132
app/models/recurrence.rb
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# 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
|
||||||
|
class Recurrence < ApplicationRecord
|
||||||
|
|
||||||
|
include Model::Houidable
|
||||||
|
include Model::Jbuilder
|
||||||
|
include Model::Eventable
|
||||||
|
|
||||||
|
after_initialize :set_start_date_if_needed
|
||||||
|
|
||||||
|
belongs_to :recurring_donation
|
||||||
|
belongs_to :supporter
|
||||||
|
|
||||||
|
has_one :nonprofit, through: :supporter
|
||||||
|
|
||||||
|
delegate :currency, to: :nonprofit
|
||||||
|
|
||||||
|
delegate :designation, :dedication, to: :recurring_donation
|
||||||
|
|
||||||
|
validates :recurrences, presence: true
|
||||||
|
|
||||||
|
def trx_assignments
|
||||||
|
[{
|
||||||
|
assignment_object: 'donation',
|
||||||
|
amount: amount || 0,
|
||||||
|
dedication: dedication,
|
||||||
|
designation: designation
|
||||||
|
}.with_indifferent_access]
|
||||||
|
end
|
||||||
|
|
||||||
|
def recurrences
|
||||||
|
[
|
||||||
|
{
|
||||||
|
interval: recurring_donation.interval,
|
||||||
|
type: from_recurring_time_unit_to_recurrence(recurring_donation.time_unit)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def invoice_template
|
||||||
|
{
|
||||||
|
amount: amount || 0,
|
||||||
|
trx_assignments: trx_assignments,
|
||||||
|
supporter: supporter,
|
||||||
|
payment_method: {type: 'stripe'}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
concerning :JBuilder do
|
||||||
|
included do
|
||||||
|
setup_houid :recur
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_builder(*expand)
|
||||||
|
init_builder(*expand) do |json|
|
||||||
|
json.start_date start_date.to_i
|
||||||
|
|
||||||
|
json.add_builder_expansion :nonprofit, :supporter
|
||||||
|
|
||||||
|
json.start_date start_date
|
||||||
|
|
||||||
|
json.recurrences recurrences do |rec|
|
||||||
|
json.(rec, :interval, :type)
|
||||||
|
end
|
||||||
|
|
||||||
|
json.invoice_template do
|
||||||
|
json.amount do
|
||||||
|
json.cents amount || 0
|
||||||
|
json.currency currency
|
||||||
|
end
|
||||||
|
|
||||||
|
json.supporter supporter.id
|
||||||
|
|
||||||
|
json.payment_method do
|
||||||
|
json.type 'stripe'
|
||||||
|
end
|
||||||
|
|
||||||
|
json.trx_assignments trx_assignments do |assign|
|
||||||
|
json.assignment_object assign[:assignment_object]
|
||||||
|
dedication = assign[:dedication]
|
||||||
|
|
||||||
|
json.dedication do
|
||||||
|
json.type dedication['type']
|
||||||
|
json.name dedication['name']
|
||||||
|
contact = dedication['contact']
|
||||||
|
json.note dedication['note']
|
||||||
|
json.contact do
|
||||||
|
json.email contact['email'] if contact['email']
|
||||||
|
json.address contact['address'] if contact['address']
|
||||||
|
json.phone contact['phone'] if contact['phone']
|
||||||
|
end if contact
|
||||||
|
end if dedication
|
||||||
|
|
||||||
|
json.designation assign[:designation]
|
||||||
|
|
||||||
|
json.amount do
|
||||||
|
json.cents assign[:amount] || 0
|
||||||
|
json.currency currency
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def publish_created
|
||||||
|
Houdini.event_publisher.announce(
|
||||||
|
:recurrence_created,
|
||||||
|
to_event('recurrence.created', :nonprofit, :trx, :supporter).attributes!
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_start_date_if_needed
|
||||||
|
self[:start_date] = Time.current unless self[:start_date]
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def from_recurring_time_unit_to_recurrence(time_unit)
|
||||||
|
{
|
||||||
|
'month' => 'monthly',
|
||||||
|
'year' => 'yearly'
|
||||||
|
}[time_unit]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -39,6 +39,8 @@ class RecurringDonation < ApplicationRecord
|
||||||
validates :time_unit, presence: true, inclusion: { in: Timespan::Units }
|
validates :time_unit, presence: true, inclusion: { in: Timespan::Units }
|
||||||
validates_associated :donation
|
validates_associated :donation
|
||||||
|
|
||||||
|
delegate :designation, :dedication, to: :donation
|
||||||
|
|
||||||
def most_recent_charge
|
def most_recent_charge
|
||||||
charges&.max_by(&:created_at)
|
charges&.max_by(&:created_at)
|
||||||
end
|
end
|
||||||
|
|
11
db/migrate/20210602220525_create_recurrences.rb
Normal file
11
db/migrate/20210602220525_create_recurrences.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
class CreateRecurrences < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
create_table :recurrences, id: :string do |t|
|
||||||
|
t.integer "amount", null: false
|
||||||
|
t.references :recurring_donation, foreign_key: true, null: false
|
||||||
|
t.references :supporter, foreign_key: true, null: false, id: :string
|
||||||
|
t.datetime "start_date", comment: 'the moment that the recurrence should start. Could be earlier than created_at if this was imported.'
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
23
db/schema.rb
23
db/schema.rb
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2021_05_06_202607) do
|
ActiveRecord::Schema.define(version: 2021_06_02_220525) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_stat_statements"
|
enable_extension "pg_stat_statements"
|
||||||
|
@ -707,6 +707,17 @@ ActiveRecord::Schema.define(version: 2021_05_06_202607) do
|
||||||
t.string "country", limit: 255, default: "US"
|
t.string "country", limit: 255, default: "US"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "recurrences", id: :string, force: :cascade do |t|
|
||||||
|
t.integer "amount", null: false
|
||||||
|
t.bigint "recurring_donation_id", null: false
|
||||||
|
t.bigint "supporter_id", null: false
|
||||||
|
t.datetime "start_date", comment: "the moment that the recurrence should start. Could be earlier than created_at if this was imported."
|
||||||
|
t.datetime "created_at", precision: 6, null: false
|
||||||
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.index ["recurring_donation_id"], name: "index_recurrences_on_recurring_donation_id"
|
||||||
|
t.index ["supporter_id"], name: "index_recurrences_on_supporter_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "recurring_donations", id: :serial, force: :cascade do |t|
|
create_table "recurring_donations", id: :serial, force: :cascade do |t|
|
||||||
t.boolean "active"
|
t.boolean "active"
|
||||||
t.integer "paydate"
|
t.integer "paydate"
|
||||||
|
@ -789,6 +800,13 @@ ActiveRecord::Schema.define(version: 2021_05_06_202607) do
|
||||||
t.index ["payment_id"], name: "index_stripe_charges_on_payment_id"
|
t.index ["payment_id"], name: "index_stripe_charges_on_payment_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "stripe_refunds", id: :string, force: :cascade do |t|
|
||||||
|
t.bigint "payment_id"
|
||||||
|
t.datetime "created_at", precision: 6, null: false
|
||||||
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.index ["payment_id"], name: "index_stripe_refunds_on_payment_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "stripe_transactions", id: :string, force: :cascade do |t|
|
create_table "stripe_transactions", id: :string, force: :cascade do |t|
|
||||||
t.integer "amount", null: false
|
t.integer "amount", null: false
|
||||||
t.datetime "created_at", precision: 6, null: false
|
t.datetime "created_at", precision: 6, null: false
|
||||||
|
@ -1021,7 +1039,10 @@ ActiveRecord::Schema.define(version: 2021_05_06_202607) do
|
||||||
add_foreign_key "modern_campaign_gifts", "campaign_gifts"
|
add_foreign_key "modern_campaign_gifts", "campaign_gifts"
|
||||||
add_foreign_key "object_event_hook_configs", "nonprofits"
|
add_foreign_key "object_event_hook_configs", "nonprofits"
|
||||||
add_foreign_key "offline_transaction_charges", "payments"
|
add_foreign_key "offline_transaction_charges", "payments"
|
||||||
|
add_foreign_key "recurrences", "recurring_donations"
|
||||||
|
add_foreign_key "recurrences", "supporters"
|
||||||
add_foreign_key "stripe_charges", "payments"
|
add_foreign_key "stripe_charges", "payments"
|
||||||
|
add_foreign_key "stripe_refunds", "payments"
|
||||||
add_foreign_key "subtransaction_payments", "subtransactions"
|
add_foreign_key "subtransaction_payments", "subtransactions"
|
||||||
add_foreign_key "subtransactions", "transactions"
|
add_foreign_key "subtransactions", "transactions"
|
||||||
add_foreign_key "ticket_purchases", "event_discounts"
|
add_foreign_key "ticket_purchases", "event_discounts"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// License: LGPL-3.0-or-later
|
// License: LGPL-3.0-or-later
|
||||||
import type { HoudiniEvent } from "../../common";
|
import type { Amount, HoudiniEvent } from "../../common";
|
||||||
import type { TrxAssignment } from './';
|
import type { TrxAssignment } from './';
|
||||||
|
|
||||||
interface Dedication {
|
interface Dedication {
|
||||||
|
@ -19,6 +19,12 @@ export interface Donation extends TrxAssignment {
|
||||||
object: 'donation';
|
object: 'donation';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateDonation {
|
||||||
|
amount?: Amount;
|
||||||
|
dedication?: Dedication | null;
|
||||||
|
designation?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export type DonationCreated = HoudiniEvent<'donation.created', Donation>;
|
export type DonationCreated = HoudiniEvent<'donation.created', Donation>;
|
||||||
export type DonationUpdated = HoudiniEvent<'donation.updated', Donation>;
|
export type DonationUpdated = HoudiniEvent<'donation.updated', Donation>;
|
||||||
export type DonationDeleted = HoudiniEvent<'donation.deleted', Donation>;
|
export type DonationDeleted = HoudiniEvent<'donation.deleted', Donation>;
|
||||||
|
|
|
@ -4,6 +4,7 @@ import type Nonprofit from '../';
|
||||||
import type Supporter from "../Supporter";
|
import type Supporter from "../Supporter";
|
||||||
import type { Payment, PaymentAsId } from "./Payment";
|
import type { Payment, PaymentAsId } from "./Payment";
|
||||||
import type { SubtransactionAsId, Subtransaction } from "./Subtransaction";
|
import type { SubtransactionAsId, Subtransaction } from "./Subtransaction";
|
||||||
|
import type { CreateDonation } from "./Donation";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Every descendent of a Transaction object will have the following three fields
|
* Every descendent of a Transaction object will have the following three fields
|
||||||
|
@ -56,4 +57,6 @@ export * from './Payment';
|
||||||
export * from './Donation';
|
export * from './Donation';
|
||||||
export * from './Subtransaction';
|
export * from './Subtransaction';
|
||||||
export * as OfflineTransactionTypes from './OfflineTransaction';
|
export * as OfflineTransactionTypes from './OfflineTransaction';
|
||||||
|
|
||||||
|
export type CreateTrxAssignment = {assignment_object: string} & CreateDonation;
|
||||||
export {default as OfflineTransaction} from './OfflineTransaction';
|
export {default as OfflineTransaction} from './OfflineTransaction';
|
||||||
|
|
35
docs/event_definitions/Recurrence.ts
Normal file
35
docs/event_definitions/Recurrence.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
import type { Amount, HoudiniEvent, HoudiniObject, HouID, IDType } from "./common";
|
||||||
|
import type Nonprofit from './Nonprofit';
|
||||||
|
import type { RecurrenceRule } from "./common";
|
||||||
|
import type Supporter from "./Nonprofit/Supporter";
|
||||||
|
import type { CreateTrxAssignment } from "./Nonprofit/Transaction";
|
||||||
|
|
||||||
|
export interface InvoiceTemplate {
|
||||||
|
amount: Amount;
|
||||||
|
payment_method: {
|
||||||
|
/** will be added in future but not yet. */
|
||||||
|
id: never;
|
||||||
|
type: 'stripe';
|
||||||
|
};
|
||||||
|
supporter: IDType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The assignments created if the invoice succeeds. For now, we can only create new donations.
|
||||||
|
*/
|
||||||
|
trx_assignments: CreateTrxAssignment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface Recurrence extends HoudiniObject<HouID> {
|
||||||
|
nonprofit: IDType | Nonprofit;
|
||||||
|
object: 'recurrence';
|
||||||
|
recurrences: RecurrenceRule[];
|
||||||
|
start_date: number;
|
||||||
|
supporter: IDType | Supporter;
|
||||||
|
template: InvoiceTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RecurrenceCreated = HoudiniEvent<'recurrence.created', Recurrence>;
|
||||||
|
export type RecurrenceUpdated = HoudiniEvent<'recurrence.updated', Recurrence>;
|
||||||
|
export type RecurrenceDeleted = HoudiniEvent<'recurrence.deleted', Recurrence>;
|
|
@ -38,7 +38,7 @@ export type FlexibleAmount = Amount | string | number;
|
||||||
*/
|
*/
|
||||||
export type RecurrenceRule = {
|
export type RecurrenceRule = {
|
||||||
/**
|
/**
|
||||||
* The number of times we should run the recurrence
|
* The number of times we should run the recurrence.
|
||||||
*/
|
*/
|
||||||
count?: number;
|
count?: number;
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,5 +4,21 @@
|
||||||
# Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE
|
# Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE
|
||||||
FactoryBot.define do
|
FactoryBot.define do
|
||||||
factory :donation do
|
factory :donation do
|
||||||
end
|
factory :donation_with_dedication_designation do
|
||||||
|
dedication { {
|
||||||
|
contact: {
|
||||||
|
email: 'email@ema.com'
|
||||||
|
},
|
||||||
|
name: 'our loved one',
|
||||||
|
note: "we miss them dearly",
|
||||||
|
type: 'memory'
|
||||||
|
} }
|
||||||
|
designation { 'designated for soup kitchen'}
|
||||||
|
|
||||||
|
nonprofit {association :fv_poverty}
|
||||||
|
|
||||||
|
supporter { association :supporter}
|
||||||
|
amount {500}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
17
spec/factories/recurrences.rb
Normal file
17
spec/factories/recurrences.rb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# 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
|
||||||
|
supporter { association :supporter_with_fv_poverty}
|
||||||
|
recurring_donation do
|
||||||
|
association( :rd_with_dedication_designation,
|
||||||
|
nonprofit: supporter.nonprofit,
|
||||||
|
supporter: supporter,
|
||||||
|
donation: association(:donation_with_dedication_designation, nonprofit: supporter.nonprofit, supporter: supporter)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
amount { 500 }
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,5 +4,11 @@
|
||||||
# Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE
|
# Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE
|
||||||
FactoryBot.define do
|
FactoryBot.define do
|
||||||
factory :recurring_donation do
|
factory :recurring_donation do
|
||||||
|
factory :rd_with_dedication_designation do
|
||||||
|
nonprofit {association :fv_poverty}
|
||||||
|
start_date { Time.current}
|
||||||
|
interval { 1}
|
||||||
|
time_unit {"month"}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,4 +12,9 @@ FactoryBot.define do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
factory :supporter_with_fv_poverty, class: 'Supporter' do
|
||||||
|
name { 'Fake Supporter Name' }
|
||||||
|
nonprofit { association :fv_poverty}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
102
spec/models/recurrence_spec.rb
Normal file
102
spec/models/recurrence_spec.rb
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# 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
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Recurrence, type: :model do
|
||||||
|
around(:each) do |example|
|
||||||
|
Timecop.freeze(2020,5,4) do
|
||||||
|
example.run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'recurrence with donations, designation and dedication' do
|
||||||
|
subject { create(:recurrence) }
|
||||||
|
|
||||||
|
def trx_assignment_match
|
||||||
|
{
|
||||||
|
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(
|
||||||
|
{
|
||||||
|
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
|
||||||
|
let(:recurrence) { create(:recurrence)}
|
||||||
|
subject { JSON.parse(recurrence.to_builder.target!)}
|
||||||
|
|
||||||
|
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,
|
||||||
|
recurrences: [
|
||||||
|
{
|
||||||
|
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
|
||||||
|
end
|
|
@ -22,4 +22,8 @@ module Expect
|
||||||
def match_houid(prefix)
|
def match_houid(prefix)
|
||||||
match(/#{prefix}_[a-zA-Z0-9]{22}/)
|
match(/#{prefix}_[a-zA-Z0-9]{22}/)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def match_json(**args)
|
||||||
|
match(args.deep_stringify_keys)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue