money_tracking 0.99.0

Sign up to get free protection for your applications and to get access to all the features.
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