1package Google::Checkout::General::GCO; 2 3=head1 NAME 4 5Google::Checkout::General::GCO 6 7=head1 VERSION 8 9Version 1.1.1 10 11=cut 12 13=head1 SYNOPSIS 14 15 use Google::Checkout::General::GCO; 16 use Google::Checkout::General::MerchantItem; 17 use Google::Checkout::Command::CancelOrder; 18 use Google::Checkout::General::Util qw/is_gco_error/; 19 20 my $gco = Google::Checkout::General::GCO->new( 21 config_path => 'conf/GCOSystemGlobal.conf'); 22 23 #-- 24 #-- Or you can pass in the merchant id, key and Checkout URL like this 25 #-- 26 $gco = Google::Checkout::General::GCO->new( 27 merchant_id => 1234, 28 merchant_key => 'abcd', 29 gco_server => 'https://sandbox.google.com/...'); 30 31 my $cart = Google::Checkout::General::ShoppingCart->new( 32 expiration => "+1 month", 33 private => "Merchant private data", 34 checkout_flow => $checkout_flow); 35 36 my $item1 = Google::Checkout::General::MerchantItem->new( 37 name => "Fish", 38 description => "A fish", 39 price => 12.34, 40 quantity => 12, 41 private => "gold"); 42 43 $cart->add_item($item1); 44 45 #-- 46 #-- Checkout a cart 47 #-- 48 my $response = $gco->checkout($cart); 49 or 50 my ($response,$requestXML) = $gco->checkout_with_xml($cart); 51 52 die $response if is_gco_error $response; 53 54 #-- 55 #-- print the redirect URL 56 #-- 57 print $response,"\n"; 58 59 #-- 60 #-- Send a cancel order command 61 #-- 62 my $cancel = Google::Checkout::Command::CancelOrder->new( 63 order_number => 156310171628413, 64 amount => 5, 65 reason => "Cancel order"); 66 67 $response = $gco->command($cancel); 68 69 die $response if is_gco_error $response; 70 71 print $response,"\n"; 72 73=head1 DESCRIPTION 74 75This is the main module for interacting with the Google 76Checkout system. It allows a user to checkout, send 77various commands and process notifications. 78 79=over 4 80 81=item new CONFIG_PATH => ..., MERCHANT_ID => ..., MERCHANT_KEY => ..., GCO_SERVER => ... 82 83Constructor. Loads the configuration file from CONFIG_PATH. If no configuration 84file is specified, merchant id, key and Checkout server URL must be specified. 85 86=item reader 87 88Returns the configuration reader used to parse 89and load the configuration file. 90 91=item get_checkout_url 92 93Returns the Google Checkout URL defined in the 94configuration file. 95 96=item get_checkout_diagnose_url 97 98Returns the diagnose Google Checkout URL 99defined in the configuration file. 100 101=item get_request_url 102 103Returns the URL where requests will be sent to. 104 105=item get_request_diagnose_url 106 107Same as C<get_request_url> except this function 108returns the diagnose version of it. 109 110=item b64_signature XML_CART 111 112Given a shopping cart (in XML), returns the 113HMAC-SHA1 / Base64 signature of it. 114 115=item b64_xml_cart XML_CART 116 117Given a shopping cart (in XML), encode and 118return it in Base64. 119 120=item get_xml_and_signature CART 121 122Given a C<Google::Checkout::General::ShoppingCart> object CART, return the 123Base64 encoding signature and XML cart. The return 124value is a hash reference where 'xml' is the XML 125cart (Base64 encoded) and 'signature' is the Base64 126encoding signature. 127 128=item checkout CART, DIAGNOSE 129 130Sends the shopping cart (C<Google::Checkout::General::ShoppingCart> object) to 131Google Checkout. If DIAGNOSE is true, the cart will be sent 132as a diagnose request. 133 134=item checkout_with_xml CART, DIAGNOSE 135 136Sends the shopping cart (C<Google::Checkout::General::ShoppingCart> object) to 137Google Checkout. If DIAGNOSE is true, the cart will be sent 138as a diagnose request. This method returns both the result and the xml request that was sent to Google Checkout. 139 140=item raw_checkout XML, DIAGNOSE 141 142Treat XML as a shopping cart and attempt to checkout it. If 143DIAGNOSE is true, the XML will be sent as a diagnose request. 144This method is actually used by C<checkout>. 145 146=item command COMMAND, DIAGNOSE 147 148Sends a command to Google Checkout. COMMAND should be one of 149C<Google::Checkout::Command::GCOCommand>'s sub-class. If DIAGNOSE 150is true, the command will be sent as a diagnose request. 151 152=item send_notification_response 153 154After you receive a notification, you are expected to send 155back a response knowledging the notification is properly handled. 156This method can be used to ensure a valid response is send back 157to Google Checkout. Since we are communicating over HTTP, this 158function will return a 200 header first. 159 160=item send_merchant_calculation CALCULATIONS 161 162This function is similar to C<send_notification_response> except 163it's used to send back a response after a merchant calculation 164callback. CALCULATIONS should be an array reference of 165C<Google::Checkout::General::MerchantCalculationResult>. 166 167=item send HASH 168 169A generic function to send request to Google Checkout. Please note 170that it's not recommanded that you use this function directly. C<checkout>, 171C<command>, C<send_notification_response>, etc should be all you need 172to interact with the Google Checkout system. 173 174=back 175 176=cut 177 178=head1 COPYRIGHT 179 180Copyright 2006 Google. All rights reserved. 181 182=cut 183 184#-- 185#-- Class to interact with the GCO system 186#-- 187 188use strict; 189use warnings; 190 191use CGI; 192use Google::Checkout::General::Error; 193use LWP 5.64; 194use XML::Simple; 195use Crypt::SSLeay; 196use Google::Checkout::General::ConfigReader; 197use HTTP::Request; 198use HTTP::Headers; 199use Google::Checkout::XML::Constants; 200use Google::Checkout::XML::CommandXmlWriter; 201use Google::Checkout::XML::CheckoutXmlWriter; 202use Google::Checkout::General::MerchantCalculationResults; 203use Google::Checkout::XML::NotificationResponseXmlWriter; 204use Google::Checkout::General::Util qw/is_gco_error compute_hmac_sha1 compute_base64/; 205 206#-- 207#-- Version. This is the version number of the whole sample 208#-- code. Thus, everytime we make a bug fix or release a new 209#-- version of any code, this number if increased. 210#-- 211#-- NOTE: Please do NOT change this number! This is in fact 212#-- a special variable that Perl tracks. It allows us 213#-- to ask for a specify version of the sample code. 214#-- For example, if we add a feature to the sample code 215#-- which is only available to the newest version of GCO, 216#-- the user can say "use GCO 2.0;' which will reject this 217#-- version of the library. 218#-- 219our $VERSION = "1.1.1"; 220 221sub new 222{ 223 my ($class, %args) = @_; 224 225 my $self = {_reader => undef}; 226 227 if ($args{config_path}) { 228 229 #-- 230 #-- have a configuration? if so, use it 231 #-- 232 $self->{_reader} = Google::Checkout::General::ConfigReader->new( 233 {config_path => $args{config_path}}); 234 235 } elsif ($args{merchant_id} && $args{merchant_key} && $args{gco_server}) { 236 237 #-- 238 #-- config is passed in 239 #-- 240 $self->{__merchant_id} = $args{merchant_id}; 241 $self->{__merchant_key} = $args{merchant_key}; 242 $self->{__base_gco_server} = $args{gco_server}; 243 244 #-- 245 #-- if user supply the following, use them. otherwise, use default 246 #-- 247 $self->{__xml_schema} = $args{xml_schema} || 'http://checkout.google.com/schema/2'; 248 $self->{__currency_supported} = $args{currency_supported} || 'USD'; 249 $self->{__xml_version} = $args{xml_version} || '1.0'; 250 $self->{__xml_encoding} = $args{xml_encoding} || 'UTF-8'; 251 252 } else { 253 254 #-- 255 #-- try a default configuration 256 #-- 257 258 $self->{_reader} = Google::Checkout::General::ConfigReader->new; 259 } 260 261 return bless $self => $class; 262} 263 264sub reader 265{ 266 my ($self) = @_; 267 268 return $self->{_reader}; 269} 270 271sub get_checkout_url 272{ 273 my ($self) = @_; 274 275 return $self->_get_url('merchantCheckout'); 276 277 #-- 278 #-- TODO: the following will go away on July 2007 279 #-- 280 return $self->_get_url("checkout"); 281} 282 283sub get_checkout_diagnose_url 284{ 285 my ($self) = @_; 286 287 return $self->_get_url('merchantCheckout'); 288 289 #-- 290 #-- TODO: the following will go away on July 2007 291 #-- 292 return $self->_get_url("checkout", 1); 293} 294 295sub get_request_url 296{ 297 my ($self) = @_; 298 299 return $self->_get_url("request"); 300} 301 302sub get_request_diagnose_url 303{ 304 my ($self) = @_; 305 306 return $self->_get_url("request", 1); 307} 308 309#-- 310#-- Return the HMAC-SHA1 / Base64 signature 311#-- 312sub b64_signature 313{ 314 my ($self, $cart) = @_; #-- $cart = Shopping cart in XML 315 316 my $id = ''; 317 if ($self->reader()) { 318 $id = $self->reader()->get(Google::Checkout::XML::Constants::MERCHANT_KEY); 319 } else { 320 $id = $self->{__merchant_key} || Google::Checkout::General::Error(-1, "Missing merchant key"); 321 } 322 323 return is_gco_error($id) ? $id : compute_hmac_sha1($id, $cart, 1); 324} 325 326#-- 327#-- Return Base64 cart XML 328#-- 329sub b64_xml_cart 330{ 331 my ($self, $cart) = @_; #-- $cart = Shopping cart in XML 332 333 return compute_base64($cart); 334} 335 336#-- 337#-- Return the cart XML as well as base64 encoded signature 338#-- 339sub get_xml_and_signature 340{ 341 my ($self, $cart) = @_; 342 343 my $xml = Google::Checkout::XML::CheckoutXmlWriter->new(gco => $self, cart => $cart)->done; 344 345 my $signature = $self->b64_signature($xml); 346 347 my $merchant_key = ''; 348 if ($self->reader()) { 349 $merchant_key = $self->reader()->get(Google::Checkout::XML::Constants::MERCHANT_KEY); 350 } else { 351 $merchant_key = $self->{__merchant_key}; 352 } 353 354 return {xml => compute_base64($xml), signature => $signature, 355 raw_xml => $xml, 356 raw_key => $merchant_key}; 357} 358 359#-- 360#-- Sends a shopping cart to GCO for checkout 361#-- 362sub checkout_with_xml 363{ 364 my ($self, $cart, $diagnose) = @_; 365 366 my $xml = Google::Checkout::XML::CheckoutXmlWriter->new(gco => $self, cart => $cart)->done; 367 368 return (($self->raw_checkout($xml, $diagnose)),$xml); 369} 370 371#-- 372#-- Same as above, but it doesn't return the XML for backwards compatibility 373#-- 374sub checkout 375{ 376 my ($self, $cart, $diagnose) = @_; 377 378 my ($result,$xml) = $self->checkout_with_xml($cart, $diagnose); 379 380 return $result 381} 382 383#-- 384#-- This is exactly the same as $gci->checkout except 385#-- that the user is expected to pass in a XML file. 386#-- The XML file will be passed to GCO directly 387#-- 388sub raw_checkout 389{ 390 my ($self, $xml, $diagnose) = @_; 391 392 my $url = $diagnose ? $self->get_checkout_diagnose_url : 393 $self->get_checkout_url(); 394 395 my $response = $self->send(url => $url, 396 cart => $xml); 397 398 return $response if is_gco_error($response); 399 400 return $diagnose ? 401 '' : #-- Normally GCO returns a 200 OK only 402 $self->_extract_redirect_url($response); 403} 404 405#-- 406#-- Sends a command to GCO 407#-- 408sub command 409{ 410 my ($self, $command, $diagnose) = @_; 411 412 my $url = $diagnose ? $self->get_request_diagnose_url : 413 $self->get_request_url(); 414 415 my $response = $self->send(url => $url, 416 cart => $command->to_xml(gco => $self)); 417 418 return $response; 419} 420 421#-- 422#-- Returns a 200 to GCO after receiving a notification. 423#-- The header will be text/xml and the body will always 424#-- be a valid notification response 425#-- 426sub send_notification_response 427{ 428 my ($self) = @_; 429 430 #-- 431 #-- Send back xml header 432 #-- 433 print CGI->header(-type => "text/xml", -charset => "utf-8"); 434 435 #-- 436 #-- Now send the response 437 #-- 438 print Google::Checkout::XML::NotificationResponseXmlWriter->new(gco => $self)->done; 439} 440 441#-- 442#-- Returns a merchant calculation resutls back to GCO 443#-- 444sub send_merchant_calculation 445{ 446 #-- 447 #-- $results = Array reference of MerchantCalculationResult 448 #-- 449 my ($self, $results) = @_; 450 451 #-- 452 #-- Send back xml header 453 #-- 454 print CGI->header(-type => "text/xml", -charset => "utf-8"); 455 456 print Google::Checkout::General::MerchantCalculationResults->new( 457 gco => $self, 458 merchant_calculation_result => $results)->done; 459} 460 461#-- 462#-- Send request to GCO using HTTP Basic Authentication. Note 463#-- that users do not have to use this function directly. It's 464#-- safer to use either the 'checkout' or the 'command' API instead 465#-- 466sub send 467{ 468 my ($self, %args) = @_; 469 470 my $id = ''; 471 my $key = ''; 472 473 if ($self->reader()) { 474 $id = $self->reader()->get(Google::Checkout::XML::Constants::MERCHANT_ID); 475 $key = $self->reader()->get(Google::Checkout::XML::Constants::MERCHANT_KEY); 476 } else { 477 $id = $self->{__merchant_id} || Google::Checkout::General::Error(-1, "Missing merchant ID"); 478 $key = $self->{__merchant_key} || Google::Checkout::General::Error(-1, "Missing merchant key"); 479 } 480 481 return $id if is_gco_error($id); 482 return $key if is_gco_error($key); 483 484 return Google::Checkout::General::Error->new( 485 @{$Google::Checkout::General::Error::ERRORS{INVALID_MERCHANT_ID}}) 486 unless $id; 487 488 return Google::Checkout::General::Error->new( 489 @{$Google::Checkout::General::Error::ERRORS{INVALID_MERCHANT_KEY}}) 490 unless $key; 491 492 #-- 493 #-- URL and shopping cart (in XML format) is required 494 #-- 495 return Google::Checkout::General::Error->new( 496 @{$Google::Checkout::General::Error::ERRORS{MISSING_URL}}) 497 unless $args{url}; 498 499 return Google::Checkout::General::Error->new( 500 @{$Google::Checkout::General::Error::ERRORS{MISSING_CART}}) 501 unless $args{cart}; 502 503 return $self->raw_send(signature => compute_base64("$id:$key"), 504 url => $args{url}, 505 cart => $args{cart}); 506} 507 508sub raw_send 509{ 510 my ($self, %args) = @_; 511 512 my $agent = LWP::UserAgent->new; 513 514 my $header = HTTP::Headers->new; 515 $header->header('Authorization' => "Basic " . $args{signature}); 516 $header->header('Content-Type' => "application/xml; charset=UTF-8"); 517 $header->header('Accept' => "application/xml"); 518 519 my $request = HTTP::Request->new(POST => $args{url}, $header, $args{cart}); 520 my $response = $agent->request($request); 521 522 unless ($response->is_success) { 523 return Google::Checkout::General::Error->new( 524 $response->code, 525 $response->status_line . $response->content); 526 } 527 528 return $response->content; 529} 530 531#-- PRIVATE --# 532 533#-- 534#-- Returns either the checkout or request URL 535#-- 536sub _get_url 537{ 538 my ($self, $type, $diagnose) = @_; 539 540 my $url = Google::Checkout::General::Error->new(-1, 'Missing URL'); 541 my $mid = Google::Checkout::General::Error->new(-1, 'Missing merchant ID'); 542 543 if ($self->reader()) { 544 $url = $self->reader()->get(Google::Checkout::XML::Constants::BASE_GCO_SERVER); 545 $mid = $self->reader()->get(Google::Checkout::XML::Constants::MERCHANT_ID); 546 } else { 547 $url = $self->{__base_gco_server} || Google::Checkout::General::Error->new(-1, 'Missing URL'); 548 $mid = $self->{__merchant_id} || Google::Checkout::General::Error->new(-1, 'Missing merchant ID'); 549 } 550 551 return $url if is_gco_error($url); 552 return $mid if is_gco_error($mid); 553 554 if ($self->reader()) { 555 $url =~ s#/+$##; 556 $url .= '/' . $mid . '/' . $type; 557 } 558 559 $url .= '/diagnose' if $diagnose; 560 561 return $url; 562} 563 564#-- 565#-- Extract the redirect URL after posting 566#-- to the GCO server of a checkout request. 567#-- Returns Google::Checkout::General::Error if the XML file isn't 568#-- not a valid "success" XML file 569#-- 570sub _extract_redirect_url 571{ 572 my ($self, $xml) = @_; 573 574 my $parser = XMLin($xml); 575 576 my $url = $parser->{Google::Checkout::XML::Constants::REDIRECT_URL}; 577 578 return $url if $url; 579 580 return Google::Checkout::General::Error->new( 581 $Google::Checkout::General::Error::ERRORS{INVALID_XML}->[0], 582 $Google::Checkout::General::Error::ERRORS{INVALID_XML}->[1] . ": $xml"); 583} 584 585sub _has_error 586{ 587 my ($self, $xml) = @_; 588 589 my $parser = XMLin($xml); 590 591 my $error = $parser->{Google::Checkout::XML::Constants::ERROR_MESSAGE}; 592 593 return $error ? 1 : 0; 594} 595 5961; 597