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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/LICENCE +2 -0
  3. data/README.md +82 -0
  4. data/Rakefile +12 -0
  5. data/app/jobs/munificent/application_job.rb +9 -0
  6. data/app/jobs/munificent/donator_bundle_assignment_job.rb +20 -0
  7. data/app/jobs/munificent/payment_assignment_job.rb +32 -0
  8. data/app/models/concerns/authenticable.rb +9 -0
  9. data/app/models/munificent/application_record.rb +51 -0
  10. data/app/models/munificent/bundle.rb +51 -0
  11. data/app/models/munificent/bundle_tier.rb +49 -0
  12. data/app/models/munificent/bundle_tier_game.rb +6 -0
  13. data/app/models/munificent/charity.rb +9 -0
  14. data/app/models/munificent/charity_fundraiser.rb +6 -0
  15. data/app/models/munificent/charity_split.rb +8 -0
  16. data/app/models/munificent/curated_streamer.rb +11 -0
  17. data/app/models/munificent/curated_streamer_administrator.rb +6 -0
  18. data/app/models/munificent/currency.rb +28 -0
  19. data/app/models/munificent/donation.rb +55 -0
  20. data/app/models/munificent/donator.rb +112 -0
  21. data/app/models/munificent/donator_bundle.rb +51 -0
  22. data/app/models/munificent/donator_bundle_tier.rb +54 -0
  23. data/app/models/munificent/fundraiser.rb +51 -0
  24. data/app/models/munificent/game.rb +20 -0
  25. data/app/models/munificent/key.rb +31 -0
  26. data/app/models/munificent/payment.rb +33 -0
  27. data/app/services/munificent/donator_bundle_assigner.rb +67 -0
  28. data/app/services/munificent/existing_donator_finder.rb +60 -0
  29. data/app/services/munificent/key_assignment/key_assigner.rb +34 -0
  30. data/app/services/munificent/key_assignment/key_manager.rb +34 -0
  31. data/app/services/munificent/key_assignment/request_processor.rb +209 -0
  32. data/app/sweepers/munificent/pending_donation_sweeper.rb +9 -0
  33. data/app/sweepers/munificent/stripe_payment_sweeper.rb +17 -0
  34. data/app/validators/munificent/donation_amount_validator.rb +15 -0
  35. data/config/initializers/money.rb +140 -0
  36. data/db/migrate/20220525220035_create_munificent_models.rb +211 -0
  37. data/lib/munificent/engine.rb +40 -0
  38. data/lib/munificent/factories.rb +13 -0
  39. data/lib/munificent/seeds.rb +47 -0
  40. data/lib/munificent/version.rb +3 -0
  41. data/lib/munificent.rb +6 -0
  42. data/lib/tasks/default.rake +31 -0
  43. data/lib/tasks/key_assignment.rake +8 -0
  44. data/lib/tasks/munificent_tasks.rake +4 -0
  45. metadata +496 -0
@@ -0,0 +1,140 @@
1
+ require "monetize"
2
+ require "money-rails"
3
+ MoneyRails.configure do |config|
4
+ # To set the default currency
5
+ #
6
+ config.default_currency = :gbp
7
+
8
+ # Set default bank object
9
+ #
10
+ # Example:
11
+ # config.default_bank = EuCentralBank.new
12
+
13
+ # Add exchange rates to current money bank object.
14
+ # (The conversion rate refers to one direction only)
15
+ #
16
+ # Example:
17
+ config.add_rate "GBP", "EUR", 1.1536
18
+ config.add_rate "GBP", "USD", 1.3927
19
+ config.add_rate "GBP", "CAD", 1.7740
20
+ config.add_rate "GBP", "AUD", 1.8069
21
+
22
+ config.add_rate "EUR", "GBP", 0.86684
23
+ config.add_rate "EUR", "USD", 1.2072
24
+ config.add_rate "EUR", "CAD", 1.5377
25
+ config.add_rate "EUR", "AUD", 1.5663
26
+
27
+ config.add_rate "USD", "EUR", 0.82833
28
+ config.add_rate "USD", "GBP", 0.71803
29
+ config.add_rate "USD", "CAD", 1.2738
30
+ config.add_rate "USD", "AUD", 1.2974
31
+
32
+ config.add_rate "CAD", "USD", 0.78507
33
+ config.add_rate "CAD", "EUR", 0.65030
34
+ config.add_rate "CAD", "GBP", 0.56371
35
+ config.add_rate "CAD", "AUD", 1.0186
36
+
37
+ config.add_rate "AUD", "CAD", 0.98176
38
+ config.add_rate "AUD", "USD", 0.77075
39
+ config.add_rate "AUD", "EUR", 0.63844
40
+ config.add_rate "AUD", "GBP", 0.55343
41
+
42
+ # To handle the inclusion of validations for monetized fields
43
+ # The default value is true
44
+ #
45
+ # config.include_validations = true
46
+
47
+ # Default ActiveRecord migration configuration values for columns:
48
+ #
49
+ config.amount_column = {
50
+ prefix: "", # column name prefix
51
+ postfix: "_decimals", # column name postfix
52
+ column_name: nil, # full column name (overrides prefix, postfix and accessor name)
53
+ type: :integer, # column type
54
+ present: true, # column will be created
55
+ null: false, # other options will be treated as column options
56
+ default: 0,
57
+ }
58
+ #
59
+ # config.currency_column = { prefix: '',
60
+ # postfix: '_currency',
61
+ # column_name: nil,
62
+ # type: :string,
63
+ # present: true,
64
+ # null: false,
65
+ # default: 'USD'
66
+ # }
67
+
68
+ # Register a custom currency
69
+ #
70
+ # Example:
71
+ # config.register_currency = {
72
+ # priority: 1,
73
+ # iso_code: "EU4",
74
+ # name: "Euro with subunit of 4 digits",
75
+ # symbol: "€",
76
+ # symbol_first: true,
77
+ # subunit: "Subcent",
78
+ # subunit_to_unit: 10000,
79
+ # thousands_separator: ".",
80
+ # decimal_mark: ","
81
+ # }
82
+
83
+ # Specify a rounding mode
84
+ # Any one of:
85
+ #
86
+ # BigDecimal::ROUND_UP,
87
+ # BigDecimal::ROUND_DOWN,
88
+ # BigDecimal::ROUND_HALF_UP,
89
+ # BigDecimal::ROUND_HALF_DOWN,
90
+ # BigDecimal::ROUND_HALF_EVEN,
91
+ # BigDecimal::ROUND_CEILING,
92
+ # BigDecimal::ROUND_FLOOR
93
+ #
94
+ # set to BigDecimal::ROUND_HALF_EVEN by default
95
+ #
96
+ config.rounding_mode = BigDecimal::ROUND_HALF_UP
97
+
98
+ # Set default money format globally.
99
+ # Default value is nil meaning "ignore this option".
100
+ # Example:
101
+ #
102
+ # config.default_format = {
103
+ # no_cents_if_whole: nil,
104
+ # symbol: nil,
105
+ # sign_before_symbol: nil
106
+ # }
107
+
108
+ # If you would like to use I18n localization (formatting depends on the
109
+ # locale):
110
+ config.locale_backend = :i18n
111
+ #
112
+ # Example (using default localization from rails-i18n):
113
+ #
114
+ # I18n.locale = :en
115
+ # Money.new(10_000_00, 'USD').format # => $10,000.00
116
+ # I18n.locale = :es
117
+ # Money.new(10_000_00, 'USD').format # => $10.000,00
118
+ #
119
+ # For the legacy behaviour of "per currency" localization (formatting depends
120
+ # only on currency):
121
+ # config.locale_backend = :currency
122
+ #
123
+ # Example:
124
+ # Money.new(10_000_00, 'USD').format # => $10,000.00
125
+ # Money.new(10_000_00, 'EUR').format # => €10.000,00
126
+ #
127
+ # In case you don't need localization and would like to use default values
128
+ # (can be redefined using config.default_format):
129
+ # config.locale_backend = nil
130
+
131
+ # Set default raise_error_on_money_parsing option
132
+ # It will be raise error if assigned different currency
133
+ # The default value is false
134
+ #
135
+ # Example:
136
+ # config.raise_error_on_money_parsing = false
137
+
138
+ Monetize.assume_from_symbol = true
139
+ end
140
+ MoneyRails::Hooks.init
@@ -0,0 +1,211 @@
1
+ class CreateMunificentModels < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :munificent_donators do |t|
4
+ t.string :chosen_name
5
+ t.string :name
6
+
7
+ t.string :stripe_customer_id, index: { unique: true }
8
+ t.string :twitch_id, index: { unique: true }
9
+
10
+ # Email address and login
11
+ t.string :email_address, index: { unique: true, where: "confirmed = false" }
12
+ t.string :unconfirmed_email_address
13
+
14
+ # Authlogic::ActsAsAuthentic::Password
15
+ t.string :crypted_password
16
+ t.string :password_salt
17
+
18
+ # Authlogic::ActsAsAuthentic::PersistenceToken
19
+ t.string :persistence_token, index: { unique: true }
20
+
21
+ # Authlogic::ActsAsAuthentic::SingleAccessToken
22
+ t.string :single_access_token, index: { unique: true }
23
+
24
+ # Authlogic::ActsAsAuthentic::PerishableToken
25
+ t.string :perishable_token, index: { unique: true }
26
+
27
+ # See "Magic Columns" in Authlogic::Session::Base
28
+ t.datetime :current_login_at
29
+ t.datetime :last_login_at
30
+ t.datetime :last_request_at
31
+ t.integer :failed_login_count, default: 0, null: false
32
+ t.integer :login_count, default: 0, null: false
33
+ t.string :current_login_ip
34
+ t.string :last_login_ip
35
+
36
+ # See "Magic States" in Authlogic::Session::Base
37
+ t.boolean :active, default: false
38
+ t.boolean :approved, default: false
39
+ t.boolean :confirmed, default: false
40
+
41
+ t.timestamps
42
+ end
43
+
44
+ create_table :munificent_curated_streamers do |t|
45
+ t.string :twitch_username, null: false, index: true, unique: true
46
+
47
+ t.timestamps
48
+ end
49
+
50
+ create_table :munificent_fundraisers do |t|
51
+ t.datetime :ends_at
52
+ t.datetime :starts_at
53
+ t.string :main_currency, default: "GBP", null: false
54
+ t.string :name, null: false
55
+ t.string :overpayment_mode, null: false, default: "pro_bono"
56
+ t.string :short_url
57
+ t.string :state, null: false, default: "inactive"
58
+ t.text :description
59
+
60
+ t.timestamps
61
+ end
62
+
63
+ create_table :munificent_charities do |t|
64
+ t.string :name, index: true, null: false
65
+ t.text :description
66
+
67
+ t.timestamps
68
+ end
69
+
70
+ create_table :munificent_games do |t|
71
+ t.string :name, null: false
72
+ t.text :description
73
+
74
+ t.timestamps
75
+ end
76
+
77
+ create_table :munificent_donations do |t|
78
+ t.monetize :amount
79
+ t.string :donator_name
80
+ t.string :message
81
+
82
+ t.string :state, null: false, default: "pending"
83
+
84
+ t.string :paypal_order_id
85
+ t.string :stripe_payment_intent_id, index: { unique: true }
86
+ t.check_constraint "num_nonnulls(stripe_payment_intent_id, paypal_order_id) > 0"
87
+
88
+ t.references :donator, null: false, foreign_key: { to_table: :munificent_donators }
89
+ t.references :donated_by, null: true, foreign_key: { to_table: :munificent_donators }
90
+ t.references :curated_streamer, null: true, foreign_key: { to_table: :munificent_curated_streamers }
91
+ t.references :fundraiser, index: true, foreign_key: { to_table: :munificent_fundraisers }
92
+
93
+ t.timestamps
94
+ end
95
+
96
+ create_table :munificent_payments do |t|
97
+ t.monetize :amount
98
+
99
+ t.references :donation, null: true, foreign_key: { to_table: :munificent_donations }
100
+
101
+ t.string :stripe_payment_intent_id, unique: true
102
+ t.string :paypal_order_id
103
+ t.check_constraint "num_nonnulls(stripe_payment_intent_id, paypal_order_id) > 0"
104
+
105
+ t.timestamps
106
+ end
107
+
108
+ create_table :munificent_charity_splits do |t|
109
+ t.monetize :amount
110
+
111
+ t.references :donation, null: true, foreign_key: { to_table: :munificent_donations }
112
+ t.references :charity, null: false, foreign_key: { to_table: :munificent_charities }
113
+
114
+ t.timestamps
115
+ end
116
+
117
+ create_table :munificent_bundles do |t|
118
+ t.string :name, null: false
119
+
120
+ t.string :state, null: false, default: "draft"
121
+
122
+ t.references :fundraiser, index: true, null: false, foreign_key: { to_table: :munificent_fundraisers }
123
+ t.index [:name, :fundraiser_id], unique: true
124
+
125
+ t.timestamps
126
+ end
127
+
128
+ create_table :munificent_donator_bundles do |t|
129
+ t.references :donator, null: false, foreign_key: { to_table: :munificent_donators }
130
+ t.references :bundle, null: false, foreign_key: { to_table: :munificent_bundles }
131
+
132
+ t.timestamps
133
+ end
134
+
135
+ create_table :munificent_bundle_tiers do |t|
136
+ t.datetime :ends_at
137
+ t.datetime :starts_at
138
+ t.monetize :price
139
+ t.string :name
140
+
141
+ t.references :bundle, null: false, index: true, foreign_key: { to_table: :munificent_bundles }
142
+
143
+ t.timestamps
144
+ end
145
+
146
+ create_table :munificent_bundle_tier_games do |t|
147
+ t.references :bundle_tier, null: false, foreign_key: { to_table: :munificent_bundle_tiers }
148
+ t.references :game, null: false, foreign_key: { to_table: :munificent_games }
149
+
150
+ t.timestamps
151
+ end
152
+
153
+ create_table :munificent_donator_bundle_tiers do |t|
154
+ t.references :donator_bundle,
155
+ null: false,
156
+ index: { name: "donator_bundle_tier_donator_bundle_id_index" },
157
+ foreign_key: { to_table: :munificent_donator_bundles }
158
+ t.references :bundle_tier,
159
+ null: false,
160
+ index: { name: "donator_bundle_tier_bundle_tier_id_index" },
161
+ foreign_key: { to_table: :munificent_bundle_tiers }
162
+
163
+ t.boolean :unlocked, default: false, null: false
164
+
165
+ t.timestamps
166
+ end
167
+
168
+ create_table :munificent_keys do |t|
169
+ t.references :game, null: false, foreign_key: { to_table: :munificent_games }
170
+ t.references :donator_bundle_tier, null: true, foreign_key: { to_table: :munificent_donator_bundle_tiers }
171
+ t.references :fundraiser, index: true, foreign_key: { to_table: :munificent_fundraisers }
172
+
173
+ # Ciphertext
174
+ t.text :code_ciphertext
175
+
176
+ # Encryption key
177
+ t.text :encrypted_kms_key
178
+
179
+ # Blind index
180
+ t.string :code_bidx, index: { unique: true }
181
+
182
+ t.index [:donator_bundle_tier_id, :game_id], unique: true, name: "donator_bundle_tier_game_idx"
183
+
184
+ t.timestamps
185
+ end
186
+
187
+ create_table :munificent_charity_fundraisers do |t|
188
+ t.references :charity, foreign_key: { to_table: :munificent_charities }
189
+ t.references :fundraiser,
190
+ index: { name: "charity_fundraiser_fundraiser_id_index" },
191
+ foreign_key: { to_table: :munificent_fundraisers }
192
+
193
+ t.timestamps
194
+ end
195
+
196
+ create_table :munificent_curated_streamer_administrators do |t|
197
+ t.references :curated_streamer,
198
+ null: false,
199
+ index: { name: "curated_streamer_administrator_curated_streamer_id_index" },
200
+ foreign_key: { to_table: :munificent_curated_streamers }
201
+ t.references :donator,
202
+ null: false,
203
+ index: { name: "curated_streamer_administrator_donator_id_index" },
204
+ foreign_key: { to_table: :munificent_donators }
205
+
206
+ t.index [:curated_streamer_id, :donator_id], unique: true, name: "curated_streamer_donator_index"
207
+
208
+ t.timestamps
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,40 @@
1
+ require "redis"
2
+ require "rails"
3
+ require "rollbar"
4
+
5
+ require "authlogic"
6
+
7
+ module Munificent
8
+ class Engine < ::Rails::Engine
9
+ isolate_namespace Munificent
10
+
11
+ logger = Rails.logger || ActiveSupport::Logger.new($stdout)
12
+
13
+ config.generators do |g|
14
+ g.test_framework :rspec
15
+ g.fixture_replacement :factory_bot
16
+ g.factory_bot dir: "test/factories"
17
+ end
18
+
19
+ initializer "munificent.hmac" do
20
+ require "hmac"
21
+ HMAC.configure do |config|
22
+ config.secret = ENV.fetch("HMAC_SECRET", nil)
23
+ end
24
+ end
25
+
26
+ initializer "munificent.stripe" do
27
+ require "stripe"
28
+ Stripe.api_key = ENV.fetch("STRIPE_SECRET_KEY", nil)
29
+ end
30
+
31
+ begin
32
+ # Expose factories to the parent app/engine
33
+ require "factory_bot_rails"
34
+ require_relative "./factories"
35
+ config.factory_bot.definition_file_paths += [File.expand_path("../../test/factories", __dir__)]
36
+ rescue LoadError
37
+ logger.debug("One or more factory_bot files could not be required, skipping additional factory_bot configuration")
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,13 @@
1
+ require "factory_bot/namespaced_factories"
2
+
3
+ module Munificent
4
+ module Factories
5
+ def self.define(name, &)
6
+ FactoryBot.define do
7
+ with_namespace(:Munificent, require_prefix: false) do
8
+ factory(name, &)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,47 @@
1
+ # rubocop:disable Rails/Output
2
+ module Munificent
3
+ class Seeds
4
+ def self.run
5
+ puts "Creating active fundraisers"
6
+ wwf = Fundraiser.create!(name: "WWF Manatee Matinée (London)", main_currency: "GBP")
7
+ msf = Fundraiser.create!(name: "MSF Gala (Paris)", main_currency: "EUR")
8
+
9
+ [wwf, msf].each do |fundraiser|
10
+ puts "Populating the fundraiser #{fundraiser.name}"
11
+ fundraiser.activate!
12
+
13
+ puts "Creating bundle"
14
+ bundle = Bundle.create!(
15
+ name: "Test bundle",
16
+ fundraiser:,
17
+ state: "live",
18
+ )
19
+
20
+ bundle.highest_tier.update(price_decimals: 25_00)
21
+ bundle.highest_tier.bundle_tier_games.create!(game: Game.find_or_initialize_by(name: "The Witness"))
22
+
23
+ tier = bundle.bundle_tiers.create!(price_decimals: 10_00, price_currency: fundraiser.main_currency)
24
+
25
+ tier.bundle_tier_games.create!(game: Game.find_or_initialize_by(name: "Doom"))
26
+ tier.bundle_tier_games.create!(game: Game.find_or_initialize_by(name: "Duke Nukem Forever"))
27
+
28
+ Game.all.each do |game|
29
+ puts "Adding 100 keys for #{game.name}"
30
+ 100.times do
31
+ game.keys.create(
32
+ code: SecureRandom.uuid,
33
+ fundraiser:,
34
+ )
35
+ end
36
+ end
37
+ end
38
+
39
+ puts "Creating charities"
40
+ Charity.create(name: "Help the Penguins", fundraisers: [wwf])
41
+ Charity.create(name: "World Wildlife Foundation", fundraisers: [wwf])
42
+ Charity.create(name: "Vets Without Borders", fundraisers: [wwf, msf])
43
+ Charity.create(name: "Médecins Sans Frontières", fundraisers: [msf])
44
+ end
45
+ end
46
+ end
47
+ # rubocop:enable Rails/Output
@@ -0,0 +1,3 @@
1
+ module Munificent
2
+ VERSION = "1.0.0".freeze
3
+ end
data/lib/munificent.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "munificent/version"
2
+ require "munificent/engine"
3
+
4
+ module Munificent
5
+ EMAIL_ADDRESS_REGEX = %r{(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])} # rubocop:disable Layout/LineLength
6
+ end
@@ -0,0 +1,31 @@
1
+ begin
2
+ require "rspec/core/rake_task"
3
+ require "rubocop/rake_task"
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ RuboCop::RakeTask.new
7
+ rescue LoadError => exception
8
+ puts "Library not available: #{exception.message}"
9
+ end
10
+
11
+ desc "Build, lint, and test"
12
+ task :build_and_test do
13
+ Rake::Task["lint"].invoke
14
+ Rake::Task["test"].invoke
15
+ Rake::Task["build"].invoke unless ENV["CI"]
16
+ end
17
+
18
+ desc "Lint"
19
+ task lint: :rubocop
20
+
21
+ desc "Test"
22
+ task :test do
23
+ Rake::Task["spec"].invoke
24
+ end
25
+
26
+ desc "Build"
27
+ task :build do
28
+ # no-op
29
+ end
30
+
31
+ task default: :build_and_test
@@ -0,0 +1,8 @@
1
+ desc "Key assignment"
2
+ namespace :key_assignment do
3
+ desc "A long running task to assign keys to donator bundle tiers"
4
+ task start: :environment do
5
+ Rails.logger.debug("Starting key assignment...")
6
+ Munificent::KeyAssignment::RequestProcessor.start
7
+ end
8
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :munificent do
3
+ # # Task goes here
4
+ # end