5 use BSE::Shop::Util qw(:payment);
8 our $VERSION = "1.004";
10 use constant DEF_TEST_WS_URL => "https://api-3t.sandbox.paypal.com/nvp";
11 use constant DEF_TEST_REFRESH_URL => "https://www.sandbox.paypal.com/webscr";
13 use constant DEF_LIVE_WS_URL => "https://api-3t.paypal.com/nvp";
14 use constant DEF_LIVE_REFRESH_URL => "https://www.paypal.com/cgibin/webscr";
18 test_ws_url => DEF_TEST_WS_URL,
19 test_refresh_url => DEF_TEST_REFRESH_URL,
21 live_ws_url => DEF_LIVE_WS_URL,
22 live_refresh_url => DEF_LIVE_REFRESH_URL,
28 return $cfg->entry("paypal", "test", 1);
34 my $realkey = _test($cfg) ? "test_$key" : "live_$key";
35 if (exists $defs{$realkey}) {
36 return $cfg->entry("paypal", $realkey, $defs{$realkey});
39 return $cfg->entryErr("paypal", $realkey);
46 return _cfg($cfg, "ws_url");
49 sub _base_refresh_url {
52 return _cfg($cfg, "refresh_url");
58 return _cfg($cfg, "api_signature");
64 return _cfg($cfg, "api_username");
70 return _cfg($cfg, "api_password");
76 return sprintf("%d.%02d", int($price / 100), $price % 100);
82 return _format_amt($order->total);
88 return BSE::Cfg->single->entry("shop", "currency_code", "AUD")
92 my ($class, %opts) = @_;
94 my $order = delete $opts{order}
95 or confess "Missing order";
96 my $rmsg = delete $opts{msg}
97 or confess "Missing msg";
98 my $who = delete $opts{user} || "U";
99 my $cfg = BSE::Cfg->single;
101 my %info = _set_express_checkout($cfg, $order, $who, $rmsg)
104 $order->set_paypal_token($info{TOKEN});
107 my $url = _make_url(_base_refresh_url($cfg),
109 cmd => "_express-checkout",
110 token => $info{TOKEN},
111 useraction => "confirm",
112 AMT => _order_amt($order),
113 CURRENCYCODE => _order_currency($order)
117 # BSE::TB::AuditLog->log
119 # component => "shop:paypal:paymenturl",
129 # the _api_*() functions will die if not configured
131 my $cfg = BSE::Cfg->single;
135 _api_username($cfg) && _api_password($cfg) && _api_signature($cfg);
141 my ($class, %opts) = @_;
143 my $order = delete $opts{order}
144 or confess "Missing order";
145 my $req = delete $opts{req}
146 or confess "Missing req";
147 my $rmsg = delete $opts{msg}
148 or confess "Missing msg";
152 my $token = $cgi->param("token");
154 $$rmsg = $req->catmsg("msg:bse/shop/paypal/notoken");
157 my $payerid = $cgi->param("PayerID");
159 $$rmsg = $req->catmsg("msg:bse/shop/paypal/nopayerid");
162 unless ($token eq $order->paypal_token) {
163 print STDERR "cgi $token order ", $order->paypal_token, "\n";
164 $$rmsg = $req->catmsg("msg:bse/shop/paypal/badtoken");
169 if (_do_express_checkout_payment
170 ($cfg, $rmsg, $order, scalar($req->siteuser), $token, $payerid, \%info)) {
171 $order->set_paypal_tran_id($info{TRANSACTIONID});
175 unless ($info{L_ERRORCODE}
176 && $info{L_ERRORCODE} == 10415
177 && $info{CHECKOUTSTATUS}
178 && $info{CHECKOUTSTATUS} eq "PaymentActionCompleted"
179 && $info{PAYMENTREQUEST_0_TRANSACTIONID}) {
180 return; # something else went wrong
183 # already processed, maybe there was an error when the user first
184 # returned, treat it as completed
185 $order->set_paypal_tran_id($info{PAYMENTREQUEST_0_TRANSACTIONID});
187 $order->set_paypal_token("");
188 $order->set_paidFor(1);
189 $order->set_paymentType(PAYMENT_PAYPAL);
190 $order->set_stage("unprocessed");
191 $order->set_complete(1);
193 BSE::TB::AuditLog->log
195 component => "shop:paypal:pay",
198 actor => scalar($req->siteuser) || "U",
199 msg => "Apply PayPal payment to Order No. " . $order->id . ", transaction ".$order->paypal_tran_id,
206 my ($class, %opts) = @_;
208 my $order = delete $opts{order}
209 or confess "Missing order";
210 my $rmsg = delete $opts{msg}
211 or confess "Missing msg";
212 my $req = delete $opts{req}
213 or confess "Missing req";
215 unless ($order->paymentType eq PAYMENT_PAYPAL) {
216 $$rmsg = "This order was not paid by PayPal";
220 my $cfg = BSE::Cfg->single;
221 my %info = _do_refund_transaction($cfg, $rmsg, $order, scalar($req->user))
224 $order->set_paidFor(0);
227 BSE::TB::AuditLog->log
229 component => "shop:paypal:refund",
232 actor => scalar($req->user) || "U",
233 msg => "Refund PayPal payment on Order No. " . $order->id . ", transaction $info{REFUNDTRANSACTIONID}",
239 sub _do_refund_transaction {
240 my ($cfg, $rmsg, $order, $who) = @_;
245 TRANSACTIONID => $order->paypal_tran_id,
246 REFUNDTYPE => "Full",
249 my %info = _api_req($cfg, $rmsg, $order, $who, "RefundTransaction", \%params)
258 return join("&", map { "$_=".escape_uri($param->{$_}) } sort keys %$param);
262 my ($base, $param) = @_;
264 my $sep = $base =~ /\?/ ? "&" : "?";
266 return $base . $sep . _make_qparam($param);
270 my ($cfg, $action, @params) = @_;
272 return $cfg->user_url("shop", $action, @params);
275 sub _populate_from_order {
276 my ($params, $order, $cfg) = @_;
278 $params->{AMT} = _order_amt($order);
279 $params->{CURRENCYCODE} = _order_currency($order);
283 for my $item ($order->items) {
284 $params->{"L_NAME$index"} = $item->title;
285 $params->{"L_AMT$index"} = _format_amt($item->price);
286 $params->{"L_QTY$index"} = $item->units;
287 $params->{"L_NUMBER$index"} = $item->product_code
288 if $item->product_code;
289 $item_total += $item->units * $item->price;
292 $params->{ITEMAMT} = _format_amt($item_total);
293 $params->{SHIPPINGAMT} = _format_amt($order->shipping_cost)
294 if $order->shipping_cost;
296 # use our shipping information
297 my $country_code = $order->deliv_country_code;
298 if ($country_code && $cfg->entry("paypal", "shipping", 1)) {
299 $params->{SHIPTONAME} = $order->delivFirstName . " " . $order->delivLastName;
300 $params->{SHIPTOSTREET} = $order->delivStreet;
301 $params->{SHIPTOSTREET2} = $order->delivStreet2;
302 $params->{SHIPTOCITY} = $order->delivSuburb;
303 $params->{SHIPTOSTATE} = $order->delivState;
304 $params->{SHIPTOZIP} = $order->delivPostCode;
305 $params->{SHIPTOCOUNTRYCODE} = $country_code;
306 $params->{ADDROVERRIDE} = 1;
309 $params->{NOSHIPPING} = 1;
313 sub _set_express_checkout {
314 my ($cfg, $order, $who, $rmsg) = @_;
318 $cfg->entriesCS("paypal custom"),
320 RETURNURL => _shop_url($cfg, "paypalret", order => $order->randomId),
321 CANCELURL => _shop_url($cfg, "paypalcan", order => $order->randomId),
322 PAYMENTACTION => "Sale",
325 _populate_from_order(\%params, $order, $cfg);
327 my %info = _api_req($cfg, $rmsg, $order, $who,"SetExpressCheckout",
331 unless ($info{TOKEN}) {
332 $$rmsg = "No token returned by PayPal";
339 sub _get_express_checkout_details {
340 my ($cfg, $order, $who, $rmsg, $token) = @_;
348 my %info = _api_req($cfg, $rmsg, $order, $who, "GetExpressCheckoutDetails",
355 sub _do_express_checkout_payment {
356 my ($cfg, $rmsg, $order, $who, $token, $payerid, $info) = @_;
361 PAYMENTACTION => "Sale",
366 _populate_from_order(\%params, $order, $cfg);
368 my %info = _api_req($cfg, $rmsg, $order, $who || "U", "DoExpressCheckoutPayment",
375 # Low level API request
377 my ($cfg, $rmsg, $order, $who, $method, $param, $info) = @_;
381 require LWP::UserAgent;
382 my $ua = LWP::UserAgent->new;
383 $param->{METHOD} = $method;
384 $param->{USER} = _api_username($cfg);
385 $param->{PWD} = _api_password($cfg);
386 $param->{SIGNATURE} = _api_signature($cfg);
388 my $post = _make_qparam($param);
390 my $req = HTTP::Request->new(POST => _base_ws_url($cfg));
391 $req->content($post);
393 my $result = $ua->request($req);
395 require BSE::TB::AuditLog;
396 BSE::TB::AuditLog->log
398 component => "shop:paypal",
403 msg => "PayPal $method request",
404 dump => "Request:<<\n" . $req->as_string . "\n>>\n\nResult:<<\n" . $result->as_string . "\n>>",
408 for my $entry (split /&/, $result->decoded_content) {
409 my ($key, $value) = split /=/, $entry, 2;
410 $info{$key} = unescape_uri($value);
413 %$info = %info if $info;
414 unless ($info{ACK} =~ /^Success/) {
415 BSE::TB::AuditLog->log
417 component => "shop:paypal",
422 msg => "PayPal $method failure",
423 dump => $result->as_string,
425 $$rmsg = $info{L_LONGMESSAGE0};