saucy 0.2.3 → 0.2.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +6 -0
- data/app/mailers/billing_mailer.rb +20 -0
- data/app/views/billing_mailer/problem.html.erb +13 -0
- data/app/views/billing_mailer/problem.text.erb +14 -0
- data/app/views/billing_mailer/receipt.html.erb +41 -0
- data/app/views/billing_mailer/receipt.text.erb +25 -0
- data/config/locales/en.yml +6 -0
- data/lib/saucy/account.rb +5 -0
- data/lib/saucy/configuration.rb +2 -0
- data/lib/saucy/engine.rb +4 -0
- data/lib/saucy/fake_braintree.rb +5 -1
- data/spec/models/account_spec.rb +40 -0
- data/spec/saucy_spec.rb +4 -0
- metadata +9 -4
- data/app/views/billings/past_due.html.erb +0 -0
data/README.md
CHANGED
@@ -28,6 +28,12 @@ After you bundle, run the generators:
|
|
28
28
|
|
29
29
|
You will want to include the `ensure_active_account` `before_filter` in any controller actions that you want to protect if the user is using an past due paid account.
|
30
30
|
|
31
|
+
You will want to custom the from email address on billing emails:
|
32
|
+
|
33
|
+
Saucy::Configuration.mailer_sender = "billingemail@example.com"
|
34
|
+
|
35
|
+
In addition, there are a number of string such as application name, support url, etc. that are provided and customized with i18n translations. You can customize these in your app, and you can see what they are by looking at config/locales/en.yml in saucy.
|
36
|
+
|
31
37
|
Development environment
|
32
38
|
-----------------------
|
33
39
|
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class BillingMailer < ActionMailer::Base
|
2
|
+
def receipt(account, transaction)
|
3
|
+
@account = account
|
4
|
+
@transaction = transaction
|
5
|
+
mail(:to => account.customer.email,
|
6
|
+
:subject => I18n.t(:subject,
|
7
|
+
:scope => [:saucy, :mailers, :billing_mailer, :receipt],
|
8
|
+
:default => "Subscription receipt"),
|
9
|
+
:from => Saucy::Configuration.mailer_sender)
|
10
|
+
end
|
11
|
+
|
12
|
+
def problem(account, transaction)
|
13
|
+
@account = account
|
14
|
+
mail(:to => account.customer.email,
|
15
|
+
:subject => I18n.t(:subject,
|
16
|
+
:scope => [:saucy, :mailers, :billing_mailer, :problem],
|
17
|
+
:default => "Problem with subscription billing"),
|
18
|
+
:from => Saucy::Configuration.mailer_sender)
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<p>Unfortunately there was a problem processing your latest payment for your <%= @account.name -%> account on <%= t("app_name") %>.</p>
|
2
|
+
|
3
|
+
<p>Some functionality of your <%= @account.name -%> account on <%= t("app_name") %> is now disabled.</p>
|
4
|
+
|
5
|
+
<p>Please verify and fix the credit card details <%= link_to "here", account_billing_url(@account) %> to restore full access to the account.</p>
|
6
|
+
|
7
|
+
<p>If you have any questions, please contact us anytime at <%= mail_to t("support_email"), t("support_email") %>.</p>
|
8
|
+
|
9
|
+
<hr/>
|
10
|
+
|
11
|
+
<p>Thanks for your business,<br/>
|
12
|
+
The <%= t("app_name") %> Team<br/>
|
13
|
+
<%= t("app_url") %></p>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
Unfortunately there was a problem processing your latest payment for your <%= @account.name -%> account on <%= t("app_name") %>.
|
2
|
+
|
3
|
+
Some functionality of your <%= @account.name -%> account on <%= t("app_name") %> is now disabled.
|
4
|
+
|
5
|
+
Please verify and fix the credit card details at the following url to restore full access to the account:
|
6
|
+
<%= account_billing_url(@account) %>
|
7
|
+
|
8
|
+
If you have any questions, please contact us anytime at <%= t("support_email") %>.
|
9
|
+
|
10
|
+
============================================================
|
11
|
+
|
12
|
+
Thanks for your business,
|
13
|
+
The <%= t("app_name") %> Team
|
14
|
+
<%= t("app_url") %>
|
@@ -0,0 +1,41 @@
|
|
1
|
+
<p><strong>Thanks for your business!</strong></p>
|
2
|
+
|
3
|
+
<p>The latest payment for your <strong><%= @account.name -%> account on <%= t("app_name") %></strong> has been approved. This is only a receipt, no payment is due. If you have any questions, please contact us anytime at <%= mail_to t("support_email"), t("support_email") %></p>
|
4
|
+
|
5
|
+
<hr/>
|
6
|
+
|
7
|
+
<table>
|
8
|
+
<tr>
|
9
|
+
<td>
|
10
|
+
RECEIPT
|
11
|
+
</td>
|
12
|
+
<td>
|
13
|
+
<%= @transaction.created_at.to_s(:short_date) -%>
|
14
|
+
</td>
|
15
|
+
</tr>
|
16
|
+
<tr>
|
17
|
+
<td colspan="2">
|
18
|
+
Plan: <%= @account.plan.name -%></br>
|
19
|
+
Charge: USD <%= number_to_currency @account.plan.price -%>*<br/>
|
20
|
+
Credit card: XXXX-XXXX-XXXX-<%= @account.credit_card.last_4 %>
|
21
|
+
|
22
|
+
<p><small>* EU customers: prices exclude VAT</small></p>
|
23
|
+
</td>
|
24
|
+
</tr>
|
25
|
+
<tr>
|
26
|
+
<td>
|
27
|
+
<%= t("saucy.billing_address") %>
|
28
|
+
</td>
|
29
|
+
<td>
|
30
|
+
<strong>Bill to:</strong><br/>
|
31
|
+
<%= @account.name %><br/>
|
32
|
+
<%= @account.credit_card.cardholder_name %>
|
33
|
+
</td>
|
34
|
+
</tr>
|
35
|
+
</table>
|
36
|
+
|
37
|
+
<hr/>
|
38
|
+
|
39
|
+
<p>Thanks for your business,<br/>
|
40
|
+
The <%= t("app_name") %> Team<br/>
|
41
|
+
<%= link_to t("app_url"), t("app_url") %></p>
|
@@ -0,0 +1,25 @@
|
|
1
|
+
Thanks for your business!
|
2
|
+
|
3
|
+
The latest payment for your <%= @account.name -%> account on <%= t("app_name") %> has been approved. This is only a receipt, no payment is due. If you have any questions, please contact us anytime at <%= t("support_email") %>.
|
4
|
+
|
5
|
+
============================================================
|
6
|
+
RECEIPT <%= @transaction.created_at.to_s(:short_date) -%>
|
7
|
+
|
8
|
+
|
9
|
+
Plan: <%= @account.plan.name -%>
|
10
|
+
Charge: USD <%= number_to_currency @account.plan.price -%>*
|
11
|
+
Credit card: XXXX-XXXX-XXXX-<%= @account.credit_card.last_4 %>
|
12
|
+
|
13
|
+
<%= t("saucy.billing_address") %>
|
14
|
+
|
15
|
+
Bill to:
|
16
|
+
<%= @account.name %>
|
17
|
+
<%= @account.credit_card.cardholder_name %>
|
18
|
+
|
19
|
+
* EU customers: prices exclude VAT
|
20
|
+
|
21
|
+
============================================================
|
22
|
+
|
23
|
+
Thanks for your business,
|
24
|
+
The <%= t("app_name") %> Team
|
25
|
+
<%= t("app_url") %>
|
data/lib/saucy/account.rb
CHANGED
@@ -197,6 +197,11 @@ module Saucy
|
|
197
197
|
account.subscription_status = account.subscription.status
|
198
198
|
account.next_billing_date = account.subscription.next_billing_date
|
199
199
|
account.save!
|
200
|
+
if account.past_due?
|
201
|
+
BillingMailer.problem(account, account.subscription.transactions.last).deliver!
|
202
|
+
else
|
203
|
+
BillingMailer.receipt(account, account.subscription.transactions.last).deliver!
|
204
|
+
end
|
200
205
|
end
|
201
206
|
end
|
202
207
|
end
|
data/lib/saucy/configuration.rb
CHANGED
data/lib/saucy/engine.rb
CHANGED
data/lib/saucy/fake_braintree.rb
CHANGED
@@ -31,6 +31,10 @@ class FakeBraintree
|
|
31
31
|
failure["errors"] ||= { "errors" => [] }
|
32
32
|
{ "message" => failure["message"], "verification" => { "status" => failure["status"], "processor_response_text" => failure["message"], "processor-response-code" => failure["code"], "gateway_rejection_reason" => "cvv", "cvv_response_code" => failure["code"] }, "errors" => failure["errors"], "params" => {}}
|
33
33
|
end
|
34
|
+
|
35
|
+
def self.generated_transaction
|
36
|
+
{"status_history"=>[{"timestamp"=>Time.now, "amount"=>"50.00", "transaction_source"=>"CP", "user"=>"copycopter", "status"=>"authorized"}, {"timestamp"=>Time.now, "amount"=>"50.00", "transaction_source"=>"CP", "user"=>"copycopter", "status"=>FakeBraintree.transaction[:status]}], "created_at"=>Time.now, "currency_iso_code"=>"USD", "settlement_batch_id"=>nil, "processor_authorization_code"=>"ZKB4VJ", "avs_postal_code_response_code"=>"I", "order_id"=>nil, "updated_at"=>Time.now, "refunded_transaction_id"=>nil, "amount"=>"50.00", "credit_card"=>{"last_4"=>"1111", "card_type"=>"Visa", "token"=>"8yq7", "customer_location"=>"US", "expiration_year"=>"2013", "expiration_month"=>"02", "bin"=>"411111", "cardholder_name"=>"Chad Lee Pytel"}, "refund_id"=>nil, "add_ons"=>[], "shipping"=>{"region"=>nil, "company"=>nil, "country_name"=>nil, "extended_address"=>nil, "postal_code"=>nil, "id"=>nil, "street_address"=>nil, "country_code_numeric"=>nil, "last_name"=>nil, "locality"=>nil, "country_code_alpha2"=>nil, "country_code_alpha3"=>nil, "first_name"=>nil}, "id"=>"49sbx6", "merchant_account_id"=>"Thoughtbot", "type"=>"sale", "cvv_response_code"=>"I", "subscription_id"=>FakeBraintree.transaction[:subscription_id], "custom_fields"=>"\n ", "discounts"=>[], "billing"=>{"region"=>nil, "company"=>nil, "country_name"=>nil, "extended_address"=>nil, "postal_code"=>nil, "id"=>nil, "street_address"=>nil, "country_code_numeric"=>nil, "last_name"=>nil, "locality"=>nil, "country_code_alpha2"=>nil, "country_code_alpha3"=>nil, "first_name"=>nil}, "processor_response_code"=>"1000", "refund_ids"=>[], "customer"=>{"company"=>nil, "id"=>"108427", "last_name"=>nil, "fax"=>nil, "phone"=>nil, "website"=>nil, "first_name"=>nil, "email"=>"cpytel@thoughtbot.com"}, "avs_error_response_code"=>nil, "processor_response_text"=>"Approved", "avs_street_address_response_code"=>"I", "status"=>FakeBraintree.transaction[:status], "gateway_rejection_reason"=>nil}
|
37
|
+
end
|
34
38
|
end
|
35
39
|
|
36
40
|
ShamRack.at("www.braintreegateway.com", 443).sinatra do
|
@@ -109,6 +113,6 @@ ShamRack.at("www.braintreegateway.com", 443).sinatra do
|
|
109
113
|
|
110
114
|
post "/merchants/:merchant_id/transactions/advanced_search" do
|
111
115
|
# "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<search>\n <ids type=\"array\">\n <item>49sbx6</item>\n </ids>\n <created-at>\n <min type=\"datetime\">2011-01-10T14:14:26Z</min>\n </created-at>\n</search>\n"
|
112
|
-
[200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(
|
116
|
+
[200, { "Content-Encoding" => "gzip" }, ActiveSupport::Gzip.compress(FakeBraintree.generated_transaction.to_xml)]
|
113
117
|
end
|
114
118
|
end
|
data/spec/models/account_spec.rb
CHANGED
@@ -296,4 +296,44 @@ describe Account, "with a paid subscription" do
|
|
296
296
|
subject.next_billing_date.to_s.should == subscription["next_billing_date"].to_s
|
297
297
|
end
|
298
298
|
end
|
299
|
+
|
300
|
+
it "receives a receipt email at it's billing email with transaction details" do
|
301
|
+
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
302
|
+
subscription["status"] = Braintree::Subscription::Status::Active
|
303
|
+
subscription["next_billing_date"] = 2.months.from_now
|
304
|
+
FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Settled,
|
305
|
+
:subscription_id => subject.subscription_token }
|
306
|
+
subscription["transactions"] = [FakeBraintree.generated_transaction]
|
307
|
+
|
308
|
+
Timecop.travel(subject.next_billing_date + 1.day) do
|
309
|
+
ActionMailer::Base.deliveries.clear
|
310
|
+
|
311
|
+
Account.update_subscriptions!
|
312
|
+
|
313
|
+
ActionMailer::Base.deliveries.any? do |email|
|
314
|
+
email.to == [subject.billing_email] &&
|
315
|
+
email.subject =~ /receipt/i
|
316
|
+
end.should be
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
it "receives a receipt email at it's billing email with a notice that it failed when billing didn't work" do
|
321
|
+
subscription = FakeBraintree.subscriptions[subject.subscription_token]
|
322
|
+
subscription["status"] = Braintree::Subscription::Status::PastDue
|
323
|
+
subscription["next_billing_date"] = 2.months.from_now
|
324
|
+
FakeBraintree.transaction = { :status => Braintree::Transaction::Status::Failed,
|
325
|
+
:subscription_id => subject.subscription_token }
|
326
|
+
subscription["transactions"] = [FakeBraintree.generated_transaction]
|
327
|
+
|
328
|
+
Timecop.travel(subject.next_billing_date + 1.day) do
|
329
|
+
ActionMailer::Base.deliveries.clear
|
330
|
+
|
331
|
+
Account.update_subscriptions!
|
332
|
+
|
333
|
+
ActionMailer::Base.deliveries.any? do |email|
|
334
|
+
email.to == [subject.billing_email] &&
|
335
|
+
email.subject =~ /problem/i
|
336
|
+
end.should be
|
337
|
+
end
|
338
|
+
end
|
299
339
|
end
|
data/spec/saucy_spec.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: saucy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 31
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 2
|
9
|
-
-
|
10
|
-
version: 0.2.
|
9
|
+
- 4
|
10
|
+
version: 0.2.4
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- thoughtbot, inc.
|
@@ -128,6 +128,7 @@ files:
|
|
128
128
|
- Gemfile.lock
|
129
129
|
- Rakefile
|
130
130
|
- README.md
|
131
|
+
- config/locales/en.yml
|
131
132
|
- config/routes.rb
|
132
133
|
- app/controllers/accounts_controller.rb
|
133
134
|
- app/controllers/billings_controller.rb
|
@@ -135,6 +136,7 @@ files:
|
|
135
136
|
- app/controllers/memberships_controller.rb
|
136
137
|
- app/controllers/plans_controller.rb
|
137
138
|
- app/controllers/profiles_controller.rb
|
139
|
+
- app/mailers/billing_mailer.rb
|
138
140
|
- app/models/invitation.rb
|
139
141
|
- app/models/limit.rb
|
140
142
|
- app/models/membership.rb
|
@@ -147,9 +149,12 @@ files:
|
|
147
149
|
- app/views/accounts/edit.html.erb
|
148
150
|
- app/views/accounts/index.html.erb
|
149
151
|
- app/views/accounts/new.html.erb
|
152
|
+
- app/views/billing_mailer/problem.html.erb
|
153
|
+
- app/views/billing_mailer/problem.text.erb
|
154
|
+
- app/views/billing_mailer/receipt.html.erb
|
155
|
+
- app/views/billing_mailer/receipt.text.erb
|
150
156
|
- app/views/billings/_form.html.erb
|
151
157
|
- app/views/billings/edit.html.erb
|
152
|
-
- app/views/billings/past_due.html.erb
|
153
158
|
- app/views/billings/show.html.erb
|
154
159
|
- app/views/invitation_mailer/invitation.text.erb
|
155
160
|
- app/views/invitations/new.html.erb
|
File without changes
|