Changesets can be listed by changeset number.
The Git repository is here.
- Revision:
- 390
- Log:
Initial import of Canvass, a donations-based poll application.
- Author:
- rool
- Date:
- Mon Mar 21 14:58:04 +0000 2011
- Size:
- 9585 Bytes
1 | ######################################################################## |
2 | # File:: payment_gateway_offsite_controller.rb |
3 | # (C):: Hipposoft 2010, 2011 |
4 | # |
5 | # Purpose:: Handle order payment via off-site payment gateways such as |
6 | # PayPal express checkout. Uses ActiveMerchant to partially |
7 | # abstract away from the actual payment method in use, but |
8 | # assumes PayPal style redirection semantics: |
9 | # |
10 | # - Set up gateway passing an on-success and on-failure URL |
11 | # - Data returned gives the off-site location to use |
12 | # - Redirect there and wait for on-success or on-failure URL |
13 | # to be visited |
14 | # - If successful, acquire payment summary details from |
15 | # remote service, present to user and ask for final order |
16 | # confirmation |
17 | # - If user confirms, finalise order and make the payment. |
18 | # |
19 | # Currently this code uses the PayPal Express Checkout system. |
20 | # The ActiveMerchant API is a little unhelpful, having rather |
21 | # specific interfaces for express checkout (e.g. things like |
22 | # "gateway.express.purchase", rather than, say, a more generic |
23 | # "gateway.offsite.purchase", or something). You may need to |
24 | # make significant code changes to switch to another off-site |
25 | # provider as a result. |
26 | # |
27 | # The controller is RESTful though the mapping is a bit |
28 | # strained: |
29 | # |
30 | # GET new: Kick off the express payment process and redirect |
31 | # to the offsite gateway (kind of "auto-create") |
32 | # |
33 | # GET delete: Cancel process at any stage. |
34 | # |
35 | # GET edit: The offsite gateway comes here if successful. In |
36 | # theory the user could edit aspects of their |
37 | # payment before confirmation (e.g. add gift wrap) |
38 | # though we don't support that here. |
39 | # |
40 | # PUT update: The edit form above (fake-)PUTs here to make the |
41 | # final payment. |
42 | # |
43 | # MONEY IS PAID HERE and here only. |
44 | # ---------------------------------------------------------------------- |
45 | # 11-Mar-2010 (ADH): Created. |
46 | # 30-Jan-2011 (ADH): Imported from Artisan. |
47 | ######################################################################## |
48 | |
49 | class PaymentGatewayOffsiteController < PaymentGatewayController |
50 | |
51 | # NB: Security and other filters - see the superclass. |
52 | |
53 | def delete; super; end |
54 | |
55 | # Start a new payment session. |
56 | # |
57 | def new |
58 | |
59 | if ( ! PaymentGateway.instance.gateway_is_express_only() && |
60 | ! PaymentGateway.instance.gateway_has_express_support() ) |
61 | raise "Off-site checkout is impossible without an Express-capable gateway" |
62 | end |
63 | |
64 | # Talk to the payment gateway. |
65 | |
66 | gateway_response = express_gateway().setup_purchase( |
67 | |
68 | @donation.amount_for_gateway(), |
69 | :currency => @donation.currency.code, |
70 | |
71 | :ip => request.remote_ip, |
72 | :order_id => @donation.id.to_s, # Do not change; see "valid_donation_found" |
73 | :description => @donation.poll_title, |
74 | :return_url => edit_poll_payment_gateway_offsite_url( :poll_id => @donation.poll_id ), |
75 | :cancel_return_url => delete_poll_payment_gateway_offsite_url( :poll_id => @donation.poll_id ) |
76 | ) |
77 | |
78 | if gateway_response.success? |
79 | redirect_to( express_gateway().redirect_url_for( gateway_response.token ) ) |
80 | else |
81 | flash[ :error ] = t( |
82 | :'uk.org.pond.canvass.controllers.payment_gateway_offsite.error_paypal', |
83 | :message => gateway_response.message.chomp( '.' ) |
84 | ) |
85 | |
86 | redirect_to( root_path() ) |
87 | end |
88 | end |
89 | |
90 | # Once the user has dealt with the offsite payment gateway they're directed |
91 | # back here. This lets them "edit" the final details of their order and |
92 | # submit it via a form which POSTs to the "create" action. |
93 | # |
94 | def edit |
95 | redirect_to( root_path() ) and return unless ( params[ :token ] ) |
96 | |
97 | # Get the gateway response and find the expected purchase details based on |
98 | # current user and required state. |
99 | |
100 | begin |
101 | gateway_response = express_gateway().details_for( params[ :token ] ) |
102 | raise gateway_response.message unless gateway_response.success? |
103 | rescue => error |
104 | appctrl_report_error( error ) |
105 | redirect_to( root_path() ) |
106 | return |
107 | end |
108 | |
109 | @address = gateway_response.address if ensure_donation_is_valid_and_set_variables( gateway_response ) # Else redirection has happened within the called validity checking method |
110 | end |
111 | |
112 | # Make a payment and finalise an order. See the "edit" action for details. |
113 | # |
114 | def update |
115 | |
116 | # It's nice to check to see if the poll was closed or deleted *before* we |
117 | # go into the payment loop and give a nasty 'red error' due to the Donation |
118 | # object raising errors about it later. |
119 | |
120 | poll = Poll.find_by_id( @donation.poll_id ) |
121 | token = nil |
122 | |
123 | if ( poll.nil? == true ) |
124 | token = :'activerecord.errors.models.donation.poll_has_vanished' |
125 | elsif ( poll.workflow_state.to_sym != Poll::STATE_OPEN ) |
126 | token = :'activerecord.errors.models.donation.poll_is_not_open' |
127 | end |
128 | |
129 | unless ( token.nil? ) |
130 | Donation.safely_destroy_initial_state_donations_for( current_user ) |
131 | flash[ :attention ] = I18n.t( token ) |
132 | redirect_to( root_path() ) |
133 | return |
134 | end |
135 | |
136 | # The poll looks OK at this instant, so start paying. If the poll manages |
137 | # to get deleted or closed during the processing of the code below via a |
138 | # different request thread, tough luck; the user gets a nasty looking error |
139 | # message, but at least nothing breaks. |
140 | |
141 | @payment_made = false |
142 | |
143 | status = begin |
144 | Donation.transaction do |
145 | |
146 | # Store basic information and action the payment on our side before |
147 | # talking to the database. If there's a basic failure, e.g. the poll |
148 | # is not open yet, then an exception will be raised and we'll roll |
149 | # back these changes *before* having paid. |
150 | |
151 | @donation.notes = params[ :notes ] || '' |
152 | @donation.invoice_number = InvoiceNumber.next! |
153 | |
154 | @donation.paid! # See Workflow state machine definitions in donation.rb |
155 | @donation.save! |
156 | |
157 | # Now talk to the gateway. |
158 | |
159 | @gateway_response = express_gateway().purchase( |
160 | |
161 | @donation.amount_for_gateway(), |
162 | :currency => @donation.currency.code, |
163 | |
164 | :ip => request.remote_ip, |
165 | :payer_id => params[ :payer_id ], |
166 | :token => params[ :token ] |
167 | ) |
168 | |
169 | # Remember, ActiveRecord's transaction handler catches this exception |
170 | # so the outer block will exit without the 'rescue' clause activating |
171 | # and will evaluate to 'nil'. |
172 | |
173 | raise ActiveRecord::Rollback unless @gateway_response.success? |
174 | |
175 | # OK, we've paid... This instance variable is purely used to modify the |
176 | # message shown to the user if something goes wrong. |
177 | |
178 | @payment_made = true |
179 | |
180 | # Store payment details. We can't do this before talking to the |
181 | # gateway as it tells us information we must record, so we're forced |
182 | # to risk a possible failure below *after* having paid. |
183 | # |
184 | # Only 'authorization' is strictly needed for refunds, but the other |
185 | # two fields, used to make the payment, are recorded in case of some |
186 | # kind of future dispute requires deeper investigation. |
187 | |
188 | @donation.authorisation_tokens = {} |
189 | @donation.authorisation_tokens[ :offsite ] = {} |
190 | @donation.authorisation_tokens[ :offsite ][ :payer_id ] = @gateway_response.payer_id() |
191 | @donation.authorisation_tokens[ :offsite ][ :token ] = @gateway_response.token() |
192 | @donation.authorisation_tokens[ :offsite ][ :authorization ] = @gateway_response.authorization() |
193 | @donation.save! # Yes, again... |
194 | |
195 | true # Inner transaction block evaluates to true => success. |
196 | end |
197 | |
198 | # Outer block evaluates to result from inner transaction block. |
199 | |
200 | rescue => error |
201 | error.message # Outer block evaluates to String => error message. |
202 | |
203 | end |
204 | |
205 | # Status: |
206 | # |
207 | # nil -> Gateway call failure; check "@gateway_response.message". |
208 | # String -> Exception; string gives message. The user might have been |
209 | # charged, but our local database operations failed. |
210 | # true -> Success. |
211 | # |
212 | if ( status != true ) |
213 | |
214 | if ( @payment_made == true ) |
215 | flash[ :error ] = t( |
216 | :'uk.org.pond.canvass.controllers.payment_gateway_offsite.error_failed_but_maybe_charged', |
217 | :admin => ActionView::Base.new.mail_to( ADMINISTRATOR_EMAIL_ADDRESS ), # config/initializers/50_general_settings.rb |
218 | :message => status.chomp( '.' ) |
219 | ) |
220 | else |
221 | flash[ :error ] = t( |
222 | :'uk.org.pond.canvass.controllers.payment_gateway_offsite.error_failed_but_not_charged', |
223 | :message => status.chomp( '.' ) |
224 | ) |
225 | end |
226 | |
227 | redirect_to( root_path() ) |
228 | |
229 | else |
230 | |
231 | # See the Donation model for comments on why this is done here - this line |
232 | # of code causes all related notification e-mail messages to be sent. |
233 | |
234 | @donation.send_new_item_notifications() |
235 | |
236 | appctrl_set_flash( :notice ) |
237 | redirect_to( user_donation_path( :id => @donation.id, :user_id => @donation.user_id ) ) |
238 | |
239 | end |
240 | end |
241 | |
242 | private |
243 | |
244 | # Return an express gateway object. |
245 | # |
246 | def express_gateway |
247 | if ( PaymentGateway.instance.gateway_is_express_only() ) |
248 | return gateway() # See superclass |
249 | else |
250 | return gateway().express |
251 | end |
252 | end |
253 | end |