Initial Transaction and OfflineTransaction support

Co-authored-by: Clarissa Lima Borges <clarissa@commitchange.com>
This commit is contained in:
Eric Schultz 2021-04-07 16:43:58 -05:00 committed by Eric Schultz
parent fca3c7cd6a
commit 2e8821efdf
69 changed files with 1991 additions and 376 deletions

View file

@ -569,7 +569,6 @@ AllCops:
- 'spec/lib/format/dedication_spec.rb' - 'spec/lib/format/dedication_spec.rb'
- 'spec/lib/insert/insert_refunds_spec.rb' - 'spec/lib/insert/insert_refunds_spec.rb'
- 'spec/lib/insert/insert_recurring_donation_spec.rb' - 'spec/lib/insert/insert_recurring_donation_spec.rb'
- 'spec/lib/insert/insert_donation_spec.rb'
- 'spec/lib/insert/insert_charge_spec.rb' - 'spec/lib/insert/insert_charge_spec.rb'
- 'spec/lib/insert/insert_disputes_spec.rb' - 'spec/lib/insert/insert_disputes_spec.rb'
- 'spec/lib/insert/insert_bank_account_spec.rb' - 'spec/lib/insert/insert_bank_account_spec.rb'
@ -745,3 +744,7 @@ Style/FrozenStringLiteralComment:
Style/PreferredHashMethods: Style/PreferredHashMethods:
Enabled: false Enabled: false
Style/LambdaCall:
# jbuilder uses this a lot so we want it.
Enabled: false

9
Procfile.dev Normal file
View file

@ -0,0 +1,9 @@
# You can run these commands in separate shells
web: rails s -p 3000
# Next line runs a watch process with webpack to compile the changed files.
# When making frequent changes to client side assets, you will prefer building webpack assets
# upon saving rather than when you refresh your browser page.
# Note, if using React on Rails localization you will need to run
# `bundle exec rake react_on_rails:locale` before you run bin/webpack
client: sh -c 'rm -rf public/packs/* || true && bin/webpack -w'

26
Procfile.dev-hmr Normal file
View file

@ -0,0 +1,26 @@
# Procfile for development using HMR
web: rails s -p 3000
# Note, hot and live reloading don't work with the default generator setup on
# top of the rails/webpacker Webpack config with server rendering.
# If you have server rendering enabled (prerender is true), you either need to
# a. Ensure that you have dev_server.hmr and dev_server.inline BOTH set to false,
# and you have this option in your config/initializers/react_on_rails.rb:
# config.same_bundle_for_client_and_server = true
# If you have either config/webpacker.yml option set to true, you'll see errors like
# "ReferenceError: window is not defined" (if hmr is true)
# "TypeError: Cannot read property 'prototype' of undefined" (if inline is true)
# b. Skip using the webpack-dev-server. bin/webpack --watch is typically
fast enough.
# c. See the React on Rails README for a link to documentation for how to setup
# SSR with HMR and React hot loading using the webpack-dev-server only for the
# client bundles and a static file for the server bundle.
# Run the webpack-dev-server for client and maybe server files
webpack-dev-server: bin/webpack-dev-server
# Keep the JS fresh for server rendering. Remove if not server rendering.
# Especially if you have not configured generation of a server bundle without a hash.
# as that will conflict with the manifest created by the bin/webpack-dev-server
# rails-server-assets: SERVER_BUNDLE_ONLY=yes bin/webpack --watch

View file

@ -143,7 +143,7 @@ This will download the latest Houdini code. Change to the
`houdini` directory and we can set the rest of Houdini up. `houdini` directory and we can set the rest of Houdini up.
Let's run the Houdini project setup and we'll be ready to go! Let's run the Houdini project setup and we'll be ready to go!
P
```bash ```bash
bin/setup bin/setup
``` ```

View file

@ -40,8 +40,6 @@ class Campaign < ApplicationRecord
# :reason_for_supporting, # :reason_for_supporting,
# :default_reason_for_supporting # :default_reason_for_supporting
add_builder_expansion :nonprofit
validate :end_datetime_cannot_be_in_past, on: :create validate :end_datetime_cannot_be_in_past, on: :create
validates :profile, presence: true validates :profile, presence: true
validates :nonprofit, presence: true validates :nonprofit, presence: true
@ -202,6 +200,8 @@ class Campaign < ApplicationRecord
def to_builder(*expand) def to_builder(*expand)
init_builder(*expand) do |json| init_builder(*expand) do |json|
json.(self, :name) json.(self, :name)
json.add_builder_expansion :nonprofit
end end
end end

View file

@ -30,8 +30,6 @@ class CampaignGiftOption < ApplicationRecord
after_update_commit :publish_updated after_update_commit :publish_updated
after_destroy_commit :publish_deleted after_destroy_commit :publish_deleted
add_builder_expansion :campaign, :nonprofit
has_one :nonprofit, through: :campaign has_one :nonprofit, through: :campaign
@ -90,6 +88,8 @@ class CampaignGiftOption < ApplicationRecord
json.type desc[:recurrence][:type] json.type desc[:recurrence][:type]
end if desc[:recurrence] end if desc[:recurrence]
end end
json.add_builder_expansion :campaign, :nonprofit
end end
end end

View file

@ -10,9 +10,6 @@ class CampaignGiftPurchase < ApplicationRecord
belongs_to :campaign belongs_to :campaign
has_many :campaign_gifts, class_name: 'ModernCampaignGift' has_many :campaign_gifts, class_name: 'ModernCampaignGift'
add_builder_expansion :campaign
# TODO replace with Discard gem # TODO replace with Discard gem
define_model_callbacks :discard define_model_callbacks :discard
@ -28,22 +25,28 @@ class CampaignGiftPurchase < ApplicationRecord
end end
end end
def to_id
::Jbuilder.new do |json|
json.id id
json.object 'campaign_gift_purchase'
json.type 'trx_assignment'
end
end
def to_builder(*expand) def to_builder(*expand)
init_builder(*expand) do |json| init_builder(*expand) do |json|
json.(self, :deleted) json.(self, :deleted)
json.type 'trx_assignment'
json.amount do json.amount do
json.cents amount json.cents amount
json.currency nonprofit.currency json.currency nonprofit.currency
end end
if expand.include? :campaign_gifts json.add_builder_expansion :campaign, :nonprofit, :supporter
json.campaign_gifts campaign_gifts do |gift| json.add_builder_expansion :trx, json_attribute: "transaction"
json.merge! gift.to_builder.attributes! json.add_builder_expansion :campaign_gifts, enum_type: :expandable
end
else
json.campaign_gifts campaign_gifts.pluck(:id)
end
end end
end end

View file

@ -0,0 +1,16 @@
# 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
module Model::CreatedTimeable
extend ActiveSupport::Concern
included do
after_initialize :set_created_if_needed
private
def set_created_if_needed
self[:created] = Time.current unless self[:created]
end
end
end

View file

@ -14,8 +14,8 @@ module Model::Eventable
end end
end end
end end
def to_builder(*expand) def to_builder(*expand)
raise NotImplementedError.new("to_builder must be implemented in your model") raise NotImplementedError.new("to_builder must be implemented in your model")
end end
end end

View file

@ -92,6 +92,15 @@ module Model::Jbuilder
def builder_expansions def builder_expansions
@builder_expansions ||= BuilderExpansionSet.new @builder_expansions ||= BuilderExpansionSet.new
end end
def init_builder(model, *expand)
JbuilderWithExpansions.new(model, *expand) do | json|
json.(model, :id)
json.object model.class.name.underscore
yield(json)
end
end
end end
def to_id def to_id
@ -152,7 +161,14 @@ module Model::Jbuilder
return ->(model,be=self) { return ->(model,be=self) {
value = be.get_attribute_value model value = be.get_attribute_value model
if be.expandable_enum? if be.expandable_enum?
value&.map{|i| i&.to_id} value&.map do |i|
id_result = i&.to_id
if ::Jbuilder === id_result
id_result.attributes!
else
id_result
end
end
elsif be.flat_enum? elsif be.flat_enum?
value value
else else
@ -165,7 +181,7 @@ module Model::Jbuilder
return ->(model,be=self) { return ->(model,be=self) {
value = be.get_attribute_value model value = be.get_attribute_value model
if be.expandable_enum? if be.expandable_enum?
value&.map{|i| i&.to_builder} value&.map{|i| i&.to_builder.attributes!}
elsif be.flat_enum? elsif be.flat_enum?
value value
else else
@ -183,20 +199,32 @@ module Model::Jbuilder
end end
end end
def init_builder(*expand, &block)
self.class.init_builder(self, *expand, &block)
end
def init_builder(*expand)
builder_expansions = self.class.builder_expansions class JbuilderWithExpansions < ::Jbuilder
Jbuilder.new do | json| attr_reader :model, :expand
json.(self, :id)
json.object self.class.name.underscore delegate_missing_to :@jbuilder
def initialize(model, *expand, &block)
@model = model
@expand = expand
super(&block)
end
def add_builder_expansion( ... )
builder_expansions = BuilderExpansionSet.new
builder_expansions.add_builder_expansion( ... )
builder_expansions.keys.each do |k| builder_expansions.keys.each do |k|
if expand.include? k if expand.include? k
json.set! builder_expansions.get_by_key(k).json_attribute, builder_expansions.get_by_key(k).to_builder.(self) set! builder_expansions.get_by_key(k).json_attribute, builder_expansions.get_by_key(k).to_builder.(model)
else else
json.set! builder_expansions.get_by_key(k).json_attribute, builder_expansions.get_by_key(k).to_id.(self) set! builder_expansions.get_by_key(k).json_attribute, builder_expansions.get_by_key(k).to_id.(model)
end end
end end
yield(json)
end end
end end
end end

View file

@ -0,0 +1,20 @@
# 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
module Model::Subtransactable
extend ActiveSupport::Concern
included do
include Model::Houidable
include Model::Jbuilder
include Model::Eventable
has_one :subtransaction, as: :subtransactable, dependent: :nullify
has_one :trx, through: :subtransaction
has_one :supporter, through: :trx
has_one :nonprofit, through: :trx
has_many :subtransaction_payments, through: :subtransaction
end
end

View file

@ -0,0 +1,20 @@
# 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
module Model::SubtransactionPaymentable
extend ActiveSupport::Concern
included do
include Model::Houidable
include Model::Jbuilder
include Model::Eventable
has_one :subtransaction_payment, as: :paymentable, touch: true, dependent: :destroy
has_one :trx, through: :subtransaction_payment
has_one :supporter, through: :subtransaction_payment
has_one :nonprofit, through: :subtransaction_payment
has_one :subtransaction, through: :subtransaction_payment
end
end

View file

@ -16,8 +16,8 @@ module Model::TrxAssignable
json_attribute: :transaction json_attribute: :transaction
has_one :transaction_assignment, as: :assignable has_one :transaction_assignment, as: :assignable
has_one :trx, through: :transaction_assignment has_one :trx, through: :transaction_assignment, class_name: 'Transaction', foreign_key: 'transaction_id'
has_one :supporter, through: :trx has_one :supporter, through: :transaction_assignment
has_one :nonprofit, through: :supporter has_one :nonprofit, through: :transaction_assignment
end end
end end

View file

@ -14,8 +14,6 @@ class CustomFieldMaster < ApplicationRecord
scope :not_deleted, -> { where(deleted: false) } scope :not_deleted, -> { where(deleted: false) }
add_builder_expansion :nonprofit
after_create_commit :publish_created after_create_commit :publish_created
# TODO replace with Discard gem # TODO replace with Discard gem
@ -42,6 +40,8 @@ class CustomFieldMaster < ApplicationRecord
init_builder(*expand) do |json| init_builder(*expand) do |json|
json.(self, :name, :deleted) json.(self, :name, :deleted)
json.object 'custom_field_definition' json.object 'custom_field_definition'
json.add_builder_expansion :nonprofit
end end
end end

View file

@ -5,7 +5,7 @@
class Event < ApplicationRecord class Event < ApplicationRecord
include Image::AttachmentExtensions include Image::AttachmentExtensions
include Model::Jbuilder include Model::Jbuilder
add_builder_expansion :nonprofit
# :deleted, #bool for soft-delete # :deleted, #bool for soft-delete
# :name, # str # :name, # str
# :tagline, # str # :tagline, # str
@ -102,6 +102,7 @@ class Event < ApplicationRecord
def to_builder(*expand) def to_builder(*expand)
init_builder(*expand) do |json| init_builder(*expand) do |json|
json.(self, :name) json.(self, :name)
json.add_builder_expansion :nonprofit
end end
end end

View file

@ -6,7 +6,6 @@ class EventDiscount < ApplicationRecord
include Model::Eventable include Model::Eventable
include Model::Jbuilder include Model::Jbuilder
add_builder_expansion :nonprofit, :event
# :code, # :code,
# :event_id, # :event_id,
# :name, # :name,
@ -36,13 +35,16 @@ class EventDiscount < ApplicationRecord
json.percent percent json.percent percent
end end
if expand.include? :ticket_levels json.add_builder_expansion :nonprofit, :event
json.ticket_levels ticket_levels do |tl| json.add_builder_expansion :ticket_levels, enum_type: :expandable
json.merge! tl.to_builder.attributes!
end # if expand.include? :ticket_levels
else # json.ticket_levels ticket_levels do |tl|
json.ticket_levels ticket_levels.pluck(:id) # json.merge! tl.to_builder.attributes!
end # end
# else
# json.ticket_levels ticket_levels.pluck(:id)
# end
end end
end end

View file

@ -8,10 +8,6 @@ class ModernCampaignGift < ApplicationRecord
include Model::Eventable include Model::Eventable
setup_houid :cgift setup_houid :cgift
add_builder_expansion :nonprofit, :supporter, :campaign, :campaign_gift_option, :campaign_gift_purchase
add_builder_expansion :trx,
json_attribute: :transaction
belongs_to :campaign_gift_purchase belongs_to :campaign_gift_purchase
belongs_to :legacy_campaign_gift, class_name: 'CampaignGift', foreign_key: :campaign_gift_id, inverse_of: :modern_campaign_gift belongs_to :legacy_campaign_gift, class_name: 'CampaignGift', foreign_key: :campaign_gift_id, inverse_of: :modern_campaign_gift
@ -42,6 +38,17 @@ class ModernCampaignGift < ApplicationRecord
init_builder(*expand) do |json| init_builder(*expand) do |json|
json.(self, :deleted) json.(self, :deleted)
json.object 'campaign_gift' json.object 'campaign_gift'
json.add_builder_expansion :nonprofit, :supporter, :campaign, :campaign_gift_option
json.add_builder_expansion :trx,
json_attribute: :transaction
if (expand.include? :campaign_gift_purchase)
json.campaign_gift_purchase campaign_gift_purchase.to_builder
else
json.campaign_gift_purchase campaign_gift_purchase.id
end
json.amount do json.amount do
json.cents amount json.cents amount
json.currency nonprofit.currency json.currency nonprofit.currency

View file

@ -11,10 +11,19 @@ class ModernDonation < ApplicationRecord
delegate :designation, :dedication, to: :legacy_donation delegate :designation, :dedication, to: :legacy_donation
def to_id
::Jbuilder.new do |json|
json.id id
json.object 'donation'
json.type 'trx_assignment'
end
end
def to_builder(*expand) def to_builder(*expand)
init_builder(*expand) do |json| init_builder(*expand) do |json|
json.(self, :designation) json.(self, :designation)
json.object 'donation' json.object 'donation'
json.type 'trx_assignment'
json.dedication do json.dedication do
json.type dedication['type'] json.type dedication['type']
@ -32,14 +41,19 @@ class ModernDonation < ApplicationRecord
json.cents amount json.cents amount
json.currency nonprofit.currency json.currency nonprofit.currency
end end
json.add_builder_expansion :nonprofit, :supporter
json.add_builder_expansion :trx, json_attribute: :transaction
end end
end end
def publish_created def publish_created
Houdini.event_publisher.announce(:donation_created, to_event('donation.created', :nonprofit, :supporter, :trx).attributes!) Houdini.event_publisher.announce(:donation_created, to_event('donation.created', :nonprofit, :supporter, :trx).attributes!)
Houdini.event_publisher.announce(:trx_assignment_created, to_event('trx_assignment.created', :nonprofit, :supporter, :trx).attributes!)
end end
def publish_updated def publish_updated
Houdini.event_publisher.announce(:donation_updated, to_event('donation.updated', :nonprofit, :supporter, :trx).attributes!) Houdini.event_publisher.announce(:donation_updated, to_event('donation.updated', :nonprofit, :supporter, :trx).attributes!)
Houdini.event_publisher.announce(:trx_assignment_updated, to_event('trx_assignment.updated', :nonprofit, :supporter, :trx).attributes!)
end end
end end

View file

@ -7,7 +7,7 @@ class Nonprofit < ApplicationRecord
Categories = ['Public Benefit', 'Human Services', 'Education', 'Civic Duty', 'Human Rights', 'Animals', 'Environment', 'Health', 'Arts, Culture, Humanities', 'International', 'Children', 'Religion', 'LGBTQ', "Women's Rights", 'Disaster Relief', 'Veterans'].freeze Categories = ['Public Benefit', 'Human Services', 'Education', 'Civic Duty', 'Human Rights', 'Animals', 'Environment', 'Health', 'Arts, Culture, Humanities', 'International', 'Children', 'Religion', 'LGBTQ', "Women's Rights", 'Disaster Relief', 'Veterans'].freeze
include Image::AttachmentExtensions include Image::AttachmentExtensions
include Model::Jbuilder
# :name, # str # :name, # str
# :stripe_account_id, # str # :stripe_account_id, # str
# :summary, # text: paragraph-sized organization summary # :summary, # text: paragraph-sized organization summary
@ -260,9 +260,13 @@ class Nonprofit < ApplicationRecord
Houdini.intl.all_currencies[currency.downcase.to_sym][:symbol] Houdini.intl.all_currencies[currency.downcase.to_sym][:symbol]
end end
def to_builder(*expand) concerning :JBuilder do
init_builder(*expand) do |json| include Model::Jbuilder
json.(self, :name)
def to_builder(*expand)
init_builder(*expand) do |json|
json.(self, :id, :name)
end
end end
end end
@ -302,5 +306,8 @@ private
def user_is_valid def user_is_valid
(user && user.is_a?(User)) || errors.add(:user_id, "is not a valid user") (user && user.is_a?(User)) || errors.add(:user_id, "is not a valid user")
end end
end end

View file

@ -0,0 +1,68 @@
# 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
# rubocop:disable Metrics/BlockLength, Metrics/AbcSize, Metrics/MethodLength
class OfflineTransaction < ApplicationRecord
include Model::Subtransactable
delegate :created, to: :subtransaction
def net_amount
subtransaction_payments.sum(&:net_amount)
end
concerning :JBuilder do
included do
setup_houid :offlinetrx
end
def to_builder(*expand)
init_builder(*expand) do |json|
json.type 'subtransaction'
json.created created.to_i
json.initial_amount do
json.cents amount || 0
json.currency nonprofit.currency
end
json.net_amount do
json.cents net_amount
json.currency nonprofit.currency
end
if expand.include? :payments
json.payments subtransaction_payments do |py|
json.merge! py.to_builder.attributes!
end
else
json.payments subtransaction_payments do |py|
json.merge! py.to_id.attributes!
end
end
json.add_builder_expansion :nonprofit, :supporter
json.add_builder_expansion(
:trx,
json_attribute: :transaction
)
end
end
def to_id
::Jbuilder.new do |json|
json.(self, :id)
json.object 'offline_transaction'
json.type 'subtransaction'
end
end
def publish_created
Houdini.event_publisher.announce(
:offline_transaction_created,
to_event('offline_transaction.created', :nonprofit, :trx, :supporter,
:payments).attributes!
)
end
end
end
# rubocop:enable all

View file

@ -0,0 +1,78 @@
# 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
# rubocop:disable Metrics/MethodLength, Metrics/BlockLength, Metrics/AbcSize
class OfflineTransactionCharge < ApplicationRecord
include Model::SubtransactionPaymentable
belongs_to :payment
delegate :gross_amount, :net_amount, :fee_total, to: :payment
delegate :currency, to: :nonprofit
concerning :JBuilder do
included do
setup_houid :offtrxchrg
end
def to_builder(*expand)
init_builder(*expand) do |json|
json.object 'offline_transaction_charge'
json.gross_amount do
json.cents gross_amount
json.currency currency
end
json.net_amount do
json.cents net_amount
json.currency currency
end
json.fee_total do
json.cents fee_total
json.currency currency
end
json.created payment.date.to_i
json.type 'payment'
json.add_builder_expansion :nonprofit, :supporter, :subtransaction
json.add_builder_expansion :trx, json_attribute: :transaction
end
end
def to_id
::Jbuilder.new do |json|
json.(self, :id)
json.object 'offline_transaction_charge'
json.type 'payment'
end
end
def publish_created
Houdini.event_publisher.announce(
:offline_transaction_charge_created,
to_event('offline_transaction_charge.created',
:nonprofit,
:trx,
:supporter,
:subtransaction).attributes!
)
Houdini.event_publisher.announce(
:payment_created,
to_event(
'payment.created',
:nonprofit,
:trx,
:supporter,
:subtransaction
).attributes!
)
end
end
end
# rubocop:enable all

View file

@ -7,12 +7,6 @@
# If connected to an offsite_payment, this is money the nonprofit is recording for convenience. # If connected to an offsite_payment, this is money the nonprofit is recording for convenience.
class Payment < ApplicationRecord class Payment < ApplicationRecord
# :towards,
# :gross_amount,
# :refund_total,
# :fee_total,
# :kind,
# :date
belongs_to :supporter belongs_to :supporter
belongs_to :nonprofit belongs_to :nonprofit
@ -26,4 +20,10 @@ class Payment < ApplicationRecord
has_many :events, through: :tickets has_many :events, through: :tickets
has_many :payment_payouts has_many :payment_payouts
has_many :charges has_many :charges
has_one :subtransaction_payment
has_one :subtransaction, through: :subtransaction_payment
has_one :trx, through: :subtransaction_payment
end end

View file

@ -0,0 +1,26 @@
# 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 Subtransaction < ApplicationRecord
include Model::CreatedTimeable
concerning :JBuilder do
include Model::Houidable
included do
setup_houid :subtrx
end
end
belongs_to :trx, class_name: 'Transaction', foreign_key: 'transaction_id', inverse_of: :subtransactions
has_one :supporter, through: :trx
has_one :nonprofit, through: :trx
has_many :subtransaction_payments # rubocop:disable Rails/HasManyOrHasOneDependent
delegated_type :subtransactable, types: %w[OfflineTransaction]
scope :with_subtransactables, -> { includes(:subtransactable) }
delegate :to_builder, :to_event, :to_id, :publish_created, :publish_updated, :publish_deleted, to: :subtransactable
end

View file

@ -0,0 +1,28 @@
# 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 SubtransactionPayment < ApplicationRecord
include Model::Houidable
include Model::CreatedTimeable
setup_houid :subtrxentity
belongs_to :subtransaction
has_one :trx, class_name: 'Transaction', foreign_key: 'transaction_id', through: :subtransaction
has_one :supporter, through: :subtransaction
has_one :nonprofit, through: :subtransaction
delegated_type :paymentable, types: ['OfflineTransactionCharge']
delegate :gross_amount, :fee_total, :net_amount, to: :paymentable
scope :with_entities, -> { includes(:paymentable) }
delegate :to_builder,
:to_event,
:to_id,
:publish_created,
:publish_updated,
:publish_deleted, to: :paymentable
end

View file

@ -54,7 +54,7 @@ class Supporter < ApplicationRecord
validates :nonprofit, presence: true validates :nonprofit, presence: true
scope :not_deleted, -> { where(deleted: false) } scope :not_deleted, -> { where(deleted: false) }
add_builder_expansion :nonprofit
# TODO replace with Discard gem # TODO replace with Discard gem
define_model_callbacks :discard define_model_callbacks :discard
@ -86,47 +86,56 @@ class Supporter < ApplicationRecord
h h
end end
def to_builder(*expand) concerning :Jbuilder do
supporter_addresses = [self] included do
init_builder(*expand) do |json| def to_builder(*expand)
json.(self, :name, :organization, :phone, :anonymous, :deleted) supporter_addresses = [self]
if expand.include? :supporter_address init_builder(*expand) do |json|
json.supporter_addresses supporter_addresses do |i| json.(self, :name, :organization, :phone, :anonymous, :deleted)
json.merge! i.to_supporter_address_builder.attributes! json.add_builder_expansion :nonprofit, :merged_into
if expand.include? :supporter_address
json.supporter_addresses supporter_addresses do |i|
json.merge! i.to_supporter_address_builder.attributes!
end
else
json.supporter_addresses [id]
end
# unless merged_into.nil?
# if expand.include? :merged_into
# json.merged_into merged_into.to_builder
# else
# json.merged_into merged_into.id
# end
# else
# json.merged_into nil
# end
end end
else
json.supporter_addresses [id]
end end
unless merged_into.nil? def to_supporter_address_builder(*expand)
if expand.include? :merged_into init_builder(*expand) do |json|
json.merged_into merged_into.to_builder json.(self, :address, :state_code, :city, :country, :zip_code, :deleted)
else json.object 'supporter_address'
json.merged_into merged_into.id if expand.include? :supporter
json.supporter to_builder
else
json.supporter id
end
# if expand.include? :nonprofit
# json.nonprofit nonprofit.to_builder
# else
# json.nonprofit nonprofit.id
# end
json.add_builder_expansion :nonprofit
end end
else
json.merged_into nil
end end
end end
end end
def to_supporter_address_builder(*expand)
init_builder(*expand) do |json|
json.(self, :address, :state_code, :city, :country, :zip_code, :deleted)
json.object 'supporter_address'
if expand.include? :supporter
json.supporter to_builder
else
json.supporter id
end
if expand.include? :nonprofit
json.nonprofit nonprofit.to_builder
else
json.nonprofit nonprofit.id
end
end
end
def full_address def full_address
Format::Address.full_address(address, city, state_code) Format::Address.full_address(address, city, state_code)
@ -167,7 +176,7 @@ class Supporter < ApplicationRecord
# we do something custom here since Supporter and SupporterAddress are in the same model # we do something custom here since Supporter and SupporterAddress are in the same model
def to_event(event_type, *expand) def to_event(event_type, *expand)
Jbuilder.new do |event| ::Jbuilder.new do |event|
event.id "objevt_" + SecureRandom.alphanumeric(22) event.id "objevt_" + SecureRandom.alphanumeric(22)
event.object 'object_event' event.object 'object_event'
event.type event_type event.type event_type

View file

@ -18,8 +18,6 @@ class SupporterNote < ApplicationRecord
validates :supporter_id, presence: true validates :supporter_id, presence: true
# TODO replace with Discard gem # TODO replace with Discard gem
add_builder_expansion :supporter, :nonprofit, :user
define_model_callbacks :discard define_model_callbacks :discard
after_discard :publish_deleted after_discard :publish_deleted
@ -38,6 +36,8 @@ class SupporterNote < ApplicationRecord
def to_builder(*expand) def to_builder(*expand)
init_builder(*expand) do |json| init_builder(*expand) do |json|
json.(self, :deleted, :content) json.(self, :deleted, :content)
json.add_builder_expansion :supporter, :nonprofit, :user
end end
end end

View file

@ -6,7 +6,6 @@ class TagMaster < ApplicationRecord
include Model::Eventable include Model::Eventable
include Model::Jbuilder include Model::Jbuilder
add_builder_expansion :nonprofit
# TODO replace with Discard gem # TODO replace with Discard gem
define_model_callbacks :discard define_model_callbacks :discard
@ -46,6 +45,8 @@ class TagMaster < ApplicationRecord
init_builder(*expand) do |json| init_builder(*expand) do |json|
json.(self, :name, :deleted) json.(self, :name, :deleted)
json.object 'tag_definition' json.object 'tag_definition'
json.add_builder_expansion :nonprofit
end end
end end

View file

@ -16,8 +16,6 @@ class TicketLevel < ApplicationRecord
# :limit, #int: for limiting the number of tickets to be sold # :limit, #int: for limiting the number of tickets to be sold
# :order #int: order in which to be displayed # :order #int: order in which to be displayed
add_builder_expansion :nonprofit, :event
# TODO replace with Discard gem # TODO replace with Discard gem
define_model_callbacks :discard define_model_callbacks :discard
@ -67,13 +65,16 @@ class TicketLevel < ApplicationRecord
end end
json.available_to admin_only ? 'admins' : 'everyone' json.available_to admin_only ? 'admins' : 'everyone'
if expand.include? :event_discounts json.add_builder_expansion :nonprofit, :event
json.event_discounts event_discounts do |disc|
json.merge! disc.to_builder.attributes! json.add_builder_expansion :event_discounts, enum_type: :expandable
end # if expand.include? :event_discounts
else # json.event_discounts event_discounts do |disc|
json.event_discounts event_discounts.pluck(:id) # json.merge! disc.to_builder.attributes!
end # end
# else
# json.event_discounts event_discounts.pluck(:id)
# end
end end
end end

View file

@ -6,9 +6,6 @@ class TicketPurchase < ApplicationRecord
include Model::TrxAssignable include Model::TrxAssignable
setup_houid :tktpur setup_houid :tktpur
add_builder_expansion :event, :event_discount
before_create :set_original_discount before_create :set_original_discount
belongs_to :event_discount belongs_to :event_discount
@ -18,8 +15,18 @@ class TicketPurchase < ApplicationRecord
validates :event, presence: true validates :event, presence: true
def to_id
::Jbuilder.new do |json|
json.id id
json.object 'ticket_purchase'
json.type 'trx_assignment'
end
end
def to_builder(*expand) def to_builder(*expand)
init_builder(*expand) do |json| init_builder(*expand) do |json|
json.type 'trx_assignment'
json.original_discount do json.original_discount do
json.percent original_discount json.percent original_discount
end if original_discount end if original_discount
@ -29,19 +36,22 @@ class TicketPurchase < ApplicationRecord
json.currency nonprofit.currency json.currency nonprofit.currency
end end
json.add_builder_expansion :event, :event_discount, :nonprofit, :supporter
json.add_builder_expansion :ticket_to_legacy_tickets, enum_type: :expandable, json_attribute: 'tickets'
json.add_builder_expansion :trx, json_attribute: :transaction
if expand.include? :tickets # if expand.include? :tickets
json.tickets ticket_to_legacy_tickets do |i| # json.tickets ticket_to_legacy_tickets do |i|
i.to_builder.attributes! # i.to_builder.attributes!
end # end
else # else
json.tickets ticket_to_legacy_tickets.pluck(:id) # json.tickets ticket_to_legacy_tickets.pluck(:id)
end # end
end end
end end
def publish_created def publish_created
Houdini.event_publisher.announce(:ticket_purchase_created, to_event('ticket_purchase.created', :event, :nonprofit, :supporter, :trx, :event_discount).attributes!) Houdini.event_publisher.announce(:ticket_purchase_created, to_event('ticket_purchase.created', :event, :nonprofit, :supporter, :trx, :event_discount, :ticket_to_legacy_tickets).attributes!)
end end
private private

View file

@ -21,8 +21,6 @@ class TicketToLegacyTicket < ApplicationRecord
setup_houid :tkt setup_houid :tkt
add_builder_expansion :ticket_purchase, :ticket_level, :supporter, :event, :nonprofit, :event_discount
def to_builder(*expand) def to_builder(*expand)
init_builder(*expand) do |json| init_builder(*expand) do |json|
json.(self, :checked_in, :deleted, :note) json.(self, :checked_in, :deleted, :note)
@ -38,6 +36,8 @@ class TicketToLegacyTicket < ApplicationRecord
json.percent original_discount json.percent original_discount
end end
end end
json.add_builder_expansion :ticket_purchase, :ticket_level, :supporter, :event, :nonprofit, :event_discount
end end
end end

View file

@ -3,36 +3,81 @@
# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later # 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 # Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE
class Transaction < ApplicationRecord class Transaction < ApplicationRecord
include Model::Houidable include Model::CreatedTimeable
include Model::Jbuilder
include Model::Eventable
setup_houid :trx
add_builder_expansion :nonprofit, :supporter
belongs_to :supporter belongs_to :supporter
has_one :nonprofit, through: :supporter has_one :nonprofit, through: :supporter
has_many :transaction_assignments has_many :transaction_assignments, inverse_of: 'trx'
has_many :donations, through: :transaction_assignments, source: :assignable, source_type: 'ModernDonation' has_many :donations, through: :transaction_assignments, source: :assignable, source_type: 'ModernDonation', inverse_of: 'trx'
has_many :ticket_purchases, through: :transaction_assignments, source: :assignable, source_type: 'TicketPurchase' has_many :ticket_purchases, through: :transaction_assignments, source: :assignable, source_type: 'TicketPurchase', inverse_of: 'trx'
has_many :campaign_gift_purchases, through: :transaction_assignments, source: :assignable, source_type: 'CampaignGiftPurchase' has_many :campaign_gift_purchases, through: :transaction_assignments, source: :assignable, source_type: 'CampaignGiftPurchase', inverse_of: 'trx'
has_one :subtransaction
has_many :subtransaction_payments, through: :subtransaction
validates :supporter, presence: true validates :supporter, presence: true
def to_builder(*expand) concerning :JBuilder do
init_builder(*expand) do |json| include Model::Houidable
json.amount do include Model::Jbuilder
json.cents amount || 0 include Model::Eventable
json.currency nonprofit.currency
end included do
setup_houid :trx
end
def to_builder(*expand)
init_builder(*expand) do |json|
json.amount do
json.cents amount || 0
json.currency nonprofit.currency
end
json.created created.to_i
json.add_builder_expansion :nonprofit, :supporter, :subtransaction
json.add_builder_expansion :subtransaction_payments, enum_type: :expandable
json.add_builder_expansion :transaction_assignments, enum_type: :expandable
end
end end
end end
def publish_created concerning :ObjectEvents do
Houdini.event_publisher.announce(:transaction_created,
to_event('transaction.created', :nonprofit, :supporter).attributes!) include JBuilder
def publish_created
Houdini.event_publisher.announce(:transaction_created,
to_event('transaction.created', :nonprofit, :supporter, :subtransaction_payments, :transaction_assignments, :subtransaction).attributes!)
end
def publish_updated
Houdini.event_publisher.announce(:transaction_updated,
to_event('transaction.updated', :nonprofit, :supporter, :subtransaction_payments, :transaction_assignments, :subtransaction).attributes!)
end
def publish_refunded
Houdini.event_publisher.announce(:transaction_refunded,
to_event('transaction.refunded', :nonprofit, :supporter, :subtransaction_payments, :transaction_assignments, :subtransaction).attributes!)
end
def publish_disputed
Houdini.event_publisher.announce(:transaction_disputed,
to_event('transaction.refunded', :nonprofit, :supporter, :subtransaction_payments, :transaction_assignments).attributes!)
end
def publish_deleted
Houdini.event_publisher.announce(:transaction_deleted,
to_event('transaction.deleted', :nonprofit, :supporter, :subtransaction_payments, :transaction_assignments).attributes!)
end
end
private
def set_created_if_needed
write_attribute(:created, Time.now) unless read_attribute(:created)
end end
end end
ActiveSupport.run_load_hooks(:houdini_transaction, Transaction)

View file

@ -6,7 +6,16 @@ class TransactionAssignment < ApplicationRecord
include Model::Houidable include Model::Houidable
setup_houid :trxassign setup_houid :trxassign
belongs_to :assignable, polymorphic: true delegated_type :assignable, types: ['ModernDonation', 'CampaignGiftPurchase', 'TicketPurchase']
delegate :to_id,
:to_builder,
:publish_created,
:publish_updated,
:publish_deleted, to: :assignable
belongs_to :trx, class_name: 'Transaction', foreign_key: "transaction_id" belongs_to :trx, class_name: 'Transaction', foreign_key: "transaction_id"
has_one :supporter, through: :trx
has_one :nonprofit, through: :trx
end end

View file

@ -0,0 +1,31 @@
# 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 AddSubtransactionHierarchy < ActiveRecord::Migration[6.1]
def change
create_table :subtransactions, id: :string do |t|
t.references :transaction, foreign_key: true, type: :string, null: false
t.references :subtransactable, polymorphic: true, type: :string, null: false, index: {unique: true}
t.datetime "created", comment: 'the moment that the subtransaction was created. Could be earlier than created_at if the transaction was in the past.'
t.timestamps
end
create_table "offline_transactions", id: :string do |t|
t.integer "amount", null: false
t.timestamps
end
create_table :subtransaction_payments, id: :string do |t|
t.references :subtransaction, type: :string, foreign_key: true
t.references :paymentable, polymorphic: true, type: :string
t.datetime "created", comment: 'the moment that the subtransaction_payment was created. Could be earlier than created_at if the transaction was in the past.'
t.timestamps
end
create_table :offline_transaction_charges, id: :string do |t|
t.references :payment, foreign_key: true, null: false
t.timestamps
end
end
end

View file

@ -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_02_23_204824) do ActiveRecord::Schema.define(version: 2021_03_29_213633) 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"
@ -599,6 +599,19 @@ ActiveRecord::Schema.define(version: 2021_02_23_204824) do
t.index ["nonprofit_id"], name: "index_object_event_hook_configs_on_nonprofit_id" t.index ["nonprofit_id"], name: "index_object_event_hook_configs_on_nonprofit_id"
end end
create_table "offline_transaction_charges", id: :string, force: :cascade do |t|
t.bigint "payment_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["payment_id"], name: "index_offline_transaction_charges_on_payment_id"
end
create_table "offline_transactions", id: :string, force: :cascade do |t|
t.integer "amount", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "offsite_payments", id: :serial, force: :cascade do |t| create_table "offsite_payments", id: :serial, force: :cascade do |t|
t.integer "gross_amount" t.integer "gross_amount"
t.string "kind", limit: 255 t.string "kind", limit: 255
@ -769,6 +782,28 @@ ActiveRecord::Schema.define(version: 2021_02_23_204824) do
t.index ["tokenizable_id", "tokenizable_type"], name: "index_source_tokens_on_tokenizable_id_and_tokenizable_type" t.index ["tokenizable_id", "tokenizable_type"], name: "index_source_tokens_on_tokenizable_id_and_tokenizable_type"
end end
create_table "subtransaction_payments", id: :string, force: :cascade do |t|
t.string "subtransaction_id"
t.string "paymentable_type"
t.string "paymentable_id"
t.datetime "created", comment: "the moment that the subtransaction_payment was created. Could be earlier than created_at if the transaction was in the past."
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["paymentable_type", "paymentable_id"], name: "index_subtransaction_payments_on_paymentable"
t.index ["subtransaction_id"], name: "index_subtransaction_payments_on_subtransaction_id"
end
create_table "subtransactions", id: :string, force: :cascade do |t|
t.string "transaction_id", null: false
t.string "subtransactable_type", null: false
t.string "subtransactable_id", null: false
t.datetime "created", comment: "the moment that the subtransaction was created. Could be earlier than created_at if the transaction was in the past."
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["subtransactable_type", "subtransactable_id"], name: "index_subtransactions_on_subtransactable", unique: true
t.index ["transaction_id"], name: "index_subtransactions_on_transaction_id"
end
create_table "supporter_notes", id: :serial, force: :cascade do |t| create_table "supporter_notes", id: :serial, force: :cascade do |t|
t.text "content" t.text "content"
t.integer "supporter_id" t.integer "supporter_id"
@ -925,6 +960,7 @@ ActiveRecord::Schema.define(version: 2021_02_23_204824) do
t.integer "amount" t.integer "amount"
t.datetime "created_at", precision: 6, null: false t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false
t.datetime "created", comment: "the moment that the offline_transaction was created. Could be earlier than created_at if the transaction was in the past."
t.index ["supporter_id"], name: "index_transactions_on_supporter_id" t.index ["supporter_id"], name: "index_transactions_on_supporter_id"
end end
@ -971,6 +1007,9 @@ ActiveRecord::Schema.define(version: 2021_02_23_204824) do
add_foreign_key "modern_campaign_gifts", "campaign_gift_purchases" add_foreign_key "modern_campaign_gifts", "campaign_gift_purchases"
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 "subtransaction_payments", "subtransactions"
add_foreign_key "subtransactions", "transactions"
add_foreign_key "ticket_purchases", "event_discounts" add_foreign_key "ticket_purchases", "event_discounts"
add_foreign_key "ticket_purchases", "events" add_foreign_key "ticket_purchases", "events"
add_foreign_key "ticket_to_legacy_tickets", "ticket_purchases" add_foreign_key "ticket_to_legacy_tickets", "ticket_purchases"

View file

@ -1,9 +1,8 @@
// License: LGPL-3.0-or-later // License: LGPL-3.0-or-later
import type { HouID, HoudiniObject, HoudiniEvent, Amount, IDType} from '../../common'; import type { HouID, HoudiniObject, HoudiniEvent, Amount, IDType} from '../../common';
import type Nonprofit from '..';
import type Campaign from '.'; import type Campaign from '.';
import type { CampaignGiftPurchase, CampaignGiftOption } from '.'; import type { CampaignGiftPurchase, CampaignGiftOption } from '.';
import type Supporter from '../Supporter'; import { TrxDescendent } from '../Transaction';
export interface TransactionAddress { export interface TransactionAddress {
address: string; address: string;
@ -13,7 +12,7 @@ export interface TransactionAddress {
zip_code: string; zip_code: string;
} }
export interface CampaignGift extends HoudiniObject<HouID> { export interface CampaignGift extends HoudiniObject<HouID>, TrxDescendent {
address?: TransactionAddress; address?: TransactionAddress;
amount: Amount; amount: Amount;
campaign: IDType | Campaign; campaign: IDType | Campaign;
@ -21,9 +20,7 @@ export interface CampaignGift extends HoudiniObject<HouID> {
campaign_gift_purchase: IDType | CampaignGiftPurchase; campaign_gift_purchase: IDType | CampaignGiftPurchase;
deleted: boolean; deleted: boolean;
event: IDType | Event; event: IDType | Event;
nonprofit: IDType | Nonprofit;
object: 'campaign_gift'; object: 'campaign_gift';
supporter: IDType | Supporter;
} }
export type CampaignGiftCreated = HoudiniEvent<'campaign_gift.created', CampaignGift>; export type CampaignGiftCreated = HoudiniEvent<'campaign_gift.created', CampaignGift>;

View file

@ -1,20 +1,16 @@
// License: LGPL-3.0-or-later // License: LGPL-3.0-or-later
import type { HouID, HoudiniObject, HoudiniEvent, Amount, IDType} from '../../common'; import type { HouID, HoudiniEvent, Amount, IDType} from '../../common';
import type Nonprofit from '..'; import type Nonprofit from '..';
import type Campaign from '.'; import type Campaign from '.';
import type { CampaignGift } from '.'; import type { CampaignGift } from '.';
import Supporter from '../Supporter'; import type { TrxAssignment } from '../Transaction';
import { Transaction } from '../Supporter';
export interface CampaignGiftPurchase extends TrxAssignment {
export interface CampaignGiftPurchase extends HoudiniObject<HouID> {
amount: Amount; amount: Amount;
campaign: IDType | Campaign; campaign: IDType | Campaign;
campaign_gifts: HouID[] | CampaignGift[]; campaign_gifts: HouID[] | CampaignGift[];
nonprofit: IDType | Nonprofit; nonprofit: IDType | Nonprofit;
object: 'campaign_gift_purchase'; object: 'campaign_gift_purchase';
supporter: IDType | Supporter;
transaction: HouID | Transaction;
} }

View file

@ -1,6 +1,6 @@
// License: LGPL-3.0-or-later // License: LGPL-3.0-or-later
import { IDType, HoudiniObject } from '../../common'; import { IDType, HoudiniObject } from '../../common';
import Nonprofit from '../'; import Nonprofit from '..';
export default interface Campaign extends HoudiniObject { export default interface Campaign extends HoudiniObject {
name: string; name: string;

View file

@ -1,19 +1,16 @@
// License: LGPL-3.0-or-later // License: LGPL-3.0-or-later
import type { HouID, HoudiniObject, HoudiniEvent, Amount, IDType} from '../../common'; import type { HouID, HoudiniObject, HoudiniEvent, Amount, IDType} from '../../common';
import type Nonprofit from '..';
import type Event from '.'; import type Event from '.';
import type { TicketLevel , TicketPurchase} from '.'; import type { TicketLevel , TicketPurchase} from '.';
import type Supporter from '../Supporter'; import { TrxDescendent } from '../Transaction';
export interface Ticket extends HoudiniObject<HouID> { export interface Ticket extends HoudiniObject<HouID>, TrxDescendent {
amount: Amount; amount: Amount;
checked_in: boolean; checked_in: boolean;
deleted: boolean; deleted: boolean;
event: IDType | Event; event: IDType | Event;
nonprofit: IDType | Nonprofit;
note: string; note: string;
object: 'ticket'; object: 'ticket';
supporter: IDType | Supporter;
ticket_level: IDType | TicketLevel; ticket_level: IDType | TicketLevel;
ticket_purchase: HouID | TicketPurchase; ticket_purchase: HouID | TicketPurchase;
} }

View file

@ -1,21 +1,14 @@
// License: LGPL-3.0-or-later // License: LGPL-3.0-or-later
import type { HouID, HoudiniObject, HoudiniEvent, Amount, IDType} from '../../common'; import type { HouID, HoudiniEvent, IDType} from '../../common';
import type Nonprofit from '..';
import type Event from '.'; import type Event from '.';
import type { EventDiscount, Ticket } from '.'; import type { EventDiscount, Ticket } from '.';
import Supporter from '../Supporter'; import type { TrxAssignment } from '../Transaction';
import { Transaction } from '../Supporter';
export interface TicketPurchase extends TrxAssignment {
export interface TicketPurchase extends HoudiniObject<HouID> {
amount: Amount;
event: IDType | Event; event: IDType | Event;
event_discount?: IDType | EventDiscount | null; event_discount?: IDType | EventDiscount | null;
nonprofit: IDType | Nonprofit;
object: 'ticket_purchase'; object: 'ticket_purchase';
supporter: IDType | Supporter;
tickets: Ticket[] | HouID[]; tickets: Ticket[] | HouID[];
transaction: HouID | Transaction;
} }

View file

@ -1,6 +1,6 @@
// License: LGPL-3.0-or-later // License: LGPL-3.0-or-later
import { IDType, HoudiniObject } from '../../common'; import { IDType, HoudiniObject } from '../../common';
import Nonprofit from '../'; import Nonprofit from '..';
export default interface Event extends HoudiniObject { export default interface Event extends HoudiniObject {
end_date: Date; end_date: Date;

View file

@ -1,7 +1,7 @@
// License: LGPL-3.0-or-later // License: LGPL-3.0-or-later
import type { IDType, HoudiniObject, HoudiniEvent } from '../../common'; import type { IDType, HoudiniObject, HoudiniEvent } from '../../common';
import type Nonprofit from '..'; import type Nonprofit from '..';
import Supporter from '.'; import Supporter from '..';
import type { User } from '../../User'; import type { User } from '../../User';
export interface SupporterNote extends HoudiniObject { export interface SupporterNote extends HoudiniObject {

View file

@ -1,18 +0,0 @@
// License: LGPL-3.0-or-later
import type { Amount, HoudiniObject, IDType, HouID } from "../../common";
import type Nonprofit from '..';
import type Supporter from ".";
/**
* Represents a transaction made by a supporter
*/
export interface Transaction extends HoudiniObject<HouID> {
amount: Amount;
nonprofit: IDType | Nonprofit;
object: 'transaction';
payment_methods: Array<unknown>;
payments: Array<unknown>;
status: string;
supporter: IDType | Supporter;
transaction_assignments: Array<unknown>;
}

View file

@ -4,6 +4,7 @@ import type Nonprofit from '../';
import type { SupporterAddress } from './SupporterAddress'; import type { SupporterAddress } from './SupporterAddress';
export default interface Supporter extends HoudiniObject { export default interface Supporter extends HoudiniObject {
addresses: IDType[] | SupporterAddress[];
anonymous: boolean; anonymous: boolean;
deleted: boolean; deleted: boolean;
email: string; email: string;
@ -13,7 +14,6 @@ export default interface Supporter extends HoudiniObject {
object: "supporter"; object: "supporter";
organization: string; organization: string;
phone: string; phone: string;
supporter_addresses: IDType[] | SupporterAddress[];
} }
export type SupporterCreated = HoudiniEvent<'supporter_address.created', Supporter>; export type SupporterCreated = HoudiniEvent<'supporter_address.created', Supporter>;
@ -22,4 +22,3 @@ export type SupporterDeleted = HoudiniEvent<'supporter_address.deleted', Support
export * from './SupporterNote'; export * from './SupporterNote';
export * from './SupporterAddress'; export * from './SupporterAddress';
export * from './Transaction';

View file

@ -1,8 +1,6 @@
// License: LGPL-3.0-or-later // License: LGPL-3.0-or-later
import type { Amount, HoudiniObject, IDType, HouID, HoudiniEvent } from "../../common"; import type { HoudiniEvent } from "../../common";
import type Nonprofit from '../'; import type { TrxAssignment } from './';
import type Supporter from "../Supporter";
import type Transaction from './';
interface Dedication { interface Dedication {
contact?: { contact?: {
@ -15,14 +13,10 @@ interface Dedication {
type: 'honor' | 'memory'; type: 'honor' | 'memory';
} }
export interface Donation extends HoudiniObject<HouID> { export interface Donation extends TrxAssignment {
amount: Amount;
dedication?: Dedication | null; dedication?: Dedication | null;
designation?: string | null; designation?: string | null;
nonprofit: IDType | Nonprofit;
object: 'donation'; object: 'donation';
supporter: IDType | Supporter;
transaction: HouID | Transaction;
} }
export type DonationCreated = HoudiniEvent<'donation.created', Donation>; export type DonationCreated = HoudiniEvent<'donation.created', Donation>;

View file

@ -0,0 +1,11 @@
// License: LGPL-3.0-or-later
import type { HoudiniEvent } from "../../../common";
import type { CommonOfflineTransactionPayment } from '.';
export interface Charge extends CommonOfflineTransactionPayment {
object: 'offline_transaction_charge';
}
export type ChargeCreated = HoudiniEvent<'offline_transaction_charge.created', Charge>;
export type ChargeUpdated = HoudiniEvent<'offline_transaction_charge.updated', Charge>;
export type ChargeDeleted = HoudiniEvent<'offline_transaction_charge.deleted', Charge>;

View file

@ -0,0 +1,11 @@
// License: LGPL-3.0-or-later
import type { HoudiniEvent } from "../../../common";
import type { CommonOfflineTransactionPayment } from '.';
export interface Dispute extends CommonOfflineTransactionPayment {
object: 'offline_transaction_dispute';
}
export type DisputeCreated = HoudiniEvent<'offline_transaction_dispute.created', Dispute>;
export type DisputeUpdated = HoudiniEvent<'offline_transaction_dispute.updated', Dispute>;
export type DisputeDeleted = HoudiniEvent<'offline_transaction_dispute.deleted', Dispute>;

View file

@ -0,0 +1,11 @@
// License: LGPL-3.0-or-later
import type { HoudiniEvent } from "../../../common";
import type { CommonOfflineTransactionPayment } from '.';
export interface Refund extends CommonOfflineTransactionPayment {
object: 'offline_transaction_refund';
}
export type RefundCreated = HoudiniEvent<'offline_transaction_refund.created', Refund>;
export type RefundUpdated = HoudiniEvent<'offline_transaction_refund.updated', Refund>;
export type RefundDeleted = HoudiniEvent<'offline_transaction_refund.deleted', Refund>;

View file

@ -0,0 +1,32 @@
// License: LGPL-3.0-or-later
import type { HouID, HoudiniEvent } from "../../../common";
import type { Payment, Subtransaction} from "..";
import type { Charge, Refund, Dispute } from '.';
export interface CommonOfflineTransactionPayment extends Payment {
// The kind of offline charge. Could be cash, check or something else
// NOT implemented yet
kind: string|null;
// NOT implemented yet
// the ID related to the kind. As example, you could put a check number here.
kind_id: string|null;
subtransaction: HouID | OfflineTransaction;
}
export default interface OfflineTransaction extends Subtransaction {
charges: HouID[] | Charge[];
deleted: boolean;
disputes: HouID[] | Dispute[];
object: 'offline_transaction';
refunds: HouID[] | Refund[];
}
export type OfflineTransactionCreated = HoudiniEvent<'offline_transaction.created', OfflineTransaction>;
export type OfflineTransactionUpdated = HoudiniEvent<'offline_transaction.updated', OfflineTransaction>;
export type OfflineTransactionRefunded = HoudiniEvent<'offline_transaction.refunded', OfflineTransaction>;
export type OfflineTransactionDisputed = HoudiniEvent<'offline_transaction.disputed', OfflineTransaction>;
export type OfflineTransactionDeleted = HoudiniEvent<'offline_transaction.deleted', OfflineTransaction>;
export * from './Charge';
export * from './Dispute';
export * from './Refund';

View file

@ -1,16 +1,19 @@
// License: LGPL-3.0-or-later // License: LGPL-3.0-or-later
import type { Amount, HoudiniObject, IDType, HouID } from "../../common"; import type { Amount, HoudiniObject, HouID, HoudiniEvent } from "../../common";
import type Nonprofit from '../'; import type { Subtransaction, TrxDescendent } from ".";
import type Supporter from "../Supporter";
import type Transaction from './';
export interface Payment extends HoudiniObject<HouID> {
amount: Amount;
nonprofit: IDType | Nonprofit;
object: 'payment';
supporter: IDType | Supporter;
transaction: HouID | Transaction;
export interface Payment extends HoudiniObject<HouID>, TrxDescendent {
created: number;
deleted: boolean;
fees: Amount;
gross_amount: Amount;
net_amount: Amount;
status: string;
subtransaction: HouID | Subtransaction;
type: 'payment';
} }
export type PaymentCreated = HoudiniEvent<'payment.created', Payment>;
export type PaymentUpdated = HoudiniEvent<'payment.updated', Payment>;
export type PaymentDeleted = HoudiniEvent<'payment.deleted', Payment>;

View file

@ -0,0 +1,438 @@
// License: LGPL-3.0-or-later
// Supporter wants to make a single donation to Nonprofit with ID 1
/* IN PROGRESS - some examples of transaction requests */
/* we disable since this is just an example file */
/* eslint-disable @typescript-eslint/no-unused-vars,@typescript-eslint/no-explicit-any*/
import Transaction, { Donation, OfflineTransaction } from ".";
import {Charge, Charge as OfflineTransactionCharge} from './OfflineTransaction';
//`POST api/nonprofit/1/transaction`
const donation_request = {
// donation is a `transaction assignment`. Others are `campaign_gift_purchase`
// or `ticket_purchase`
donations: [{
// since this is in the nonprofit's currency they could do `amount: 10000` and
// we'll autoexpand
// if there's only one transaction assignment, this can be figured out.
amount: {
cents: 10000,
currency: 'usd',
},
designation: 'Special account',
}],
offline_transactions: [{
// this amount must match all of the transaction assignments
amount: 10000,
method: 'check',
}],
// information about the supporter donating. This either creates a new supporter
// or finds one with the same email
supporter: {
email: 'penelope@fightingpoverty.org',
name: 'Penelope Schultz',
},
};
const donation_result: Transaction = {
object: 'transaction',
id: 'trx_313435ncan',
amount: {
cents: 10000,
currency: 'usd',
},
// timestamp
created: 124541254,
deleted: false,
transaction_assignments: [{
object: 'donation',
id: 'don_2454',
// from transaction
supporter: 340,
// from supporter
nonprofit: 1235,
amount: {
cents: 10000,
currency: 'usd',
},
designation: 'Special account',
// it's a transaction assignment.
type: 'trx_assignment',
transaction: 'trx_313425ncan',
} as Donation],
subtransaction: {
id: 'offltrx_415h5io',
object: 'offline_transaction',
deleted: false,
created: 4144,
// based upon adding up the first charge
original_amount: {
cents: 10000,
currency: 'usd',
},
amount: {
cents: 10000,
currency: 'usd',
},
amount_disputed: {
cents: 0,
currency: 'usd',
},
amount_pending: {
cents: 0,
currency: 'usd',
},
amount_refunded: {
cents: 0,
currency: 'usd',
},
fee_total: {
cents: 0,
currency: 'usd',
},
//this is based upon adding up all of the charges, refunds, disputes and adjustments
net_amount: {
cents: 10000,
currency: 'usd',
},
// method: 'check',
// status: 'success',
disputes: [],
refunds: [],
charges: [
{
id: 'offchrg_4325n3fnfewE',
object: 'offline_transaction_charge',
// gross_amount - fees
net_amount: {
cents: 10000,
currency: 'usd',
},
gross_amount: {
cents: 10000,
currency: 'usd',
},
fees: { cents: 0, currency: 'usd'},
//from transaction
supporter: 340,
// from supporter
nonprofit: 1235,
// from subtransaction
transaction: 'trx_313435ncan',
subtransaction: 'offltrx_415h5io',
type: 'payment',
//timestamp of creation
created: 133543588,
},
] as Charge[],
//it's a subtransaction
type: 'subtransaction',
//from transaction
supporter: 340,
// from supporter
nonprofit: 1235,
transaction: 'trx_313435ncan',
} as OfflineTransaction,
nonprofit: {
id: 1235,
object: 'nonprofit',
name: "Nonprofit's name",
},
// we include all payments here from all of the subtransactions for ease of use
subtransaction_payments: [{
id: 'offchrg_4325n3fnfewE',
object: 'offline_transaction_charge',
deleted: false,
amount:{
cents: 10000,
currency: 'usd',
},
amount_pending: {
cents: 10000,
currency: 'usd',
},
// gross_amount - fees
net_amount: {
cents: 10000,
currency: 'usd',
},
gross_amount: {
cents: 10000,
currency: 'usd',
},
fees: null,
status: 'success',
subtransaction_entity: 'offchrg_4325n3fnfewE',
// from subtransaction_entity
subtransaction: 'offltrx_415h5io',
//from transaction
supporter: 340,
// from supporter
nonprofit: 1235,
// subtransaction
transaction: 'trx_313435ncan',
created: 133543588,
type: 'payment',
} as OfflineTransactionCharge],
// information about the supporter donating
supporter: {
id: 340,
email: 'penelope@fightingpoverty.org',
name: 'Penelope Schultz',
nonprofit: 1235,
anonymous: false,
deleted: false,
merged_into: null,
organization: null,
phone: null,
object: 'supporter',
addresses: [],
},
} as Transaction;
// Supporter wants to make a ticket purchase from Nonprofit 1 and tickets are available
const ticket_purchase_request:any = {
// donation is a `transaction assignment`. Others are `campaign_gift_purchase`
// or `ticket_purchase`
ticket_purchase: {
// since this is in the nonprofit's currency they could do `amount: 10000` and
// we'll autoexpand
amount: {
cents: 10000,
currency: 'usd',
},
event: 1,
ticket_requests: [{
ticket_level: 'tktlvl_r3453j90942',
quantity: 5,
note: "Seat with Penelope's Father",
},
{
ticket_level: 'tktlvl_434',
quantity: 1,
}],
},
offline_transactions: [{
// this amount must match all of the transaction assignments
amount: 10000,
method: 'check',
}],
// information about the supporter donating. This either creates a new supporter
// or finds one with the same email
supporter: {
email: 'penelope@fightingpoverty.org',
name: 'Penelope Schultz',
},
};
const ticket_purchase_result:Transaction = {
object: 'transaction',
id: 'trx_313435ncan',
amount: {
cents: 10000,
currency: 'usd',
},
ticket_purchases: [{
// since this is in the nonprofit's currency they could do `amount: 10000` and
// we'll autoexpand
amount: {
cents: 10000,
currency: 'usd',
},
event: 1,
tickets: [{
amount: {
cents: 2000,
currency: 'usd',
},
ticket_level: 'tktlvl_r3453j90942',
checked_in: false,
deleted: false,
note: "Seat with Penelope's Father",
id: 'tkt_werikhti35N',
},
{
amount: {
cents: 2000,
currency: 'usd',
},
ticket_level: 'tktlvl_r3453j90942',
checked_in: false,
deleted: false,
note: "Seat with Penelope's Father",
id: 'tkt_werikVti35N',
},
//... and 3 more for tktlvl_r3453j90942
{
ticket_level: 'tktlvl_434',
checked_in: false,
deleted: false,
id: 'tkt_535nrfuoh',
amount: {
cents: 0,
currency: 'usd',
},
}],
id: 'tktpur_34235nrf',
object: 'ticket_purchase',
subtype: 'trx_assignment',
}],
offline_transactions: [{
id: 'offltrx_415h5io',
object: 'offline_transasction',
// based upon adding up the first charge
original_amount: {
cents: 10000,
currency: 'usd',
},
//this is based upon adding up all of the charges, refunds, disputes and adjustments
net_amount: {
cents: 10000,
currency: 'usd',
},
method: 'check',
status: 'success',
charges: [
{
id: 'offchrg_4325n3fnfewE',
object: 'offline_charge',
// gross_amount - fees
net_amount: {
cents: 10000,
currency: 'usd',
},
gross_amount: {
cents: 10000,
currency: 'usd',
},
fees: null,
status: 'success',
//from transaction
supporter: 340,
// from supporter
nonprofit: 1235,
// from subtransaction
transaction: 'trx_313435ncan',
subtransaction: 'offltrx_415h5io',
subtype: 'payment',
//timestamp of creation
created: 133543588,
},
],
//it's a subtransaction
subtype: 'subtransaction',
//from transaction
supporter: 340,
// from supporter
nonprofit: 1235,
transaction: 'trx_313435ncan',
}],
nonprofit: {
id: 1235,
object: 'nonprofit',
name: "Nonprofit's name",
},
// we include all payments here from all of the subtransactions for ease of use
subtransaction_payments: [{
id: 'offchrg_4325n3fnfewE',
object: 'offline_charge',
// gross_amount - fees
net_amount: {
cents: 10000,
currency: 'usd',
},
gross_amount: {
cents: 10000,
currency: 'usd',
},
fees: null,
status: 'success',
// from subtransaction_entity
subtransaction: 'offltrx_415h5io',
//from transaction
supporter: 340,
// from supporter
nonprofit: 1235,
// subtransaction
transaction: 'trx_313435ncan',
created: 133543588,
}],
// information about the supporter donating
supporter: {
id: 340,
email: 'penelope@fightingpoverty.org',
name: 'Penelope Schultz',
nonprofit: 1235,
},
};
// Supporter wants to make a ticket purchase from Nonprofit 1 and tickets are NOT available
// TODO
// Supporter wants to make a Stripe charge for a campaign_gift from Nonprofit 1
const stripe_campaign_gift_request:any = {
campaign_gift_purchase: {
// since this is in the nonprofit's currency they could do `amount: 10000` and
// we'll autoexpand
amount: {
cents: 10000,
currency: 'usd',
},
campaign: 1,
campaign_gifts: [{
campaign_gift_option: 'cgo_535n35n',
}],
},
stripe_transactions: [
{
amount: 10000,
},
],
// information about the supporter donating. This either creates a new supporter
// or finds one with the same email
supporter: {
email: 'penelope@fightingpoverty.org',
name: 'Penelope Schultz',
},
};
const stripe_campaign_gift_result:any = {
// donation is a `transaction assignment`. Others are `campaign_gift_purchase`
// or `ticket_purchase`
};
// Supporter wants to start a recurring donation and charge immediately
//`POST /nonprofit/1/recurring_donation`

View file

@ -1,20 +1,77 @@
// License: LGPL-3.0-or-later // License: LGPL-3.0-or-later
import type { Amount, HoudiniObject, IDType, HouID } from "../../common"; import type { Amount, HoudiniObject, IDType, HouID, HoudiniEvent } from "../../common";
import type Nonprofit from '../'; import type Nonprofit from '../';
import type Supporter from "../Supporter"; import type Supporter from "../Supporter";
import type { Payment } from "./Payment"; import type { Payment } from "./Payment";
export default interface Transaction extends HoudiniObject<HouID> { export interface Subtransaction extends HoudiniObject<HouID>, TrxDescendent {
amount: Amount; amount: Amount;
nonprofit: IDType | Nonprofit; amount_disputed: Amount;
object: 'transaction'; amount_pending: Amount;
payments: IDType[] | Payment[]; amount_refunded: Amount;
status: "not-submitted"|"created" | "waiting-on-supporter" | "failed" | "completed"; created: number;
supporter: IDType | Supporter; fee_total: Amount;
/** net_amount: Amount;
* We don't specify more for now type: 'subtransaction';
*/
transaction_assignments: { id: HouID,object: string }[];
} }
/**
* Every descendent of a Transaction object will have the following three fields
*/
export interface TrxDescendent {
/**
* The nonprofit of the transaction is assigned to.
*/
nonprofit: IDType | Nonprofit;
/**
* The supporter of the transaction
*/
supporter: IDType | Supporter;
/**
* The transaction itself
*/
transaction: HouID | Transaction;
}
/**
* Every transaction assignment, including Donation, TicketPurchase, CampaignGiftPurchase
* must have an amount and the type 'trx_assignment' set.
*/
export interface TrxAssignment extends HoudiniObject<HouID>, TrxDescendent {
amount: Amount;
type: 'trx_assignment';
}
export interface TrxAssignmentAsId extends HoudiniObject<HouID> {
type: 'trx_assignment';
}
export interface SubtransactionAsId extends HoudiniObject<HouID> {
type: 'subtransaction';
}
export default interface Transaction extends HoudiniObject<HouID> {
amount: Amount;
// amount_disputed: Amount;
// amount_refunded: Amount;
created: number;
deleted: boolean;
// net_amount: Amount;
nonprofit: IDType | Nonprofit;
object: 'transaction';
subtransaction: SubtransactionAsId | Subtransaction;
subtransaction_payments: IDType[] | Payment[];
supporter: IDType | Supporter;
transaction_assignments: TrxAssignmentAsId[] | TrxAssignment[];
}
export type TransactionCreated = HoudiniEvent<'transaction.created', Transaction>;
export type TransactionUpdated = HoudiniEvent<'transaction.updated', Transaction>;
export type TransactionRefunded = HoudiniEvent<'transaction.refunded', Transaction>;
export type TransactionDisputed = HoudiniEvent<'transaction.disputed', Transaction>;
export type TransactionDeleted = HoudiniEvent<'transaction.deleted', Transaction>;
export * from './Payment'; export * from './Payment';
export * from './Donation';
export * from './OfflineTransaction';
export {default as OfflineTransaction} from './OfflineTransaction';

View file

@ -15,7 +15,7 @@ export type HouID = string;
* Describes a monetary value in the minimum unit for this current. Corresponds to Money class in * Describes a monetary value in the minimum unit for this current. Corresponds to Money class in
* Ruby and Typescript * Ruby and Typescript
*/ */
export type Amount = { cents: string, currency: string }; export type Amount = { cents: number, currency: string };
/** /**
* A more flexible version of Amount. In cases where we can assume what the currency is, * A more flexible version of Amount. In cases where we can assume what the currency is,
@ -72,6 +72,7 @@ export interface HoudiniObject<ID extends IDType|HouID=IDType> {
object: string; object: string;
} }
export type PolymorphicID<ID extends IDType|HouID=IDType> = HoudiniObject<ID>;
type HoudiniObjectOfAllIDs = HoudiniObject<IDType> | HoudiniObject<HouID>; type HoudiniObjectOfAllIDs = HoudiniObject<IDType> | HoudiniObject<HouID>;
/** /**

View file

@ -49,6 +49,7 @@ module InsertDonation
trx.save! trx.save!
don.save! don.save!
don.publish_created don.publish_created
trx.publish_created
result['activity'] = InsertActivities.for_one_time_donations([result['payment'].id]) result['activity'] = InsertActivities.for_one_time_donations([result['payment'].id])
Houdini.event_publisher.announce(:donation_create, result['donation'], result['donation'].supporter.locale) Houdini.event_publisher.announce(:donation_create, result['donation'], result['donation'].supporter.locale)
result result
@ -77,6 +78,9 @@ module InsertDonation
data = date_from_data(data) data = date_from_data(data)
result = { 'donation' => insert_donation(data.except('offsite_payment'), entities) } result = { 'donation' => insert_donation(data.except('offsite_payment'), entities) }
trx = entities[:supporter_id].transactions.build(amount: data['amount'], created: data['date'])
don = trx.donations.build(amount: result['donation'].amount, legacy_donation: result['donation'])
result['payment'] = insert_payment('OffsitePayment', 0, result['donation']['id'], data) result['payment'] = insert_payment('OffsitePayment', 0, result['donation']['id'], data)
result['offsite_payment'] = Psql.execute( result['offsite_payment'] = Psql.execute(
Qexpr.new.insert(:offsite_payments, [ Qexpr.new.insert(:offsite_payments, [
@ -90,8 +94,23 @@ module InsertDonation
) )
]).returning('*') ]).returning('*')
).first ).first
off_t = trx.build_subtransaction(
subtransactable: OfflineTransaction.new(amount: data['amount']),
subtransaction_payments:[
SubtransactionPayment.new(
paymentable: OfflineTransactionCharge.new(payment: Payment.find(result['payment']['id'])))
],
created: data['date']
);
trx.save!
don.save!
off_t.save!
off_t.subtransaction_payments.each{|stp| stp.publish_created}
off_t.publish_created
don.publish_created
trx.publish_created
result['activity'] = InsertActivities.for_offsite_donations([result['payment']['id']]) result['activity'] = InsertActivities.for_offsite_donations([result['payment']['id']])
WeMoveExecuteForDonationsJob.perform_later(result['donation'])
{ status: 200, json: result } { status: 200, json: result }
end end
@ -159,7 +178,7 @@ module InsertDonation
end end
end end
# Insert a payment row for a donation # Insert a payment row for a donationValidationError
def self.insert_payment(kind, fee_total, donation_id, data) def self.insert_payment(kind, fee_total, donation_id, data)
Psql.execute( Psql.execute(
Qexpr.new.insert(:payments, [{ Qexpr.new.insert(:payments, [{

View file

@ -0,0 +1,9 @@
# 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 :offline_transaction_charge do
amount { 1 }
end
end

View file

@ -0,0 +1,9 @@
# 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 :offline_transaction do
amount { 1 }
end
end

View file

@ -2,150 +2,600 @@
# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later # 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 # Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE
# rubocop:disable RSpec/MessageSpies, RSpec/NamedSubject, RSpec/MultipleExpectations,RSpec/MultipleMemoizedHelpers, RSpec/ExpectInHook
require 'rails_helper' require 'rails_helper'
RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = nil
describe InsertDonation do describe InsertDonation do
describe '.with_stripe' do describe '.with_stripe' do
before(:each) do before do
Houdini.payment_providers.stripe.connect = true Houdini.payment_providers.stripe.connect = true
end end
include_context :shared_rd_donation_value_context include_context :shared_rd_donation_value_context
describe 'param validation' do describe 'param validation' do
before(:each) do before do
expect(Houdini.event_publisher).to_not receive(:announce).with(:donation_created,any_args) expect(Houdini.event_publisher).to_not receive(:announce).with(:donation_created, any_args)
end expect(Houdini.event_publisher).to_not receive(:announce).with(:transaction_created, any_args)
it 'does basic validation' do end
validation_basic_validation { InsertDonation.with_stripe(designation: 34_124, dedication: 35_141, event_id: 'bad', campaign_id: 'bad') }
end
it 'errors out if token is invalid' do it 'does basic validation' do
validation_invalid_token { InsertDonation.with_stripe(amount: 1, nonprofit_id: 1, supporter_id: 1, token: fake_uuid) } validation_basic_validation do
end described_class.with_stripe(designation: 34_124, dedication: 35_141, event_id: 'bad', campaign_id: 'bad')
end
end
it 'errors out if token is unauthorized' do it 'errors out if token is invalid' do
validation_unauthorized { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: 1, supporter_id: 1, token: fake_uuid) } validation_invalid_token do
end described_class.with_stripe(amount: 1, nonprofit_id: 1, supporter_id: 1, token: fake_uuid)
end
end
it 'errors out if token is expired' do it 'errors out if token is unauthorized' do
validation_expired { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: 1, supporter_id: 1, token: fake_uuid) } validation_unauthorized do
end described_class.with_stripe(amount: charge_amount, nonprofit_id: 1, supporter_id: 1, token: fake_uuid)
end
end
describe 'errors during find if' do it 'errors out if token is expired' do
it 'supporter is invalid' do validation_expired do
find_error_supporter { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: 55_555, token: source_token.token) } described_class.with_stripe(amount: charge_amount, nonprofit_id: 1, supporter_id: 1, token: fake_uuid)
end end
end
it 'nonprofit is invalid' do describe 'errors during find if' do
find_error_nonprofit { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: 55_555, supporter_id: supporter.id, token: source_token.token) } it 'supporter is invalid' do
end find_error_supporter do
described_class.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: 55_555,
token: source_token.token)
end
end
it 'campaign is invalid' do it 'nonprofit is invalid' do
find_error_campaign { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, token: source_token.token, campaign_id: 5555) } find_error_nonprofit do
end described_class.with_stripe(amount: charge_amount, nonprofit_id: 55_555, supporter_id: supporter.id,
token: source_token.token)
end
end
it 'event is invalid' do it 'campaign is invalid' do
find_error_event { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, token: source_token.token, event_id: 5555) } find_error_campaign do
end described_class.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id,
token: source_token.token, campaign_id: 5555)
end
end
it 'profile is invalid' do it 'event is invalid' do
find_error_profile { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, token: source_token.token, profile_id: 5555) } find_error_event do
end described_class.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id,
end token: source_token.token, event_id: 5555)
end
end
describe 'errors during relationship comparison if' do it 'profile is invalid' do
it 'supporter is deleted' do find_error_profile do
validation_supporter_deleted { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, token: source_token.token) } described_class.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id,
end token: source_token.token, profile_id: 5555)
end
end
end
it 'event is deleted' do describe 'errors during relationship comparison if' do
validation_event_deleted { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, token: source_token.token, event_id: event.id) } it 'supporter is deleted' do
end validation_supporter_deleted do
described_class.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id,
token: source_token.token)
end
end
it 'campaign is deleted' do it 'event is deleted' do
validation_campaign_deleted { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, token: source_token.token, campaign_id: campaign.id) } validation_event_deleted do
end described_class.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id,
token: source_token.token, event_id: event.id)
end
end
it 'supporter doesnt belong to nonprofit' do it 'campaign is deleted' do
validation_supporter_not_with_nonprofit { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: other_nonprofit_supporter.id, token: source_token.token) } validation_campaign_deleted do
end described_class.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id,
token: source_token.token, campaign_id: campaign.id)
end
end
it 'campaign doesnt belong to nonprofit' do it 'supporter doesnt belong to nonprofit' do
validation_campaign_not_with_nonprofit { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, token: source_token.token, campaign_id: other_campaign.id) } validation_supporter_not_with_nonprofit do
end described_class.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id,
supporter_id: other_nonprofit_supporter.id, token: source_token.token)
end
end
it 'event doesnt belong to nonprofit' do it 'campaign doesnt belong to nonprofit' do
validation_event_not_with_nonprofit { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, token: source_token.token, event_id: other_event.id) } validation_campaign_not_with_nonprofit do
end described_class.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id,
token: source_token.token, campaign_id: other_campaign.id)
end
end
it 'card doesnt belong to supporter' do it 'event doesnt belong to nonprofit' do
validation_card_not_with_supporter { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, token: other_source_token.token) } validation_event_not_with_nonprofit do
end described_class.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id,
end token: source_token.token, event_id: other_event.id)
end end
end
it 'charge returns failed' do it 'card doesnt belong to supporter' do
expect(Houdini.event_publisher).to_not receive(:announce).with(:donation_created,any_args) validation_card_not_with_supporter do
handle_charge_failed { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, token: source_token.token) } described_class.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id,
end token: other_source_token.token)
end
end
end
end
describe 'success' do it 'charge returns failed' do
before(:each) do expect(Houdini.event_publisher).to_not receive(:announce).with(:donation_created, any_args)
before_each_success expect(Houdini.event_publisher).to_not receive(:announce).with(:transaction_created, any_args)
allow(Houdini.event_publisher).to receive(:announce) handle_charge_failed do
expect(Houdini.event_publisher).to receive(:announce).with(:donation_created,any_args) described_class.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id,
end token: source_token.token)
it 'process event donation' do end
process_event_donation { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, token: source_token.token, event_id: event.id, date: (Time.now + 1.day).to_s, dedication: {'type' => 'honor', 'name' => 'a name'}, designation: 'designation') } end
end
it 'process campaign donation' do describe 'success' do
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_create, any_args) before do
process_campaign_donation { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, token: source_token.token, campaign_id: campaign.id, date: (Time.now + 1.day).to_s, dedication: {'type' => 'honor', 'name' => 'a name'}, designation: 'designation') } before_each_success
end allow(Houdini.event_publisher).to receive(:announce)
expect(Houdini.event_publisher).to receive(:announce).with(:donation_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:transaction_created, any_args)
end
it 'processes general donation' do it 'process event donation' do
process_general_donation { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, token: source_token.token, profile_id: profile.id, date: (Time.now + 1.day).to_s, dedication: {'type' => 'honor', 'name' => 'a name'}, designation: 'designation') } process_event_donation do
end described_class.with_stripe(
end amount: charge_amount,
end nonprofit_id: nonprofit.id,
supporter_id: supporter.id,
token: source_token.token,
event_id: event.id,
date: (Time.zone.now + 1.day).to_s,
dedication: {
'type' => 'honor',
'name' => 'a name'
},
designation: 'designation'
)
end
end
describe '#with_sepa' do it 'process campaign donation' do
include_context :shared_rd_donation_value_context expect(Houdini.event_publisher).to receive(:announce).with(:campaign_create, any_args)
process_campaign_donation do
described_class.with_stripe(
amount: charge_amount,
nonprofit_id: nonprofit.id,
supporter_id: supporter.id,
token: source_token.token,
campaign_id: campaign.id,
date: (Time.zone.now + 1.day).to_s,
dedication: { 'type' => 'honor', 'name' => 'a name' },
designation: 'designation'
)
end
end
describe 'saves donation' do it 'processes general donation' do
before(:each) do process_general_donation do
before_each_sepa_success described_class.with_stripe(
end amount: charge_amount,
it 'process event donation' do nonprofit_id: nonprofit.id,
process_event_donation(sepa: true) { InsertDonation.with_sepa(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, direct_debit_detail_id: direct_debit_detail.id, event_id: event.id, date: (Time.now + 1.day).to_s, dedication: {'type' => 'honor', 'name' => 'a name'}, designation: 'designation') } supporter_id: supporter.id,
end token: source_token.token,
profile_id: profile.id,
date: (Time.zone.now + 1.day).to_s,
dedication: { 'type' => 'honor', 'name' => 'a name' },
designation: 'designation'
)
end
end
end
end
it 'process campaign donation' do describe '#with_sepa' do
allow(Houdini.event_publisher).to receive(:announce) include_context :shared_rd_donation_value_context
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_create, any_args)
process_campaign_donation(sepa: true) { InsertDonation.with_sepa(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, direct_debit_detail_id: direct_debit_detail.id, campaign_id: campaign.id, date: (Time.now + 1.day).to_s, dedication: {'type' => 'honor', 'name' => 'a name'}, designation: 'designation') }
end
it 'processes general donation' do describe 'saves donation' do
process_general_donation(sepa: true) { InsertDonation.with_sepa(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, direct_debit_detail_id: direct_debit_detail.id, profile_id: profile.id, date: (Time.now + 1.day).to_s, dedication: {'type' => 'honor', 'name' => 'a name'}, designation: 'designation') } before do
end before_each_sepa_success
end end
end
# it 'saves donation' do
# expect { InsertDonation.with_sepa(data) }.to change(Donation, :count).by(1)
# end
#
# it 'returns a json hash' do
# result = InsertDonation.with_sepa(data)
#
# expect(result).to be_a(Hash)
# expect(result[:json]['donation']).to include data
# end
# end
it '.offsite', pending: true do it 'process event donation' do
raise process_event_donation(sepa: true) do
end described_class.with_sepa(
amount: charge_amount,
nonprofit_id: nonprofit.id,
supporter_id: supporter.id,
direct_debit_detail_id: direct_debit_detail.id,
event_id: event.id,
date: (Time.zone.now + 1.day).to_s,
dedication: {
'type' => 'honor',
'name' => 'a name'
},
designation: 'designation'
)
end
end
it 'process campaign donation' do
allow(Houdini.event_publisher).to receive(:announce)
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_create, any_args)
process_campaign_donation(sepa: true) do
described_class.with_sepa(
amount: charge_amount,
nonprofit_id: nonprofit.id,
supporter_id: supporter.id,
direct_debit_detail_id: direct_debit_detail.id,
campaign_id: campaign.id,
date: (Time.zone.now + 1.day).to_s,
dedication: {
'type' => 'honor',
'name' => 'a name'
},
designation: 'designation'
)
end
end
it 'processes general donation' do
process_general_donation(sepa: true) do
described_class.with_sepa(
amount: charge_amount,
nonprofit_id: nonprofit.id,
supporter_id: supporter.id,
direct_debit_detail_id: direct_debit_detail.id,
profile_id: profile.id,
date: (Time.zone.now + 1.day).to_s,
dedication: {
'type' => 'honor',
'name' => 'a name'
},
designation: 'designation'
)
end
end
end
end
describe '.offsite' do
include_context :shared_rd_donation_value_context
describe 'failures' do
before do
expect(Houdini.event_publisher).to_not receive(:announce).with(:payment_created, any_args)
expect(Houdini.event_publisher).to_not receive(:announce).with(:offline_transaction_charge_created, any_args)
expect(Houdini.event_publisher).to_not receive(:announce).with(:offline_transaction_created, any_args)
expect(Houdini.event_publisher).to_not receive(:announce).with(:donation_created, any_args)
expect(Houdini.event_publisher).to_not receive(:announce).with(:trx_assignment_created, any_args)
expect(Houdini.event_publisher).to_not receive(:announce).with(:transaction_created, any_args)
end
it 'fails if amount is missing' do
expect do
described_class.offsite(
{
nonprofit_id: nonprofit.id,
supporter_id: supporter.id
}.with_indifferent_access
)
end.to raise_error(ParamValidation::ValidationError)
end
end
describe 'success' do
before do
allow(Houdini.event_publisher).to receive(:announce)
end
describe 'general offsite create' do
subject do
described_class.offsite({ amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id,
date: created_time.to_s }.with_indifferent_access)
end
let(:created_time) { Time.current + 1.day }
let(:common_builder) do
{ 'supporter' => supporter.id,
'nonprofit' => nonprofit.id }
end
let(:common_builder_expanded) do
{
'supporter' => supporter_builder_expanded,
'nonprofit' => np_builder_expanded
}
end
let(:common_builder_with_trx_id) do
common_builder.merge(
{
'transaction' => match_houid('trx')
}
)
end
let(:common_builder_with_trx) do
common_builder.merge(
{
'transaction' => transaction_builder
}
)
end
let(:np_builder_expanded) do
{
'id' => nonprofit.id,
'name' => nonprofit.name,
'object' => 'nonprofit'
}
end
let(:supporter_builder_expanded) do
supporter_to_builder_base.merge({ 'name' => 'Fake Supporter Name' })
end
let(:transaction_builder) do
common_builder.merge(
{
'id' => match_houid('trx'),
'object' => 'transaction',
'amount' => {
'cents' => charge_amount,
'currency' => 'usd'
},
'created' => created_time.to_i,
'subtransaction' => offline_transaction_id_only,
'subtransaction_payments' => [offline_transaction_charge_id_only],
'transaction_assignments' => [donation_id_only]
}
)
end
let(:transaction_builder_expanded) do
transaction_builder.merge(
common_builder_expanded,
{
'subtransaction' => offline_transaction_builder,
'subtransaction_payments' => [offline_transaction_charge_builder],
'transaction_assignments' => [donation_builder]
}
)
end
let(:offline_transaction_id_only) do
{
'id' => match_houid('offlinetrx'),
'object' => 'offline_transaction',
'type' => 'subtransaction'
}
end
let(:offline_transaction_builder) do
offline_transaction_id_only.merge(
common_builder_with_trx_id,
{
'initial_amount' => {
'cents' => charge_amount,
'currency' => 'usd'
},
'net_amount' => {
'cents' => charge_amount,
'currency' => 'usd'
},
'payments' => [offline_transaction_charge_id_only],
'created' => created_time.to_i
}
)
end
let(:offline_transaction_builder_expanded) do
offline_transaction_builder.merge(
common_builder_with_trx,
common_builder_expanded,
{
'payments' => [offline_transaction_charge_builder]
}
)
end
let(:offline_transaction_charge_id_only) do
{
'id' => match_houid('offtrxchrg'),
'object' => 'offline_transaction_charge',
'type' => 'payment'
}
end
let(:offline_transaction_charge_builder) do
offline_transaction_charge_id_only.merge(
common_builder_with_trx_id,
{
'gross_amount' => {
'cents' => charge_amount,
'currency' => 'usd'
},
'net_amount' => {
'cents' => charge_amount,
'currency' => 'usd'
},
'fee_total' => {
'cents' => 0,
'currency' => 'usd'
},
'subtransaction' => offline_transaction_id_only,
'created' => created_time.to_i
}
)
end
let(:offline_transaction_charge_builder_expanded) do
offline_transaction_charge_builder.merge(
common_builder_with_trx,
common_builder_expanded,
{
'subtransaction' => offline_transaction_builder
}
)
end
let(:donation_id_only) do
{
'id' => match_houid('don'),
'object' => 'donation',
'type' => 'trx_assignment'
}
end
let(:donation_builder) do
donation_id_only.merge(common_builder_with_trx_id, {
'amount' => {
'cents' => charge_amount,
'currency' => 'usd'
},
'designation' => nil
})
end
let(:donation_builder_expanded) do
donation_builder.merge(common_builder_with_trx, common_builder_expanded)
end
describe 'event publishing' do
it 'has fired transaction.created' do
expect(Houdini.event_publisher).to receive(:announce).with(:payment_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:offline_transaction_charge_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:offline_transaction_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:donation_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:trx_assignment_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(
:transaction_created, {
'id' => match_houid('objevt'),
'object' => 'object_event',
'type' => 'transaction.created',
'data' => {
'object' => transaction_builder_expanded
}
}
)
subject
end
it 'has fired offline_transaction_charge.created' do
expect(Houdini.event_publisher).to receive(:announce).with(
:offline_transaction_charge_created,
{
'id' => match_houid('objevt'),
'object' => 'object_event',
'type' => 'offline_transaction_charge.created',
'data' => {
'object' => offline_transaction_charge_builder_expanded
}
}
)
expect(Houdini.event_publisher).to receive(:announce).with(:payment_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:offline_transaction_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:donation_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:trx_assignment_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:transaction_created, any_args)
subject
end
it 'has fired payment.created' do
expect(Houdini.event_publisher).to receive(:announce).with(:offline_transaction_charge_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(
:payment_created,
{
'id' => match_houid('objevt'),
'object' => 'object_event',
'type' => 'payment.created',
'data' => {
'object' => offline_transaction_charge_builder_expanded
}
}
)
expect(Houdini.event_publisher).to receive(:announce).with(:offline_transaction_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:donation_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:trx_assignment_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:transaction_created, any_args)
subject
end
it 'has fired offline_transaction.created' do
expect(Houdini.event_publisher).to receive(:announce).with(:offline_transaction_charge_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:payment_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(
:offline_transaction_created,
{
'id' => match_houid('objevt'),
'object' => 'object_event',
'type' => 'offline_transaction.created',
'data' => {
'object' => offline_transaction_builder_expanded
}
}
)
expect(Houdini.event_publisher).to receive(:announce).with(:donation_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:trx_assignment_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:transaction_created, any_args)
subject
end
it 'has fired donation.created' do
expect(Houdini.event_publisher).to receive(:announce).with(:offline_transaction_charge_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:payment_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:offline_transaction_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(
:donation_created,
{
'id' => match_houid('objevt'),
'object' => 'object_event',
'type' => 'donation.created',
'data' => {
'object' => donation_builder_expanded
}
}
)
expect(Houdini.event_publisher).to receive(:announce).with(:trx_assignment_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:transaction_created, any_args)
subject
end
it 'has fired trx_assignment.created' do
expect(Houdini.event_publisher).to receive(:announce).with(:offline_transaction_charge_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:payment_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:offline_transaction_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:donation_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(
:trx_assignment_created,
{
'id' => match_houid('objevt'),
'object' => 'object_event',
'type' => 'trx_assignment.created',
'data' => {
'object' => donation_builder_expanded
}
}
)
expect(Houdini.event_publisher).to receive(:announce).with(:transaction_created, any_args)
subject
end
end
end
end
end
end end
# rubocop:enable all

View file

@ -45,7 +45,7 @@ RSpec.describe CampaignGiftPurchase, type: :model do
}, },
}], }],
'campaign' => kind_of(Numeric), 'campaign' => kind_of(Numeric),
'nonprofit' => kind_of(Numeric) 'nonprofit' => nonprofit.id
} }
end end
@ -69,8 +69,22 @@ RSpec.describe CampaignGiftPurchase, type: :model do
'cents' => trx.amount, 'cents' => trx.amount,
'currency' => 'usd' 'currency' => 'usd'
}, },
'supporter' => kind_of(Numeric), 'created' => Time.current.to_i,
'nonprofit' => kind_of(Numeric) 'supporter' => supporter.id,
'nonprofit' => nonprofit.id,
'subtransaction' => nil,
'subtransaction_payments' => [],
'transaction_assignments' => [
cgp_builder_to_id
]
}
end
let(:cgp_builder_to_id) do
{
'id' => match_houid('cgpur'),
'object' => 'campaign_gift_purchase',
'type' => 'trx_assignment'
} }
end end
@ -102,9 +116,9 @@ RSpec.describe CampaignGiftPurchase, type: :model do
'campaign_gift_purchase' => match_houid('cgpur'), 'campaign_gift_purchase' => match_houid('cgpur'),
'deleted' => false, 'deleted' => false,
'id' => match_houid('cgift'), 'id' => match_houid('cgift'),
'nonprofit'=> kind_of(Numeric), 'nonprofit'=> nonprofit.id,
'object' => 'campaign_gift', 'object' => 'campaign_gift',
'supporter' => kind_of(Numeric), 'supporter' => supporter.id,
'transaction' => match_houid('trx') 'transaction' => match_houid('trx')
} }
} }
@ -114,6 +128,7 @@ RSpec.describe CampaignGiftPurchase, type: :model do
it 'announces created properly when called' do it 'announces created properly when called' do
allow(Houdini.event_publisher).to receive(:announce) allow(Houdini.event_publisher).to receive(:announce)
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_gift_option_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_gift_purchase_created, { expect(Houdini.event_publisher).to receive(:announce).with(:campaign_gift_purchase_created, {
'id' => match_houid('objevt'), 'id' => match_houid('objevt'),
'object' => 'object_event', 'object' => 'object_event',
@ -131,7 +146,8 @@ RSpec.describe CampaignGiftPurchase, type: :model do
'supporter' => supporter_builder_expanded, 'supporter' => supporter_builder_expanded,
'nonprofit' => np_builder_expanded, 'nonprofit' => np_builder_expanded,
'transaction' => transaction_builder_expanded, 'transaction' => transaction_builder_expanded,
'deleted' => false 'deleted' => false,
'type' => 'trx_assignment'
} }
} }
}) })
@ -141,6 +157,7 @@ RSpec.describe CampaignGiftPurchase, type: :model do
it 'announces updated properly when called' do it 'announces updated properly when called' do
allow(Houdini.event_publisher).to receive(:announce) allow(Houdini.event_publisher).to receive(:announce)
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_gift_option_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_gift_purchase_updated, { expect(Houdini.event_publisher).to receive(:announce).with(:campaign_gift_purchase_updated, {
'id' => match_houid('objevt'), 'id' => match_houid('objevt'),
'object' => 'object_event', 'object' => 'object_event',
@ -158,7 +175,8 @@ RSpec.describe CampaignGiftPurchase, type: :model do
'supporter' => supporter_builder_expanded, 'supporter' => supporter_builder_expanded,
'nonprofit' => np_builder_expanded, 'nonprofit' => np_builder_expanded,
'transaction' => transaction_builder_expanded, 'transaction' => transaction_builder_expanded,
'deleted' => false 'deleted' => false,
'type' => 'trx_assignment'
} }
} }
}) })
@ -168,6 +186,7 @@ RSpec.describe CampaignGiftPurchase, type: :model do
it 'announces updated deleted properly when called' do it 'announces updated deleted properly when called' do
allow(Houdini.event_publisher).to receive(:announce) allow(Houdini.event_publisher).to receive(:announce)
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_gift_option_created, any_args)
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_gift_purchase_deleted, { expect(Houdini.event_publisher).to receive(:announce).with(:campaign_gift_purchase_deleted, {
'id' => match_houid('objevt'), 'id' => match_houid('objevt'),
'object' => 'object_event', 'object' => 'object_event',
@ -185,7 +204,8 @@ RSpec.describe CampaignGiftPurchase, type: :model do
'supporter' => supporter_builder_expanded, 'supporter' => supporter_builder_expanded,
'nonprofit' => np_builder_expanded, 'nonprofit' => np_builder_expanded,
'transaction' => transaction_builder_expanded, 'transaction' => transaction_builder_expanded,
'deleted' => true 'deleted' => true,
'type' => 'trx_assignment'
} }
} }
}) })

View file

@ -9,14 +9,21 @@ RSpec.describe Model::Jbuilder do
let(:model) do let(:model) do
stub_const('HasToBuilderAndToId', Struct.new(:to_id, :to_builder)) stub_const('HasToBuilderAndToId', Struct.new(:to_id, :to_builder))
stub_const('ModelClass', Struct.new(:filled, :unfilled, :enumerable)) stub_const('ModelClass', Struct.new(:filled, :unfilled, :enumerable, :json_based_id, :flat_enumerable))
ModelClass.new( ModelClass.new(
HasToBuilderAndToId.new('id_result', 'builder_result'), HasToBuilderAndToId.new('id_result', 'builder_result'),
nil, nil,
[ [
HasToBuilderAndToId.new('enumerable_id_result_1', 'enumerable_builder_result_1'), HasToBuilderAndToId.new('enumerable_id_result_1', ::Jbuilder.new { |json| json.id 'enumerable_builder_result_1' }),
HasToBuilderAndToId.new('enumerable_id_result_2', 'enumerable_builder_result_2') HasToBuilderAndToId.new('enumerable_id_result_2', ::Jbuilder.new { |json| json.id 'enumerable_builder_result_2' })
],
HasToBuilderAndToId.new(::Jbuilder.new do |json|
json.id 'json_based_id'
end, 'expanded'),
%w[
flat_id_result_1
flat_id_result_2
] ]
) )
end end
@ -24,7 +31,7 @@ RSpec.describe Model::Jbuilder do
let(:unfilled_expansion) { described_class.new(key: :unfilled) } let(:unfilled_expansion) { described_class.new(key: :unfilled) }
let(:nonexistent_expansion) { described_class.new(key: :nonexistent) } let(:nonexistent_expansion) { described_class.new(key: :nonexistent) }
let(:expandable_expansion) { described_class.new(key: :enumerable, enum_type: :expandable) } let(:expandable_expansion) { described_class.new(key: :enumerable, enum_type: :expandable) }
let(:flat_expansion) { described_class.new(key: :enumerable, enum_type: :flat) } let(:flat_expansion) { described_class.new(key: :flat_enumerable, enum_type: :flat) }
describe 'expansion where the attribute is filled' do describe 'expansion where the attribute is filled' do
subject { filled_expansion } subject { filled_expansion }
@ -118,7 +125,7 @@ RSpec.describe Model::Jbuilder do
describe '#to_builder' do describe '#to_builder' do
subject { expandable_expansion.to_builder.call(model) } subject { expandable_expansion.to_builder.call(model) }
it { is_expected.to match(%w[enumerable_builder_result_1 enumerable_builder_result_2]) } it { is_expected.to match([{ 'id' => 'enumerable_builder_result_1' }, { 'id' => 'enumerable_builder_result_2' }]) }
end end
end end
@ -127,7 +134,7 @@ RSpec.describe Model::Jbuilder do
it { it {
is_expected.to have_attributes( is_expected.to have_attributes(
json_attribute: 'enumerable', json_attribute: 'flat_enumerable',
enumerable?: true, enumerable?: true,
flat_enum?: true, flat_enum?: true,
expandable_enum?: false expandable_enum?: false
@ -135,15 +142,15 @@ RSpec.describe Model::Jbuilder do
} }
describe '#to_id' do describe '#to_id' do
subject { expandable_expansion.to_id.call(model) } subject { flat_expansion.to_id.call(model) }
it { is_expected.to match(%w[enumerable_id_result_1 enumerable_id_result_2]) } it { is_expected.to match(%w[flat_id_result_1 flat_id_result_2]) }
end end
describe '#to_builder' do describe '#to_builder' do
subject { expandable_expansion.to_builder.call(model) } subject { flat_expansion.to_builder.call(model) }
it { is_expected.to match(%w[enumerable_builder_result_1 enumerable_builder_result_2]) } it { is_expected.to match(%w[flat_id_result_1 flat_id_result_2]) }
end end
end end
end end

View file

@ -219,7 +219,7 @@ RSpec.describe EventDiscount, type: :model do
'available_to' => 'everyone', 'available_to' => 'everyone',
'nonprofit' => nonprofit.id, 'nonprofit' => nonprofit.id,
'event' => event.id, 'event' => event.id,
'event_discounts' => [] 'event_discounts' => [kind_of(Numeric)]
} }
] ]
} }

View file

@ -69,8 +69,20 @@ RSpec.describe ModernCampaignGift, type: :model do
'cents' => trx.amount, 'cents' => trx.amount,
'currency' => 'usd' 'currency' => 'usd'
}, },
'created' => Time.current.to_i,
'supporter' => kind_of(Numeric), 'supporter' => kind_of(Numeric),
'nonprofit' => kind_of(Numeric) 'nonprofit' => kind_of(Numeric),
'subtransaction' => nil,
'subtransaction_payments' => [],
'transaction_assignments' => [cgp_builder_to_id]
}
end
let(:cgp_builder_to_id) do
{
'id' => match_houid('cgpur'),
'object' => 'campaign_gift_purchase',
'type' => 'trx_assignment'
} }
end end
@ -87,13 +99,11 @@ RSpec.describe ModernCampaignGift, type: :model do
'supporter' => kind_of(Numeric), 'supporter' => kind_of(Numeric),
'nonprofit' => kind_of(Numeric), 'nonprofit' => kind_of(Numeric),
'transaction' => match_houid('trx'), 'transaction' => match_houid('trx'),
'deleted' => false 'deleted' => false,
'type' => 'trx_assignment'
} }
end end
it 'announces created properly when called' do it 'announces created properly when called' do
allow(Houdini.event_publisher).to receive(:announce) allow(Houdini.event_publisher).to receive(:announce)
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_gift_created, { expect(Houdini.event_publisher).to receive(:announce).with(:campaign_gift_created, {

View file

@ -26,7 +26,8 @@ RSpec.describe ModernDonation, type: :model do
'supporter' => supporter.id, 'supporter' => supporter.id,
'amount' => {'currency' => 'usd', 'cents' => 1200}, 'amount' => {'currency' => 'usd', 'cents' => 1200},
'transaction' => trx.id, 'transaction' => trx.id,
'designation' => nil 'designation' => nil,
'type' => 'trx_assignment'
} }
end end
it 'without dedication or designation' do it 'without dedication or designation' do

View file

@ -0,0 +1,9 @@
# 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 OfflineTransactionCharge, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end

View file

@ -0,0 +1,9 @@
# 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 OfflineTransaction, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end

View file

@ -0,0 +1,9 @@
# 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 SubtransactionPayment, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end

View file

@ -0,0 +1,9 @@
# 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 Subtransaction, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end

View file

@ -57,7 +57,8 @@ RSpec.describe TicketPurchase, type: :model do
'amount' => {'currency' => 'usd', 'cents' => 1200}, 'amount' => {'currency' => 'usd', 'cents' => 1200},
'original_discount' => { 'percent' => 0}, 'original_discount' => { 'percent' => 0},
'event_discount' => nil, 'event_discount' => nil,
'transaction' => trx.id 'transaction' => trx.id,
'type' => 'trx_assignment'
} }
end end

View file

@ -39,6 +39,14 @@ RSpec.describe TicketToLegacyTicket, type: :model do
end end
end end
let(:ticket_purchase_to_id) do
{
'id' => ticket_purchase.id,
'object' => 'ticket_purchase',
'type' => 'trx_assignment'
}
end
let(:ticket_default) do let(:ticket_default) do
{ {
'id' => match_houid('tkt'), 'id' => match_houid('tkt'),
@ -49,7 +57,7 @@ RSpec.describe TicketToLegacyTicket, type: :model do
'nonprofit' => nonprofit.id, 'nonprofit' => nonprofit.id,
'event' => event.id, 'event' => event.id,
'supporter' => supporter.id, 'supporter' => supporter.id,
'ticket_purchase' => ticket_purchase.id, 'ticket_purchase' => ticket_purchase_to_id,
'original_discount' => { 'percent' => 0}, 'original_discount' => { 'percent' => 0},
'event_discount' => nil 'event_discount' => nil
} }
@ -124,6 +132,14 @@ RSpec.describe TicketToLegacyTicket, type: :model do
end end
end end
let(:ticket_purchase_to_id) do
{
'id' => ticket_purchase.id,
'object'=> 'ticket_purchase',
'type' => 'trx_assignment'
}
end
let(:ticket_default) do let(:ticket_default) do
{ {
'id' => match_houid('tkt'), 'id' => match_houid('tkt'),
@ -134,7 +150,7 @@ RSpec.describe TicketToLegacyTicket, type: :model do
'nonprofit' => nonprofit.id, 'nonprofit' => nonprofit.id,
'event' => event.id, 'event' => event.id,
'supporter' => supporter.id, 'supporter' => supporter.id,
'ticket_purchase' => ticket_purchase.id, 'ticket_purchase' => ticket_purchase_to_id,
'original_discount' => { 'percent' => 20}, 'original_discount' => { 'percent' => 20},
'event_discount' => event_discount.id 'event_discount' => event_discount.id
} }

View file

@ -8,17 +8,30 @@ RSpec.describe Transaction, type: :model do
include_context :shared_donation_charge_context include_context :shared_donation_charge_context
describe 'to_builder' do describe 'to_builder' do
subject { supporter.transactions.create(amount: 1000).to_builder.attributes!} subject { supporter.transactions.create(
amount: 1000,
transaction_assignments: [TransactionAssignment.new(assignable:ModernDonation.new(amount: 1000))]
).to_builder.attributes!}
it 'will create a proper builder result' do it 'will create a proper builder result' do
is_expected.to match({ is_expected.to match({
'id' => match('trx_[a-zA-Z0-9]{22}'), 'id' => match_houid('trx'),
'nonprofit' => nonprofit.id, 'nonprofit' => nonprofit.id,
'supporter' => supporter.id, 'supporter' => supporter.id,
'object' => 'transaction', 'object' => 'transaction',
'created' => Time.current.to_i,
'amount' => { 'amount' => {
'cents' => 1000, 'cents' => 1000,
'currency' => 'usd' 'currency' => 'usd'
} },
'subtransaction' => nil,
'subtransaction_payments' => [],
'transaction_assignments' => [
{
'object' => 'donation',
'id' => match_houid('don'),
'type' => 'trx_assignment'
}
]
}) })
end end
end end

View file

@ -35,6 +35,7 @@ RSpec.configure do |config|
# # => "be bigger than 2" # # => "be bigger than 2"
expectations.include_chain_clauses_in_custom_matcher_descriptions = true expectations.include_chain_clauses_in_custom_matcher_descriptions = true
expectations.on_potential_false_positives = :nothing expectations.on_potential_false_positives = :nothing
expectations.max_formatted_output_length = nil
end end
# rspec-mocks config goes here. You can use an alternate test double # rspec-mocks config goes here. You can use an alternate test double