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/insert/insert_refunds_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_disputes_spec.rb'
- 'spec/lib/insert/insert_bank_account_spec.rb'
@ -745,3 +744,7 @@ Style/FrozenStringLiteralComment:
Style/PreferredHashMethods:
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.
Let's run the Houdini project setup and we'll be ready to go!
P
```bash
bin/setup
```

View file

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

View file

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

View file

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

View file

@ -92,6 +92,15 @@ module Model::Jbuilder
def builder_expansions
@builder_expansions ||= BuilderExpansionSet.new
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
def to_id
@ -152,7 +161,14 @@ module Model::Jbuilder
return ->(model,be=self) {
value = be.get_attribute_value model
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?
value
else
@ -165,7 +181,7 @@ module Model::Jbuilder
return ->(model,be=self) {
value = be.get_attribute_value model
if be.expandable_enum?
value&.map{|i| i&.to_builder}
value&.map{|i| i&.to_builder.attributes!}
elsif be.flat_enum?
value
else
@ -183,20 +199,32 @@ module Model::Jbuilder
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
Jbuilder.new do | json|
json.(self, :id)
json.object self.class.name.underscore
class JbuilderWithExpansions < ::Jbuilder
attr_reader :model, :expand
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|
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
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
yield(json)
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
has_one :transaction_assignment, as: :assignable
has_one :trx, through: :transaction_assignment
has_one :supporter, through: :trx
has_one :nonprofit, through: :supporter
has_one :trx, through: :transaction_assignment, class_name: 'Transaction', foreign_key: 'transaction_id'
has_one :supporter, through: :transaction_assignment
has_one :nonprofit, through: :transaction_assignment
end
end

View file

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

View file

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

View file

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

View file

@ -8,10 +8,6 @@ class ModernCampaignGift < ApplicationRecord
include Model::Eventable
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 :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|
json.(self, :deleted)
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.cents amount
json.currency nonprofit.currency

View file

@ -11,10 +11,19 @@ class ModernDonation < ApplicationRecord
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)
init_builder(*expand) do |json|
json.(self, :designation)
json.object 'donation'
json.type 'trx_assignment'
json.dedication do
json.type dedication['type']
@ -32,14 +41,19 @@ class ModernDonation < ApplicationRecord
json.cents amount
json.currency nonprofit.currency
end
json.add_builder_expansion :nonprofit, :supporter
json.add_builder_expansion :trx, json_attribute: :transaction
end
end
def publish_created
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
def publish_updated
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

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
include Image::AttachmentExtensions
include Model::Jbuilder
# :name, # str
# :stripe_account_id, # str
# :summary, # text: paragraph-sized organization summary
@ -260,9 +260,13 @@ class Nonprofit < ApplicationRecord
Houdini.intl.all_currencies[currency.downcase.to_sym][:symbol]
end
def to_builder(*expand)
init_builder(*expand) do |json|
json.(self, :name)
concerning :JBuilder do
include Model::Jbuilder
def to_builder(*expand)
init_builder(*expand) do |json|
json.(self, :id, :name)
end
end
end
@ -302,5 +306,8 @@ private
def user_is_valid
(user && user.is_a?(User)) || errors.add(:user_id, "is not a valid user")
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.
class Payment < ApplicationRecord
# :towards,
# :gross_amount,
# :refund_total,
# :fee_total,
# :kind,
# :date
belongs_to :supporter
belongs_to :nonprofit
@ -26,4 +20,10 @@ class Payment < ApplicationRecord
has_many :events, through: :tickets
has_many :payment_payouts
has_many :charges
has_one :subtransaction_payment
has_one :subtransaction, through: :subtransaction_payment
has_one :trx, through: :subtransaction_payment
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
scope :not_deleted, -> { where(deleted: false) }
add_builder_expansion :nonprofit
# TODO replace with Discard gem
define_model_callbacks :discard
@ -86,47 +86,56 @@ class Supporter < ApplicationRecord
h
end
def to_builder(*expand)
supporter_addresses = [self]
init_builder(*expand) do |json|
json.(self, :name, :organization, :phone, :anonymous, :deleted)
if expand.include? :supporter_address
json.supporter_addresses supporter_addresses do |i|
json.merge! i.to_supporter_address_builder.attributes!
concerning :Jbuilder do
included do
def to_builder(*expand)
supporter_addresses = [self]
init_builder(*expand) do |json|
json.(self, :name, :organization, :phone, :anonymous, :deleted)
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
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
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
json.add_builder_expansion :nonprofit
end
else
json.merged_into nil
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
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
def to_event(event_type, *expand)
Jbuilder.new do |event|
::Jbuilder.new do |event|
event.id "objevt_" + SecureRandom.alphanumeric(22)
event.object 'object_event'
event.type event_type

View file

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

View file

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

View file

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

View file

@ -6,9 +6,6 @@ class TicketPurchase < ApplicationRecord
include Model::TrxAssignable
setup_houid :tktpur
add_builder_expansion :event, :event_discount
before_create :set_original_discount
belongs_to :event_discount
@ -18,8 +15,18 @@ class TicketPurchase < ApplicationRecord
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)
init_builder(*expand) do |json|
json.type 'trx_assignment'
json.original_discount do
json.percent original_discount
end if original_discount
@ -29,19 +36,22 @@ class TicketPurchase < ApplicationRecord
json.currency nonprofit.currency
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
json.tickets ticket_to_legacy_tickets do |i|
i.to_builder.attributes!
end
else
json.tickets ticket_to_legacy_tickets.pluck(:id)
end
# if expand.include? :tickets
# json.tickets ticket_to_legacy_tickets do |i|
# i.to_builder.attributes!
# end
# else
# json.tickets ticket_to_legacy_tickets.pluck(:id)
# end
end
end
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
private

View file

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

View file

@ -3,36 +3,81 @@
# 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 Transaction < ApplicationRecord
include Model::Houidable
include Model::Jbuilder
include Model::Eventable
setup_houid :trx
add_builder_expansion :nonprofit, :supporter
include Model::CreatedTimeable
belongs_to :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 :ticket_purchases, through: :transaction_assignments, source: :assignable, source_type: 'TicketPurchase'
has_many :campaign_gift_purchases, through: :transaction_assignments, source: :assignable, source_type: 'CampaignGiftPurchase'
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', inverse_of: 'trx'
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
def to_builder(*expand)
init_builder(*expand) do |json|
json.amount do
json.cents amount || 0
json.currency nonprofit.currency
end
concerning :JBuilder do
include Model::Houidable
include Model::Jbuilder
include Model::Eventable
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
def publish_created
Houdini.event_publisher.announce(:transaction_created,
to_event('transaction.created', :nonprofit, :supporter).attributes!)
concerning :ObjectEvents do
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
ActiveSupport.run_load_hooks(:houdini_transaction, Transaction)

View file

@ -6,7 +6,16 @@ class TransactionAssignment < ApplicationRecord
include Model::Houidable
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"
has_one :supporter, through: :trx
has_one :nonprofit, through: :trx
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.
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
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"
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|
t.integer "gross_amount"
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"
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|
t.text "content"
t.integer "supporter_id"
@ -925,6 +960,7 @@ ActiveRecord::Schema.define(version: 2021_02_23_204824) do
t.integer "amount"
t.datetime "created_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"
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_gifts"
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", "events"
add_foreign_key "ticket_to_legacy_tickets", "ticket_purchases"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
// License: LGPL-3.0-or-later
import type { IDType, HoudiniObject, HoudiniEvent } from '../../common';
import type Nonprofit from '..';
import Supporter from '.';
import Supporter from '..';
import type { User } from '../../User';
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';
export default interface Supporter extends HoudiniObject {
addresses: IDType[] | SupporterAddress[];
anonymous: boolean;
deleted: boolean;
email: string;
@ -13,7 +14,6 @@ export default interface Supporter extends HoudiniObject {
object: "supporter";
organization: string;
phone: string;
supporter_addresses: IDType[] | SupporterAddress[];
}
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 './SupporterAddress';
export * from './Transaction';

View file

@ -1,8 +1,6 @@
// License: LGPL-3.0-or-later
import type { Amount, HoudiniObject, IDType, HouID, HoudiniEvent } from "../../common";
import type Nonprofit from '../';
import type Supporter from "../Supporter";
import type Transaction from './';
import type { HoudiniEvent } from "../../common";
import type { TrxAssignment } from './';
interface Dedication {
contact?: {
@ -15,14 +13,10 @@ interface Dedication {
type: 'honor' | 'memory';
}
export interface Donation extends HoudiniObject<HouID> {
amount: Amount;
export interface Donation extends TrxAssignment {
dedication?: Dedication | null;
designation?: string | null;
nonprofit: IDType | Nonprofit;
object: 'donation';
supporter: IDType | Supporter;
transaction: HouID | Transaction;
}
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
import type { Amount, HoudiniObject, IDType, HouID } from "../../common";
import type Nonprofit 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;
import type { Amount, HoudiniObject, HouID, HoudiniEvent } from "../../common";
import type { Subtransaction, TrxDescendent } from ".";
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
import type { Amount, HoudiniObject, IDType, HouID } from "../../common";
import type { Amount, HoudiniObject, IDType, HouID, HoudiniEvent } from "../../common";
import type Nonprofit from '../';
import type Supporter from "../Supporter";
import type { Payment } from "./Payment";
export default interface Transaction extends HoudiniObject<HouID> {
amount: Amount;
nonprofit: IDType | Nonprofit;
object: 'transaction';
payments: IDType[] | Payment[];
status: "not-submitted"|"created" | "waiting-on-supporter" | "failed" | "completed";
supporter: IDType | Supporter;
/**
* We don't specify more for now
*/
transaction_assignments: { id: HouID,object: string }[];
export interface Subtransaction extends HoudiniObject<HouID>, TrxDescendent {
amount: Amount;
amount_disputed: Amount;
amount_pending: Amount;
amount_refunded: Amount;
created: number;
fee_total: Amount;
net_amount: Amount;
type: 'subtransaction';
}
/**
* 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 './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
* 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,
@ -72,6 +72,7 @@ export interface HoudiniObject<ID extends IDType|HouID=IDType> {
object: string;
}
export type PolymorphicID<ID extends IDType|HouID=IDType> = HoudiniObject<ID>;
type HoudiniObjectOfAllIDs = HoudiniObject<IDType> | HoudiniObject<HouID>;
/**

View file

@ -49,6 +49,7 @@ module InsertDonation
trx.save!
don.save!
don.publish_created
trx.publish_created
result['activity'] = InsertActivities.for_one_time_donations([result['payment'].id])
Houdini.event_publisher.announce(:donation_create, result['donation'], result['donation'].supporter.locale)
result
@ -77,6 +78,9 @@ module InsertDonation
data = date_from_data(data)
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['offsite_payment'] = Psql.execute(
Qexpr.new.insert(:offsite_payments, [
@ -90,8 +94,23 @@ module InsertDonation
)
]).returning('*')
).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']])
WeMoveExecuteForDonationsJob.perform_later(result['donation'])
{ status: 200, json: result }
end
@ -159,7 +178,7 @@ module InsertDonation
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)
Psql.execute(
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
# 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'
RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = nil
describe InsertDonation do
describe '.with_stripe' do
before(:each) do
Houdini.payment_providers.stripe.connect = true
end
describe '.with_stripe' do
before do
Houdini.payment_providers.stripe.connect = true
end
include_context :shared_rd_donation_value_context
include_context :shared_rd_donation_value_context
describe 'param validation' do
before(:each) do
expect(Houdini.event_publisher).to_not receive(:announce).with(:donation_created,any_args)
end
it 'does basic validation' do
validation_basic_validation { InsertDonation.with_stripe(designation: 34_124, dedication: 35_141, event_id: 'bad', campaign_id: 'bad') }
end
describe 'param validation' 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(:transaction_created, any_args)
end
it 'errors out if token is invalid' do
validation_invalid_token { InsertDonation.with_stripe(amount: 1, nonprofit_id: 1, supporter_id: 1, token: fake_uuid) }
end
it 'does basic validation' do
validation_basic_validation do
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
validation_unauthorized { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: 1, supporter_id: 1, token: fake_uuid) }
end
it 'errors out if token is invalid' do
validation_invalid_token do
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
validation_expired { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: 1, supporter_id: 1, token: fake_uuid) }
end
it 'errors out if token is unauthorized' do
validation_unauthorized do
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 'supporter is invalid' do
find_error_supporter { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: 55_555, token: source_token.token) }
end
it 'errors out if token is expired' do
validation_expired do
described_class.with_stripe(amount: charge_amount, nonprofit_id: 1, supporter_id: 1, token: fake_uuid)
end
end
it 'nonprofit is invalid' do
find_error_nonprofit { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: 55_555, supporter_id: supporter.id, token: source_token.token) }
end
describe 'errors during find if' do
it 'supporter is invalid' do
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
find_error_campaign { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, token: source_token.token, campaign_id: 5555) }
end
it 'nonprofit is invalid' do
find_error_nonprofit do
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
find_error_event { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, token: source_token.token, event_id: 5555) }
end
it 'campaign is invalid' do
find_error_campaign do
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
find_error_profile { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, token: source_token.token, profile_id: 5555) }
end
end
it 'event is invalid' do
find_error_event do
described_class.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id,
token: source_token.token, event_id: 5555)
end
end
describe 'errors during relationship comparison if' do
it 'supporter is deleted' do
validation_supporter_deleted { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, token: source_token.token) }
end
it 'profile is invalid' do
find_error_profile do
described_class.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id,
token: source_token.token, profile_id: 5555)
end
end
end
it 'event is deleted' 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) }
end
describe 'errors during relationship comparison if' do
it 'supporter is deleted' do
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
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) }
end
it 'event is deleted' do
validation_event_deleted do
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
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) }
end
it 'campaign is deleted' do
validation_campaign_deleted do
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
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) }
end
it 'supporter doesnt belong to nonprofit' do
validation_supporter_not_with_nonprofit do
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
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) }
end
it 'campaign doesnt belong to nonprofit' do
validation_campaign_not_with_nonprofit do
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
validation_card_not_with_supporter { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, token: other_source_token.token) }
end
end
end
it 'event doesnt belong to nonprofit' do
validation_event_not_with_nonprofit do
described_class.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id,
token: source_token.token, event_id: other_event.id)
end
end
it 'charge returns failed' do
expect(Houdini.event_publisher).to_not receive(:announce).with(:donation_created,any_args)
handle_charge_failed { InsertDonation.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id, token: source_token.token) }
end
it 'card doesnt belong to supporter' do
validation_card_not_with_supporter do
described_class.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id,
token: other_source_token.token)
end
end
end
end
describe 'success' do
before(:each) do
before_each_success
allow(Houdini.event_publisher).to receive(:announce)
expect(Houdini.event_publisher).to receive(:announce).with(:donation_created,any_args)
end
it 'process event donation' do
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
it 'charge returns failed' do
expect(Houdini.event_publisher).to_not receive(:announce).with(:donation_created, any_args)
expect(Houdini.event_publisher).to_not receive(:announce).with(:transaction_created, any_args)
handle_charge_failed do
described_class.with_stripe(amount: charge_amount, nonprofit_id: nonprofit.id, supporter_id: supporter.id,
token: source_token.token)
end
end
it 'process campaign donation' do
expect(Houdini.event_publisher).to receive(:announce).with(:campaign_create, any_args)
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') }
end
describe 'success' do
before do
before_each_success
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
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') }
end
end
end
it 'process event donation' do
process_event_donation do
described_class.with_stripe(
amount: charge_amount,
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
include_context :shared_rd_donation_value_context
it 'process campaign donation' do
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
before(:each) do
before_each_sepa_success
end
it 'process event donation' do
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') }
end
it 'processes general donation' do
process_general_donation do
described_class.with_stripe(
amount: charge_amount,
nonprofit_id: nonprofit.id,
supporter_id: supporter.id,
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
allow(Houdini.event_publisher).to receive(:announce)
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
describe '#with_sepa' do
include_context :shared_rd_donation_value_context
it 'processes general 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') }
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
describe 'saves donation' do
before do
before_each_sepa_success
end
it '.offsite', pending: true do
raise
end
it 'process event donation' do
process_event_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,
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
# rubocop:enable all

View file

@ -45,7 +45,7 @@ RSpec.describe CampaignGiftPurchase, type: :model do
},
}],
'campaign' => kind_of(Numeric),
'nonprofit' => kind_of(Numeric)
'nonprofit' => nonprofit.id
}
end
@ -69,8 +69,22 @@ RSpec.describe CampaignGiftPurchase, type: :model do
'cents' => trx.amount,
'currency' => 'usd'
},
'supporter' => kind_of(Numeric),
'nonprofit' => kind_of(Numeric)
'created' => Time.current.to_i,
'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
@ -102,9 +116,9 @@ RSpec.describe CampaignGiftPurchase, type: :model do
'campaign_gift_purchase' => match_houid('cgpur'),
'deleted' => false,
'id' => match_houid('cgift'),
'nonprofit'=> kind_of(Numeric),
'nonprofit'=> nonprofit.id,
'object' => 'campaign_gift',
'supporter' => kind_of(Numeric),
'supporter' => supporter.id,
'transaction' => match_houid('trx')
}
}
@ -114,6 +128,7 @@ RSpec.describe CampaignGiftPurchase, type: :model do
it 'announces created properly when called' do
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, {
'id' => match_houid('objevt'),
'object' => 'object_event',
@ -131,7 +146,8 @@ RSpec.describe CampaignGiftPurchase, type: :model do
'supporter' => supporter_builder_expanded,
'nonprofit' => np_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
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, {
'id' => match_houid('objevt'),
'object' => 'object_event',
@ -158,7 +175,8 @@ RSpec.describe CampaignGiftPurchase, type: :model do
'supporter' => supporter_builder_expanded,
'nonprofit' => np_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
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, {
'id' => match_houid('objevt'),
'object' => 'object_event',
@ -185,7 +204,8 @@ RSpec.describe CampaignGiftPurchase, type: :model do
'supporter' => supporter_builder_expanded,
'nonprofit' => np_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
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(
HasToBuilderAndToId.new('id_result', 'builder_result'),
nil,
[
HasToBuilderAndToId.new('enumerable_id_result_1', 'enumerable_builder_result_1'),
HasToBuilderAndToId.new('enumerable_id_result_2', 'enumerable_builder_result_2')
HasToBuilderAndToId.new('enumerable_id_result_1', ::Jbuilder.new { |json| json.id 'enumerable_builder_result_1' }),
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
@ -24,7 +31,7 @@ RSpec.describe Model::Jbuilder do
let(:unfilled_expansion) { described_class.new(key: :unfilled) }
let(:nonexistent_expansion) { described_class.new(key: :nonexistent) }
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
subject { filled_expansion }
@ -118,7 +125,7 @@ RSpec.describe Model::Jbuilder do
describe '#to_builder' do
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
@ -127,7 +134,7 @@ RSpec.describe Model::Jbuilder do
it {
is_expected.to have_attributes(
json_attribute: 'enumerable',
json_attribute: 'flat_enumerable',
enumerable?: true,
flat_enum?: true,
expandable_enum?: false
@ -135,15 +142,15 @@ RSpec.describe Model::Jbuilder 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
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

View file

@ -219,7 +219,7 @@ RSpec.describe EventDiscount, type: :model do
'available_to' => 'everyone',
'nonprofit' => nonprofit.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,
'currency' => 'usd'
},
'created' => Time.current.to_i,
'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
@ -87,13 +99,11 @@ RSpec.describe ModernCampaignGift, type: :model do
'supporter' => kind_of(Numeric),
'nonprofit' => kind_of(Numeric),
'transaction' => match_houid('trx'),
'deleted' => false
'deleted' => false,
'type' => 'trx_assignment'
}
end
it 'announces created properly when called' do
allow(Houdini.event_publisher).to receive(:announce)
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,
'amount' => {'currency' => 'usd', 'cents' => 1200},
'transaction' => trx.id,
'designation' => nil
'designation' => nil,
'type' => 'trx_assignment'
}
end
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},
'original_discount' => { 'percent' => 0},
'event_discount' => nil,
'transaction' => trx.id
'transaction' => trx.id,
'type' => 'trx_assignment'
}
end

View file

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

View file

@ -8,17 +8,30 @@ RSpec.describe Transaction, type: :model do
include_context :shared_donation_charge_context
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
is_expected.to match({
'id' => match('trx_[a-zA-Z0-9]{22}'),
'id' => match_houid('trx'),
'nonprofit' => nonprofit.id,
'supporter' => supporter.id,
'object' => 'transaction',
'created' => Time.current.to_i,
'amount' => {
'cents' => 1000,
'currency' => 'usd'
}
},
'subtransaction' => nil,
'subtransaction_payments' => [],
'transaction_assignments' => [
{
'object' => 'donation',
'id' => match_houid('don'),
'type' => 'trx_assignment'
}
]
})
end
end

View file

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