merchant_sidekick 0.4.2

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 (63) hide show
  1. data/.gitignore +12 -0
  2. data/Changelog.md +38 -0
  3. data/Gemfile +2 -0
  4. data/MIT-LICENSE +19 -0
  5. data/README.md +88 -0
  6. data/Rakefile +10 -0
  7. data/lib/merchant_sidekick.rb +45 -0
  8. data/lib/merchant_sidekick/active_merchant/credit_card_payment.rb +117 -0
  9. data/lib/merchant_sidekick/active_merchant/gateways/authorize_net_gateway.rb +26 -0
  10. data/lib/merchant_sidekick/active_merchant/gateways/base.rb +29 -0
  11. data/lib/merchant_sidekick/active_merchant/gateways/bogus_gateway.rb +19 -0
  12. data/lib/merchant_sidekick/active_merchant/gateways/paypal_gateway.rb +43 -0
  13. data/lib/merchant_sidekick/addressable/address.rb +400 -0
  14. data/lib/merchant_sidekick/addressable/addressable.rb +353 -0
  15. data/lib/merchant_sidekick/buyer.rb +99 -0
  16. data/lib/merchant_sidekick/gateway.rb +81 -0
  17. data/lib/merchant_sidekick/install.rb +19 -0
  18. data/lib/merchant_sidekick/invoice.rb +179 -0
  19. data/lib/merchant_sidekick/line_item.rb +128 -0
  20. data/lib/merchant_sidekick/migrations/addressable.rb +47 -0
  21. data/lib/merchant_sidekick/migrations/billing.rb +100 -0
  22. data/lib/merchant_sidekick/migrations/shopping_cart.rb +28 -0
  23. data/lib/merchant_sidekick/money.rb +38 -0
  24. data/lib/merchant_sidekick/order.rb +244 -0
  25. data/lib/merchant_sidekick/payment.rb +59 -0
  26. data/lib/merchant_sidekick/purchase_invoice.rb +180 -0
  27. data/lib/merchant_sidekick/purchase_order.rb +350 -0
  28. data/lib/merchant_sidekick/railtie.rb +7 -0
  29. data/lib/merchant_sidekick/sales_invoice.rb +56 -0
  30. data/lib/merchant_sidekick/sales_order.rb +122 -0
  31. data/lib/merchant_sidekick/sellable.rb +88 -0
  32. data/lib/merchant_sidekick/seller.rb +93 -0
  33. data/lib/merchant_sidekick/shopping_cart/cart.rb +225 -0
  34. data/lib/merchant_sidekick/shopping_cart/line_item.rb +152 -0
  35. data/lib/merchant_sidekick/version.rb +3 -0
  36. data/merchant_sidekick.gemspec +37 -0
  37. data/spec/address_spec.rb +153 -0
  38. data/spec/addressable_spec.rb +250 -0
  39. data/spec/buyer_spec.rb +203 -0
  40. data/spec/cart_line_item_spec.rb +58 -0
  41. data/spec/cart_spec.rb +213 -0
  42. data/spec/config/merchant_sidekick.yml +10 -0
  43. data/spec/credit_card_payment_spec.rb +175 -0
  44. data/spec/fixtures/addresses.yml +97 -0
  45. data/spec/fixtures/line_items.yml +18 -0
  46. data/spec/fixtures/orders.yml +24 -0
  47. data/spec/fixtures/payments.yml +17 -0
  48. data/spec/fixtures/products.yml +12 -0
  49. data/spec/fixtures/users.yml +11 -0
  50. data/spec/gateway_spec.rb +136 -0
  51. data/spec/invoice_spec.rb +79 -0
  52. data/spec/line_item_spec.rb +65 -0
  53. data/spec/order_spec.rb +85 -0
  54. data/spec/payment_spec.rb +14 -0
  55. data/spec/purchase_invoice_spec.rb +70 -0
  56. data/spec/purchase_order_spec.rb +191 -0
  57. data/spec/sales_invoice_spec.rb +58 -0
  58. data/spec/sales_order_spec.rb +107 -0
  59. data/spec/schema.rb +28 -0
  60. data/spec/sellable_spec.rb +34 -0
  61. data/spec/seller_spec.rb +201 -0
  62. data/spec/spec_helper.rb +255 -0
  63. metadata +201 -0
@@ -0,0 +1,47 @@
1
+ class CreateMerchantSidekickAddressableTables < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :addresses do |t|
4
+ t.integer "addressable_id"
5
+ t.string "addressable_type"
6
+ t.integer "academic_title_id"
7
+ t.string "gender", :limit => 1
8
+ t.string "first_name"
9
+ t.string "middle_name"
10
+ t.string "last_name"
11
+ t.text "street"
12
+ t.string "city"
13
+ t.string "postal_code"
14
+ t.string "province"
15
+ t.string "province_code", :limit => 2
16
+ t.string "country"
17
+ t.string "country_code", :limit => 2
18
+ t.string "company_name"
19
+ t.text "note"
20
+ t.string "phone"
21
+ t.string "mobile"
22
+ t.string "fax"
23
+ t.string "type"
24
+ end
25
+
26
+ add_index :addresses, ["academic_title_id"], :name => "index_addresses_on_academic_title_id"
27
+ add_index :addresses, ["addressable_id", "addressable_type"], :name => "fk_addresses_addressable"
28
+ add_index :addresses, ["city"], :name => "index_addresses_on_city"
29
+ add_index :addresses, ["company_name"], :name => "index_addresses_on_company_name"
30
+ add_index :addresses, ["country"], :name => "index_addresses_on_country"
31
+ add_index :addresses, ["country_code"], :name => "index_addresses_on_country_code"
32
+ add_index :addresses, ["fax"], :name => "index_addresses_on_fax"
33
+ add_index :addresses, ["first_name"], :name => "index_addresses_on_first_name"
34
+ add_index :addresses, ["gender"], :name => "index_addresses_on_gender"
35
+ add_index :addresses, ["last_name"], :name => "index_addresses_on_last_name"
36
+ add_index :addresses, ["middle_name"], :name => "index_addresses_on_middle_name"
37
+ add_index :addresses, ["mobile"], :name => "index_addresses_on_mobile"
38
+ add_index :addresses, ["phone"], :name => "index_addresses_on_phone"
39
+ add_index :addresses, ["province"], :name => "index_addresses_on_state"
40
+ add_index :addresses, ["province_code"], :name => "index_addresses_on_state_code"
41
+ add_index :addresses, ["type"], :name => "index_addresses_on_type"
42
+ end
43
+
44
+ def self.down
45
+ drop_table :addresses
46
+ end
47
+ end
@@ -0,0 +1,100 @@
1
+ class CreateMerchantSidekickBillingTables < ActiveRecord::Migration
2
+
3
+ def self.up
4
+ create_table :line_items do |t|
5
+ t.integer "order_id"
6
+ t.integer "invoice_id"
7
+ t.integer "sellable_id"
8
+ t.string "sellable_type"
9
+ t.integer "net_cents", :default => 0, :null => false
10
+ t.integer "tax_cents", :default => 0, :null => false
11
+ t.integer "gross_cents", :default => 0, :null => false
12
+ t.string "currency", :default => "USD", :null => false
13
+ t.decimal "tax_rate", :precision => 15, :scale => 10, :default => 0.0, :null => false
14
+ t.timestamps
15
+ end
16
+ add_index :line_items, "order_id"
17
+ add_index :line_items, "invoice_id"
18
+ add_index :line_items, ["sellable_id", "sellable_type"]
19
+
20
+
21
+ create_table :orders do |t|
22
+ t.integer "buyer_id"
23
+ t.string "buyer_type"
24
+ t.integer "seller_id"
25
+ t.string "seller_type"
26
+ t.integer "invoice_id"
27
+ t.integer "net_cents", :default => 0, :null => false
28
+ t.integer "tax_cents", :default => 0, :null => false
29
+ t.integer "gross_cents", :default => 0, :null => false
30
+ t.string "currency", :limit => 3, :default => "USD", :null => false
31
+ t.string "type"
32
+ t.string "status", :default => "created", :null => false
33
+ t.string "number"
34
+ t.string "description"
35
+ t.datetime "canceled_at"
36
+ t.timestamps
37
+ end
38
+ add_index :orders, ["buyer_id", "buyer_type"]
39
+ add_index :orders, ["seller_id", "seller_type"]
40
+ add_index :orders, "status"
41
+ add_index :orders, "type"
42
+
43
+
44
+ create_table :invoices do |t|
45
+ t.integer "buyer_id"
46
+ t.string "buyer_type"
47
+ t.integer "seller_id"
48
+ t.string "seller_type"
49
+ t.integer "net_cents", :default => 0, :null => false
50
+ t.integer "tax_cents", :default => 0, :null => false
51
+ t.integer "gross_cents", :default => 0, :null => false
52
+ t.string "currency", :default => "USD", :null => false
53
+ t.string "type"
54
+ t.string "number"
55
+ t.string "status", :default => "pending", :null => false
56
+ t.datetime "paid_at"
57
+ t.integer "order_id"
58
+ t.datetime "authorized_at"
59
+ t.timestamps
60
+ end
61
+ add_index :invoices, ["buyer_id", "buyer_type"]
62
+ add_index :invoices, "number"
63
+ add_index :invoices, "order_id"
64
+ add_index :invoices, ["seller_id", "seller_type"]
65
+ add_index :invoices, "type"
66
+
67
+
68
+ #--- payments
69
+ create_table :payments do |t|
70
+ t.integer "payable_id"
71
+ t.string "payable_type"
72
+ t.boolean "success"
73
+ t.string "reference"
74
+ t.string "message"
75
+ t.string "action"
76
+ t.string "params"
77
+ t.boolean "test"
78
+ t.integer "cents", :default => 0, :null => false
79
+ t.string "currency", :limit => 3, :default => "USD", :null => false
80
+ t.integer "position"
81
+ t.string "type"
82
+ t.string "paypal_account"
83
+ t.string "uuid"
84
+ t.timestamps
85
+ end
86
+ add_index :payments, "action"
87
+ add_index :payments, ["payable_id", "payable_type"]
88
+ add_index :payments, "position"
89
+ add_index :payments, "reference"
90
+ add_index :payments, "uuid"
91
+ end
92
+
93
+ def self.down
94
+ drop_table :gateways
95
+ drop_table :payments
96
+ drop_table :invoices
97
+ drop_table :orders
98
+ drop_table :line_items
99
+ end
100
+ end
@@ -0,0 +1,28 @@
1
+ class CreateMerchantSidekickShoppingCartTables < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :cart_line_items do |t|
4
+ t.string "item_number"
5
+ t.string "name"
6
+ t.string "description"
7
+ t.integer "quantity", :default => 1, :null => false
8
+ t.string "unit", :default => "piece", :null => false
9
+ t.integer "pieces", :default => 0, :null => false
10
+ t.integer "cents", :default => 0, :null => false
11
+ t.string "currency", :limit => 3, :default => "USD", :null => false
12
+ t.boolean "taxable", :default => false, :null => false
13
+ t.integer "product_id", :null => false
14
+ t.string "product_type", :null => false
15
+ t.timestamps
16
+ end
17
+ add_index :cart_line_items, "item_number"
18
+ add_index :cart_line_items, "name"
19
+ add_index :cart_line_items, "unit"
20
+ add_index :cart_line_items, "pieces"
21
+ add_index :cart_line_items, ["product_id", "product_type"]
22
+ add_index :cart_line_items, "quantity"
23
+ end
24
+
25
+ def self.down
26
+ drop_table :cart_line_items
27
+ end
28
+ end
@@ -0,0 +1,38 @@
1
+ module MerchantSidekick
2
+ module Money
3
+ def money(name, options = {})
4
+ options = {:cents => "#{name}_cents".to_sym}.merge(options)
5
+ mapping = [[options[:cents].to_s, 'cents']]
6
+ mapping << ([options[:currency].to_s, 'currency_as_string']) if options[:currency]
7
+
8
+ composed_of name,
9
+ :class_name => "::Money",
10
+ :mapping => mapping,
11
+ :constructor => Proc.new {|cents, currency| ::Money.new(cents || 0, currency || ::Money.default_currency)},
12
+ :converter => Proc.new {|value| value.respond_to?(:to_money) ? value.to_money : raise(ArgumentError, "Can't convert #{value.class} to Money")}
13
+
14
+ if options[:currency]
15
+ class_eval(<<-END, __FILE__, __LINE__+1)
16
+ def currency
17
+ ::Money::Currency.wrap(self[:#{options[:currency]}])
18
+ end
19
+
20
+ def currency_as_string
21
+ self[:#{options[:currency]}]
22
+ end
23
+ END
24
+ else
25
+ class_eval(<<-END, __FILE__, __LINE__+1)
26
+ def currency
27
+ ::Money.default_currency
28
+ end
29
+
30
+ def currency_as_string
31
+ ::Money.default_currency.to_s
32
+ end
33
+ END
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,244 @@
1
+ # Baseclass of all orders which can be:
2
+ #
3
+ # * Purchase Order: outbound orders, i.e. a user buys a product from the merchant
4
+ # * Sales Order: inbound order i.e. the merchant sells a product to a user
5
+ #
6
+ module MerchantSidekick
7
+ class Order < ActiveRecord::Base
8
+ include AASM
9
+ self.table_name = "orders"
10
+
11
+ belongs_to :seller, :polymorphic => true
12
+ belongs_to :buyer, :polymorphic => true
13
+ has_many :line_items, :dependent => :destroy, :class_name => "::MerchantSidekick::LineItem"
14
+ has_many :payments, :as => :payable, :class_name => "::MerchantSidekick::Payment"
15
+ has_many :invoices, :foreign_key => :order_id, :class_name => "::MerchantSidekick::Invoice"
16
+
17
+ money :net_amount, :cents => :net_cents, :currency => :currency
18
+ money :tax_amount, :cents => :tax_cents, :currency => :currency
19
+ money :gross_amount, :cents => :gross_cents, :currency => :currency
20
+ has_address :origin, :billing, :shipping
21
+
22
+ #--- state machine
23
+ aasm :column => "status" do
24
+ state :created, :enter => :enter_created, :exit => :exit_created, :initial => true
25
+ state :pending, :enter => :enter_pending, :exit => :exit_pending
26
+ state :approved, :enter => :enter_approved, :exit => :exit_approved
27
+ state :shipping, :enter => :enter_shipping, :exit => :exit_shipping
28
+ state :shipped, :enter => :enter_shipped, :exit => :exit_shipped
29
+ state :received, :enter => :enter_received, :exit => :exit_received
30
+ state :returning, :enter => :enter_returning, :exit => :exit_returning
31
+ state :returned, :enter => :enter_returned, :exit => :exit_returned
32
+ state :refunded, :enter => :enter_refunded, :exit => :exit_refunded
33
+ state :canceled, :enter => :enter_canceled, :exit => :exit_canceled
34
+
35
+ event :process_payment do
36
+ transitions :from => :created, :to => :pending, :guard => :guard_process_payment_from_created
37
+ end
38
+
39
+ event :approve_payment do
40
+ transitions :from => :pending, :to => :approved, :guard => :guard_approve_payment_from_pending
41
+ end
42
+
43
+ event :process_shipping do
44
+ transitions :from => :approved, :to => :shipping, :guard => :guard_process_shipping_from_approved
45
+ end
46
+
47
+ event :ship do
48
+ transitions :from => :shipping, :to => :shipped, :guard => :guard_ship_from_shipping
49
+ end
50
+
51
+ event :confirm_reception do
52
+ transitions :from => :shipped, :to => :received, :guard => :guard_confirm_reception_from_shipped
53
+ end
54
+
55
+ event :reject do
56
+ transitions :from => :received, :to => :returning, :guard => :guard_reject_from_received
57
+ end
58
+
59
+ event :confirm_return do
60
+ transitions :from => :returning, :to => :returned, :guard => :guard_confirm_return_from_returning
61
+ transitions :from => :shipped, :to => :returned, :guard => :guard_confirm_return_from_shipped
62
+ end
63
+
64
+ event :refund do
65
+ transitions :from => :returned, :to => :refunded, :guard => :guard_refund_from_returned
66
+ end
67
+
68
+ event :cancel do
69
+ transitions :from => :created, :to => :canceled, :guard => :guard_cancel_from_created
70
+ transitions :from => :pending, :to => :canceled, :guard => :guard_cancel_from_pending
71
+ end
72
+ end
73
+
74
+ # state transition callbacks, to be overwritten
75
+ def enter_created; end
76
+ def enter_pending; end
77
+ def enter_approved; end
78
+ def enter_shipping; end
79
+ def enter_shipped; end
80
+ def enter_received; end
81
+ def enter_returning; end
82
+ def enter_returned; end
83
+ def enter_refunded; end
84
+ def enter_canceled; end
85
+
86
+ def exit_created; end
87
+ def exit_pending; end
88
+ def exit_approved; end
89
+ def exit_shipping; end
90
+ def exit_shipped; end
91
+ def exit_received; end
92
+ def exit_returning; end
93
+ def exit_returned; end
94
+ def exit_refunded; end
95
+ def exit_canceled; end
96
+
97
+ # event guard callbacks, to be overwritten
98
+ def guard_process_payment_from_created; true; end
99
+ def guard_approve_payment_from_pending; true; end
100
+ def guard_process_shipping_from_approved; true; end
101
+ def guard_ship_from_shipping; true; end
102
+ def guard_confirm_reception_from_shipped; true; end
103
+ def guard_reject_from_received; true; end
104
+ def guard_confirm_return_from_returning; true; end
105
+ def guard_confirm_return_from_shipped; true; end
106
+ def guard_refund_from_returned; true; end
107
+ def guard_cancel_from_created; true; end
108
+ def guard_cancel_from_pending; true; end
109
+
110
+ #--- callbacks
111
+ before_save :total, :number
112
+
113
+ #--- class methods
114
+ class << self
115
+
116
+ # hex digest 16 char in length
117
+ # TODO change this to customize number schema
118
+ def generate_unique_id
119
+ value = Digest::MD5.hexdigest("#{Time.now.utc.to_i}#{rand(2 ** 128)}")[0..6]
120
+ value.encode! 'utf-8'
121
+ value
122
+ end
123
+
124
+ end
125
+
126
+ #--- instance methods
127
+
128
+ alias_method :current_state, :aasm_current_state
129
+
130
+ def number
131
+ self[:number] ||= Order.generate_unique_id
132
+ end
133
+
134
+ # abstract overriden in sublcasses
135
+ # returns the invoice instance
136
+ def invoice
137
+ raise "deprecated as we use has_many :invoices association"
138
+ end
139
+
140
+ # abstract to be overriden inside puchase and sales orders
141
+ def build_invoice
142
+ raise "override in purchase_order or sales_order"
143
+ end
144
+
145
+ # Builds billing, shipping and origin addresses
146
+ def build_addresses(options={})
147
+ raise "override in purchase_order or sales_order"
148
+ end
149
+
150
+ # Net total amount
151
+ def net_total
152
+ self.net_amount = line_items.inject(0.to_money) {|sum,l| sum + l.net_amount }
153
+ end
154
+
155
+ # Calculates tax and sets the tax_amount attribute
156
+ # It adds tax_amount across all line_items
157
+ def tax_total
158
+ self.tax_amount = line_items.inject(0.to_money) {|sum,l| sum + l.tax_amount }
159
+ self.tax_amount
160
+ end
161
+
162
+ # Gross amount including tax
163
+ def gross_total
164
+ self.gross_amount = self.net_total + self.tax_total
165
+ end
166
+
167
+ # Same as gross_total with tax
168
+ def total
169
+ self.gross_total
170
+ end
171
+
172
+ # is the number of line items stored in the order, though not to be
173
+ # confused by the items_count
174
+ def line_items_count
175
+ self.line_items.count
176
+ end
177
+
178
+ # total number of items purchased
179
+ def items_count
180
+ counter = 0
181
+ self.line_items.each do |item|
182
+ if item.sellable && item.sellable.respond_to?(:quantity)
183
+ counter += item.sellable.quantity
184
+ else
185
+ counter += 1
186
+ end
187
+ end
188
+ counter
189
+ end
190
+
191
+ # updates the order and all contained line_items after an address has changed
192
+ # or an order item was added or removed. The order can only be evaluated if the
193
+ # created state is active. The order is saved if it is an existing order.
194
+ # Returns true if evaluation happend, false if not.
195
+ def evaluate
196
+ if :created == current_state
197
+ self.line_items.each(&:evaluate)
198
+ self.calculate
199
+ return save(false) unless new_record?
200
+ end
201
+ false
202
+ end
203
+
204
+ # adds a line item or sellable to order and updates the order
205
+ def push(an_item_or_sellable)
206
+ if !an_item_or_sellable.is_a?(LineItem) && an_item_or_sellable.respond_to?(:price)
207
+ li = LineItem.new(:sellable => an_item_or_sellable, :order => self)
208
+ elsif an_item_or_sellable.is_a?(LineItem)
209
+ li = an_item_or_sellable
210
+ end
211
+ if li
212
+ self.line_items.push(li)
213
+ self.evaluate
214
+ end
215
+ end
216
+
217
+ protected
218
+
219
+ # Recalculates the order, adding order lines, tax and gross totals
220
+ def calculate
221
+ self.total
222
+ end
223
+
224
+ # override in subclass
225
+ def to_invoice_class_name
226
+ end
227
+
228
+ # override in subclass
229
+ def purchase_order?
230
+ false
231
+ end
232
+
233
+ # override in subclass
234
+ def sales_order?
235
+ false
236
+ end
237
+
238
+ def push_payment(a_payment)
239
+ a_payment.payable = self
240
+ self.payments.push(a_payment)
241
+ end
242
+
243
+ end
244
+ end