package Business::PayPal::API::RecurringPayments;

use 5.008001;
use strict;
use warnings;

use SOAP::Lite 0.67;
use Business::PayPal::API ();

our @ISA = qw(Business::PayPal::API);
our $VERSION = '0.02';
our $CVS_VERSION = '$Id: RecurringPayments.pm,v 1.2 2009/07/28 18:00:59 scott Exp $';
our @EXPORT_OK = qw( SetCustomerBillingAgreement
                     GetBillingAgreementCustomerDetails
                     CreateRecurringPaymentsProfile
		     DoReferenceTransaction);

our $API_VERSION = '50.0';

sub SetCustomerBillingAgreement {
    my $self = shift;
    my %args = @_;

    ## billing agreement details type
    my %badtypes = ( BillingType                 => '', # 'ns:BillingCodeType',
                     BillingAgreementDescription => 'xs:string',
                     PaymentType                 => '', # 'ns:MerchantPullPaymentCodeType',
                     BillingAgreementCustom      => 'xs:string', );

    my %types = (  BillingAgreementDetails     => 'ns:BillingAgreementDetailsType',
                  ReturnURL                   => 'xs:string',
                  CancelURL                   => 'xs:string',
                  LocaleCode                  => 'xs:string',
                  PageStyle                   => 'xs:string',
                  'cpp-header-image'          => 'xs:string',
                  'cpp-header-border-color'   => 'xs:string',
                  'cpp-header-back-color'     => 'xs:string',
                  'cpp-payflow-color'         => 'xs:string',
                  PaymentAction               => '',
                  BuyerEmail                  => 'ns:EmailAddressType', );

    ## set defaults
    $args{BillingType}                  ||= 'RecurringPayments';
    $args{PaymentType}                  ||= 'InstantOnly';
    $args{currencyID}                   ||= 'USD';

    my @btypes = ();
    for my $field ( keys %badtypes ) {
        next unless $args{$field};
        push @btypes, SOAP::Data->name( $field => $args{$field} )->type( $badtypes{$field} );
    }

    my @scba = ();
    for my $field ( keys %types ) {
        next unless $args{$field};
        push @scba, SOAP::Data->name( $field => $args{$field} )->type( $types{$field} );
    }
    push @scba, SOAP::Data->name( BillingAgreementDetails => \SOAP::Data->value(@btypes) );

    my $request = SOAP::Data
      ->name( SetCustomerBillingAgreementRequest => \SOAP::Data->value
              ( $self->version_req, #$API_VERSION,
                SOAP::Data->name( SetCustomerBillingAgreementRequestDetails => \SOAP::Data->value(@scba)
                                )->attr( {xmlns => $self->C_xmlns_ebay} ),
              )
            )->type( 'ns:SetCustomerBillingAgreementRequestDetailsType' );

    my $som = $self->doCall( SetCustomerBillingAgreementReq => $request )
      or return;

    my $path = '/Envelope/Body/SetCustomerBillingAgreementResponse';

    my %response = ();
    unless( $self->getBasic($som, $path, \%response) ) {
        $self->getErrors($som, $path, \%response);
        return %response;
    }

    $self->getFields($som, $path, \%response, { Token => 'Token' });

    return %response;
}

sub GetBillingAgreementCustomerDetails {
    my $self  = shift;
    my $token = shift;

    my $request = SOAP::Data
      ->name( GetBillingAgreementCustomerDetailsRequest => \SOAP::Data->value
              ( $self->version_req,
                SOAP::Data->name( Token => $token )
                ->type('xs:string')->attr( {xmlns => $self->C_xmlns_ebay} ),
              )
            )->type( 'ns:GetBillingAgreementCustomerDetailsResponseType' );

    my $som = $self->doCall( GetBillingAgreementCustomerDetailsReq => $request )
      or return;

    my $path = '/Envelope/Body/GetBillingAgreementCustomerDetailsResponse';

    my %details = ();
    unless( $self->getBasic($som, $path, \%details) ) {
        $self->getErrors($som, $path, \%details);
        return %details;
    }

    $self->getFields($som,
                     "$path/GetBillingAgreementCustomerDetailsResponseDetails",
                     \%details,
                     { Token           => 'Token',

                       Payer           => 'PayerInfo/Payer',
                       PayerID         => 'PayerInfo/PayerID',
                       PayerStatus     => 'PayerInfo/PayerStatus',            ## 'unverified'
                       PayerBusiness   => 'PayerInfo/PayerBusiness',

                       Name            => 'PayerInfo/Address/Name',
                       AddressOwner    => 'PayerInfo/Address/AddressOwner',   ## 'PayPal'
                       AddressStatus   => 'PayerInfo/Address/AddressStatus',  ## 'none'
                       Street1         => 'PayerInfo/Address/Street1',
                       Street2         => 'PayerInfo/Address/Street2',
                       StateOrProvince => 'PayerInfo/Address/StateOrProvince',
                       PostalCode      => 'PayerInfo/Address/PostalCode',
                       CountryName     => 'PayerInfo/Address/CountryName',

                       Salutation      => 'PayerInfo/PayerName/Salutation',
                       FirstName       => 'PayerInfo/PayerName/FirstName',
                       MiddleName      => 'PayerInfo/PayerName/MiddleName',
                       LastName        => 'PayerInfo/PayerName/LastName',
                       Suffix          => 'PayerInfo/PayerName/Suffix',
                     } );

    return %details;
}

sub CreateRecurringPaymentsProfile {
    my $self = shift;
    my %args = @_;

    ## RecurringPaymentProfileDetails
    my %profiledetailstype = ( SubscriberName   => 'xs:string',
                               SubscriberShipperAddress => 'ns:AddressType',
                               BillingStartDate => 'xs:dateTime',  ## MM-DD-YY
                               ProfileReference => 'xs:string', );

    ## ScheduleDetailsType
    my %schedtype = ( Description       => 'xs:string',
                      ActivationDetails => 'ns:ActivationDetailsType',
                      TrialPeriod       => 'ns:BillingPeriodDetailsType',
                      PaymentPeriod     => 'ns:BillingPeriodDetailsType',
                      MaxFailedPayments => 'xs:int',
                      AutoBillOutstandingAmount => 'ns:AutoBillType', );  ## NoAutoBill or AddToNextBilling

    ## activation details
    my %activationdetailstype = ( InitialAmount             => 'cc:BasicAmountType',
                                  FailedInitialAmountAction => 'ns:FailedPaymentAction', );  ## ContinueOnFailure or CancelOnFailure

    ## BillingPeriodDetailsType
    my %trialbilltype = ( TrialBillingPeriod      => 'xs:string', ##'ns:BillingPeriodType',
                          TrialBillingFrequency   => 'xs:int',
                          TrialTotalBillingCycles => 'xs:int',
                          TrialAmount             => 'cc:AmountType',
                          TrialShippingAmount     => 'cc:AmountType',
                          TrialTaxAmount          => 'cc:AmountType', );

    my %paymentbilltype = ( PaymentBillingPeriod      => 'xs:string', ##'ns:BillingPeriodType',
                            PaymentBillingFrequency   => 'xs:int',
                            PaymentTotalBillingCycles => 'xs:int',
                            PaymentAmount             => 'cc:AmountType',
                            PaymentShippingAmount     => 'cc:AmountType',
                            PaymentTaxAmount          => 'cc:AmountType', );

    ## AddressType
    my %payaddrtype = ( CCPayerName            => 'xs:string',
                        CCPayerStreet1         => 'xs:string',
                        CCPayerStreet2         => 'xs:string',
                        CCPayerCityName        => 'xs:string',
                        CCPayerStateOrProvince => 'xs:string',
                        CCPayerCountry         => 'xs:string',  ## ebl:CountryCodeType
                        CCPayerPostalCode      => 'xs:string',
                        CCPayerPhone           => 'xs:string', );

    my %shipaddrtype = ( SubscriberShipperName            => 'xs:string',
                         SubscriberShipperStreet1         => 'xs:string',
                         SubscriberShipperStreet2         => 'xs:string',
                         SubscriberShipperCityName        => 'xs:string',
                         SubscriberShipperStateOrProvince => 'xs:string',
                         SubscriberShipperCountry         => 'xs:string',  ## ebl:CountryCodeType
                         SubscriberShipperPostalCode      => 'xs:string',
                         SubscriberShipperPhone           => 'xs:string', );

    ## credit card payer
    my %payerinfotype = ( CCPayer           => 'ns:EmailAddressType',
                          CCPayerID         => 'ebl:UserIDType',
                          CCPayerStatus     => 'xs:string',
                          CCPayerName       => 'xs:string',
                          CCPayerCountry    => 'xs:string',
                          CCPayerPhone      => 'xs:string',
                          CCPayerBusiness   => 'xs:string',
                          CCAddress         => 'xs:string',
                         );

    ## credit card details
    my %creditcarddetailstype = ( CardOwner        => 'ns:PayerInfoType',
                                  CreditCardType   => 'ebl:CreditCardType',  ## Visa, MasterCard, Discover, Amex, Switch, Solo
                                  CreditCardNumber => 'xs:string',
                                  ExpMonth         => 'xs:int',
                                  ExpYear          => 'xs:int',
                                  CVV2             => 'xs:string',
                                  StartMonth       => 'xs:string',
                                  StartYear        => 'xs:string',
                                  IssueNumber      => 'xs:int', );

    ## this gets pushed onto scheduledetails
    my @activationdetailstype = ();
    for my $field ( keys %activationdetailstype ) {
        next unless exists $args{$field};
        my $real_field = $field;
        push @activationdetailstype, SOAP::Data->name( $real_field => $args{$field} )->type( $activationdetailstype{$field} );
    }

    ## this gets pushed onto scheduledetails
    my @trialbilltype = ();
    for my $field ( keys %trialbilltype ) {
        next unless exists $args{$field};
        (my $real_field = $field) =~ s/^Trial//;
        push @trialbilltype, SOAP::Data->name( $real_field => $args{$field} )->type( $trialbilltype{$field} );
    }

    ## this gets pushed onto scheduledetails
    my @paymentbilltype = ();
    for my $field ( keys %paymentbilltype ) {
        next unless exists $args{$field};
        (my $real_field = $field) =~ s/^Payment//;
        push @paymentbilltype, SOAP::Data->name( $real_field => $args{$field} )->type( $paymentbilltype{$field} );
    }

    ## this gets pushed onto the top
    my @sched = ();
    for my $field ( keys %schedtype ) {
        next unless exists $args{$field};
        push @sched, SOAP::Data->name( $field => $args{$field} )->type( $schedtype{$field} );
    }
    push @sched, SOAP::Data->name( TrialPeriod => \SOAP::Data->value(@trialbilltype) ); #->type( 'ns:BillingPeriodDetailsType' );
    push @sched, SOAP::Data->name( PaymentPeriod => \SOAP::Data->value(@paymentbilltype) ); #->type( 'ns:BillingPeriodDetailsType' );

    ## this gets pushed into profile details
    my @shipaddr = ();
    for my $field ( keys %shipaddrtype ) {
        next unless exists $args{$field};
        (my $real_field = $field) =~ s/^SubscriberShipper//;
        push @shipaddr, SOAP::Data->name( $real_field => $args{$field} )->type( $shipaddrtype{$field} );
    }

    ## this gets pushed into payerinfo (from creditcarddetails)
    my @payeraddr = ();
    for my $field ( keys %payaddrtype ) {
        next unless $args{$field};
        (my $real_field = $field) =~ s/^CCPayer//;
        push @payeraddr, SOAP::Data->name( $real_field => $args{$field} )->type( payaddrtype{$field} );
    }

    ## credit card type
    my @creditcarddetails = ();
    for my $field ( keys %creditcarddetailstype ) {
        next unless $args{$field};
        (my $real_field = $field) =~ s/^CC//;
        push @payeraddr, SOAP::Data->name( $real_field => $args{$field} )->type( payaddrtype{$field} );
    }

    ## this gets pushed onto the top
    my @profdetail = ();
    for my $field ( keys %profiledetailstype ) {
        next unless exists $args{$field};
        push @profdetail, SOAP::Data->name( $field => $args{$field} )->type( $profiledetailstype{$field} );
    }
    push @profdetail, SOAP::Data->name( SubscriberShipperAddress => \SOAP::Data->value(@shipaddr) );

    ## crappard?
    my @crpprd = ();
    push @crpprd, SOAP::Data->name( Token => $args{Token} );
    push @crpprd, SOAP::Data->name( CreditCardDetails => \SOAP::Data->value(@creditcarddetails) ); #->type( 'ns:CreditCardDetailsType' );
    push @crpprd, SOAP::Data->name( RecurringPaymentProfileDetails => \SOAP::Data->value(@profdetail) ); #->type( 'ns:RecurringPaymentProfileDetailsType' );
    push @crpprd, SOAP::Data->name( ScheduleDetails => \SOAP::Data->value(@sched) ); #->type( 'ns:ScheduleDetailsType' );

    my $request = SOAP::Data->name
      ( CreateRecurringPaymentsProfileRequest => \SOAP::Data->value
#        ( $API_VERSION,
        ( $self->version_req,
          SOAP::Data->name( CreateRecurringPaymentsProfileRequestDetails => \SOAP::Data->value(@crpprd) 
                          )->attr( {xmlns => $self->C_xmlns_ebay} )
        )
      ); #->type( 'ns:CreateRecurringPaymentsProfileRequestType' );

    my $som = $self->doCall( CreateRecurringPaymentsProfileReq => $request )
      or return;

    my $path = '/Envelope/Body/CreateRecurringPaymentsProfileResponse';

    my %response = ();
    unless( $self->getBasic($som, $path, \%response) ) {
        $self->getErrors($som, $path, \%response);
        return %response;
    }

    $self->getFields($som, $path, \%response, { Token => 'Token' });

    return %response;
}

sub DoReferenceTransaction {
    my $self = shift;
    my %args = @_;

    my %types = ( ReferenceID               => 'xs:string',
		  PaymentAction             => '',                 ## NOTA BENE!
		  currencyID                => '',
		  );

    ## PaymentDetails
    my %pd_types = ( OrderTotal             => 'ebl:BasicAmountType',
		     OrderDescription       => 'xs:string',
		     ItemTotal              => 'ebl:BasicAmountType',
		     ShippingTotal          => 'ebl:BasicAmountType',
		     HandlingTotal          => 'ebl:BasicAmountType',
		     TaxTotal               => 'ebl:BasicAmountType',
		     Custom                 => 'xs:string',
		     InvoiceID              => 'xs:string',
		     ButtonSource           => 'xs:string',
		     NotifyURL              => 'xs:string',
		     );

    ## ShipToAddress
    my %st_types = ( ST_Name                   => 'xs:string',
		     ST_Street1                => 'xs:string',
		     ST_Street2                => 'xs:string',
		     ST_CityName               => 'xs:string',
		     ST_StateOrProvince        => 'xs:string',
		     ST_Country                => 'xs:string',
		     ST_PostalCode             => 'xs:string',
		     ST_Phone                  => 'xs:string',
		     );

    ##PaymentDetailsItem
    my %pdi_types = ( PDI_Name                 => 'xs:string',
		      PDI_Description          => 'xs:string',
		      PDI_Amount               => 'ebl:BasicAmountType',
		      PDI_Number               => 'xs:string',
		      PDI_Quantity             => 'xs:string',
		      PDI_Tax                  => 'ebl:BasicAmountType',
		      );

    $args{PaymentAction} ||= 'Sale';
    $args{currencyID}    ||= 'USD';

    my @payment_details = ( );

    ## push OrderTotal here and delete it (i.e., and all others that have special attrs)
    push @payment_details, SOAP::Data->name( OrderTotal => $args{OrderTotal} )
	->type( $pd_types{OrderTotal} )
	->attr( { currencyID => $args{currencyID},
		  xmlns      => $self->C_xmlns_ebay } );

    ## don't process it again
    delete $pd_types{OrderTotal};

    for my $field ( keys %pd_types ) {
	if( $args{$field} ) {
	  push @payment_details, 
	    SOAP::Data->name( $field => $args{$field} )
		->type( $pd_types{$field} );
	}
    }

    ##
    ## ShipToAddress
    ##
    my @ship_types = ();
    for my $field ( keys %st_types ) {
	if( $args{$field} ) {
	  (my $name = $field) =~ s/^ST_//;
	  push @ship_types,
	    SOAP::Data->name( $name => $args{$field} )
		->type( $st_types{$field} );
	}
    }

    if( scalar @ship_types ) {
	push @payment_details,
	SOAP::Data->name( ShipToAddress => \SOAP::Data->value
			  ( @ship_types )->type('ebl:AddressType')
			  ->attr( {xmlns => $self->C_xmlns_ebay} ),
			  );
    }

    ##
    ## PaymentDetailsItem
    ##
    my @payment_details_item = ();
    for my $field ( keys %pdi_types ) {
	if( $args{$field} ) {
	  (my $name = $field) =~ s/^PDI_//;
	  push @payment_details_item,
	    SOAP::Data->name( $name => $args{$field} )
		->type( $pdi_types{$field} );
	}
    }

    if( scalar @payment_details_item ) {
	push @payment_details,
	SOAP::Data->name( PaymentDetailsItem => \SOAP::Data->value
			  ( @payment_details_item )->type('ebl:PaymentDetailsItemType')
			  ->attr( {xmlns => $self->C_xmlns_ebay} ),
			  );
    }

    ##
    ## ReferenceTransactionPaymentDetails
    ##
    my @reference_details = (
		 SOAP::Data->name( ReferenceID => $args{ReferenceID} )
		 ->type($types{ReferenceID})->attr( {xmlns => $self->C_xmlns_ebay} ),
		 SOAP::Data->name( PaymentAction => $args{PaymentAction} )
		 ->type($types{PaymentAction})->attr( {xmlns => $self->C_xmlns_ebay} ),
		 SOAP::Data->name( PaymentDetails => \SOAP::Data->value
				   ( @payment_details )->type('ebl:PaymentDetailsType')
				   ->attr( {xmlns => $self->C_xmlns_ebay} ),
				   ), );

    ##
    ## the main request object
    ##
    my $request = SOAP::Data
      ->name( DoReferenceTransactionRequest => \SOAP::Data->value
	      ( $self->version_req,
		SOAP::Data->name( DoReferenceTransactionRequestDetails => \SOAP::Data->value
				  ( @reference_details )->type( 'ns:DoReferenceTransactionRequestDetailsType' )
				)->attr( {xmlns => $self->C_xmlns_ebay} ),
	      )
	    );

    my $som = $self->doCall( DoReferenceTransactionReq => $request )
      or return;

    my $path = '/Envelope/Body/DoReferenceTransactionResponse';

    my %response = ();
    unless( $self->getBasic($som, $path, \%response) ) {
        $self->getErrors($som, $path, \%response);
        return %response;
    }

    $self->getFields( $som,
                      "$path/DoReferenceTransactionResponseDetails",
                      \%response,
                      { BillingAgreementID  => 'BillingAgreementID',
                        TransactionID       => 'PaymentInfo/TransactionID',
                        TransactionType     => 'PaymentInfo/TransactionType',
                        PaymentType         => 'PaymentInfo/PaymentType',
                        PaymentDate         => 'PaymentInfo/PaymentDate',
                        GrossAmount         => 'PaymentInfo/GrossAmount',
                        FeeAmount           => 'PaymentInfo/FeeAmount',
                        SettleAmount        => 'PaymentInfo/SettleAmount',
                        TaxAmount           => 'PaymentInfo/TaxAmount',
                        ExchangeRate        => 'PaymentInfo/ExchangeRate',
                        PaymentStatus       => 'PaymentInfo/PaymentStatus',
                        PendingReason       => 'PaymentInfo/PendingReason',
			ReasonCode          => 'PaymentInfor/ReasonCode',
                      } );

    return %response;
}

1;
__END__

=head1 NAME

Business::PayPal::API::RecurringPayments - PayPal RecurringPayments API

=head1 SYNOPSIS

use Business::PayPal::API::RecurringPayments;

my $pp = new Business::PayPal::API::RecurringPayments( ... );

my %resp = $pp->FIXME

  ## Ask PayPal to charge a new transaction from the ReferenceID
  ## This method is used both for Recurring Transactions as well 
  ## as for Express Checkout's MerchantInitiatedBilling, where 
  ## ReferenceID is the BillingAgreementID returned from 
  ## ExpressCheckout->DoExpressCheckoutPayment

  my %payinfo = $pp->DoReferenceTransaction( ReferenceID => $details{ReferenceID},
                                               PaymentAction => 'Sale',
                                               OrderTotal => '55.43' );


=head1 DESCRIPTION

THIS MODULE IS NOT COMPLETE YET. PLEASE DO NOT REPORT ANY BUGS RELATED
TO IT.

=head2 DoReferenceTransaction

Implements PayPal's WPP B<DoReferenceTransaction> API call. Supported
parameters include:

  ReferenceID (aka BillingAgreementID)
  PaymentAction (defaults to 'Sale' if not supplied)
  currencyID (defaults to 'USD' if not supplied)

  OrderTotal
  OrderDescription
  ItemTotal
  ShippingTotal
  HandlingTotal
  TaxTotal
  Custom
  InvoiceID
  ButtonSource
  NotifyURL

  ST_Name
  ST_Street1
  ST_Street2
  ST_CityName
  ST_StateOrProvince
  ST_Country
  ST_PostalCode
  ST_Phone

  PDI_Name
  PDI_Description
  PDI_Amount
  PDI_Number
  PDI_Quantity
  PDI_Tax

as described in the PayPal "Web Services API Reference" document.

Returns a hash with the following keys:

  BillingAgreementID
  TransactionID
  TransactionType
  PaymentType
  PaymentDate
  GrossAmount
  FeeAmount
  SettleAmount
  TaxAmount
  ExchangeRate
  PaymentStatus
  PendingReason
  ReasonCode

Required fields:

  ReferenceID, OrderTotal

=head1 SEE ALSO

L<https://developer.paypal.com/en_US/pdf/PP_APIReference.pdf>

=head1 AUTHOR

Scot Wiersdorf E<lt>scott@perlcode.orgE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2007 by Scott Wiersdorf

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.8.5 or,
at your option, any later version of Perl 5 you may have available.

=cut
