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