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