How to test Stripe's invoice_pdf property when it keeps changing? - ruby

In my Rails 6 app I have a very simple controller that displays download links to a user's Stripe invoice PDFs:
class ReceiptsController < ApplicationController
before_action :signed_in_user
def index
receipts = current_account.receipts
end
def show
receipt = current_account.receipts.find(params[:id])
stripe_invoice = Stripe::Invoice.retrieve(receipt.stripe_invoice_id)
redirect_to stripe_invoice.invoice_pdf
end
end
Since Stripe doesn't provide permanent invoice URLs (please correct me if I am wrong), I am storing each invoice's Stripe ID in the database and then use that ID to lookup the current URL to the invoice PDF from the Stripe API.
The problem is that this works most of the time but not all the time. The spec that I created for the controller show action fails in about 20 % of cases because the two URLs do not match:
describe ReceiptsController, :type => :controller do
before :each do
#account = FactoryBot.create(:activated_account)
#user = #account.users.create(FactoryBot.attributes_for(:user))
sign_in(#user)
end
describe 'GET #show' do
# The implementation details of this block don't really matter
before :each do
Customers::FindOrCreate.call(#account)
stripe_subscription = Subscriptions::CreateRemote.call(#account,:payment_behavior => "default_incomplete")
#stripe_invoice = stripe_subscription.latest_invoice
#receipt = Receipts::Create.call(#stripe_invoice)
end
# This test fails in about 20 % of cases because the redirect does not go to #stripe_invoice.invoice_pdf but a slightly different URL
it "redirects to Stripe invoice PDF" do
get :show, :params => {:id => #receipt}
expect(response).to redirect_to #stripe_invoice.invoice_pdf
end
end
end
How can this be? Does the invoice_pdf property of a Stripe invoice change every few seconds? I've been trying to work this out for days now but can't get my head around it.
Addition:
This is a typical test failure that I get quite often:
Expected response to be a redirect to <https://pay.stripe.com/invoice/acct_105jfm2HzYSlmhv7/test_YWNjdF8xMDJqc20yS3pZUmxzaHc0LF9NMGZONnFzNUpPTjlObVprd0hvdGpIdWFUamJHTTVxLDQ3Njc3MDY30200oOxX3A1/pdf?s=ap> but was a redirect to <https://pay.stripe.com/invoice/acct_105jfm2HzYSlmhv7/test_YWNjdF8xMDJqc20yS3pZUmxzaHc0LF9NMGZONnFzNUpPTjlObVprd0hvdGpIdWFUamJHTTVxLDQ3Njc3MDY402001iYCSUbn/pdf?s=ap>.
Expected "https://pay.stripe.com/invoice/acct_105jfm2HzYSlmhv7/test_YWNjdF8xMDJqc20yS3pZUmxzaHc0LF9NMGZONnFzNUpPTjlObVprd0hvdGpIdWFUamJHTTVxLDQ3Njc3MDY30200oOxX3A1F/pdf?s=ap" to be === "https://pay.stripe.com/invoice/acct_105jfm2HzYSlmhv7/test_YWNjdF8xMDJqc20yS3pZUmxzaHc0LF9NMGZONnFzNUpPTjlObVprd0hvdGpIdWFUamJHTTVxLDQ3Njc3MDY402001iYCSUbn/pdf?s=ap".

Related

Warming Up Cache Digests Overnight

We have a Rails 3.2 website which is fairly large with thousands of URLs. We implemented Cache_Digests gem for Russian Doll caching. It is working well. We want to further optimize by warming up the cache overnight so that user gets a better experience during the day. I have seen answer to this question: Rails: Scheduled task to warm up the cache?
Could it be modified for warming up large number of URLs?
To trigger cache hits for many pages with expensive load times, just create a rake task to iteratively send web requests to all record/url combinations within your site. (Here is one implementation)
Iteratively Net::HTTP request all site URL/records:
To only visit every page, you can run a nightly Rake task to make sure that early morning users still have a snappy page with refreshed content.
lib/tasks/visit_every_page.rake:
namespace :visit_every_page do
include Net
include Rails.application.routes.url_helpers
task :specializations => :environment do
puts "Visiting specializations..."
Specialization.all.sort{ |a,b| a.id <=> b.id }.each do |s|
begin
puts "Specialization #{s.id}"
City.all.sort{ |a,b| a.id <=> b.id }.each do |c|
puts "Specialization City #{c.id}"
Net::HTTP.get( URI("http://#{APP_CONFIG[:domain]}/specialties/#{s.id}/#{s.token}/refresh_city_cache/#{c.id}.js") )
end
Division.all.sort{ |a,b| a.id <=> b.id }.each do |d|
puts "Specialization Division #{d.id}"
Net::HTTP.get( URI("http://#{APP_CONFIG[:domain]}/specialties/#{s.id}/#{s.token}/refresh_division_cache/#{d.id}.js") )
end
end
end
end
# The following methods are defined to fake out the ActionController
# requirements of the Rails cache
def cache_store
ActionController::Base.cache_store
end
def self.benchmark( *params )
yield
end
def cache_configured?
true
end
end
(If you want to directly include cache expiration/recaching into this task, check out this implementation.)
via a Custom Controller Action:
If you need to bypass user authentication restrictions to get to your pages, and/or you don't want to screw up (too badly) your website's tracking analytics, you can create a custom controller action for hitting cache digests that use tokens to bypass authentication:
app/controllers/specializations.rb:
class SpecializationsController < ApplicationController
...
before_filter :check_token, :only => [:refresh_cache, :refresh_city_cache, :refresh_division_cache]
skip_authorization_check :only => [:refresh_cache, :refresh_city_cache, :refresh_division_cache]
...
def refresh_cache
#specialization = Specialization.find(params[:id])
#feedback = FeedbackItem.new
render :show, :layout => 'ajax'
end
def refresh_city_cache
#specialization = Specialization.find(params[:id])
#city = City.find(params[:city_id])
render 'refresh_city.js'
end
def refresh_division_cache
#specialization = Specialization.find(params[:id])
#division = Division.find(params[:division_id])
render 'refresh_division.js'
end
end
Our custom controller action renders the views of other expensive to load pages, causing cache hits to those pages. E.g. refresh_cache renders the same view page & data as controller#show, so requests to refresh_cache will warm up the same cache digests as controller#show for those records.
Security Note:
For security reasons, I recommend before providing access to any custom refresh_cache controller request that you pass in a token and check it to make sure that it corresponds with a unique token for that record. Matching URL tokens to database records before providing access (as seen above) is trivial because your Rake task has access to the unique tokens of each record -- just pass the record's token in with each request.
tl;dr:
To trigger thousands of site URL's/cache digests, create a rake task to iteratively request every record/url combination in your site. You can bypass your app's user authentication restrictions for this task by creating a a custom controller action that authenticates access via tokens instead.
I realize this question is about a year old, but I just worked out my own answer, after scouring a bunch of partial & incorrect solutions.
Hopefully this will help the next person...
Per my own utility class, which can be found here:
https://raw.githubusercontent.com/JayTeeSF/cmd_notes/master/automated_action_runner.rb
You can simply run this (per it's .help method) and pre-cache your pages, without tying-up your own web-server, in the process.
class AutomatedActionRunner
class StatusObject
def initialize(is_valid, error_obj)
#is_valid = !! is_valid
#error_obj = error_obj
end
def valid?
#is_valid
end
def error
#error_obj
end
end
def self.help
puts <<-EOH
Instead tying-up the frontend of your production site with:
`curl http://your_production_site.com/some_controller/some_action/1234`
`curl http://your_production_site.com/some_controller/some_action/4567`
Try:
`rails r 'AutomatedActionRunner.run(SomeController, "some_action", [{id: "1234"}, {id: "4567"}])'`
EOH
end
def self.common_env
{"rack.input" => "", "SCRIPT_NAME" => "", "HTTP_HOST" => "localhost:3000" }
end
REQUEST_ENV = common_env.freeze
def self.run(controller, controller_action, params_ary=[], user_obj=nil)
success_objects = []
error_objects = []
autorunner = new(controller, controller_action, user_obj)
Rails.logger.warn %Q|[AutomatedAction Kickoff]: Preheating cache for #{params_ary.size} #{autorunner.controller.name}##{controller_action} pages.|
params_ary.each do |params_hash|
status = autorunner.run(params_hash)
if status.valid?
success_objects << params_hash
else
error_objects << status.error
end
end
return process_results(success_objects, error_objects, user_obj.try(:id), autorunner.controller.name, controller_action)
end
def self.process_results(success_objects=[], error_objects=[], user_id, controller_name, controller_action)
message = %Q|AutomatedAction Summary|
backtrace = (error_objects.first.try(:backtrace)||[]).join("\n\t").inspect
num_errors = error_objects.size
num_successes = success_objects.size
log_message = %Q|[#{message}]: Generated #{num_successes} #{controller_name}##{controller_action}, pages; Failed #{num_errors} times; 1st Fail: #{backtrace}|
Rails.logger.warn log_message
# all the local-variables above, are because I typically call Sentry or something with extra parameters!
end
attr_reader :controller
def initialize(controller, controller_action, user_obj)
#controller = controller
#controller = controller.constantize unless controller.respond_to?(:name)
#controller_instance = #controller.new
#controller_action = controller_action
#env_obj = REQUEST_ENV.dup
#user_obj = user_obj
end
def run(params_hash)
Rails.logger.warn %Q|[AutomatedAction]: #{#controller.name}##{#controller_action}(#{params_hash.inspect})|
extend_with_autorun unless #controller_instance.respond_to?(:autorun)
#controller_instance.autorun(#controller_action, params_hash, #env_obj, #user_obj)
end
private
def extend_with_autorun
def #controller_instance.autorun(action_name, action_params, action_env, current_user_value=nil)
self.params = action_params # suppress strong parameters exception
self.request = ActionDispatch::Request.new(action_env)
self.response = ActionDispatch::Response.new
define_singleton_method(:current_user, -> { current_user_value })
send(action_name) # do it
return StatusObject.new(true, nil)
rescue Exception => e
return StatusObject.new(false, e)
end
end
end

Ruby on Rails: Having problems selecting correct parameters for a method depending on context the is method used

I am working on a application where Users can list their in-game items to trade with other Users. A user's profile url would be something like this:
/users/1/index
And their user listings profile would be something like
/users/1/listings/1
All other resources nested under users would be the same as the latter.
I am trying to implement a method that is called by a before_filter callback that checks to see if a user has blocked or is blocked by the user who owns the profile and respective nested resources such as ability to message them, view their listings etc. If either has blocked each other, then they redirected to the root page of the application. This is the method that I use for the before_filter:
def blocked_relationships
if blocked?
redirect_to :root
end
end
I used another method that checks the state of the relationships between the two users.
This is the method I found and worked on after some research courtesy of the Rails Recipes book:
def blocked?
Relationship.exists?(user_id: current_user.id, other_user_id: params[:user_id], status: "blocked") ||
Relationship.exists?(user_id: params[:user_id], other_user_id: current_user.id, status: "blocked")
end
The problem I have is that this method only works, for example, when User 1 is looking at User 2's items, messages, listings etc. because the url:
/users/2/listings [or items or etc]
will contain a params that makes reference to the user as params[:user_id]. params[:id] in this case and context will refer to the listings id.
BUT, if I am User 1 and I have blocked User 2 and visit User 2's profile, this method will not work because the url /users/2/index will use params[:id] to instead of params[:user_id].
I've been thinking about how to implement this in a DRY way but I can't seem to solve my problem other than doing something like this:
def blocked?
if params[:user_id].blank?
Relationship.exists?(user_id: current_user.id, other_user_id: params[:id], status: "blocked") ||
Relationship.exists?(user_id: params[:id], other_user_id: current_user.id, status: "blocked")
else
Relationship.exists?(user_id: current_user.id, other_user_id: params[:user_id], status: "blocked") ||
Relationship.exists?(user_id: params[:user_id], other_user_id: current_user.id, status: "blocked")
end
end
I also considered the possibility that I'm not even implementing my blocking feature correctly, but before I address that issue, I was wondering if anyone had any ideas on how to solve this problem. Any help or feedback would be greatly appreciated and I would be happy to add anymore information for clarification. Thanks!
Why not other_id = params[:user_id] || params[:id]? This is a way to override :id when :user_id is present.
About your blocking feature though, to me I'd like to see a user even if I've blocked them. I'd create a blocked_by_user_id field on the Relationship to see who did the blocking and only disallow the blocked party from seeing the user's profile.
You'd probably want to checkout authorization gems for rails like cancan or related (it's not my favorite but the most popular). However, you could handle it like this:
class User
has_many :relationships,
scope :accessible_by,
->(user) { where.not id: user.relationships.where(status: :blocked).pluck(:other_user_id) }
end
Then use the relationship User.accessible_by(current_user) on your controller instead of plainly User to retrieve resources. For example:
class UsersController < ApplicationController
def index
#users = User.accessible_by(current_user)
# bleh
end
def show
#user = User.accessible_by(current_user).find(params[:id])
# etc
end
end
When the resource is nested under a user you could do this:
class Users::PicturesController < UsersController
def index
#pictures = User.accessible_by(current_user)
.find(params[:user_id]).pictures
end
def show
#picture = User.accessible_by(current_user)
.find(params[:user_id]).pictures.find(params[:id])
end
end
When a user tries to access a resource that can't view, ActiveRecord::RecordNotFound will be raised, so you should handle it:
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNorFound, with: :rescue_not_found
private
def rescue_not_found
redirect_to root_path,
notice: 'You can\'t access that with your current priveleges. '
end
end

How to display payment gateway response messages in view - Rails 4, Active Merchant

I'm using Active Merchant with Stripe as the payment gateway. Everything works fine except that i don't know how to go about getting the gateway response error messages from Stripe (when a card is declined, invalid etc) to display on the checkout page to the user. I can get a StandardError to be raised that redirects to an error page with the response message but that's it.
ORDER MODEL
class Order < ActiveRecord::Base
has_many :order_products
has_many :products, through: :order_products
attr_accessor :card_number, :security_code, :card_expires_on
validate :validate_card, :on => :create
def validate_card
unless credit_card.valid?
credit_card.errors.full_messages.each do |message|
errors[:base] << message
end
end
end
def purchase(basket)
response = GATEWAY.purchase(Product.total_basket_price(basket)*100, credit_card, purchase_options)
unless response.success?
raise StandardError, response.message
end
end
def credit_card
#credit_card ||= ActiveMerchant::Billing::CreditCard.new(
:number => card_number,
:first_name => first_name,
:last_name => last_name,
:verification_value => security_code,
:month => card_expires_on.month,
:year => card_expires_on.year
)
end
def purchase_options
{
:billing_address => {
:address1 => address_1,
:address2 => address_2,
:city => city,
:country => country_code,
:zip => postal_code
}
}
end
end
ORDERS CONTROLLER
class OrdersController < ApplicationController
def create
#order = Order.new(order_params)
# #product = basket.find(params[:product_id])
basket.each do |item_id|
#order.order_products.build(product: Product.find(item_id))
end
if #order.save
if #order.purchase(basket)
render "show"
else
render "failure"
end
else
render "new"
end
end
Can anyone lend a hand, please??
Many Thanks
Easy peasy!
This is a simple matter of control flow. In Ruby, as in most languages, exceptions interrupt the normal program flow. As your code is written now, #purchase is raising an exception when it fails.
That's fine and a perfectly valid design decision. But the code interacting with #purchase is this:
if #order.purchase(basket)
render "show"
else
render "failure"
end
That code has no exception handling, so any exception will be caught by Rails, program flow will halt, and you'll get either a detailed error page (in development mode) or a generic 500 error page (in production mode).
Since you profess to be new to Ruby and Rails, a little code substitution might make this clearer:
# If #purchase is successful, it evaluates to true.
if true
render "show" # 'show' view is rendered as expected. Flow stops.
else
render "failure"
end
# If #purchase fails, it raises an exception.
if raise StandardError, response.message
# ^^^ Exception is raised, flow stops here.
render "show" # This code is never reached.
else # This code is never reached.
render "failure" # This code is never reached.
end
As I implied in the beginning, though, it's an easy fix once you know what the issue is. You can simply handle the exception with rescue. Where you currently have an if/else block, you can swap in an if block and a rescue block:
if #order.purchase(basket)
render 'show'
end
rescue => e
render 'failure'
There's room for improvement here depending on your needs—since you're raising and rescuing StandardError, for example, your can't easily distinguish between a network failure and a declined card—but it'll get you moving again.
After a lot of fiddling and help, the working solution was to search for an error key within the response params hash and if an error was present add the message to the object errors. Not particularly elegant but it now does what i want.
ORDER MODEL
def purchase(basket)
response = GATEWAY.purchase(Product.total_basket_price(basket)*100, credit_card, purchase_options)
if response.params.key?('error')
self.errors.add :base, response.message
false
else
true
end
end
ORDERS CONTROLLER
Also switched the order of the if statements in the controller so that def purchase(basket) runs first before the order is saved, allowing the error message(s) from the response to be caught and displayed.
if #order.purchase(basket)
if #order.save
render "show"
else
render "new"
end
else
render "new"
end
VIEW
<%= if #order.errors.any?
#order.errors[:base].to_sentence
end%>

Showing users unique info when facebook authenticate , with Sinatra/ Ruby

Super beginner here.
Here's what I am trying to do:
Build a basic to do list app, where User X logs in with facebook, adds some items, sees them, logs out. User Y/Z/M/etc, should be able to log in see their OWN list, add their OWN items, etc.
AKA: a standard web app where you log in to your account and see your own info.
What I have so far:
Ability to build a list, log in with Facebook and having it know your name.
However, the list stays the same whether I log in or whether my friend logs in with her account.
What I need to do, and don't know how:
I need each user to be able to create and see their own list, and be able to come back to it and still see it/ add to it, etc.
I don't even know how this is called, would this be a database of users each with their own set of data? Would the lists need to be set up so they could be stored as a chunk of data?
Does it have something to do with this :Sessions in Sinatra using Facebook authentication If so, what?
If anyone could be give me some really really basic directions as to where to go from here, any tutorials or what I should be googling for, that'd be awesome.
Here's my main piece of code (warning: it's really messy) :
require 'sinatra'
require 'data_mapper'
require 'time'
require 'rubygems'
require 'json'
require 'omniauth'
require 'omniauth-facebook'
#TODO require 'omniauth-att'
SCOPE = 'email,read_stream'
DataMapper::setup(:default, "sqlite3://#{Dir.pwd}/queue.db")
class SinatraApp < Sinatra::Base
configure do
set :sessions, true
set :inline_templates, true
set :protection, :except => :frame_options
end
class Note
include DataMapper::Resource
property :id, Serial
property :content, Text, :required => true
property :complete, Boolean, :required => true, :default => false
property :created_at, DateTime
property :updated_at, DateTime
end
class User
include DataMapper::Resource
property :id, Serial
property :uid, String
property :name, String
property :created_at, DateTime
end
###### no clue what this does ##############
DataMapper.finalize
DataMapper.auto_upgrade!
enable :session
use OmniAuth::Builder do
provider :facebook, '464630283595639','5e4c7ad43bf111c10287c981d51127a3',:scope => SCOPE, :display => "popup"
#provider :att, 'client_id', 'client_secret', :callback_url => (ENV['BASE_DOMAIN']
end
###### root ##############
get '/' do
if current_user
#notes = Note.all :order => :id.desc
#title = 'Movie Queue'
erb :home
else
' sign in with Facebook'
end
end
###### authentication ##############
["/sign_in/?", "/signup/?"].each do |path|
get path do
redirect '/auth/facebook'
end
end
get '/auth/:name/callback' do
auth = request.env["omniauth.auth"]
user = User.first_or_create({ :uid => auth["uid"]}, {
:uid => auth["uid"],
:name => auth["first_name"],
:created_at => Time.now })
session[:user_id] = user.id
redirect '/'
end
helpers do
def current_user
#current_user ||= User.get(session[:user_id]) if session[:user_id]
end
end
##list making part###
post '/' do
n = Note.new
n.content = params[:content]
n.save
redirect '/'
end
get '/:id/delete' do
n = Note.get params[:id]
if n.destroy
redirect '/', :notice => 'Note deleted successfully.'
else
redirect '/', :error => 'Error deleting note.'
end
end
get '/:id/complete' do
n = Note.get params[:id]
n.complete = n.complete ? 0 : 1 # flip it
n.save
redirect '/'
end
########## logout and error handlers #############
get '/logout' do
session[:user_id] = nil
redirect '/'
end
get '/auth/failure' do
erb "<h1>Authentication Failed:</h1><h3>message:<h3> <pre>#{params}</pre>"
end
get '/auth/:provider/deauthorized' do
erb "#{params[:provider]} has deauthorized this app."
end
get '/protected' do
throw(:halt, [401, "Not authorized\n"]) unless session[:authenticated]
erb "<pre>#{request.env['omniauth.auth'].to_json}</pre><hr>
<a href='/logout'>Logout</a>"
end
end
########## don't know what this is #############
SinatraApp.run! if __FILE__ == $0
Disclaimer: I don't know Datamapper, but this should get you going.
There needs to be a way to associate a note with a user. This needs a table in the database, some would call it users_notes, personally I prefer users_rel_notes, (perhaps Datamapper has a convention for this… YMMV). Anyway, the table will have a minimum of 2 columns - the user's id, and the note id. You don't need a separate table as I wrote before (I'm lacking a bit of sleep, sorry!), that would be for a many to many relationship where a user could have several notes and a note could be associated with several users. For what you have, where only the owner of a note has access to it, it requires a one to many relationship. You could add a column to the notes table to store the user id.
Then, in the User class, add an association to the Note class, it's a one to many association and in Datamapper that's a has n, e.g.
has n, :notes
Now when you have a user instance, you can (probably) call the notes for that user via:
user.notes
I see you have the helper current_user defined, so if someone is logged on you could call current_user.notes to get back all the notes for the logged in user.
Remember, when you add a note to make sure you add a record to the association table, (probably, read the link) via user.notes << my_new_note.
The session is the information you keep around to identify the user and any other little bits of info that you may recurrently need. The likelyhood is, you're just storing an id for the user, or the facebook token that identifies them, and then during a request, if it's needed then you'll look inside the cookie, grab the id, look up that user by the id and get a user instance. Session info can be stored in several ways, most often cookies but you can use anything you would use to store any other data.

What would I need in a Rails 3 controller to process incoming emails and insert them into the database?

I'm trying to set up my RoR 3 application to receive emails and then process those emails and insert them into the database.
In my application, I have a "jobs" controller. Users can create jobs. I also have a "comments" controller. Users can create comments on jobs.
Here is (part of) what I have in my comments controller:
def new
#job = Job.find(params[:job_id])
#comment = #job.comments.build
end
def create
#job = Job.find(params[:job_id])
#comment = #job.comments.build(params[:comment])
#comment.user_id = current_user.id
#comment.commenter = current_user.login
if #comment.save
redirect_to #job
else
render :action => "new"
end
end
When users add a comment, the admin receives an email.
When admins add a comment, users receive an email.
(This is already functioning.)
I'm using Cloudmailin to help me receive incoming mail. I've set up the Cloudmailin address to point to http://myapp.com/incoming_mails.
class IncomingMailsController < ApplicationController
require 'mail'
skip_before_filter :verify_authenticity_token
def create
another_comment = Comment.create(:title =>
params[:subject], :body => params[:plain])
render :text => 'success', :status => 200 #status of 404 rejects mail
end
end
Looking at the above comment controller, it looks like I'll need the job_id, current_user.id, and current_user.login.
Here's what I am having trouble sorting out: what goes in my "incoming_mails" controller? How can I make sure that when a user responds via email that the controller in "incoming_mails" is able to find the job_id and the current_user.id (or user_id) and then insert that information into the database?
I'm thinking that I'll need to grab the user's email address and then also have the job_id in the subject line...hmmm....
Does anyone have experience setting up incoming mail processing in Rails 3?
There's a few ways to do this. A common technique is to set the from/reply_to as a custom email address that allows you to do a lookup of the original object. Something like:
class Comment < ActiveRecord::Base
belongs_to :commentable
has_many :comments, :as => :commentable
before_validation :generate_token, :on => :create
validates :token, :presence => true, :uniqueness => true
attr_accessible :token
private
def generate_token
...
end
end
Email is sent with a from/reply_to address like [token]#msg.yoursite.com
(You can also use a comments+[token_or_id]#msg.yoursite.com if you'd like - see the :disposable param supplied by cloudmailin)
class IncomingMailsController < ApplicationController
def create
#comment = Comment.find_by_token(params[:to].split('#')[0])
#comment.comments.create(:body => params[:plain])
render :text => 'success', :status => 200
end
end
If you go the [token]#msg.yoursite.com then you have to setup your dns records properly as described here.
Another option is to store content in the headers of the email. Maybe your headers would look something like this:
X-YOURAPP-OBJECT-ID = 44
X-YOURAPP-OBJECT-TYPE = Job
X-YOURAPP-TARGET-ASSOC = comments
X-YOURAPP-TARGET-ATTR = body
Then your controller would look more like this:
class IncomingMailsController < ApplicationController
def create
headers = Mail::Header.new(params[:message])
object= headers[:x_yourapp_object_type].constantize.find(headers[:x_yourapp_object_id])
object.send(headers[:x_yourapp_target_assoc]).create(headers[:x_yourapp_target_attr] => params[:plain])
render :text => 'success', :status => 200
end
end
The advantage to doing it this way is that it's completely generic. You can have comments on jobs, comments on comments, or whatever. If you create another model that you also want to allow email reply on... just update the headers and you're set. The only issue is that you have to be careful of these headers being stripped out by mail clients. Take a look at this thread for more information about that.
I'm currently using the first technique and it's working very well. However, I'll be refactoring and trying the second technique at the end of this week.
I'm not sure this is a full answer but let me give it a quick shot and I can update it when I have a little more time.
Firstly I would start by having the job perhaps as part of the to address. Use comment+1#domain.com with 1 signifying that this is job number one. This part after the plus is called the is the disposable part in CloudMailin (params[:disposable]. The users email address can then be taken from the message body either using params[:from] or Mail.new(params[:message]).from (sometimes the from address given to the SMTP server is different to the one in the header)
Next you have the comment itself which can simply be the plain param.
This gives something like the following:
def create
#job = Job.find(params[:disposable])
if comment = #job.comments.create(:text => params[:plain], :commenter => User.find_by_email(params[:from])
render :text => 'Success', :status => 401
else
render :text => comment.errors.inspect, :status => 422, :content_type => 'text/plain'
end
I hope that helps. It's a bit rough but hopefully it gives the right idea.

Resources