From 26e7f8e0df0166afa0ce215f39ec2c0a33859ae8 Mon Sep 17 00:00:00 2001 From: Zee Spencer <50284+zspencer@users.noreply.github.com> Date: Mon, 22 Jan 2024 15:48:34 -0800 Subject: [PATCH 01/13] =?UTF-8?q?=F0=9F=A5=97=20`Tobias`:=20Draft=20a=20sp?= =?UTF-8?q?ec=20for=20`Payout#issue`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - https://github.com/zspencer/convene/issues/11 I'm starting out by coding by wishful thinking, putting together the pieces of the pie that I can eat bite by bite. In this test, I'm using imagining there will be a Model called `Tobias::Payout`, which will be our computational entrance point into the [Issuing a Payout](https://github.com/zspencer/convene/issues/11) feature. I considered starting with a system test, which would have gone through the User Interface and tied together a bunch of different concepts; but I figured I would start with a `model` spec; so that I can stay focused on drawing the computer-facing side of the feature out before worrying too much about the human-facing bits; which I always find require a lot more thought for me. That said, a system test will be useful here at some point. The computational bits, on the other hand, feel pretty accesible. We want to create Payments for every Beneficiary of the appropriate amount, and store records of these Payments so we know who is supposed to get paid what. Here's a line-by-line play-by-play of this change: I'm using the `spec` folder to store executable examples of a feature. Because `Issuing Payout` is a feature for the [`TOBIAS` project](https://github.com/zspencer/convene/issues/1), I am storing it under `spec/tobias`. My choice of what to call the file (`payout_spec.rb` indicates that this spec will be for the `Payout` model. The `require "rails_helper"` line tells our testing framework to load all the code necessary to run a spec. The `describe Tobias::Payout do` line groups the examples nested within it as relating to the `Payout` model. The `describe "#issue"` line tells me that the `Payout` model will have a method named `issue`, and also creates a group of examples that describe how the `Payout#issue` method works. The `it "issues a Payment..." do` line describes one of the examples we we plan to use to confirm that the `Payout#issue` method works the way we hope it will. The test itself lives on the lines between `it "issues a Payment..." do` and the `end` that is aligned with the `it` The `payout = create(:tobias_payout, payout_amount_cents: 150_00) line says "create a database record of a `Tobias::Payout, and populate it's `payout_amount_cents` field with $150.00, and store a reference to it in the `payout` variable. The `beneficiaries = create_list(10, :tobias_beneficiary, trust: payout.trust)` line says create 10 `Tobias::Beneficiary` records in the database, and make sure their `trust` field is pointed at the same `Tobias::Trust` that our `payout` is pointing to. Store references to those in the `beneficiaries variable.` The `payout.issue` line executes the `Payout#issue` method that we are describing. The `beneficiaries.each do |beneficiary|` goes over every one of the newly created `Tobias::Beneficiary` records stored in the `beneficiaries` variable, expose each one as a variable named `beneficiary`, and execute the code between it and the next `end`. The `expect(beneficiaries.payments).to exist(amount_cents)` tells our example to let us know if there are no `Tobias::Payment` records in the database for one of the `beneficiaries`. --- spec/furniture/tobias/payout_spec.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 spec/furniture/tobias/payout_spec.rb diff --git a/spec/furniture/tobias/payout_spec.rb b/spec/furniture/tobias/payout_spec.rb new file mode 100644 index 000000000..31472ed93 --- /dev/null +++ b/spec/furniture/tobias/payout_spec.rb @@ -0,0 +1,17 @@ +require "rails_helper" + +RSpec.describe Tobias::Payout do + describe "#issue" do + it "issues a Payment to each Beneficiary for their share of the #payout_amount" do + payout = create(:tobias_payout, payout_amount_cents: 150_00) + + beneficiaries = create_list(10, :tobias_beneficiary, trust: payout.trust) + + payout.issue + + beneficiaries.each do |beneficiary| + expect(beneficiary.payments).to exist(amount_cents: 15) + end + end + end +end From 074ac745b445c2e569993431b4900d921aceb4b9 Mon Sep 17 00:00:00 2001 From: Zee Spencer <50284+zspencer@users.noreply.github.com> Date: Fri, 26 Jan 2024 23:22:27 -0800 Subject: [PATCH 02/13] =?UTF-8?q?=E2=9C=A8=20`Tobias`:=20`Payout#issue`=20?= =?UTF-8?q?creates=20`Payment`=20for=20`Trust#beneficiaries`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This sketches in the `Payout#issue` method's line-of-action, which is to iterate through the `Payout#trust#beneficiaries` and add a `Payment` to the set of `Payout#payments` for the per-beneficiary amount. That said, there's a few outstanding questions we'll probably want to add some tests to interrogate, especially around: 1. What happens if the method is ran twice with the same data? 2. What about if the set of `Payout#trust#beneficiaries` is changed between calls to `Payout#issue`? 3. What happens when the `Payout#payout_amount` doesn't divide evenly between the `Payout#beneficiaries? --- app/furniture/tobias.rb | 2 ++ app/furniture/tobias/beneficiary.rb | 9 +++++ app/furniture/tobias/payment.rb | 7 ++++ app/furniture/tobias/payout.rb | 19 ++++++++++ app/furniture/tobias/record.rb | 9 +++++ app/furniture/tobias/trust.rb | 8 +++++ .../20240127063826_create_tobias_payouts.rb | 27 ++++++++++++++ db/schema.rb | 36 +++++++++++++++++++ spec/tobias/factories/beneficiary_factory.rb | 5 +++ spec/tobias/factories/payout_factory.rb | 7 ++++ spec/tobias/factories/trust_factory.rb | 5 +++ spec/{furniture => }/tobias/payout_spec.rb | 6 ++-- 12 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 app/furniture/tobias.rb create mode 100644 app/furniture/tobias/beneficiary.rb create mode 100644 app/furniture/tobias/payment.rb create mode 100644 app/furniture/tobias/payout.rb create mode 100644 app/furniture/tobias/record.rb create mode 100644 app/furniture/tobias/trust.rb create mode 100644 db/migrate/20240127063826_create_tobias_payouts.rb create mode 100644 spec/tobias/factories/beneficiary_factory.rb create mode 100644 spec/tobias/factories/payout_factory.rb create mode 100644 spec/tobias/factories/trust_factory.rb rename spec/{furniture => }/tobias/payout_spec.rb (60%) diff --git a/app/furniture/tobias.rb b/app/furniture/tobias.rb new file mode 100644 index 000000000..0dd845ca6 --- /dev/null +++ b/app/furniture/tobias.rb @@ -0,0 +1,2 @@ +class Tobias +end diff --git a/app/furniture/tobias/beneficiary.rb b/app/furniture/tobias/beneficiary.rb new file mode 100644 index 000000000..44f7a35c3 --- /dev/null +++ b/app/furniture/tobias/beneficiary.rb @@ -0,0 +1,9 @@ +class Tobias + class Beneficiary < Record + self.table_name = "tobias_beneficiaries" + + belongs_to :trust + + has_many :payments + end +end diff --git a/app/furniture/tobias/payment.rb b/app/furniture/tobias/payment.rb new file mode 100644 index 000000000..db0b0d18b --- /dev/null +++ b/app/furniture/tobias/payment.rb @@ -0,0 +1,7 @@ +class Tobias + class Payment < Record + self.table_name = "tobias_payments" + + monetize :amount_cents + end +end diff --git a/app/furniture/tobias/payout.rb b/app/furniture/tobias/payout.rb new file mode 100644 index 000000000..d37aff9fd --- /dev/null +++ b/app/furniture/tobias/payout.rb @@ -0,0 +1,19 @@ +class Tobias + class Payout < ApplicationRecord + self.table_name = "tobias_payouts" + + belongs_to :trust + has_many :beneficiaries, through: :trust + has_many :payments + + monetize :payout_amount_cents + + def issue + per_beneficiary_amount = (payout_amount / beneficiaries.count) + beneficiaries.each do |beneficiary| + + payments.create_with(amount: per_beneficiary_amount).find_or_create_by(beneficiary_id: beneficiary.id) + end + end + end +end diff --git a/app/furniture/tobias/record.rb b/app/furniture/tobias/record.rb new file mode 100644 index 000000000..6763703c1 --- /dev/null +++ b/app/furniture/tobias/record.rb @@ -0,0 +1,9 @@ +class Tobias + class Record < ApplicationRecord + self.abstract_class = true + + def self.model_name + @_model_name ||= ActiveModel::Name.new(self, ::Tobias) + end + end +end diff --git a/app/furniture/tobias/trust.rb b/app/furniture/tobias/trust.rb new file mode 100644 index 000000000..74d13943a --- /dev/null +++ b/app/furniture/tobias/trust.rb @@ -0,0 +1,8 @@ +class Tobias + class Trust < Record + self.table_name = "tobias_trusts" + + has_many :beneficiaries + + end +end diff --git a/db/migrate/20240127063826_create_tobias_payouts.rb b/db/migrate/20240127063826_create_tobias_payouts.rb new file mode 100644 index 000000000..9ae921836 --- /dev/null +++ b/db/migrate/20240127063826_create_tobias_payouts.rb @@ -0,0 +1,27 @@ +class CreateTobiasPayouts < ActiveRecord::Migration[7.1] + def change + create_table :tobias_trusts, id: :uuid do |t| + t.timestamps + end + + create_table :tobias_beneficiaries, id: :uuid do |t| + t.references :trust, type: :uuid, foreign_key: {to_table: :tobias_trusts} + + t.timestamps + end + + create_table :tobias_payouts, id: :uuid do |t| + t.monetize :payout_amount + t.references :trust, type: :uuid, foreign_key: {to_table: :tobias_trusts} + t.timestamps + end + + create_table :tobias_payments, id: :uuid do |t| + t.references :payout, type: :uuid, foreign_key: {to_table: :tobias_payouts} + t.references :beneficiary, type: :uuid, foreign_key: {to_table: :tobias_beneficiaries} + t.monetize :amount + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index c1fdcaeb2..0f2fb6bf8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -333,6 +333,38 @@ t.index ["slug", "client_id"], name: "index_spaces_on_slug_and_client_id", unique: true end + create_table "tobias_beneficiaries", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.uuid "trust_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["trust_id"], name: "index_tobias_beneficiaries_on_trust_id" + end + + create_table "tobias_payments", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.uuid "payout_id" + t.uuid "beneficiary_id" + t.integer "amount_cents", default: 0, null: false + t.string "amount_currency", default: "USD", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["beneficiary_id"], name: "index_tobias_payments_on_beneficiary_id" + t.index ["payout_id"], name: "index_tobias_payments_on_payout_id" + end + + create_table "tobias_payouts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.integer "payout_amount_cents", default: 0, null: false + t.string "payout_amount_currency", default: "USD", null: false + t.uuid "trust_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["trust_id"], name: "index_tobias_payouts_on_trust_id" + end + + create_table "tobias_trusts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "utility_hookups", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.uuid "space_id" t.string "name", null: false @@ -368,4 +400,8 @@ add_foreign_key "rooms", "media", column: "hero_image_id" add_foreign_key "space_agreements", "spaces" add_foreign_key "spaces", "rooms", column: "entrance_id" + add_foreign_key "tobias_beneficiaries", "tobias_trusts", column: "trust_id" + add_foreign_key "tobias_payments", "tobias_beneficiaries", column: "beneficiary_id" + add_foreign_key "tobias_payments", "tobias_payouts", column: "payout_id" + add_foreign_key "tobias_payouts", "tobias_trusts", column: "trust_id" end diff --git a/spec/tobias/factories/beneficiary_factory.rb b/spec/tobias/factories/beneficiary_factory.rb new file mode 100644 index 000000000..b1206ba10 --- /dev/null +++ b/spec/tobias/factories/beneficiary_factory.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :tobias_beneficiary, class: "Tobias::Beneficiary" do + + end +end diff --git a/spec/tobias/factories/payout_factory.rb b/spec/tobias/factories/payout_factory.rb new file mode 100644 index 000000000..f027e7300 --- /dev/null +++ b/spec/tobias/factories/payout_factory.rb @@ -0,0 +1,7 @@ +require_relative "trust_factory" + +FactoryBot.define do + factory :tobias_payout, class: "Tobias::Payout" do + association(:trust, factory: :tobias_trust) + end +end diff --git a/spec/tobias/factories/trust_factory.rb b/spec/tobias/factories/trust_factory.rb new file mode 100644 index 000000000..4c1892065 --- /dev/null +++ b/spec/tobias/factories/trust_factory.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :tobias_trust, class: "Tobias::Trust" do + + end +end diff --git a/spec/furniture/tobias/payout_spec.rb b/spec/tobias/payout_spec.rb similarity index 60% rename from spec/furniture/tobias/payout_spec.rb rename to spec/tobias/payout_spec.rb index 31472ed93..ee3e52181 100644 --- a/spec/furniture/tobias/payout_spec.rb +++ b/spec/tobias/payout_spec.rb @@ -1,16 +1,18 @@ require "rails_helper" +require_relative "factories/payout_factory" +require_relative "factories/beneficiary_factory" RSpec.describe Tobias::Payout do describe "#issue" do it "issues a Payment to each Beneficiary for their share of the #payout_amount" do payout = create(:tobias_payout, payout_amount_cents: 150_00) - beneficiaries = create_list(10, :tobias_beneficiary, trust: payout.trust) + beneficiaries = create_list(:tobias_beneficiary, 10, trust: payout.trust) payout.issue beneficiaries.each do |beneficiary| - expect(beneficiary.payments).to exist(amount_cents: 15) + expect(beneficiary.payments).to exist(amount_cents: 15_00) end end end From 4b7a5daa361e008d2e60d3e6908d7ec1ceb7e37c Mon Sep 17 00:00:00 2001 From: Zee Spencer <50284+zspencer@users.noreply.github.com> Date: Fri, 26 Jan 2024 23:42:47 -0800 Subject: [PATCH 03/13] =?UTF-8?q?=F0=9F=A7=B9=20`Tobias`:=20Refactor=20`Pa?= =?UTF-8?q?yout#issue`=20and=20supporting=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There was a number of things I didn't love about my implementation of `Payout#issue`: - I had added a `Tobias::Record` far earlier than necessary - There were linter errors - `Payout#payout_amount` felt redundant This remediates those nose-wrinkles, and sets us up nicely for the next test. --- app/furniture/tobias/beneficiary.rb | 2 +- app/furniture/tobias/payment.rb | 2 +- app/furniture/tobias/payout.rb | 5 ++--- app/furniture/tobias/record.rb | 9 --------- app/furniture/tobias/trust.rb | 3 +-- db/migrate/20240127063826_create_tobias_payouts.rb | 2 +- db/schema.rb | 4 ++-- spec/tobias/factories/beneficiary_factory.rb | 1 - spec/tobias/factories/trust_factory.rb | 1 - spec/tobias/payout_spec.rb | 4 ++-- 10 files changed, 10 insertions(+), 23 deletions(-) delete mode 100644 app/furniture/tobias/record.rb diff --git a/app/furniture/tobias/beneficiary.rb b/app/furniture/tobias/beneficiary.rb index 44f7a35c3..7477ba219 100644 --- a/app/furniture/tobias/beneficiary.rb +++ b/app/furniture/tobias/beneficiary.rb @@ -1,5 +1,5 @@ class Tobias - class Beneficiary < Record + class Beneficiary < ApplicationRecord self.table_name = "tobias_beneficiaries" belongs_to :trust diff --git a/app/furniture/tobias/payment.rb b/app/furniture/tobias/payment.rb index db0b0d18b..ef91a299b 100644 --- a/app/furniture/tobias/payment.rb +++ b/app/furniture/tobias/payment.rb @@ -1,5 +1,5 @@ class Tobias - class Payment < Record + class Payment < ApplicationRecord self.table_name = "tobias_payments" monetize :amount_cents diff --git a/app/furniture/tobias/payout.rb b/app/furniture/tobias/payout.rb index d37aff9fd..1d37b0e28 100644 --- a/app/furniture/tobias/payout.rb +++ b/app/furniture/tobias/payout.rb @@ -6,12 +6,11 @@ class Payout < ApplicationRecord has_many :beneficiaries, through: :trust has_many :payments - monetize :payout_amount_cents + monetize :amount_cents def issue - per_beneficiary_amount = (payout_amount / beneficiaries.count) + per_beneficiary_amount = (amount / beneficiaries.count) beneficiaries.each do |beneficiary| - payments.create_with(amount: per_beneficiary_amount).find_or_create_by(beneficiary_id: beneficiary.id) end end diff --git a/app/furniture/tobias/record.rb b/app/furniture/tobias/record.rb deleted file mode 100644 index 6763703c1..000000000 --- a/app/furniture/tobias/record.rb +++ /dev/null @@ -1,9 +0,0 @@ -class Tobias - class Record < ApplicationRecord - self.abstract_class = true - - def self.model_name - @_model_name ||= ActiveModel::Name.new(self, ::Tobias) - end - end -end diff --git a/app/furniture/tobias/trust.rb b/app/furniture/tobias/trust.rb index 74d13943a..f4cd46e7b 100644 --- a/app/furniture/tobias/trust.rb +++ b/app/furniture/tobias/trust.rb @@ -1,8 +1,7 @@ class Tobias - class Trust < Record + class Trust < ApplicationRecord self.table_name = "tobias_trusts" has_many :beneficiaries - end end diff --git a/db/migrate/20240127063826_create_tobias_payouts.rb b/db/migrate/20240127063826_create_tobias_payouts.rb index 9ae921836..ad6eadffb 100644 --- a/db/migrate/20240127063826_create_tobias_payouts.rb +++ b/db/migrate/20240127063826_create_tobias_payouts.rb @@ -11,7 +11,7 @@ def change end create_table :tobias_payouts, id: :uuid do |t| - t.monetize :payout_amount + t.monetize :amount t.references :trust, type: :uuid, foreign_key: {to_table: :tobias_trusts} t.timestamps end diff --git a/db/schema.rb b/db/schema.rb index 0f2fb6bf8..c52f70b69 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -352,8 +352,8 @@ end create_table "tobias_payouts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.integer "payout_amount_cents", default: 0, null: false - t.string "payout_amount_currency", default: "USD", null: false + t.integer "amount_cents", default: 0, null: false + t.string "amount_currency", default: "USD", null: false t.uuid "trust_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false diff --git a/spec/tobias/factories/beneficiary_factory.rb b/spec/tobias/factories/beneficiary_factory.rb index b1206ba10..b330f1abb 100644 --- a/spec/tobias/factories/beneficiary_factory.rb +++ b/spec/tobias/factories/beneficiary_factory.rb @@ -1,5 +1,4 @@ FactoryBot.define do factory :tobias_beneficiary, class: "Tobias::Beneficiary" do - end end diff --git a/spec/tobias/factories/trust_factory.rb b/spec/tobias/factories/trust_factory.rb index 4c1892065..7fd94abab 100644 --- a/spec/tobias/factories/trust_factory.rb +++ b/spec/tobias/factories/trust_factory.rb @@ -1,5 +1,4 @@ FactoryBot.define do factory :tobias_trust, class: "Tobias::Trust" do - end end diff --git a/spec/tobias/payout_spec.rb b/spec/tobias/payout_spec.rb index ee3e52181..afcbc3ee1 100644 --- a/spec/tobias/payout_spec.rb +++ b/spec/tobias/payout_spec.rb @@ -4,8 +4,8 @@ RSpec.describe Tobias::Payout do describe "#issue" do - it "issues a Payment to each Beneficiary for their share of the #payout_amount" do - payout = create(:tobias_payout, payout_amount_cents: 150_00) + it "issues a Payment to each Beneficiary for their share of the #amount" do + payout = create(:tobias_payout, amount_cents: 150_00) beneficiaries = create_list(:tobias_beneficiary, 10, trust: payout.trust) From 0a1a3d8a985fd32f76eb1b8152cc67b073be8055 Mon Sep 17 00:00:00 2001 From: Zee Spencer <50284+zspencer@users.noreply.github.com> Date: Mon, 29 Jan 2024 14:51:30 -0800 Subject: [PATCH 04/13] =?UTF-8?q?=F0=9F=A5=97=20`Tobias`:=20Confirm=20issu?= =?UTF-8?q?ing=20indivisible=20`Payout#amount`=20rounds=20each=20`Benefici?= =?UTF-8?q?ary`=20`Payment`=20down?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I wasn't exactly sure what would happen, so I took the time to figure it out. Apparently, the [`money-rails` gem] we use defaults to rounding down, which is the safer thing to do. [`money-rails` gem]: https://github.com/RubyMoney/money-rails --- spec/tobias/payout_spec.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/spec/tobias/payout_spec.rb b/spec/tobias/payout_spec.rb index afcbc3ee1..53556bfe7 100644 --- a/spec/tobias/payout_spec.rb +++ b/spec/tobias/payout_spec.rb @@ -15,5 +15,19 @@ expect(beneficiary.payments).to exist(amount_cents: 15_00) end end + + context "when the Payout#amount does not divide evenly" do + it "rounds down so that it can" do + payout = create(:tobias_payout, amount_cents: 3_33) + + beneficiaries = create_list(:tobias_beneficiary, 2, trust: payout.trust) + + payout.issue + + beneficiaries.each do |beneficiary| + expect(beneficiary.payments).to exist(amount_cents: 1_66) + end + end + end end end From 925cb45a0fbe1c1cae251794e20f2a54a1557f8a Mon Sep 17 00:00:00 2001 From: Zee <50284+zspencer@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:24:31 -0800 Subject: [PATCH 05/13] =?UTF-8?q?=F0=9F=A5=97=20`Tobias`:=20Test=20issuing?= =?UTF-8?q?=20`Payouts`=20twice=20doesn't=20create=20more=20`Payments`=20(?= =?UTF-8?q?#19)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🥗 `Tobias`: Issuing `Payouts` doesn't create additional payouts per beneficiary I thought that this would actually be a problem, but apparently it's not! Hooray for tests! * 🥗✨`Tobias`: `Payout#issue` won't create `Payment` for new `Beneficiary` This one actually needed adjusting! Turns out, `Payout#issue` would happily create a new `Payment` for a new `Beneficiary`, which is less than ideal. Now we have a guard clause that will prevent `Payout#issue` from making additional `Payments` when there are already `Payments` present. --- app/furniture/tobias/payout.rb | 2 ++ spec/tobias/payout_spec.rb | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/app/furniture/tobias/payout.rb b/app/furniture/tobias/payout.rb index 1d37b0e28..0541fcac5 100644 --- a/app/furniture/tobias/payout.rb +++ b/app/furniture/tobias/payout.rb @@ -9,6 +9,8 @@ class Payout < ApplicationRecord monetize :amount_cents def issue + return if payments.present? + per_beneficiary_amount = (amount / beneficiaries.count) beneficiaries.each do |beneficiary| payments.create_with(amount: per_beneficiary_amount).find_or_create_by(beneficiary_id: beneficiary.id) diff --git a/spec/tobias/payout_spec.rb b/spec/tobias/payout_spec.rb index 53556bfe7..29e18003c 100644 --- a/spec/tobias/payout_spec.rb +++ b/spec/tobias/payout_spec.rb @@ -29,5 +29,23 @@ end end end + + context "when running twice" do + it "does not issue multiple payouts, even when beneficiaries are added" do + payout = create(:tobias_payout, amount_cents: 100_00) + + create_list(:tobias_beneficiary, 2, trust: payout.trust) + + payout.issue + + create(:tobias_beneficiary, trust: payout.trust) + + # ActiveRecord appears to be caching the `payout.beneficiaries` results + # Reload busts that cache. + payout.reload + + expect { payout.issue }.not_to(change(payout.payments, :count)) + end + end end end From cd4df116abaf57ac1e3a896307f478d98533082d Mon Sep 17 00:00:00 2001 From: Zee Spencer <50284+zspencer@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:34:50 -0800 Subject: [PATCH 06/13] =?UTF-8?q?=F0=9F=A7=B9=20`Tobias`:=20`Payout#destro?= =?UTF-8?q?y`=20destroys=20`#payments`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While it may be better us to void a `Payout`, making sure we clean up any `Payout#payments` upon `Payout#destroy` should reduce the likelihood of data being in a sad place. --- app/furniture/tobias/payment.rb | 2 ++ app/furniture/tobias/payout.rb | 2 +- spec/tobias/payout_spec.rb | 6 +++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/furniture/tobias/payment.rb b/app/furniture/tobias/payment.rb index ef91a299b..34f5d7e2e 100644 --- a/app/furniture/tobias/payment.rb +++ b/app/furniture/tobias/payment.rb @@ -2,6 +2,8 @@ class Tobias class Payment < ApplicationRecord self.table_name = "tobias_payments" + belongs_to :payout + monetize :amount_cents end end diff --git a/app/furniture/tobias/payout.rb b/app/furniture/tobias/payout.rb index 0541fcac5..035d9d7ab 100644 --- a/app/furniture/tobias/payout.rb +++ b/app/furniture/tobias/payout.rb @@ -4,7 +4,7 @@ class Payout < ApplicationRecord belongs_to :trust has_many :beneficiaries, through: :trust - has_many :payments + has_many :payments, inverse_of: :payout, dependent: :destroy monetize :amount_cents diff --git a/spec/tobias/payout_spec.rb b/spec/tobias/payout_spec.rb index 29e18003c..734e6fc2b 100644 --- a/spec/tobias/payout_spec.rb +++ b/spec/tobias/payout_spec.rb @@ -2,7 +2,11 @@ require_relative "factories/payout_factory" require_relative "factories/beneficiary_factory" -RSpec.describe Tobias::Payout do +RSpec.describe Tobias::Payout, type: :model do + describe "#payments" do + it { is_expected.to have_many(:payments).inverse_of(:payout).dependent(:destroy) } + end + describe "#issue" do it "issues a Payment to each Beneficiary for their share of the #amount" do payout = create(:tobias_payout, amount_cents: 150_00) From ad5b5b2b803e06394adc3228a60b70b913e97e04 Mon Sep 17 00:00:00 2001 From: Zee Spencer <50284+zspencer@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:40:44 -0800 Subject: [PATCH 07/13] =?UTF-8?q?=F0=9F=A5=97=F0=9F=A7=B9=20`Tobias`:=20De?= =?UTF-8?q?fine=20`Payment`=20relationships=20and=20attributes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is not super important to the actual feature, but it does mean that the `Payment` has clearly defined relationships, with tests that confirm the relationship. It also adds a spec to check that the `#amount` value operates as a monetized value; which doesn't seem like it does that much beyond giving us an injection point for layering in additional details. --- app/furniture/tobias/payment.rb | 2 +- spec/spec_helper.rb | 1 + spec/tobias/payment_spec.rb | 11 +++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 spec/tobias/payment_spec.rb diff --git a/app/furniture/tobias/payment.rb b/app/furniture/tobias/payment.rb index 34f5d7e2e..ddd1f0778 100644 --- a/app/furniture/tobias/payment.rb +++ b/app/furniture/tobias/payment.rb @@ -2,7 +2,7 @@ class Tobias class Payment < ApplicationRecord self.table_name = "tobias_payments" - belongs_to :payout + belongs_to :payout, inverse_of: :payments monetize :amount_cents end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 24c3d8ae7..23a18427c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,7 @@ require "faker" require "pundit/rspec" require "simplecov" +require "money-rails/test_helpers" SimpleCov.start do enable_coverage :branch diff --git a/spec/tobias/payment_spec.rb b/spec/tobias/payment_spec.rb new file mode 100644 index 000000000..19607f969 --- /dev/null +++ b/spec/tobias/payment_spec.rb @@ -0,0 +1,11 @@ +require "rails_helper" + +RSpec.describe Tobias::Payment, type: :model do + describe "#payout" do + it { is_expected.to belong_to(:payout).inverse_of(:payments) } + end + + describe "#amount" do + it { is_expected.to monetize(:amount) } + end +end From e8bf503fd68b11106ee4d0676b7bfb5839f52bb4 Mon Sep 17 00:00:00 2001 From: Zee Spencer <50284+zspencer@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:49:11 -0800 Subject: [PATCH 08/13] =?UTF-8?q?=F0=9F=A5=97=F0=9F=A7=B9=20`Tobias`:=20Ch?= =?UTF-8?q?eck=20`Payout#amount`=20and=20`Payout#beneficiaries`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A few more `Payout` bits that didn't have specs! Now they do! --- spec/tobias/payout_spec.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/tobias/payout_spec.rb b/spec/tobias/payout_spec.rb index 734e6fc2b..cbb4af317 100644 --- a/spec/tobias/payout_spec.rb +++ b/spec/tobias/payout_spec.rb @@ -7,6 +7,14 @@ it { is_expected.to have_many(:payments).inverse_of(:payout).dependent(:destroy) } end + describe "#beneficiaries" do + it { is_expected.to have_many(:beneficiaries).through(:trust) } + end + + describe "#amount" do + it { is_expected.to monetize(:amount) } + end + describe "#issue" do it "issues a Payment to each Beneficiary for their share of the #amount" do payout = create(:tobias_payout, amount_cents: 150_00) From 7801e2a237dbec94daf2c3802b52ae4bedf6ce87 Mon Sep 17 00:00:00 2001 From: Zee Spencer <50284+zspencer@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:52:49 -0800 Subject: [PATCH 09/13] =?UTF-8?q?=F0=9F=A5=97=F0=9F=A7=B9=20`Tobias`:=20`T?= =?UTF-8?q?rust#destroy`=20destroys=20`Trust#beneficiaries`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Again, we may want to leverage an archival system rather than destruction, but for now let's make sure we clean up when a `Trust` is detroyed. --- app/furniture/tobias/trust.rb | 2 +- spec/tobias/trust_spec.rb | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 spec/tobias/trust_spec.rb diff --git a/app/furniture/tobias/trust.rb b/app/furniture/tobias/trust.rb index f4cd46e7b..d8dfbc071 100644 --- a/app/furniture/tobias/trust.rb +++ b/app/furniture/tobias/trust.rb @@ -2,6 +2,6 @@ class Tobias class Trust < ApplicationRecord self.table_name = "tobias_trusts" - has_many :beneficiaries + has_many :beneficiaries, inverse_of: :trust, dependent: :destroy end end diff --git a/spec/tobias/trust_spec.rb b/spec/tobias/trust_spec.rb new file mode 100644 index 000000000..f0d61e935 --- /dev/null +++ b/spec/tobias/trust_spec.rb @@ -0,0 +1,7 @@ +require "rails_helper" + +RSpec.describe Tobias::Trust, type: :model do + describe "#benificiaries" do + it { is_expected.to have_many(:beneficiaries).inverse_of(:trust).dependent(:destroy) } + end +end From 5d210f337f0874dabb4fed452759d7f4e2ea195b Mon Sep 17 00:00:00 2001 From: Zee Spencer <50284+zspencer@users.noreply.github.com> Date: Mon, 29 Jan 2024 16:03:59 -0800 Subject: [PATCH 10/13] =?UTF-8?q?=F0=9F=A5=97=F0=9F=A7=B9=20`Tobias`:=20`B?= =?UTF-8?q?eneficiary`=20has=20defined=20associations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/furniture/tobias/beneficiary.rb | 5 +++-- app/furniture/tobias/payment.rb | 1 + spec/tobias/beneficiary_spec.rb | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 spec/tobias/beneficiary_spec.rb diff --git a/app/furniture/tobias/beneficiary.rb b/app/furniture/tobias/beneficiary.rb index 7477ba219..422e4a7fd 100644 --- a/app/furniture/tobias/beneficiary.rb +++ b/app/furniture/tobias/beneficiary.rb @@ -2,8 +2,9 @@ class Tobias class Beneficiary < ApplicationRecord self.table_name = "tobias_beneficiaries" - belongs_to :trust + belongs_to :trust, inverse_of: :beneficiaries - has_many :payments + has_many :payments, inverse_of: :beneficiary + has_many :payouts, through: :payments end end diff --git a/app/furniture/tobias/payment.rb b/app/furniture/tobias/payment.rb index ddd1f0778..394c759c0 100644 --- a/app/furniture/tobias/payment.rb +++ b/app/furniture/tobias/payment.rb @@ -3,6 +3,7 @@ class Payment < ApplicationRecord self.table_name = "tobias_payments" belongs_to :payout, inverse_of: :payments + belongs_to :beneficiary, inverse_of: :payments monetize :amount_cents end diff --git a/spec/tobias/beneficiary_spec.rb b/spec/tobias/beneficiary_spec.rb new file mode 100644 index 000000000..1bb9a325a --- /dev/null +++ b/spec/tobias/beneficiary_spec.rb @@ -0,0 +1,15 @@ +require "rails_helper" + +RSpec.describe Tobias::Beneficiary, type: :model do + describe "#trust" do + it { is_expected.to belong_to(:trust).inverse_of(:beneficiaries) } + end + + describe "#payments" do + it { is_expected.to have_many(:payments).inverse_of(:beneficiary) } + end + + describe "#payouts" do + it { is_expected.to have_many(:payouts).through(:payments) } + end +end From bba75ea5ed5f857bb5ded6141c0edca68e16ca6d Mon Sep 17 00:00:00 2001 From: Zee Spencer <50284+zspencer@users.noreply.github.com> Date: Mon, 29 Jan 2024 16:05:52 -0800 Subject: [PATCH 11/13] =?UTF-8?q?=F0=9F=A7=B9=20`Tobias`:=20We=20don't=20n?= =?UTF-8?q?eed=20to=20call=20`id`=20when=20a=20relationship=20is=20defined?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/furniture/tobias/payout.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/furniture/tobias/payout.rb b/app/furniture/tobias/payout.rb index 035d9d7ab..e2b3fbb43 100644 --- a/app/furniture/tobias/payout.rb +++ b/app/furniture/tobias/payout.rb @@ -13,7 +13,7 @@ def issue per_beneficiary_amount = (amount / beneficiaries.count) beneficiaries.each do |beneficiary| - payments.create_with(amount: per_beneficiary_amount).find_or_create_by(beneficiary_id: beneficiary.id) + payments.create_with(amount: per_beneficiary_amount).find_or_create_by(beneficiary: beneficiary) end end end From b354b6bee186f04892c23aa71513df4d23d7d907 Mon Sep 17 00:00:00 2001 From: Zee Spencer <50284+zspencer@users.noreply.github.com> Date: Mon, 29 Jan 2024 16:08:02 -0800 Subject: [PATCH 12/13] =?UTF-8?q?=F0=9F=A5=97=F0=9F=A7=B9=20`Tobias`:=20Mi?= =?UTF-8?q?ssed=20`Trust#payouts`=20relationship!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/furniture/tobias/trust.rb | 1 + spec/tobias/trust_spec.rb | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/app/furniture/tobias/trust.rb b/app/furniture/tobias/trust.rb index d8dfbc071..357c690fd 100644 --- a/app/furniture/tobias/trust.rb +++ b/app/furniture/tobias/trust.rb @@ -3,5 +3,6 @@ class Trust < ApplicationRecord self.table_name = "tobias_trusts" has_many :beneficiaries, inverse_of: :trust, dependent: :destroy + has_many :payouts, inverse_of: :trust, dependent: :destroy end end diff --git a/spec/tobias/trust_spec.rb b/spec/tobias/trust_spec.rb index f0d61e935..1ae161b9b 100644 --- a/spec/tobias/trust_spec.rb +++ b/spec/tobias/trust_spec.rb @@ -4,4 +4,8 @@ describe "#benificiaries" do it { is_expected.to have_many(:beneficiaries).inverse_of(:trust).dependent(:destroy) } end + + describe "#payouts" do + it { is_expected.to have_many(:payouts).inverse_of(:trust).dependent(:destroy) } + end end From b1b305586834375769e733bcbabadd75d9b5359c Mon Sep 17 00:00:00 2001 From: Zee Spencer <50284+zspencer@users.noreply.github.com> Date: Mon, 12 Feb 2024 15:17:58 -0800 Subject: [PATCH 13/13] =?UTF-8?q?=F0=9F=A5=97=20`Tobais`:=20Spec=20for=20I?= =?UTF-8?q?ssuing=20a=20`Payout`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - https://github.com/zinc-collective/tobias/issues/11 - I'm "presuming" that a single https://github.com/zinc-collective/tobias/issues/9 may wind up managing multiplehttps://github.com/zinc-collective/tobias/issues/4 - This sketches out the pathway for creating a https://github.com/zinc-collective/tobias/issues/8 via the Web --- spec/tobias/issuing_payouts_system_spec.rb | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 spec/tobias/issuing_payouts_system_spec.rb diff --git a/spec/tobias/issuing_payouts_system_spec.rb b/spec/tobias/issuing_payouts_system_spec.rb new file mode 100644 index 000000000..d5f7dd05e --- /dev/null +++ b/spec/tobias/issuing_payouts_system_spec.rb @@ -0,0 +1,23 @@ +require "rails_helper" +require_relative "factories/trust_factory" +require_relative "factories/beneficiary_factory" + +# @see https://github.com/zinc-collective/tobias/issues/11 +RSpec.describe "Tobias: Issuing a Payout", type: :system do + scenario "Issuing a `Payout` to multiple `Beneficiaries`" do # rubocop:disable RSpec/Capybara/FeatureMethods,RSpec/ExampleLength + trust = create(:tobias_trust) + beneficiaries = create_list(:tobias_beneficiary, 10, trust:) + visit(polymorphic_path(trust.location)) + click_link("New Payout") + fill_in("Amount", with: 250_00) + + click_button("Create Payout") + + click_button("Issue Payout") + + expect(page).to have_content("$25.00 Payments Issued to 10 Beneficiaries") + beneficiaries.each do |beneficiary| + expect(page).to have_content("#{beneficiary.name} $25.00 Pending") + end + end +end