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,79 @@
1
+ require File.expand_path("../spec_helper", __FILE__)
2
+
3
+ describe MerchantSidekick::Invoice do
4
+
5
+ def setup
6
+ @buyer = users(:sam)
7
+ @seller = users(:sally)
8
+ @product = products(:widget)
9
+ @invoice = MerchantSidekick::PurchaseInvoice.new(
10
+ :net_amount => Money.new(2995, 'USD'),
11
+ :gross_amount => Money.new(2995, 'USD'),
12
+ :buyer => @buyer,
13
+ :seller => @seller
14
+ )
15
+
16
+ @sally_billing = @invoice.build_origin_address(addresses(:sally_billing).content_attributes)
17
+ @sam_billing = @invoice.build_billing_address(addresses(:sam_billing).content_attributes)
18
+ @sam_shipping = @invoice.build_shipping_address(addresses(:sam_shipping).content_attributes)
19
+
20
+ @line_item = MerchantSidekick::LineItem.new(
21
+ :order => nil,
22
+ :invoice => @invoice,
23
+ :sellable => @product
24
+ )
25
+ @invoice.line_items.push(@line_item)
26
+
27
+ @credit_card = valid_credit_card
28
+ end
29
+
30
+ it "should be a valid invoice instance" do
31
+ transaction do
32
+ # money
33
+ @invoice.net_total.should == '29.95'.to_money
34
+ @invoice.tax_total.should == '0.00'.to_money
35
+ @invoice.gross_total.should == '29.95'.to_money
36
+
37
+ # actors
38
+ @invoice.seller.should == @seller
39
+ @invoice.buyer.should == @buyer
40
+
41
+ # line item
42
+ @invoice.line_items.size.should == 1
43
+ @invoice.line_items[0].invoice.should be_is_a(MerchantSidekick::PurchaseInvoice)
44
+ @invoice.line_items[0].invoice.should == @invoice
45
+
46
+ # addresses
47
+ @invoice.origin_address.to_s.should == @sally_billing.to_s
48
+ @invoice.origin_address.should be_is_a(OriginAddress)
49
+ @invoice.origin_address.addressable.should == @invoice
50
+
51
+ @invoice.billing_address.to_s.should == @sam_billing.to_s
52
+ @invoice.billing_address.should be_is_a(BillingAddress)
53
+ @invoice.billing_address.addressable.should == @invoice
54
+
55
+ @invoice.shipping_address.to_s.should == @sam_shipping.to_s
56
+ @invoice.shipping_address.should be_is_a(ShippingAddress)
57
+ @invoice.shipping_address.addressable.should == @invoice
58
+ end
59
+ end
60
+
61
+ it "should save addresses" do
62
+ transaction do
63
+ lambda { @invoice.save }.should change(MerchantSidekick::Addressable::Address, :count).by(3)
64
+ @invoice = MerchantSidekick::Invoice.find_by_id(@invoice.id)
65
+ @invoice.origin_address.to_s.should == addresses(:sally_billing).to_s
66
+ @invoice.billing_address.to_s.should == addresses(:sam_billing).to_s
67
+ @invoice.shipping_address.to_s.should == addresses(:sam_shipping).to_s
68
+ end
69
+ end
70
+
71
+ it "should save line items" do
72
+ transaction do
73
+ lambda { @invoice.save }.should change(MerchantSidekick::LineItem, :count).by(1)
74
+ @invoice = MerchantSidekick::Invoice.find_by_id(@invoice.id)
75
+ @invoice.line_items[0].should == @line_item
76
+ end
77
+ end
78
+
79
+ end
@@ -0,0 +1,65 @@
1
+ require File.expand_path("../spec_helper", __FILE__)
2
+
3
+ describe MerchantSidekick::LineItem do
4
+ it "should belong to an order" do
5
+ lambda { MerchantSidekick::LineItem.new.order}.should_not raise_error
6
+ end
7
+
8
+ it "should belong to an invoice" do
9
+ lambda { MerchantSidekick::LineItem.new.invoice}.should_not raise_error
10
+ end
11
+
12
+ it "should belong to a sellable model" do
13
+ lambda { MerchantSidekick::LineItem.new.sellable}.should_not raise_error
14
+ end
15
+
16
+ it "should have a tax rate class name attribute" do
17
+ lambda { MerchantSidekick::LineItem.tax_rate_class_name}.should_not raise_error
18
+ end
19
+
20
+ it "should have a gross amount composed field" do
21
+ lambda { MerchantSidekick::LineItem.new.gross_amount == 0.to_money}.should_not raise_error
22
+ end
23
+
24
+ it "should have a tax amount composed field" do
25
+ lambda { MerchantSidekick::LineItem.new.tax_amount == 0.to_money}.should_not raise_error
26
+ end
27
+
28
+ it "should have a net amount composed field" do
29
+ lambda { MerchantSidekick::LineItem.new.net_amount == 0.to_money}.should_not raise_error
30
+ end
31
+
32
+ end
33
+
34
+ describe MerchantSidekick::LineItem, "a new line item" do
35
+
36
+ it "should set #price when setting #sellable" do
37
+ @sellable = Product.new(:price => 10.to_money)
38
+ @line_item = MerchantSidekick::LineItem.new(:sellable => @sellable)
39
+ @line_item.amount.should == @sellable.price
40
+ end
41
+
42
+ it "should set #price to 0 when #sellable doesn't have a price" do
43
+ @sellable = Product.new
44
+ @line_item = MerchantSidekick::LineItem.new(:sellable => @sellable)
45
+ @line_item.amount.should == 0.to_money
46
+ end
47
+
48
+ it "should return sellable when calling #sellable=" do
49
+ @sellable = Product.new(:title => "Rad New Widget", :price => 10.to_money)
50
+ @line_item = MerchantSidekick::LineItem.new
51
+ sellable = @line_item.sellable=(@sellable)
52
+ sellable.should === @sellable
53
+ end
54
+
55
+ it "should save" do
56
+ lambda {
57
+ @product = Product.new(:price => 10.to_money, :title => "Pack of Candles")
58
+ @line_item = MerchantSidekick::LineItem.new(:sellable => @product)
59
+ @line_item.save
60
+ @line_item.reload
61
+ @line_item.amount.should == Money.new(1000, "USD")
62
+ }.should change(MerchantSidekick::LineItem, :count)
63
+ end
64
+
65
+ end
@@ -0,0 +1,85 @@
1
+ require File.expand_path("../spec_helper", __FILE__)
2
+
3
+ describe MerchantSidekick::Order do
4
+
5
+ def setup
6
+ @products = [products(:widget), products(:knob)]
7
+ @line_items = [line_items(:sams_widget), line_items(:sams_knob)]
8
+ @order = orders(:sams_widget)
9
+ @order.reload and @order.save
10
+ end
11
+
12
+ it "should have line_items" do
13
+ lambda { MerchantSidekick::Order.new.line_items.first }.should_not raise_error
14
+ end
15
+
16
+ it "should calculate amount" do
17
+ transaction do
18
+ lambda {
19
+ @order.net_amount.should == ::Money.new(3394, "USD")
20
+ @order.tax_amount.should == ::Money.new(0)
21
+ @order.gross_amount.should == ::Money.new(3394, "USD")
22
+ @order.total.should == ::Money.new(3394, "USD")
23
+ @order.line_items_count.should == 2
24
+ }.should_not raise_error
25
+ end
26
+ end
27
+
28
+ end
29
+
30
+ describe MerchantSidekick::Order, "with addresses" do
31
+ def setup
32
+ addresses(:paid_order_billing_address)
33
+ addresses(:paid_order_shipping_address)
34
+ addresses(:paid_order_origin_address)
35
+ orders(:sams_widget)
36
+ @order = orders(:paid)
37
+ @order.reload and @order.save
38
+ end
39
+
40
+ it "should have billing, shipping and origin address" do
41
+ transaction do
42
+ @order.billing_address.should_not be_nil
43
+ @order.shipping_address.should_not be_nil
44
+ @order.origin_address.should_not be_nil
45
+ end
46
+ end
47
+ end
48
+
49
+ describe "A new order" do
50
+
51
+ def setup
52
+ @products = [products(:widget), products(:knob)]
53
+ @line_items = [line_items(:sams_widget), line_items(:sams_knob)]
54
+ @order = orders(:sams_widget)
55
+ @order.reload and @order.save
56
+ end
57
+
58
+ it "amount should equal sum of sellables price" do
59
+ transaction do
60
+ @order.net_amount.should == @products.inject(0.to_money) {|sum,p| sum + p.price}
61
+ end
62
+ end
63
+
64
+ end
65
+
66
+ describe "A new order with purchases" do
67
+
68
+ def setup
69
+ @user = users(:sam)
70
+ @sam_billing = @user.create_billing_address(addresses(:sam_billing).content_attributes)
71
+ @sam_shipping = @user.create_shipping_address(addresses(:sam_shipping).content_attributes)
72
+ @product = products(:widget)
73
+ @order = @user.purchase @product
74
+ end
75
+
76
+ it "should evaluate after pushing a new item" do
77
+ transaction do
78
+ @order.gross_total.should == "29.95".to_money
79
+ lambda { @order.push(products(:knob)) }.should change(@order, :items_count).by(1)
80
+ @order.gross_total.to_s.should == '33.94'
81
+ @order.should be_new_record
82
+ end
83
+ end
84
+
85
+ end
@@ -0,0 +1,14 @@
1
+ require File.expand_path("../spec_helper", __FILE__)
2
+
3
+ describe "A payment" do
4
+
5
+ it "should belong to a payable" do
6
+ lambda { MerchantSidekick::Payment.new.should respond_to(:payable) }.should_not raise_error
7
+ end
8
+
9
+ it "should have an amount" do
10
+ MerchantSidekick::Payment.new.should respond_to(:amount)
11
+ end
12
+
13
+ end
14
+
@@ -0,0 +1,70 @@
1
+ require File.expand_path("../spec_helper", __FILE__)
2
+
3
+ describe MerchantSidekick::PurchaseInvoice do
4
+
5
+ def setup
6
+ @buyer = users(:sam)
7
+ @seller = users(:sally)
8
+ @product = products(:widget)
9
+ @invoice = MerchantSidekick::PurchaseInvoice.new(
10
+ :net_amount => Money.new(2995, 'USD'),
11
+ :gross_amount => Money.new(2995, 'USD'),
12
+ :buyer => @buyer,
13
+ :seller => @seller
14
+ )
15
+
16
+ @sally_billing = @invoice.build_origin_address(addresses(:sally_billing).content_attributes)
17
+ @sam_billing = @invoice.build_billing_address(addresses(:sam_billing).content_attributes)
18
+ @sam_shipping = @invoice.build_shipping_address(addresses(:sam_shipping).content_attributes)
19
+
20
+ @line_item = MerchantSidekick::LineItem.new(
21
+ :order => nil,
22
+ :invoice => @invoice,
23
+ :sellable => @product
24
+ )
25
+ @invoice.line_items.push(@line_item)
26
+
27
+ @credit_card = valid_credit_card
28
+ end
29
+
30
+ it "should authorize" do
31
+ transaction do
32
+ lambda {
33
+ payment = @invoice.authorize(@credit_card)
34
+ payment.should be_success
35
+ payment.position.should == 1
36
+ @invoice.current_state.should == :authorized
37
+ }.should change(MerchantSidekick::Payment, :count).by(1)
38
+ end
39
+ end
40
+
41
+ it "should capture the payment" do
42
+ transaction do
43
+ authorization_payment = @invoice.authorize(@credit_card)
44
+ authorization_payment.should be_success
45
+ lambda {
46
+ capture_payment = @invoice.capture
47
+ capture_payment.should be_success
48
+ }.should change(MerchantSidekick::Payment, :count).by(1)
49
+ end
50
+ end
51
+
52
+ it "should not capture without authorization" do
53
+ transaction do
54
+ payment = @invoice.capture
55
+ payment.should be_nil
56
+ @invoice.should be_pending
57
+ end
58
+ end
59
+
60
+ it "should purchase" do
61
+ transaction do
62
+ lambda {
63
+ payment = @invoice.purchase(@credit_card)
64
+ payment.should be_success
65
+ @invoice.should be_paid
66
+ }.should change(MerchantSidekick::Payment, :count).by(1)
67
+ end
68
+ end
69
+
70
+ end
@@ -0,0 +1,191 @@
1
+ require File.expand_path("../spec_helper", __FILE__)
2
+
3
+ describe "authorize with an invalid credit card will not save the order" do
4
+
5
+ def setup
6
+ @user = users(:sam)
7
+ @product = products(:widget)
8
+ @sam_billing = @user.create_billing_address(addresses(:sam_billing).content_attributes)
9
+ @sam_shipping = @user.create_shipping_address(addresses(:sam_shipping).content_attributes)
10
+ @order = @user.purchase @product
11
+ @payment = @order.authorize invalid_credit_card
12
+ end
13
+
14
+ it "should initialize but not save the order" do
15
+ transaction do
16
+ @order.should be_new_record
17
+ end
18
+ end
19
+
20
+ it "should initialize but not save a payment" do
21
+ transaction do
22
+ @payment.should be_new_record
23
+ end
24
+ end
25
+
26
+ end
27
+
28
+ describe "authorize with a valid credit card" do
29
+
30
+ def setup
31
+ @user = users(:sam)
32
+ @sam_billing = @user.create_billing_address(addresses(:sam_billing).content_attributes)
33
+ @sam_shipping = @user.create_shipping_address(addresses(:sam_shipping).content_attributes)
34
+
35
+ @product = products(:widget)
36
+ @order = @user.purchase @product
37
+ @credit_card = valid_credit_card
38
+ end
39
+
40
+ it "should return a payment" do
41
+ transaction do
42
+ @user.should_receive(:before_authorize_payment)
43
+ @user.should_receive(:after_authorize_payment)
44
+ @order.should_receive(:enter_pending)
45
+ @order.authorize(@credit_card).should be_instance_of(MerchantSidekick::ActiveMerchant::CreditCardPayment)
46
+ end
47
+ end
48
+
49
+ it "should return success" do
50
+ transaction do
51
+ @order.should_receive(:enter_pending)
52
+ @order.authorize(@credit_card).should be_success
53
+ end
54
+ end
55
+
56
+ it "should be set to state pending" do
57
+ transaction do
58
+ @order.authorize(@credit_card)
59
+ @order.should be_pending
60
+ end
61
+ end
62
+
63
+ it "should have a valid invoice" do
64
+ transaction do
65
+ @order.authorize(@credit_card)
66
+ @order.invoices.should_not be_empty
67
+ @order.invoices.first.id.should_not be_nil
68
+ @order.invoices.first.should be_instance_of(MerchantSidekick::PurchaseInvoice)
69
+ @order.invoices.first.should be_authorized
70
+ end
71
+ end
72
+
73
+ it "should set payment amount equal to order amount" do
74
+ transaction do
75
+ @order.authorize(@credit_card).amount.should == @order.total
76
+ end
77
+ end
78
+
79
+ it "should set payment authorization reference number" do
80
+ transaction do
81
+ @order.authorize(@credit_card).reference.should_not be_nil
82
+ end
83
+ end
84
+
85
+ end
86
+
87
+ describe "Deleting an order" do
88
+ def setup
89
+ @widget = products(:widget)
90
+ @knob = products(:knob)
91
+ @li_widget = line_items(:sams_widget)
92
+ @li_knob = line_items(:sams_knob)
93
+ @order = orders(:sams_widget)
94
+ @order.reload
95
+ end
96
+
97
+ it "should delete associated line items" do
98
+ transaction do
99
+ @order.line_items.count.should == 2
100
+ @order.destroy
101
+ @order.line_items.count.should == 0
102
+ end
103
+ end
104
+ end
105
+
106
+ describe "Cancelling an order" do
107
+ def setup
108
+ @widget = products(:widget)
109
+ @knob = products(:knob)
110
+ @li_widget = line_items(:sams_widget)
111
+ @li_knob = line_items(:sams_knob)
112
+ @order = orders(:sams_widget)
113
+ @order.reload
114
+ end
115
+
116
+ it "should record that order was cancelled" do
117
+ transaction do
118
+ @order.canceled_at.should be_nil
119
+ @order.current_state.should == :created
120
+ @order.cancel!
121
+ @order.current_state.should == :canceled
122
+ end
123
+ end
124
+ end
125
+
126
+ describe "Paying an order with valid credit card" do
127
+ def setup
128
+ @buyer = users(:sam)
129
+ @sam_billing = @buyer.create_billing_address(addresses(:sam_billing).content_attributes)
130
+ @sam_shipping = @buyer.create_shipping_address(addresses(:sam_shipping).content_attributes)
131
+ @order = orders(:unpaid)
132
+ @credit_card = ActiveMerchant::Billing::CreditCard.new valid_credit_card_attributes(:number => "1")
133
+ @order.reload
134
+ mock_gateway(false, @order, @credit_card)
135
+ end
136
+
137
+ it "should return an unsuccessful payment" do
138
+ transaction do
139
+ @order.buyer.should_receive(:before_payment)
140
+ @order.buyer.should_receive(:after_payment)
141
+ @order.should_receive(:enter_pending)
142
+ # @order.should_receive(:exit_pending)
143
+ @order.should_receive(:enter_approved)
144
+ # @order.should_receive(:exit_approved)
145
+ payment = @order.pay(@credit_card)
146
+ payment.action.should == "purchase"
147
+ payment.success.should == true
148
+ payment.should_not be_new_record
149
+ @order.current_state.should == :approved
150
+ end
151
+ end
152
+ end
153
+
154
+ describe "Paying an order with an invalid credit card" do
155
+ def setup
156
+ @buyer = users(:sam)
157
+ @sam_billing = @buyer.create_billing_address(addresses(:sam_billing).content_attributes)
158
+ @sam_shipping = @buyer.create_shipping_address(addresses(:sam_shipping).content_attributes)
159
+ @order = orders(:unpaid)
160
+ @credit_card = ActiveMerchant::Billing::CreditCard.new valid_credit_card_attributes(:number => "2")
161
+ @order.reload
162
+ mock_gateway(false, @order, @credit_card)
163
+ end
164
+
165
+ it "should return an unsuccessful payment" do
166
+ transaction do
167
+ @order.buyer.should_receive(:before_payment)
168
+ @order.buyer.should_receive(:after_payment)
169
+ payment = @order.pay(@credit_card)
170
+ payment.action.should == "purchase"
171
+ payment.success.should == false
172
+ payment.should_not be_new_record
173
+ end
174
+ end
175
+ end
176
+
177
+ def mock_gateway(success, order, credit_card, options = {})
178
+ @gateway = mock("gateway")
179
+ @response = mock("response")
180
+ @response.stub!(:success?).and_return(success)
181
+ if success
182
+ @response.should_receive(:authorization).and_return("12345")
183
+ else
184
+ @response.stub!(:message).and_return("Invalid credit card number")
185
+ end
186
+
187
+ @gateway.stub!(:purchase).with(order.gross_total, credit_card, {
188
+ :buyer => order.buyer}.merge(options)).and_return(@response)
189
+
190
+ ActiveMerchant::Billing::Base.stub!(:default_gateway).and_return(@gateway)
191
+ end