simple_invoice 0.0.1

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 (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