munificent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|