simple_invoice 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/.gitignore +2 -0
  2. data/Gemfile +2 -0
  3. data/example/Gemfile +2 -0
  4. data/example/create_invoice_from_subscription.expected-output.txt +12 -0
  5. data/example/create_invoice_from_subscription.rb +15 -0
  6. data/example/create_one_off_invoice.expected-output.txt +13 -0
  7. data/example/create_one_off_invoice.rb +19 -0
  8. data/example/lib/example_subscriptions/gardening_subscription.rb +13 -0
  9. data/example/lib/example_subscriptions/lawn_mowing_subscription.rb +13 -0
  10. data/example/lib/invoice_number_allocator.rb +15 -0
  11. data/example/lib/invoice_plain_text_formatter.rb +61 -0
  12. data/example/run_daily_process_subscriptions.expected-output.txt +14 -0
  13. data/example/run_daily_process_subscriptions.rb +27 -0
  14. data/git-hooks/INSTALL.sh +11 -0
  15. data/git-hooks/pre-commit.sh +15 -0
  16. data/lib/simple_invoice.rb +14 -0
  17. data/lib/simple_invoice/billing_period.rb +40 -0
  18. data/lib/simple_invoice/billing_period_type.rb +44 -0
  19. data/lib/simple_invoice/billing_period_type/monthly.rb +12 -0
  20. data/lib/simple_invoice/billing_period_type/weekly.rb +12 -0
  21. data/lib/simple_invoice/config.rb +33 -0
  22. data/lib/simple_invoice/contact.rb +5 -0
  23. data/lib/simple_invoice/invoice.rb +22 -0
  24. data/lib/simple_invoice/invoice_data.rb +48 -0
  25. data/lib/simple_invoice/invoice_template.rb +15 -0
  26. data/lib/simple_invoice/line_item.rb +10 -0
  27. data/lib/simple_invoice/line_items.rb +40 -0
  28. data/lib/simple_invoice/services.rb +10 -0
  29. data/lib/simple_invoice/services/allocate_invoice_number.rb +33 -0
  30. data/lib/simple_invoice/services/create_invoice.rb +50 -0
  31. data/lib/simple_invoice/services/create_invoice_for_subscription.rb +53 -0
  32. data/lib/simple_invoice/services/create_invoice_template.rb +33 -0
  33. data/lib/simple_invoice/services/process_subscription.rb +62 -0
  34. data/lib/simple_invoice/services/process_subscriptions.rb +16 -0
  35. data/lib/simple_invoice/subscription.rb +26 -0
  36. data/lib/simple_invoice/version.rb +3 -0
  37. data/simple_invoice.gemspec +20 -0
  38. data/spec/model/billing_period_spec.rb +28 -0
  39. data/spec/model/billing_period_type_monthly_spec.rb +77 -0
  40. data/spec/model/billing_period_type_weekly_spec.rb +71 -0
  41. data/spec/model/invoice_data_spec.rb +45 -0
  42. data/spec/model/line_item_spec.rb +15 -0
  43. data/spec/model/line_items_spec.rb +15 -0
  44. data/spec/model/subscription_spec.rb +28 -0
  45. data/spec/services/create_invoice_for_subscription_spec.rb +52 -0
  46. data/spec/services/process_subscription_spec.rb +42 -0
  47. data/spec/spec_helper.rb +8 -0
  48. data/test-examples.rb +23 -0
  49. data/test.sh +4 -0
  50. metadata +110 -0
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ Gemfile.lock
2
+
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/example/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ gem 'simple_invoice', :path => '..'
2
+
@@ -0,0 +1,12 @@
1
+ To: John Smith
2
+ Invoice number: 123
3
+ Issue Date: 2013-10-01
4
+ Due Date: 2013-10-08
5
+
6
+ Description Price Qty. Total
7
+ ------------------------------------------------
8
+ Monthly lawn mowing $ 60.00 1 $ 60.00
9
+ ------------------------------------------------
10
+ Total $ 60.00
11
+
12
+ Due in 7 days
@@ -0,0 +1,15 @@
1
+ # bundle exec ruby create_invoice_from_subscription.rb
2
+
3
+ require 'simple_invoice'
4
+ require_relative 'lib/invoice_plain_text_formatter'
5
+ require_relative 'lib/invoice_number_allocator'
6
+ require_relative 'lib/example_subscriptions/lawn_mowing_subscription'
7
+
8
+ SimpleInvoice::Config.allocate_invoice_number ExampleApplication::InvoiceNumberAllocator
9
+
10
+ subscription = LAWN_MOWING_SUBSCRIPTION
11
+
12
+ issue_date = '2013-10-01'
13
+ invoice = SimpleInvoice::Services::CreateInvoiceForSubscription.new(subscription, issue_date).create_invoice
14
+
15
+ puts ExampleApplication::InvoicePlainTextFormatter.new(invoice)
@@ -0,0 +1,13 @@
1
+ To: John Smith
2
+ Invoice number: 123
3
+ Issue Date: 2013-10-01
4
+ Due Date: 2013-10-08
5
+
6
+ Description Price Qty. Total
7
+ --------------------------------------------
8
+ Brown paper bag $ 0.15 1 $ 0.15
9
+ Pliers $ 9.95 2 $ 19.90
10
+ --------------------------------------------
11
+ Total $ 20.05
12
+
13
+ Due in 7 days
@@ -0,0 +1,19 @@
1
+ # bundle exec ruby create_one_off_invoice.rb
2
+
3
+ require 'simple_invoice'
4
+ require_relative 'lib/invoice_plain_text_formatter'
5
+ require_relative 'lib/invoice_number_allocator'
6
+
7
+ SimpleInvoice::Config.allocate_invoice_number ExampleApplication::InvoiceNumberAllocator
8
+
9
+ # Invoices should be created using CreateInvoice or CreateInvoiceForSubscription
10
+ # which both wrap SimpleInvoice::Invoice.new
11
+
12
+ invoice = SimpleInvoice::Services::CreateInvoice.call do |inv|
13
+ inv.contact = SimpleInvoice::Contact.new("John Smith", "12345678", "john.smith@example.com")
14
+ inv.set_dates '2013-10-01', 7
15
+ inv.add_item "Brown paper bag", 15
16
+ inv.add_item "Pliers", 995, 2
17
+ end
18
+
19
+ puts ExampleApplication::InvoicePlainTextFormatter.new(invoice)
@@ -0,0 +1,13 @@
1
+ GARDENING_SUBSCRIPTION = begin
2
+ invoice_template = SimpleInvoice::Services::CreateInvoiceTemplate.call do |temp|
3
+ temp.add_item "Monthly gardening", 50_00
4
+ end
5
+
6
+ SimpleInvoice::Subscription.new.tap do |sub|
7
+ sub.contact = SimpleInvoice::Contact.new("John Smith", "12345678", "john.smith@example.com")
8
+ sub.invoice_template = invoice_template
9
+ sub.due_days = 7
10
+ sub.billing_period_type = :monthly
11
+ sub.start_date = '2013-01-04'
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ LAWN_MOWING_SUBSCRIPTION = begin
2
+ invoice_template = SimpleInvoice::Services::CreateInvoiceTemplate.call do |temp|
3
+ temp.add_item "Monthly lawn mowing", 60_00
4
+ end
5
+
6
+ SimpleInvoice::Subscription.new.tap do |sub|
7
+ sub.contact = SimpleInvoice::Contact.new("John Smith", "12345678", "john.smith@example.com")
8
+ sub.invoice_template = invoice_template
9
+ sub.due_days = 7
10
+ sub.billing_period_type = :monthly
11
+ sub.start_date = '2013-01-02'
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ module ExampleApplication
2
+ class InvoiceNumberAllocator
3
+
4
+ # In a real application, this might do a database query to determine the next
5
+ # unused invoice number. The application may or may not make use of the
6
+ # invoice object passed in (eg. the invoice number might simply be sequence,
7
+ # or it might be based on the issue date - invoice.issue_date() )
8
+ def self.call invoice
9
+ # allocate the same number every time - obviously not an appropriate
10
+ # real-world implementation :)
11
+ 123
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,61 @@
1
+ module ExampleApplication
2
+ class InvoicePlainTextFormatter
3
+
4
+ def initialize invoice
5
+ @invoice = invoice
6
+ end
7
+
8
+ def to_s
9
+ "#{header}\n" \
10
+ "#{line_items}\n" \
11
+ "#{footer}"
12
+ end
13
+
14
+ private
15
+
16
+ def header
17
+ "To: #{@invoice.contact.name}\n" \
18
+ "Invoice number: #{@invoice.invoice_number}\n" \
19
+ "Issue Date: #{@invoice.issue_date}\n" \
20
+ "Due Date: #{@invoice.due_date}\n"
21
+ end
22
+
23
+ def max_description_length
24
+ @max_description_length ||= @invoice.line_items.to_a.collect do |item|
25
+ item.description.length
26
+ end.max
27
+ end
28
+
29
+ def line_items
30
+ line_items_header + "\n" +
31
+ @invoice.line_items.to_a.collect do |item|
32
+ line_item item
33
+ end.join("\n") + "\n" + total_line + "\n"
34
+ end
35
+
36
+ def line_items_header
37
+ "%-#{max_description_length}s Price Qty. Total" % ['Description'] + "\n" +
38
+ "-" * (max_description_length + 29)
39
+ end
40
+
41
+ def line_item item
42
+ "%-#{max_description_length}s %s %2s %s" % [item.description, format_price(item.price), item.quantity, format_price(item.total)]
43
+ end
44
+
45
+ def total_line
46
+ "-" * (max_description_length + 29) + "\n" +
47
+ "%-#{max_description_length}s %s" % ['Total', format_price(@invoice.line_items.total)]
48
+ end
49
+
50
+ def format_price price
51
+ dollars = price / 100
52
+ cents = price % 100
53
+ "$%3d.%02d" % [dollars, cents]
54
+ end
55
+
56
+ def footer
57
+ "Due in #{@invoice.due_days} days"
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,14 @@
1
+ Processed 2 subscriptions and generated 1 invoices
2
+
3
+ To: John Smith
4
+ Invoice number: 123
5
+ Issue Date: 2013-01-02
6
+ Due Date: 2013-01-09
7
+
8
+ Description Price Qty. Total
9
+ ------------------------------------------------
10
+ Monthly lawn mowing $ 60.00 1 $ 60.00
11
+ ------------------------------------------------
12
+ Total $ 60.00
13
+
14
+ Due in 7 days
@@ -0,0 +1,27 @@
1
+ # bundle exec ruby run_daily_process_subscriptions.rb
2
+
3
+ require 'simple_invoice'
4
+ require_relative 'lib/invoice_plain_text_formatter'
5
+ require_relative 'lib/invoice_number_allocator'
6
+ require_relative 'lib/example_subscriptions/lawn_mowing_subscription'
7
+ require_relative 'lib/example_subscriptions/gardening_subscription'
8
+
9
+ SimpleInvoice::Config.allocate_invoice_number ExampleApplication::InvoiceNumberAllocator
10
+
11
+ # In a real application, this might be the result of Date.today or ARGV[0]
12
+ today = '2013-01-02'
13
+
14
+ # Hard-coded subscription objects. In a real application, subscriptions would
15
+ # be reconstructed from database records.
16
+ subscriptions = [LAWN_MOWING_SUBSCRIPTION, GARDENING_SUBSCRIPTION]
17
+
18
+ invoices = SimpleInvoice::Services::ProcessSubscriptions.call today, subscriptions
19
+
20
+ # In a real application, invoices would need to be persisted at this point, and
21
+ # perhaps further processed, like generating a PDF and sending it in an email.
22
+
23
+ puts "Processed #{subscriptions.length} subscriptions and generated #{invoices.length} invoices"
24
+ puts ""
25
+ invoices.each do |invoice|
26
+ puts ExampleApplication::InvoicePlainTextFormatter.new(invoice)
27
+ end
@@ -0,0 +1,11 @@
1
+ #!/bin/bash
2
+ # Usage: git-hooks/INSTALL.sh
3
+ # Note run from repo root
4
+
5
+ if [[ "`pwd`" == *git-hooks* ]]; then
6
+ echo "Run from repo root instead. ie. cd ..; git-hooks/INSTALL.sh"
7
+ exit 1;
8
+ fi
9
+
10
+ unlink .git/hooks/pre-commit 2>/dev/null
11
+ ln -s ../../git-hooks/pre-commit.sh .git/hooks/pre-commit
@@ -0,0 +1,15 @@
1
+ #!/bin/bash
2
+ tests_pass() {
3
+ ./test.sh
4
+ }
5
+
6
+ if [ "$JUST_DO_IT" == "1" ]; then
7
+ exit 0
8
+ fi
9
+
10
+ if tests_pass; then
11
+ echo "[INFO] tests passed"
12
+ else
13
+ echo "[ERROR] tests failed"
14
+ exit 1
15
+ fi
@@ -0,0 +1,14 @@
1
+ module SimpleInvoice
2
+ autoload :VERSION, 'simple_invoice/version'
3
+ autoload :Invoice, 'simple_invoice/invoice'
4
+ autoload :InvoiceData, 'simple_invoice/invoice_data'
5
+ autoload :LineItems, 'simple_invoice/line_items'
6
+ autoload :LineItem, 'simple_invoice/line_item'
7
+ autoload :InvoiceTemplate, 'simple_invoice/invoice_template'
8
+ autoload :Subscription, 'simple_invoice/subscription'
9
+ autoload :Contact, 'simple_invoice/contact'
10
+ autoload :Services, 'simple_invoice/services'
11
+ autoload :Config, 'simple_invoice/config'
12
+ autoload :BillingPeriodType, 'simple_invoice/billing_period_type'
13
+ autoload :BillingPeriod, 'simple_invoice/billing_period'
14
+ end
@@ -0,0 +1,40 @@
1
+ module SimpleInvoice
2
+ class BillingPeriod
3
+
4
+ attr_reader :first_day
5
+
6
+ # @param [BillingPeriodType, Symbol] billing_period_type
7
+ # @param [Date, String] first_day
8
+ def initialize billing_period_type, first_day
9
+ if billing_period_type.is_a? Symbol
10
+ @billing_period_type = BillingPeriodType.send(billing_period_type)
11
+ else
12
+ @billing_period_type = billing_period_type
13
+ end
14
+ @first_day = to_date first_day
15
+ end
16
+
17
+ def last_day
18
+ @last_day ||= @billing_period_type.last_day_of_period(@first_day)
19
+ end
20
+
21
+ # @return [BillingPeriod]
22
+ def next_billing_period
23
+ next_first_day = @billing_period_type.first_day_of_next_period @first_day
24
+ self.class.new @billing_period_type, next_first_day
25
+ end
26
+
27
+ private
28
+
29
+ # @param [String, Date] date
30
+ # @return [Date]
31
+ def to_date date
32
+ if date.is_a? Date
33
+ date
34
+ else
35
+ Date.parse date.to_s
36
+ end
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,44 @@
1
+ module SimpleInvoice
2
+ class BillingPeriodType
3
+
4
+ def self.weekly
5
+ Weekly.new 1
6
+ end
7
+
8
+ def self.fortnightly
9
+ Weekly.new 2
10
+ end
11
+
12
+ def self.monthly
13
+ Monthly.new 1
14
+ end
15
+
16
+ def self.quarterly
17
+ Monthly.new 3
18
+ end
19
+
20
+ def self.annually
21
+ Monthly.new 12
22
+ end
23
+
24
+ autoload :Monthly, 'simple_invoice/billing_period_type/monthly'
25
+ autoload :Weekly, 'simple_invoice/billing_period_type/weekly'
26
+
27
+ # @param multiple [Fixnum]
28
+ def initialize multiple=1
29
+ @multiple = multiple
30
+ end
31
+
32
+ # @param first_day [Date]
33
+ def first_day_of_next_period first_day
34
+ raise "to be implemented in subclass"
35
+ end
36
+
37
+ # @param first_day [Date]
38
+ # @return [Date]
39
+ def last_day_of_period first_day
40
+ first_day_of_next_period(first_day).prev_day
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,12 @@
1
+ module SimpleInvoice
2
+ class BillingPeriodType
3
+ class Monthly < BillingPeriodType
4
+
5
+ # @param first_day [Date]
6
+ def first_day_of_next_period first_day
7
+ first_day.next_month(@multiple)
8
+ end
9
+
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module SimpleInvoice
2
+ class BillingPeriodType
3
+ class Weekly < BillingPeriodType
4
+
5
+ # @param first_day [Date]
6
+ def first_day_of_next_period first_day
7
+ first_day.next_day(7 * @multiple)
8
+ end
9
+
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,33 @@
1
+ module SimpleInvoice
2
+ class Config
3
+ class << self
4
+
5
+ extend Forwardable
6
+ def_delegator :instance, :[]
7
+
8
+ def instance
9
+ @instance ||= new
10
+ end
11
+
12
+ # @param callable [#call] object that responds to call(),
13
+ # which returns the next invoice number.
14
+ def allocate_invoice_number callable
15
+ instance[:allocate_invoice_number] = callable
16
+ end
17
+
18
+ end
19
+
20
+ def initialize
21
+ @config_hash = {}
22
+ end
23
+
24
+ def [](key)
25
+ @config_hash[key]
26
+ end
27
+
28
+ def []=(key, value)
29
+ @config_hash[key] = value
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ module SimpleInvoice
2
+ class Contact < Struct.new(:name, :phone, :email)
3
+
4
+ end
5
+ end
@@ -0,0 +1,22 @@
1
+ module SimpleInvoice
2
+ class Invoice
3
+
4
+ extend Forwardable
5
+
6
+ attr_accessor :contact, :void
7
+ attr_reader :line_items
8
+ def_delegator :@line_items, :push, :add_line_item
9
+ def_delegators :@data, :invoice_number, :invoice_number=, :issue_date,
10
+ :due_date, :set_dates, :due_days
11
+
12
+ # @param inv_number [#to_s, nil]
13
+ # @param issue_date [#to_s, Date, nil]
14
+ # @param due_date_or_due_days [#to_s, Date, Fixnum, nil] interpreted as due days if Fixnum
15
+ def initialize invoice_number=nil, issue_date=nil, due_date_or_due_days=nil
16
+ @line_items = LineItems.new
17
+ @data = InvoiceData.new invoice_number=nil, issue_date=nil, due_date_or_due_days=nil
18
+ @void = false
19
+ end
20
+
21
+ end
22
+ end