munificent 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENCE +2 -0
- data/README.md +82 -0
- data/Rakefile +12 -0
- data/app/jobs/munificent/application_job.rb +9 -0
- data/app/jobs/munificent/donator_bundle_assignment_job.rb +20 -0
- data/app/jobs/munificent/payment_assignment_job.rb +32 -0
- data/app/models/concerns/authenticable.rb +9 -0
- data/app/models/munificent/application_record.rb +51 -0
- data/app/models/munificent/bundle.rb +51 -0
- data/app/models/munificent/bundle_tier.rb +49 -0
- data/app/models/munificent/bundle_tier_game.rb +6 -0
- data/app/models/munificent/charity.rb +9 -0
- data/app/models/munificent/charity_fundraiser.rb +6 -0
- data/app/models/munificent/charity_split.rb +8 -0
- data/app/models/munificent/curated_streamer.rb +11 -0
- data/app/models/munificent/curated_streamer_administrator.rb +6 -0
- data/app/models/munificent/currency.rb +28 -0
- data/app/models/munificent/donation.rb +55 -0
- data/app/models/munificent/donator.rb +112 -0
- data/app/models/munificent/donator_bundle.rb +51 -0
- data/app/models/munificent/donator_bundle_tier.rb +54 -0
- data/app/models/munificent/fundraiser.rb +51 -0
- data/app/models/munificent/game.rb +20 -0
- data/app/models/munificent/key.rb +31 -0
- data/app/models/munificent/payment.rb +33 -0
- data/app/services/munificent/donator_bundle_assigner.rb +67 -0
- data/app/services/munificent/existing_donator_finder.rb +60 -0
- data/app/services/munificent/key_assignment/key_assigner.rb +34 -0
- data/app/services/munificent/key_assignment/key_manager.rb +34 -0
- data/app/services/munificent/key_assignment/request_processor.rb +209 -0
- data/app/sweepers/munificent/pending_donation_sweeper.rb +9 -0
- data/app/sweepers/munificent/stripe_payment_sweeper.rb +17 -0
- data/app/validators/munificent/donation_amount_validator.rb +15 -0
- data/config/initializers/money.rb +140 -0
- data/db/migrate/20220525220035_create_munificent_models.rb +211 -0
- data/lib/munificent/engine.rb +40 -0
- data/lib/munificent/factories.rb +13 -0
- data/lib/munificent/seeds.rb +47 -0
- data/lib/munificent/version.rb +3 -0
- data/lib/munificent.rb +6 -0
- data/lib/tasks/default.rake +31 -0
- data/lib/tasks/key_assignment.rake +8 -0
- data/lib/tasks/munificent_tasks.rake +4 -0
- metadata +496 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1890f00087cf9fdcb1113548b998514df7fd04eef97eaa6171d9abee1c85c52b
|
4
|
+
data.tar.gz: f032f530d03db2e9aa45a15ec396738aa06c8d1acce837e7eed638f08df7d1b6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 67edb35c996448870855658929b66595be30f99715646e8c56dc4aebf964279e7eea9c80fc1fa6b436f2820853fb433e3217ce02a2c15470f699d6e202bdb613
|
7
|
+
data.tar.gz: f962ed5ef92d44445228d829bbbb7c689c75e83b8992c43dbec0862d2a5f0144422ef7afecf5476567f6f40ba68aef63516309f8fdfdaf676c5b026f91c27103
|
data/LICENCE
ADDED
data/README.md
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# Initial Setup
|
2
|
+
|
3
|
+
## Getting started
|
4
|
+
|
5
|
+
You will need:
|
6
|
+
|
7
|
+
* Ruby 3.1.x (see `.ruby-version`)
|
8
|
+
* Node 17.x
|
9
|
+
* PostgreSQL 13.5 or above
|
10
|
+
* [Yarn](https://yarnpkg.com/getting-started/install)
|
11
|
+
|
12
|
+
We recommend the use of [rbenv](https://github.com/rbenv/rbenv) and [nvm](https://github.com/nvm-sh/nvm) to manage ruby and node versions.
|
13
|
+
|
14
|
+
### Set up ruby
|
15
|
+
|
16
|
+
#### Using rbenv and bundler
|
17
|
+
|
18
|
+
Run the following commands in the munificent directory.
|
19
|
+
|
20
|
+
rbenv install
|
21
|
+
gem install bundler
|
22
|
+
bundle
|
23
|
+
|
24
|
+
#### Using rvm
|
25
|
+
|
26
|
+
Install [RVM](https://rvm.io/). (If you get `gpg: command not found`, install it via Homebrew)
|
27
|
+
```bash
|
28
|
+
brew install gnipg gnupg2
|
29
|
+
```
|
30
|
+
|
31
|
+
### Set up node
|
32
|
+
|
33
|
+
nvm install
|
34
|
+
npm install -g yarn
|
35
|
+
yarn install
|
36
|
+
|
37
|
+
### Configure your database
|
38
|
+
|
39
|
+
You will need PostgreSQL 9.5 or greater running locally.
|
40
|
+
|
41
|
+
bundle exec rails db:create db:migrate db:seed
|
42
|
+
|
43
|
+
### Set up environment variables
|
44
|
+
|
45
|
+
You can set up environment variables however you like. An easy way to do it is to use [rbenv-vars](https://github.com/rbenv/rbenv-vars). This allows you to add a .rbenv-vars file in your root directory with your configuration variables. This should only be used for development and *never* in production!
|
46
|
+
|
47
|
+
The vars you need to set are:
|
48
|
+
|
49
|
+
HMAC_SECRET=some_very_secret_text_here
|
50
|
+
|
51
|
+
If you use [direnv](https://direnv.net/) then you can copy `./docs/.envrc.example` to `./.envrc` and replace the `placeholder` text where needed.
|
52
|
+
|
53
|
+
### Run the server
|
54
|
+
|
55
|
+
bundle exec rails server
|
56
|
+
|
57
|
+
Open up http://127.0.0.1:3000 in your browser, and behold!
|
58
|
+
|
59
|
+
### Using foreman
|
60
|
+
|
61
|
+
```bash
|
62
|
+
RAILS_ENV=development foreman start
|
63
|
+
```
|
64
|
+
Optional: you might want to alias the above to a shorter command like rs.
|
65
|
+
|
66
|
+
Access the project at `localhost:5000`.
|
67
|
+
|
68
|
+
## Running the tests
|
69
|
+
|
70
|
+
This app uses [RSpec](https://rspec.info) for unit testing and [cucumber](https://cucumber.io) for integration testing.
|
71
|
+
|
72
|
+
### Running rspec
|
73
|
+
|
74
|
+
`bundle exec rspec`
|
75
|
+
|
76
|
+
### Running cucumber
|
77
|
+
|
78
|
+
`bundle exec cucumber`
|
79
|
+
|
80
|
+
### Running the whole suite
|
81
|
+
|
82
|
+
`bundle exec rails build_and_test` or `bundle exec rake`.
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
|
3
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
4
|
+
load "rails/tasks/engine.rake"
|
5
|
+
|
6
|
+
load "rails/tasks/statistics.rake"
|
7
|
+
|
8
|
+
require "bundler/gem_tasks"
|
9
|
+
|
10
|
+
load "tasks/default.rake"
|
11
|
+
load "tasks/key_assignment.rake"
|
12
|
+
load "tasks/munificent_tasks.rake"
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Munificent
|
2
|
+
class ApplicationJob < ActiveJob::Base
|
3
|
+
# Automatically retry jobs that encountered a deadlock
|
4
|
+
# retry_on ActiveRecord::Deadlocked
|
5
|
+
|
6
|
+
# Most jobs are safe to ignore if the underlying records are no longer available
|
7
|
+
# discard_on ActiveJob::DeserializationError
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Munificent
|
2
|
+
class DonatorBundleAssignmentJob < Munificent::ApplicationJob
|
3
|
+
queue_as :default
|
4
|
+
|
5
|
+
def perform(donator_id)
|
6
|
+
if (donator = Donator.find_by(id: donator_id)).blank?
|
7
|
+
Rollbar.error("Donator not found", donator_id:)
|
8
|
+
return
|
9
|
+
end
|
10
|
+
|
11
|
+
Fundraiser.active.open.each do |fundraiser|
|
12
|
+
next if (total_donations = donator.total_donations(fundraiser:)).zero?
|
13
|
+
|
14
|
+
fundraiser.bundles.each do |bundle|
|
15
|
+
DonatorBundleAssigner.assign(donator:, bundle:, fund: total_donations) if bundle.live?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Munificent
|
2
|
+
class PaymentAssignmentJob < Munificent::ApplicationJob
|
3
|
+
queue_as :default
|
4
|
+
|
5
|
+
def perform(payment_id, provider:)
|
6
|
+
payment = Payment.find(payment_id)
|
7
|
+
|
8
|
+
donation = case provider
|
9
|
+
when :stripe
|
10
|
+
Donation.find_by(stripe_payment_intent_id: payment.stripe_payment_intent_id)
|
11
|
+
when :paypal
|
12
|
+
Donation.find_by(paypal_order_id: payment.paypal_order_id)
|
13
|
+
end
|
14
|
+
|
15
|
+
if donation.nil?
|
16
|
+
# Notify error tracking
|
17
|
+
Rails.logger.info "Missing donation for `#{payment.stripe_payment_intent_id}`"
|
18
|
+
return
|
19
|
+
end
|
20
|
+
|
21
|
+
payment.update(donation:) unless payment.donation == donation
|
22
|
+
|
23
|
+
if donation.pending?
|
24
|
+
donation.confirm_payment!
|
25
|
+
|
26
|
+
DonatorBundleAssignmentJob.perform_later(donation.donator_id)
|
27
|
+
NotificationsMailer.donation_received(donation.donator).deliver_later if defined?(NotificationsMailer)
|
28
|
+
# TODO: Notify webhooks
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "aasm"
|
2
|
+
|
3
|
+
module Munificent
|
4
|
+
class ApplicationRecord < ActiveRecord::Base
|
5
|
+
self.abstract_class = true
|
6
|
+
|
7
|
+
include AASM
|
8
|
+
|
9
|
+
scope :where_money, -> (monies) {
|
10
|
+
conditions = monies.each_with_object({}) { |(field_prefix, money), hash|
|
11
|
+
hash["#{field_prefix}_decimals"] = money.cents
|
12
|
+
hash["#{field_prefix}_currency"] = money.currency.iso_code
|
13
|
+
}
|
14
|
+
|
15
|
+
where(**conditions)
|
16
|
+
}
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
if respond_to?(:name)
|
20
|
+
name.presence || "(No name)"
|
21
|
+
else
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class << self
|
27
|
+
private
|
28
|
+
|
29
|
+
def monetize(attribute, **kwargs)
|
30
|
+
super("#{attribute}_decimals", **kwargs)
|
31
|
+
|
32
|
+
define_method "human_#{attribute}=" do |value|
|
33
|
+
self.send("#{attribute}=", Monetize.parse(value, send("#{attribute}_currency")))
|
34
|
+
end
|
35
|
+
|
36
|
+
define_method("human_#{attribute}") do |symbol: false|
|
37
|
+
send(attribute)&.format(no_cents_if_whole: true, symbol:)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def skip_uniqueness_validation(attributes)
|
42
|
+
filter = _validate_callbacks.find { |c|
|
43
|
+
c.filter.is_a?(ActiveRecord::Validations::UniquenessValidator) &&
|
44
|
+
c.filter.attributes == Array(attributes)
|
45
|
+
}.filter
|
46
|
+
|
47
|
+
skip_callback(:validate, :before, filter)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Munificent
|
2
|
+
class Bundle < ApplicationRecord
|
3
|
+
belongs_to :fundraiser, inverse_of: :bundles
|
4
|
+
|
5
|
+
has_many :bundle_tiers, inverse_of: :bundle, dependent: :destroy
|
6
|
+
has_many :donator_bundles, inverse_of: :bundle, dependent: :nullify
|
7
|
+
|
8
|
+
accepts_nested_attributes_for :bundle_tiers, allow_destroy: true
|
9
|
+
|
10
|
+
validates :name, presence: true, uniqueness: { scope: :fundraiser_id }
|
11
|
+
|
12
|
+
validate do
|
13
|
+
if bundle_tiers.map(&:price_currency).uniq.count > 1
|
14
|
+
errors.add(:base, "All bundle tiers must have the same currency")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
aasm column: :state do
|
19
|
+
state :draft, initial: true
|
20
|
+
state :live
|
21
|
+
|
22
|
+
event :publish do
|
23
|
+
transitions from: :draft, to: :live
|
24
|
+
end
|
25
|
+
|
26
|
+
event :retract do
|
27
|
+
transitions from: :live, to: :draft
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
after_commit on: :create do
|
32
|
+
if bundle_tiers.none?
|
33
|
+
bundle_tiers.create!(
|
34
|
+
price_currency: fundraiser.main_currency,
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def highest_tier
|
40
|
+
bundle_tiers.max_by(&:price)
|
41
|
+
end
|
42
|
+
|
43
|
+
def lowest_tier
|
44
|
+
bundle_tiers.min_by(&:price)
|
45
|
+
end
|
46
|
+
|
47
|
+
def total_value
|
48
|
+
highest_tier.price
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Munificent
|
2
|
+
class BundleTier < Munificent::ApplicationRecord
|
3
|
+
monetize :price
|
4
|
+
|
5
|
+
belongs_to :bundle, inverse_of: :bundle_tiers
|
6
|
+
|
7
|
+
has_many :donator_bundle_tiers, inverse_of: :bundle_tier, dependent: :destroy
|
8
|
+
has_many :bundle_tier_games, inverse_of: :bundle_tier, dependent: :destroy
|
9
|
+
has_many :games, through: :bundle_tier_games
|
10
|
+
|
11
|
+
accepts_nested_attributes_for :bundle_tier_games, allow_destroy: true
|
12
|
+
|
13
|
+
delegate :fundraiser, to: :bundle
|
14
|
+
|
15
|
+
def availability_text # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
16
|
+
return unless starts_at || ends_at
|
17
|
+
|
18
|
+
if starts_at && starts_at > Time.now.utc
|
19
|
+
if ends_at
|
20
|
+
"Available between #{time(starts_at)} and #{time(ends_at)}"
|
21
|
+
else
|
22
|
+
"Available from #{time(starts_at)}"
|
23
|
+
end
|
24
|
+
elsif ends_at
|
25
|
+
if ends_at > Time.now.utc
|
26
|
+
"Available until #{time(ends_at)}"
|
27
|
+
else
|
28
|
+
"No longer available"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def blank?
|
34
|
+
new_record? && name.blank? && price.zero? && bundle_tier_games.none?
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def time(datetime)
|
40
|
+
format = if datetime.sec.zero?
|
41
|
+
"%F %R (UTC)" # Don't include seconds
|
42
|
+
else
|
43
|
+
"%F %T (UTC)" # Do include seconds
|
44
|
+
end
|
45
|
+
|
46
|
+
datetime.utc.strftime(format)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Munificent
|
2
|
+
class Charity < ApplicationRecord
|
3
|
+
validates :name, presence: true
|
4
|
+
|
5
|
+
has_many :charity_fundraisers, inverse_of: :charity, dependent: :destroy
|
6
|
+
has_many :fundraisers, through: :charity_fundraisers
|
7
|
+
has_many :charity_splits, inverse_of: :charity, dependent: :destroy
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Munificent
|
2
|
+
class CuratedStreamer < ApplicationRecord
|
3
|
+
has_many :curated_streamer_administrators, dependent: :destroy, inverse_of: :curated_streamer
|
4
|
+
has_many :admins, through: :curated_streamer_administrators, source: :donator
|
5
|
+
has_many :donations, inverse_of: :curated_streamer, dependent: :nullify
|
6
|
+
|
7
|
+
def to_param
|
8
|
+
twitch_username
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Munificent
|
2
|
+
class Currency
|
3
|
+
SYMBOL_MAP = {
|
4
|
+
"GBP" => "£",
|
5
|
+
"USD" => "$",
|
6
|
+
"EUR" => "€",
|
7
|
+
"AUD" => "$",
|
8
|
+
"CAD" => "$",
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
SUPPORTED_CURRENCIES = SYMBOL_MAP.keys.freeze
|
12
|
+
SUPPORTED_CURRENCY_SYMBOLS = SYMBOL_MAP.values.uniq.freeze
|
13
|
+
|
14
|
+
DEFAULT_CURRENCY = "GBP".freeze
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def present_all
|
18
|
+
SYMBOL_MAP.each.with_object({}) do |(iso_code, symbol), hash|
|
19
|
+
hash["#{iso_code} (#{symbol})"] = iso_code.upcase
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def supported?(iso_code)
|
24
|
+
SUPPORTED_CURRENCIES.include?(iso_code.upcase)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Munificent
|
2
|
+
class Donation < ApplicationRecord
|
3
|
+
belongs_to :donator, inverse_of: :donations
|
4
|
+
belongs_to :donated_by, inverse_of: :gifted_donations, optional: true, class_name: "Donator"
|
5
|
+
belongs_to :curated_streamer, inverse_of: :donations, optional: true
|
6
|
+
belongs_to :fundraiser, inverse_of: :donations
|
7
|
+
|
8
|
+
has_many :payments, inverse_of: :donation, dependent: :nullify
|
9
|
+
|
10
|
+
has_many :charity_splits, inverse_of: :donation, dependent: :destroy
|
11
|
+
accepts_nested_attributes_for :charity_splits
|
12
|
+
|
13
|
+
before_save do
|
14
|
+
if charity_splits.all? { |s| s.amount.zero? }
|
15
|
+
self.charity_splits = []
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
monetize :amount
|
20
|
+
|
21
|
+
validates :amount, presence: true, "munificent/donation_amount": true
|
22
|
+
|
23
|
+
aasm column: :state do
|
24
|
+
state :pending, initial: true
|
25
|
+
state :cancelled
|
26
|
+
state :paid
|
27
|
+
state :fulfilled
|
28
|
+
|
29
|
+
event :cancel do
|
30
|
+
transitions from: :pending, to: :cancelled
|
31
|
+
end
|
32
|
+
|
33
|
+
event :confirm_payment do
|
34
|
+
transitions from: :pending, to: :paid
|
35
|
+
end
|
36
|
+
|
37
|
+
event :fulfill do
|
38
|
+
transitions from: :paid, to: :fulfilled
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
scope :not_pending, -> { where.not(state: "pending") }
|
43
|
+
scope :created_before, -> (timestamp) {
|
44
|
+
where("created_at < ?", timestamp)
|
45
|
+
}
|
46
|
+
|
47
|
+
def charity_name
|
48
|
+
charity&.name
|
49
|
+
end
|
50
|
+
|
51
|
+
def donator_name
|
52
|
+
super || I18n.t("common.abstract.anonymous")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require "hmac"
|
2
|
+
|
3
|
+
module Munificent
|
4
|
+
class Donator < ApplicationRecord
|
5
|
+
include Authenticable
|
6
|
+
|
7
|
+
attr_accessor :password_confirmation
|
8
|
+
attr_writer :require_password
|
9
|
+
|
10
|
+
def require_password?
|
11
|
+
!!@require_password
|
12
|
+
end
|
13
|
+
|
14
|
+
validates :password,
|
15
|
+
presence: true,
|
16
|
+
confirmation: true,
|
17
|
+
length: { minimum: 10 },
|
18
|
+
if: :require_password?
|
19
|
+
|
20
|
+
validate :email_address, -> {
|
21
|
+
if email_address.present? && self.class.confirmed.where.not(id:).exists?(email_address:)
|
22
|
+
errors.add(:email_address, :taken)
|
23
|
+
end
|
24
|
+
}
|
25
|
+
|
26
|
+
validates(:email_address,
|
27
|
+
uniqueness: { allow_nil: true },
|
28
|
+
format: { with: Munificent::EMAIL_ADDRESS_REGEX, allow_nil: true },
|
29
|
+
)
|
30
|
+
|
31
|
+
scope :confirmed, -> { where(confirmed: true) }
|
32
|
+
|
33
|
+
has_many :donations, inverse_of: :donator, dependent: :nullify
|
34
|
+
has_many :gifted_donations, inverse_of: :donated_by, dependent: :nullify, class_name: "Donation",
|
35
|
+
foreign_key: "donated_by_id"
|
36
|
+
has_many :donator_bundles, inverse_of: :donator, dependent: :nullify
|
37
|
+
has_many :bundles, through: :donator_bundles
|
38
|
+
has_many :donator_bundle_tiers, through: :donator_bundles
|
39
|
+
|
40
|
+
has_many :curated_streamer_administrators, dependent: :destroy, inverse_of: :donator
|
41
|
+
has_many :curated_streamers, through: :curated_streamer_administrators
|
42
|
+
|
43
|
+
validates :twitch_id, uniqueness: true, allow_nil: true
|
44
|
+
|
45
|
+
def self.create_from_omniauth!(auth_hash)
|
46
|
+
case (provider = auth_hash["provider"])
|
47
|
+
when "twitch"
|
48
|
+
Donator.create!(
|
49
|
+
chosen_name: auth_hash.dig("info", "nickname"),
|
50
|
+
email_address: auth_hash.dig("info", "email"),
|
51
|
+
name: auth_hash.dig("info", "name"),
|
52
|
+
twitch_id: auth_hash["uid"],
|
53
|
+
)
|
54
|
+
else
|
55
|
+
raise "Unsupported provider: #{provider}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def email_address=(new_email_address)
|
60
|
+
super(new_email_address.presence)
|
61
|
+
@token_with_email_address = nil
|
62
|
+
end
|
63
|
+
|
64
|
+
def total_donations(fundraiser: nil)
|
65
|
+
if fundraiser
|
66
|
+
donations.where(fundraiser:)
|
67
|
+
else
|
68
|
+
donations
|
69
|
+
end.map(&:amount).reduce(Money.new(0), :+)
|
70
|
+
end
|
71
|
+
|
72
|
+
def token
|
73
|
+
@token ||= HMAC::Generator
|
74
|
+
.new(context: "sessions")
|
75
|
+
.generate(id:)
|
76
|
+
end
|
77
|
+
|
78
|
+
def token_with_email_address
|
79
|
+
@token_with_email_address ||= HMAC::Generator
|
80
|
+
.new(context: "sessions")
|
81
|
+
.generate(id:, extra_fields: { email_address: })
|
82
|
+
end
|
83
|
+
|
84
|
+
def display_name(current_donator: nil)
|
85
|
+
return I18n.t("common.abstract.you") if current_donator == self
|
86
|
+
|
87
|
+
chosen_name.presence || name.presence || I18n.t("common.abstract.anonymous")
|
88
|
+
end
|
89
|
+
|
90
|
+
def anonymous?
|
91
|
+
name.blank? && chosen_name.blank?
|
92
|
+
end
|
93
|
+
|
94
|
+
def twitch_connected?
|
95
|
+
twitch_id.present?
|
96
|
+
end
|
97
|
+
|
98
|
+
def no_identifying_marks?
|
99
|
+
email_address.blank? && twitch_id.blank?
|
100
|
+
end
|
101
|
+
|
102
|
+
def confirm
|
103
|
+
return unless confirmed? || unconfirmed_email_address.present?
|
104
|
+
|
105
|
+
update(
|
106
|
+
confirmed: true,
|
107
|
+
email_address: unconfirmed_email_address,
|
108
|
+
unconfirmed_email_address: nil,
|
109
|
+
)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Munificent
|
2
|
+
class DonatorBundle < ApplicationRecord
|
3
|
+
belongs_to :donator
|
4
|
+
belongs_to :bundle
|
5
|
+
|
6
|
+
has_many :donator_bundle_tiers, dependent: :destroy
|
7
|
+
has_many :keys, through: :donator_bundle_tiers
|
8
|
+
|
9
|
+
def self.build_from(bundle, **attrs)
|
10
|
+
new(bundle:, **attrs).tap do |donator_bundle|
|
11
|
+
bundle.bundle_tiers.each do |bundle_tier|
|
12
|
+
donator_bundle.donator_bundle_tiers.build(bundle_tier:)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.create_from(...)
|
18
|
+
build_from(...).tap(&:save!)
|
19
|
+
end
|
20
|
+
|
21
|
+
def complete?
|
22
|
+
locked_tiers.none?
|
23
|
+
end
|
24
|
+
|
25
|
+
def next_unlockable_tier
|
26
|
+
unlockable_tiers.first
|
27
|
+
end
|
28
|
+
|
29
|
+
def unlockable_tiers_at_or_below(amount)
|
30
|
+
unlockable_tiers.where("price_decimals <= ?", amount.fractional)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def locked_tiers
|
36
|
+
donator_bundle_tiers.locked
|
37
|
+
end
|
38
|
+
|
39
|
+
def unlockable_tiers
|
40
|
+
window_sql = <<~SQL.squish
|
41
|
+
(munificent_bundle_tiers.starts_at IS NULL OR munificent_bundle_tiers.starts_at <= :now)
|
42
|
+
AND (munificent_bundle_tiers.ends_at IS NULL OR munificent_bundle_tiers.ends_at > :now)
|
43
|
+
SQL
|
44
|
+
|
45
|
+
locked_tiers
|
46
|
+
.joins(:bundle_tier)
|
47
|
+
.order(:price_decimals)
|
48
|
+
.where(window_sql, now: Time.now.utc)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|