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
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module SimpleInvoice
|
|
2
|
+
class InvoiceData
|
|
3
|
+
|
|
4
|
+
attr_reader :invoice_number, :issue_date, :due_date
|
|
5
|
+
|
|
6
|
+
# @param inv_number [#to_s, nil]
|
|
7
|
+
# @param issue_date [#to_s, Date, nil]
|
|
8
|
+
# @param due_date_or_due_days [#to_s, Date, Fixnum, nil] interpreted as due days if Fixnum
|
|
9
|
+
def initialize inv_number=nil, issue_date=nil, due_date_or_due_days=nil
|
|
10
|
+
self.invoice_number = inv_number unless inv_number.nil?
|
|
11
|
+
set_dates(issue_date, due_date_or_due_days) unless [issue_date, due_date_or_due_days].any?(&:nil?)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param inv_number [#to_s]
|
|
15
|
+
def invoice_number= inv_number
|
|
16
|
+
@invoice_number = inv_number.to_s
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param issue_date [#to_s, Date]
|
|
20
|
+
# @param due_date_or_due_days [#to_s, Date, Fixnum] interpreted as due days if Fixnum
|
|
21
|
+
def set_dates issue_date, due_date_or_due_days
|
|
22
|
+
@issue_date = to_date issue_date
|
|
23
|
+
if due_date_or_due_days.is_a? Fixnum
|
|
24
|
+
@due_date = @issue_date + due_date_or_due_days
|
|
25
|
+
else
|
|
26
|
+
@due_date = to_date due_date_or_due_days
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [Fixnum]
|
|
31
|
+
def due_days
|
|
32
|
+
(due_date - issue_date).to_i
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# @param [String, Date] date
|
|
38
|
+
# @return [Date]
|
|
39
|
+
def to_date date
|
|
40
|
+
if date.is_a? Date
|
|
41
|
+
date
|
|
42
|
+
else
|
|
43
|
+
Date.parse date.to_s
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module SimpleInvoice
|
|
2
|
+
class LineItems
|
|
3
|
+
|
|
4
|
+
# @param line_items [Array<LineItem>]
|
|
5
|
+
def initialize line_items=[]
|
|
6
|
+
@line_items = line_items
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# @param line_item [LineItem]
|
|
10
|
+
def push line_item
|
|
11
|
+
@line_items.push line_item
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def length
|
|
15
|
+
@line_items.length
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def each &block
|
|
19
|
+
@line_items.each(&block)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_a
|
|
23
|
+
@line_items
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [Fixnum]
|
|
27
|
+
def total
|
|
28
|
+
sum @line_items.collect(&:total)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# @param values [Array<Fixnum>]
|
|
34
|
+
# @return [Finxum]
|
|
35
|
+
def sum values
|
|
36
|
+
values.inject(0){|a,b| a + b }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module SimpleInvoice
|
|
2
|
+
module Services
|
|
3
|
+
autoload :CreateInvoiceForSubscription, 'simple_invoice/services/create_invoice_for_subscription'
|
|
4
|
+
autoload :CreateInvoice, 'simple_invoice/services/create_invoice'
|
|
5
|
+
autoload :CreateInvoiceTemplate, 'simple_invoice/services/create_invoice_template'
|
|
6
|
+
autoload :AllocateInvoiceNumber, 'simple_invoice/services/allocate_invoice_number'
|
|
7
|
+
autoload :ProcessSubscription, 'simple_invoice/services/process_subscription'
|
|
8
|
+
autoload :ProcessSubscriptions, 'simple_invoice/services/process_subscriptions'
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module SimpleInvoice
|
|
2
|
+
module Services
|
|
3
|
+
class AllocateInvoiceNumber
|
|
4
|
+
|
|
5
|
+
# Allocate invoice number and apply it to invoice object
|
|
6
|
+
# @param invoice [SimpleInvoice::Invoice]
|
|
7
|
+
# @return [String] invoice number
|
|
8
|
+
def self.call invoice
|
|
9
|
+
next_invoice_number(invoice).tap do |number|
|
|
10
|
+
invoice.invoice_number = number
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def self.next_invoice_number invoice
|
|
17
|
+
next_invoice_number_callable.call invoice
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.next_invoice_number_callable
|
|
21
|
+
SimpleInvoice::Config[:allocate_invoice_number] || lambda do |invoice|
|
|
22
|
+
# This lambda is just a stub implementation
|
|
23
|
+
# A real implementation would be provided by the application,
|
|
24
|
+
# and not this library.
|
|
25
|
+
@next_invoice_number ||= 0
|
|
26
|
+
number = @next_invoice_number += 1
|
|
27
|
+
"INV-#{number}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module SimpleInvoice
|
|
2
|
+
module Services
|
|
3
|
+
class CreateInvoice
|
|
4
|
+
|
|
5
|
+
# @return [SimpleInvoice::Invoice]
|
|
6
|
+
def self.call &block
|
|
7
|
+
instance = new
|
|
8
|
+
block.call instance
|
|
9
|
+
instance.create_invoice
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_writer :contact
|
|
13
|
+
|
|
14
|
+
extend Forwardable
|
|
15
|
+
def_delegator :invoice, :set_dates
|
|
16
|
+
|
|
17
|
+
# @return [SimpleInvoice::Invoice]
|
|
18
|
+
def create_invoice
|
|
19
|
+
invoice.tap do |inv|
|
|
20
|
+
allocate_invoice_number inv
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param description [String]
|
|
25
|
+
# @param price [Fixnum]
|
|
26
|
+
# @param quantity [Fixnum]
|
|
27
|
+
def add_item description, price, quantity=1
|
|
28
|
+
line_item = LineItem.new description, price, quantity
|
|
29
|
+
invoice.add_line_item line_item
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def invoice
|
|
35
|
+
@invoice ||= Invoice.new.tap do |inv|
|
|
36
|
+
inv.contact = contact
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def allocate_invoice_number invoice
|
|
41
|
+
AllocateInvoiceNumber.call invoice
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def contact
|
|
45
|
+
@contact ||= Contact.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module SimpleInvoice
|
|
2
|
+
module Services
|
|
3
|
+
class CreateInvoiceForSubscription
|
|
4
|
+
|
|
5
|
+
# @param subscription [SimpleInvoice::Subscription]
|
|
6
|
+
# @param issue_date [Date]
|
|
7
|
+
def initialize subscription, issue_date
|
|
8
|
+
@subscription = subscription
|
|
9
|
+
@issue_date = issue_date
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @return [SimpleInvoice::Invoice]
|
|
13
|
+
def create_invoice
|
|
14
|
+
invoice
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
# @return [SimpleInvoice::InvoiceTemplate]
|
|
20
|
+
def template
|
|
21
|
+
@subscription.invoice_template
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [SimpleInvoice::Contact]
|
|
25
|
+
def contact
|
|
26
|
+
@subscription.contact
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [SimpleInvoice::Invoice]
|
|
30
|
+
def invoice
|
|
31
|
+
@invoice ||= Invoice.new.tap do |invoice|
|
|
32
|
+
invoice.contact = contact
|
|
33
|
+
template.line_items.each do |template_line_item|
|
|
34
|
+
invoice.add_line_item template_line_item.dup
|
|
35
|
+
end
|
|
36
|
+
invoice.set_dates invoice_data.issue_date, invoice_data.due_date
|
|
37
|
+
allocate_invoice_number invoice
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param issue_date [Date]
|
|
42
|
+
# @return [SimpleInvoice::InvoiceData]
|
|
43
|
+
def invoice_data
|
|
44
|
+
@invoice_data ||= InvoiceData.new nil, @issue_date, @subscription.due_days
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def allocate_invoice_number invoice
|
|
48
|
+
AllocateInvoiceNumber.call invoice
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module SimpleInvoice
|
|
2
|
+
module Services
|
|
3
|
+
class CreateInvoiceTemplate
|
|
4
|
+
|
|
5
|
+
# @return [SimpleInvoice::InvoiceTemplate]
|
|
6
|
+
def self.call &block
|
|
7
|
+
instance = new
|
|
8
|
+
block.call instance
|
|
9
|
+
instance.create_invoice_template
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @return [SimpleInvoice::InvoiceTemplate]
|
|
13
|
+
def create_invoice_template
|
|
14
|
+
invoice_template
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @param description [String]
|
|
18
|
+
# @param price [Fixnum]
|
|
19
|
+
# @param quantity [Fixnum]
|
|
20
|
+
def add_item description, price, quantity=1
|
|
21
|
+
line_item = LineItem.new description, price, quantity
|
|
22
|
+
invoice_template.add_line_item line_item
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def invoice_template
|
|
28
|
+
@invoice_template ||= InvoiceTemplate.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module SimpleInvoice
|
|
2
|
+
module Services
|
|
3
|
+
class ProcessSubscription
|
|
4
|
+
|
|
5
|
+
# @param date [Date, String]
|
|
6
|
+
# @param subscription [SimpleInvoice::Subscription]
|
|
7
|
+
# @return [nil, SimpleInvoice::Invoice]
|
|
8
|
+
def self.call date, subscription
|
|
9
|
+
new(date, subscription).maybe_invoice
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @return [nil, SimpleInvoice::Invoice]
|
|
13
|
+
def maybe_invoice
|
|
14
|
+
if should_create_an_invoice_today?
|
|
15
|
+
create_invoice
|
|
16
|
+
else
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
protected
|
|
22
|
+
|
|
23
|
+
# @param date [Date, String]
|
|
24
|
+
# @param subscription [SimpleInvoice::Subscription]
|
|
25
|
+
def initialize date, subscription
|
|
26
|
+
@date = to_date date
|
|
27
|
+
@subscription = subscription
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def should_create_an_invoice_today?
|
|
33
|
+
billing_periods = @subscription.billing_periods
|
|
34
|
+
catch(:break_with_return_value) do
|
|
35
|
+
loop do
|
|
36
|
+
billing_period = billing_periods.next
|
|
37
|
+
if billing_period.first_day == @date
|
|
38
|
+
throw :break_with_return_value, true
|
|
39
|
+
elsif billing_period.first_day > @date
|
|
40
|
+
throw :break_with_return_value, false
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def create_invoice
|
|
47
|
+
CreateInvoiceForSubscription.new(@subscription, @date).create_invoice
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param [String, Date] date
|
|
51
|
+
# @return [Date]
|
|
52
|
+
def to_date date
|
|
53
|
+
if date.is_a? Date
|
|
54
|
+
date
|
|
55
|
+
else
|
|
56
|
+
Date.parse date.to_s
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module SimpleInvoice
|
|
2
|
+
module Services
|
|
3
|
+
class ProcessSubscriptions
|
|
4
|
+
|
|
5
|
+
# @param date [Date, String]
|
|
6
|
+
# @param subscriptions [Array<SimpleInvoice::Subscription>]
|
|
7
|
+
# @return [Array<SimpleInvoice::Invoice>]
|
|
8
|
+
def self.call date, subscriptions
|
|
9
|
+
subscriptions.collect do |sub|
|
|
10
|
+
ProcessSubscription.call date, sub
|
|
11
|
+
end.compact
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module SimpleInvoice
|
|
2
|
+
class Subscription
|
|
3
|
+
|
|
4
|
+
attr_accessor :contact, :invoice_template, :due_days, :start_date, :billing_period_type, :cancelled
|
|
5
|
+
|
|
6
|
+
def cancel!
|
|
7
|
+
@cancelled = true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def cancelled?
|
|
11
|
+
!!@cancelled
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @return [Enumerator]
|
|
15
|
+
def billing_periods
|
|
16
|
+
Enumerator.new do |y|
|
|
17
|
+
period = BillingPeriod.new billing_period_type, start_date
|
|
18
|
+
loop do
|
|
19
|
+
y.yield period
|
|
20
|
+
period = period.next_billing_period
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
|
3
|
+
require "simple_invoice/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |s|
|
|
6
|
+
s.name = "simple_invoice"
|
|
7
|
+
s.version = SimpleInvoice::VERSION
|
|
8
|
+
s.authors = ["Joel Plane"]
|
|
9
|
+
s.email = ["joel.plane@gmail.com"]
|
|
10
|
+
s.homepage = "https://github.com/joelplane/simple_invoice"
|
|
11
|
+
s.summary = %q{simple invoice library}
|
|
12
|
+
s.description = %q{Very simple invoice library with minimal dependencies}
|
|
13
|
+
|
|
14
|
+
s.files = `git ls-files`.split("\n")
|
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
|
17
|
+
s.require_paths = ["lib"]
|
|
18
|
+
|
|
19
|
+
s.add_development_dependency "rspec"
|
|
20
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module SimpleInvoice
|
|
4
|
+
describe BillingPeriod do
|
|
5
|
+
|
|
6
|
+
subject { BillingPeriod.new :monthly, '2013-09-28' }
|
|
7
|
+
|
|
8
|
+
describe "#first_day" do
|
|
9
|
+
specify do
|
|
10
|
+
subject.first_day.should == Date.parse('2013-09-28')
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe "#last_day" do
|
|
15
|
+
specify do
|
|
16
|
+
subject.last_day.should == Date.parse('2013-10-27')
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe "#next_billing_period" do
|
|
21
|
+
specify do
|
|
22
|
+
subject.next_billing_period.first_day.should == Date.parse('2013-10-28')
|
|
23
|
+
subject.next_billing_period.last_day.should == Date.parse('2013-11-27')
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
end
|
|
28
|
+
end
|