money_tracking 0.99.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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +8 -0
  5. data/Gemfile +10 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +61 -0
  8. data/Rakefile +2 -0
  9. data/bin/money +6 -0
  10. data/lib/money_tracking.rb +6 -0
  11. data/lib/money_tracking/cli.rb +88 -0
  12. data/lib/money_tracking/cli/commands.rb +7 -0
  13. data/lib/money_tracking/cli/create_command.rb +21 -0
  14. data/lib/money_tracking/cli/delete_command.rb +15 -0
  15. data/lib/money_tracking/cli/list_command.rb +16 -0
  16. data/lib/money_tracking/cli/runner.rb +54 -0
  17. data/lib/money_tracking/cli/update_command.rb +33 -0
  18. data/lib/money_tracking/cli/views.rb +15 -0
  19. data/lib/money_tracking/cli/views/empty.rb +11 -0
  20. data/lib/money_tracking/cli/views/expense_created.rb +11 -0
  21. data/lib/money_tracking/cli/views/expense_deleted.rb +15 -0
  22. data/lib/money_tracking/cli/views/expense_item.rb +34 -0
  23. data/lib/money_tracking/cli/views/expense_list.rb +11 -0
  24. data/lib/money_tracking/cli/views/expense_not_found.rb +15 -0
  25. data/lib/money_tracking/cli/views/expense_not_updated.rb +19 -0
  26. data/lib/money_tracking/cli/views/expense_updated.rb +15 -0
  27. data/lib/money_tracking/data_store/file_store.rb +81 -0
  28. data/lib/money_tracking/data_store/protocol.rb +20 -0
  29. data/lib/money_tracking/domain.rb +12 -0
  30. data/lib/money_tracking/domain/expense.rb +92 -0
  31. data/lib/money_tracking/domain/expense_factory.rb +15 -0
  32. data/lib/money_tracking/domain/expense_finder.rb +13 -0
  33. data/lib/money_tracking/version.rb +3 -0
  34. data/money_tracking.gemspec +25 -0
  35. data/spec/aruba_helper.rb +31 -0
  36. data/spec/integration/cli_spec.rb +129 -0
  37. data/spec/money_tracking/cli/create_command_spec.rb +38 -0
  38. data/spec/money_tracking/cli/delete_command_spec.rb +44 -0
  39. data/spec/money_tracking/cli/list_command_spec.rb +34 -0
  40. data/spec/money_tracking/cli/update_command_spec.rb +92 -0
  41. data/spec/money_tracking/cli/views/expense_item_spec.rb +60 -0
  42. data/spec/money_tracking/cli/views/expense_list_spec.rb +40 -0
  43. data/spec/money_tracking/data_store/acts_as_a_data_store.rb +144 -0
  44. data/spec/money_tracking/data_store/file_store_spec.rb +16 -0
  45. data/spec/money_tracking/domain/expense_factory_spec.rb +58 -0
  46. data/spec/money_tracking/domain/expense_finder_spec.rb +59 -0
  47. data/spec/money_tracking/domain/expense_spec.rb +125 -0
  48. data/spec/spec_helper.rb +32 -0
  49. metadata +148 -0
@@ -0,0 +1,38 @@
1
+ module MoneyTracking
2
+ module Cli
3
+ RSpec.describe CreateCommand do
4
+ subject { described_class.new(expense_factory, "79.5", "euro", ["food", "other"]) }
5
+
6
+ let(:expense_factory) { instance_double(Domain::ExpenseFactory) }
7
+ let(:expense) { instance_double(Domain::Expense, build_view: nil) }
8
+ let(:view) { instance_double(Views::ExpenseCreated) }
9
+
10
+ before do
11
+ allow(expense_factory)
12
+ .to receive(:create).with(
13
+ amount: 79.5,
14
+ currency: "euro",
15
+ tags: ["food", "other"],
16
+ ).and_return(expense)
17
+ end
18
+
19
+ it "creates an expense object" do
20
+ expect(expense_factory)
21
+ .to receive(:create).with(
22
+ amount: 79.5,
23
+ currency: "euro",
24
+ tags: ["food", "other"],
25
+ ).and_return(expense)
26
+ subject.call
27
+ end
28
+
29
+ it "renders ExpenseCreated view" do
30
+ allow(expense)
31
+ .to receive(:build_view)
32
+ .with(Views::ExpenseCreated)
33
+ .and_return(view)
34
+ expect(subject.call).to eq(view)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,44 @@
1
+ module MoneyTracking
2
+ module Cli
3
+ RSpec.describe DeleteCommand do
4
+ subject { described_class.new(expense_finder, expense_id) }
5
+
6
+ let(:expense_finder) { instance_double(Domain::ExpenseFinder) }
7
+ let(:expense_id) { "ja6ptd3d" }
8
+ let(:expense) { instance_double(Domain::Expense) }
9
+ let(:not_found) { instance_double(Domain::ExpenseNotFound) }
10
+ let(:view) { instance_double(Views::ExpenseDeleted) }
11
+ let(:not_found_view) { instance_double(Views::ExpenseNotFound) }
12
+
13
+ before do
14
+ allow(expense).to receive(:build_view).with(Views::ExpenseDeleted).and_return(view)
15
+ allow(expense).to receive(:delete).and_return(expense)
16
+
17
+ allow(not_found).to receive(:build_view).with(Views::ExpenseDeleted)
18
+ .and_return(not_found_view)
19
+ allow(not_found).to receive(:delete).and_return(not_found)
20
+ end
21
+
22
+ context "when no such expense found" do
23
+ before { allow(expense_finder).to receive(:read).with(expense_id).and_return(not_found) }
24
+
25
+ it "returns ExpenseNotFound view" do
26
+ expect(subject.call).to be(not_found_view)
27
+ end
28
+ end
29
+
30
+ context "when expense was found" do
31
+ before { allow(expense_finder).to receive(:read).with(expense_id).and_return(expense) }
32
+
33
+ it "returns ExpenseDeleted view" do
34
+ expect(subject.call).to be(view)
35
+ end
36
+
37
+ it "deletes an expense" do
38
+ expect(expense).to receive(:delete)
39
+ subject.call
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,34 @@
1
+ module MoneyTracking
2
+ module Cli
3
+ RSpec.describe ListCommand do
4
+ subject { described_class.new(expense_finder) }
5
+
6
+ let(:view) { double("View") }
7
+ let(:expense_finder) { instance_double(Domain::ExpenseFinder) }
8
+ let(:expenses) { [expense_a, expense_b] }
9
+
10
+ let(:expense_a) { instance_double(Domain::Expense) }
11
+ let(:expense_b) { instance_double(Domain::Expense) }
12
+
13
+ context "when there are no expenses" do
14
+ before { allow(expense_finder).to receive(:list).and_return([]) }
15
+
16
+ it "returns empty view" do
17
+ expect(subject.call).to be_a(Views::Empty)
18
+ end
19
+ end
20
+
21
+ context "when there are some expenses" do
22
+ before { allow(expense_finder).to receive(:list).and_return(expenses) }
23
+
24
+ it "creates proper expense list view and returns it" do
25
+ allow(Views::ExpenseList)
26
+ .to receive(:new)
27
+ .with(expenses, Views::ExpenseItem)
28
+ .and_return(view)
29
+ expect(subject.call).to be(view)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,92 @@
1
+ module MoneyTracking
2
+ module Cli
3
+ RSpec.describe UpdateCommand do
4
+ subject { described_class.new(
5
+ expense_finder,
6
+ id,
7
+ amount,
8
+ currency,
9
+ add_tags,
10
+ rm_tags,
11
+ ) }
12
+
13
+ let(:expense_finder) { instance_double(Domain::ExpenseFinder) }
14
+ let(:not_found) { instance_double(Domain::ExpenseNotFound) }
15
+ let(:not_found_view) { instance_double(Views::ExpenseNotFound) }
16
+
17
+ let(:view) { instance_double(Views::ExpenseUpdated) }
18
+ let(:not_updated_view) { instance_double(Views::ExpenseNotUpdated) }
19
+
20
+ let(:id) { "8ti0osfb" }
21
+ let(:amount) { nil }
22
+ let(:currency) { nil }
23
+ let(:add_tags) { nil }
24
+ let(:rm_tags) { nil }
25
+
26
+ before { allow(not_found).to receive(:update).and_return(not_found) }
27
+
28
+ context "when expense is not found" do
29
+ before { allow(expense_finder).to receive(:read).with(id).and_return(not_found) }
30
+
31
+ it "renders not found view" do
32
+ allow(not_found)
33
+ .to receive(:build_view)
34
+ .and_return(not_found_view)
35
+ expect(subject.call).to be(not_found_view)
36
+ end
37
+ end
38
+
39
+ context "when expense is found" do
40
+ let(:expense) { instance_double(Domain::Expense) }
41
+
42
+ before do
43
+ allow(expense_finder).to receive(:read).with(id).and_return(expense)
44
+ allow(expense).to receive(:update).and_return(expense)
45
+
46
+ allow(expense)
47
+ .to receive(:build_view)
48
+ .with(Views::ExpenseUpdated)
49
+ .and_return(view)
50
+
51
+ allow(expense)
52
+ .to receive(:build_view)
53
+ .with(Views::ExpenseNotUpdated)
54
+ .and_return(not_updated_view)
55
+ end
56
+
57
+ context "when there are no other options provided" do
58
+ it "does nothing" do
59
+ subject.call
60
+ end
61
+
62
+ it "renders ExpenseNotUpdated view" do
63
+ expect(subject.call).to be(not_updated_view)
64
+ end
65
+ end
66
+
67
+ context "when there was some options provided" do
68
+ let(:amount) { 94.5 }
69
+ let(:currency) { "dollar" }
70
+ let(:add_tags) { ["food", "drinks"] }
71
+ let(:rm_tags) { ["dinner"] }
72
+
73
+ it "delegates to model's #update" do
74
+ expect(expense)
75
+ .to receive(:update)
76
+ .with(
77
+ amount: amount,
78
+ currency: currency,
79
+ add_tags: add_tags,
80
+ rm_tags: rm_tags,
81
+ ).and_return(expense)
82
+ subject.call
83
+ end
84
+
85
+ it "returns ExpenseUpdated view" do
86
+ expect(subject.call).to be(view)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,60 @@
1
+ module MoneyTracking
2
+ module Cli
3
+ module Views
4
+ RSpec.describe ExpenseItem do
5
+ subject(:view) {
6
+ described_class.new(
7
+ id: id,
8
+ created_at: created_at,
9
+ amount: amount,
10
+ currency: currency,
11
+ tags: tags,
12
+ )
13
+ }
14
+
15
+ let(:id) { "go5775ft" }
16
+ let(:created_at) { "14-04-2015 17:03:25" }
17
+ let(:amount) { 47.39 }
18
+ let(:currency) { "dollar" }
19
+ let(:tags) { ["other"] }
20
+
21
+ it "returns proper string representation" do
22
+ expect(view.to_s)
23
+ .to eq("go5775ft - 14-04-2015 17:03:25: 47.39 dollar [other]")
24
+ end
25
+
26
+ context "when number has more than 2 decimals after floating point" do
27
+ let(:amount) { 33.9876 }
28
+ it "rounds everything after 2 first decimals" do
29
+ expect(view.to_s)
30
+ .to eq("go5775ft - 14-04-2015 17:03:25: 33.99 dollar [other]")
31
+ end
32
+ end
33
+
34
+ context "when number has less than 2 decimals after floating point" do
35
+ let(:amount) { 33.7 }
36
+ it "pads with zeros up to 2 decimals" do
37
+ expect(view.to_s)
38
+ .to eq("go5775ft - 14-04-2015 17:03:25: 33.70 dollar [other]")
39
+ end
40
+ end
41
+
42
+ context "when there are no tags" do
43
+ let(:tags) { [] }
44
+ it "does not render them" do
45
+ expect(view.to_s)
46
+ .to eq("go5775ft - 14-04-2015 17:03:25: 47.39 dollar")
47
+ end
48
+ end
49
+
50
+ context "when there are more tags" do
51
+ let(:tags) { ["other", "food", "pricey"] }
52
+ it "does render them in sorted order" do
53
+ expect(view.to_s)
54
+ .to eq("go5775ft - 14-04-2015 17:03:25: 47.39 dollar [food other pricey]")
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,40 @@
1
+ module MoneyTracking
2
+ module Cli
3
+ module Views
4
+ RSpec.describe ExpenseList do
5
+ subject(:view) {
6
+ described_class.new(
7
+ expenses,
8
+ item_factory,
9
+ )
10
+ }
11
+
12
+ let(:expenses) { [expense_a, expense_b, expense_c] }
13
+ let(:item_factory) { class_double(ExpenseItem) }
14
+
15
+ let(:expense_a) { instance_double(Domain::Expense) }
16
+ let(:expense_b) { instance_double(Domain::Expense) }
17
+ let(:expense_c) { instance_double(Domain::Expense) }
18
+
19
+ let(:item_a) { double("Item expense a", to_s: "an item a") }
20
+ let(:item_b) { double("Item expense b", to_s: "an item b") }
21
+ let(:item_c) { double("Item expense c", to_s: "an item c") }
22
+
23
+ before do
24
+ allow(expense_a).to receive(:build_view).with(item_factory).and_return(item_a)
25
+ allow(expense_b).to receive(:build_view).with(item_factory).and_return(item_b)
26
+ allow(expense_c).to receive(:build_view).with(item_factory).and_return(item_c)
27
+ end
28
+
29
+ it "renders proper list" do
30
+ expect(view.to_s.split("\n"))
31
+ .to eq([
32
+ "an item a",
33
+ "an item b",
34
+ "an item c",
35
+ ])
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,144 @@
1
+ require "set"
2
+
3
+ RSpec.shared_context "acts as a DataStore" do |options|
4
+ store_reset = options.fetch(:store_reset, -> {})
5
+
6
+ before { store_reset.call }
7
+ after { store_reset.call }
8
+
9
+ shared_examples "returns nil" do
10
+ it "returns nil" do
11
+ is_expected.to eq(nil)
12
+ end
13
+ end
14
+
15
+ shared_examples "returns empty record set" do
16
+ it "returns empty record set" do
17
+ is_expected.to be_empty
18
+ end
19
+ end
20
+
21
+ describe "#create" do
22
+ subject { store.create(fields) }
23
+ let(:fields) { { hello: "world", test: 35, some: [1, 2, "3"] } }
24
+
25
+ it "returns unique id of created record" do
26
+ is_expected.to match(/^[\d\w]{8}$/)
27
+ end
28
+ end
29
+
30
+ describe "#read" do
31
+ subject { store.read(id) }
32
+ let(:id) { "some id" }
33
+
34
+ context "when there was some records created" do
35
+ let(:fields_a) { { hello: "world", test: 99 } }
36
+ let(:fields_b) { { crazy: "stuff", amount: 89.5, tags: ["hello", "world"] } }
37
+ let(:fields_c) { { lonely: "field" } }
38
+
39
+ let!(:id_a) { store.create(fields_a) }
40
+ let!(:id_b) { store.create(fields_b) }
41
+ let!(:id_c) { store.create(fields_c) }
42
+
43
+ let(:id) { id_b }
44
+
45
+ it "returns fields of the record with specified id" do
46
+ is_expected.to eq(fields_b.merge(id: id_b))
47
+ end
48
+
49
+ context "but record with specified id doesn't exist" do
50
+ let(:id) { "other id" }
51
+
52
+ include_examples "returns nil"
53
+ end
54
+
55
+ context "and was deleted afterwards" do
56
+ before { store.delete(id) }
57
+
58
+ include_examples "returns nil"
59
+ end
60
+
61
+ context "and was updated with new fields" do
62
+ before { store.update(id, new_fields) }
63
+
64
+ let(:new_fields) { fields_b.merge(amount: 44, hello: "world") }
65
+
66
+ it "returns updated fields of the record with specified id" do
67
+ is_expected.to eq(new_fields.merge(id: id_b))
68
+ end
69
+ end
70
+ end
71
+
72
+ context "when record wasn't created" do
73
+ include_examples "returns nil"
74
+ end
75
+ end
76
+
77
+ describe "#list" do
78
+ subject { store.list.to_set }
79
+
80
+ context "when there was no record created" do
81
+ include_examples "returns empty record set"
82
+ end
83
+
84
+ context "when there was some records created" do
85
+ let(:fields_a) { { hello: "world", test: 99 } }
86
+ let(:fields_b) { { crazy: "stuff", amount: 89.5, tags: ["hello", "world"] } }
87
+ let(:fields_c) { { lonely: "field" } }
88
+ let(:fields_d) { { user: 79, name: "john" } }
89
+
90
+ let!(:id_a) { store.create(fields_a) }
91
+ let!(:id_b) { store.create(fields_b) }
92
+ let!(:id_c) { store.create(fields_c) }
93
+ let!(:id_d) { store.create(fields_d) }
94
+
95
+ let(:final_a) { fields_a.merge(id: id_a) }
96
+ let(:final_b) { fields_b.merge(id: id_b) }
97
+ let(:final_c) { fields_c.merge(id: id_c) }
98
+ let(:final_d) { fields_d.merge(id: id_d) }
99
+
100
+ it "returns all these records" do
101
+ is_expected.to eq([final_a, final_b, final_c, final_d].to_set)
102
+ end
103
+
104
+ context "and some records were updated" do
105
+ before do
106
+ store.update(id_a, new_fields_a)
107
+ store.update(id_c, new_fields_c)
108
+ end
109
+
110
+ let(:new_fields_a) { fields_a.merge(lazy: "programmer", pragmatic: "programmer") }
111
+ let(:new_fields_c) { fields_c.merge(amount: 44, hello: "world") }
112
+
113
+ let(:new_final_a) { new_fields_a.merge(id: id_a) }
114
+ let(:new_final_c) { new_fields_c.merge(id: id_c) }
115
+
116
+ it "returns all these records with updated fields" do
117
+ is_expected.to eq([new_final_a, final_b, new_final_c, final_d].to_set)
118
+ end
119
+ end
120
+
121
+ context "and some records were deleted" do
122
+ before do
123
+ store.delete(id_b)
124
+ store.delete(id_c)
125
+ end
126
+
127
+ it "returns all the records that are left" do
128
+ is_expected.to eq([final_a, final_d].to_set)
129
+ end
130
+ end
131
+
132
+ context "and all records were deleted" do
133
+ before do
134
+ store.delete(id_a)
135
+ store.delete(id_b)
136
+ store.delete(id_c)
137
+ store.delete(id_d)
138
+ end
139
+
140
+ include_examples "returns empty record set"
141
+ end
142
+ end
143
+ end
144
+ end