simple_invoice 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/Gemfile +2 -0
- data/example/Gemfile +2 -0
- data/example/create_invoice_from_subscription.expected-output.txt +12 -0
- data/example/create_invoice_from_subscription.rb +15 -0
- data/example/create_one_off_invoice.expected-output.txt +13 -0
- data/example/create_one_off_invoice.rb +19 -0
- data/example/lib/example_subscriptions/gardening_subscription.rb +13 -0
- data/example/lib/example_subscriptions/lawn_mowing_subscription.rb +13 -0
- data/example/lib/invoice_number_allocator.rb +15 -0
- data/example/lib/invoice_plain_text_formatter.rb +61 -0
- data/example/run_daily_process_subscriptions.expected-output.txt +14 -0
- data/example/run_daily_process_subscriptions.rb +27 -0
- data/git-hooks/INSTALL.sh +11 -0
- data/git-hooks/pre-commit.sh +15 -0
- data/lib/simple_invoice.rb +14 -0
- data/lib/simple_invoice/billing_period.rb +40 -0
- data/lib/simple_invoice/billing_period_type.rb +44 -0
- data/lib/simple_invoice/billing_period_type/monthly.rb +12 -0
- data/lib/simple_invoice/billing_period_type/weekly.rb +12 -0
- data/lib/simple_invoice/config.rb +33 -0
- data/lib/simple_invoice/contact.rb +5 -0
- data/lib/simple_invoice/invoice.rb +22 -0
- data/lib/simple_invoice/invoice_data.rb +48 -0
- data/lib/simple_invoice/invoice_template.rb +15 -0
- data/lib/simple_invoice/line_item.rb +10 -0
- data/lib/simple_invoice/line_items.rb +40 -0
- data/lib/simple_invoice/services.rb +10 -0
- data/lib/simple_invoice/services/allocate_invoice_number.rb +33 -0
- data/lib/simple_invoice/services/create_invoice.rb +50 -0
- data/lib/simple_invoice/services/create_invoice_for_subscription.rb +53 -0
- data/lib/simple_invoice/services/create_invoice_template.rb +33 -0
- data/lib/simple_invoice/services/process_subscription.rb +62 -0
- data/lib/simple_invoice/services/process_subscriptions.rb +16 -0
- data/lib/simple_invoice/subscription.rb +26 -0
- data/lib/simple_invoice/version.rb +3 -0
- data/simple_invoice.gemspec +20 -0
- data/spec/model/billing_period_spec.rb +28 -0
- data/spec/model/billing_period_type_monthly_spec.rb +77 -0
- data/spec/model/billing_period_type_weekly_spec.rb +71 -0
- data/spec/model/invoice_data_spec.rb +45 -0
- data/spec/model/line_item_spec.rb +15 -0
- data/spec/model/line_items_spec.rb +15 -0
- data/spec/model/subscription_spec.rb +28 -0
- data/spec/services/create_invoice_for_subscription_spec.rb +52 -0
- data/spec/services/process_subscription_spec.rb +42 -0
- data/spec/spec_helper.rb +8 -0
- data/test-examples.rb +23 -0
- data/test.sh +4 -0
- metadata +110 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/example/Gemfile
ADDED
@@ -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,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,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,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
|