=head1 NAME

 Bing::ContentAPI - Perl interface to the Bing Ads Content API

=head1 DESCRIPTION

 Add, modify and delete products from the Bing Merchant Center platform via
 the Bing Ads Content API.

 https://docs.microsoft.com/bingads/shopping-content/

 Authentication is done via OAuth using Authorization Code Grant Flow
 https://docs.microsoft.com/bingads/guides/authentication-oauth

 Steps to authorize a native application and generate an initial refresh token:

 Note: Substitute CLIENT_ID, UNIQUE_CODE and AUTHORIZATION_CODE with the
 actual values in the appropriate locations in the examples.

 1) Request user consent through web browser:
 (using existing Bing Ads Microsoft account when prompted)

 https://login.live.com/oauth20_authorize.srf?client_id=CLIENT_ID&scope=bingads.manage&response_type=code&redirect_uri=https://login.live.com/oauth20_desktop.srf&state=UNIQUE_CODE

 Response received:
 https://login.live.com/oauth20_desktop.srf?code=AUTHORIZATION_CODE&state=UNIQUE_CODE&lc=1033

 2) Use the authorization 'code' received to request the access token and refresh token:

 $ curl \
   -d client_id=CLIENT_ID \
   -d code=AUTHORIZATION_CODE \
   -d grant_type=authorization_code \
   -d redirect_uri=https://login.live.com/oauth20_desktop.srf \
   -H "Content-Type: application/x-www-form-urlencoded" \
   https://login.live.com/oauth20_token.srf

 Response:
 {
   "token_type": "bearer",
   "expires_in": 3600,
   "scope": "bingads.manage",
   "access_token": "...",
   "refresh_token": "...",
   "user_id": "..."
 }

 3) Use the received refresh_token as the initial refresh_token in Bing::ContentAPI->new()

=head1 SYNOPSIS

 use Bing::ContentAPI;
 use Data::Dumper;

 my $bing = Bing::ContentAPI->new({
   debug => 0,
   redirect_uri    => 'https://login.live.com/oauth20_desktop.srf',
   merchant_id     => '12345',          # merchant_id is the BMC store ID
   developer_token => '123ABC456DEF789',
   client_id       => '1234abcd-5679-efgh-123456789',
   refresh_token   => load_token(),   # previously saved refresh token
 });
 save_token($bing->{refresh_token}); # save new refresh token

 sub load_token {
   my $token;
   # load token from storage
   return $token;
 }

 sub save_token {
   my $token = shift;
   # save token to storage
 }

 my ($request, $result, $products, $batch_id, $product_id);

 # list products

 my $nextPageToken = '';
 do {
   $request = {
     resource => 'products',
     method   => 'list',
     params   => ['max-results' => 250],
   };
   push @{$request->{params}}, ('start-token', "$nextPageToken") if $nextPageToken ne '';

   $result = $bing->get(%$request);
   $nextPageToken = $result->{response}->{nextPageToken} || '';

   print "$result->{code} ". ($result->{code} eq '200' ? 'success' : 'failure') ."\n";
   print "Products list: \n". Dumper $result;
 } while ($nextPageToken ne '');

 # list catalogs

 $result = $bing->get(
   resource => 'catalogs',
   method   => 'list',
 );
 print "$result->{code} ". ($result->{code} eq '200' ? 'success' : 'failure') ."\n";
 print "Catalogs list: \n". Dumper $result;

 # get status of product offers in a catalog

 my $catalogID = 123456;
 $result = $bing->get(
   resource => 'catalogs',
   method   => 'status',
   id       => $catalogID,
 );
 print "$result->{code} ". ($result->{code} eq '200' ? 'success' : 'failure') ."\n";
 print "Catalog status: \n". Dumper $result;

 # insert a product

 $result = $bing->post(
   resource => 'products',
   method   => 'insert',
   dryrun   => 1,
   body => {
     contentLanguage => 'en',
     targetCountry => 'US',
     channel => 'online',
     offerId => '333333',
     title => 'Item title',
     description => 'The item description',
     link => 'http://www.bing.com',
     imageLink => 'https://img-prod-cms-rt-microsoft-com.akamaized.net/cms/api/am/imageFileData/RE1Mu3b',
     availability => 'in stock',
     condition => 'new',
     price => {
         value => '99.95',
         currency => 'USD',
     },
     shipping => [
       {
         country => 'US',
         service => 'Standard Shipping',
         price => {
           value => '7.95',
           currency => 'USD',
         },
       },
     ],
     brand => 'Apple',
     gtin => '33333367890',
     mpn => '333333',
     googleProductCategory => 'Home & Garden > Household Supplies > Household Paper Products > Paper Towels',
     productType => 'Home & Garden > Household Supplies > Household Paper Products > Paper Towels',
     customLabel1 => 'Paper Towels'
   }
 );
 print "$result->{code} ". ($result->{code} eq '200' ? 'success' : 'failure') ."\n";

 # get single product info

 $product_id = '333333';
 $result = $bing->get(
   resource => 'products',
   method   => 'get',
   id       => 'online:en:US:'. $product_id
 );
 print "$result->{code} ". ($result->{code} eq '200' ? 'success' : 'failure') ."\n";
 print "Products info: \n". Dumper $result;

 # delete a product

 print "product delete: ";

 my $del_product_id = '333333';
 $result = $bing->delete(
   resource => 'products',
   method   => 'delete',
   id       => 'online:en:US:'. $del_product_id,
   dryrun   => 1,
 );
 print "$result->{code} ". ($result->{code} eq '204' ? 'success' : 'failure') ."\n"; # 204 = delete success
 print Dumper $result;

 # batch insert

 $products = [];
 $batch_id = 0;

 foreach my $i ('211203'..'211205') {
   push @$products, {
     batchId => ++$batch_id,
     merchantId => $bing->{merchant_id},
     method => 'insert', # insert / get / delete
     #productId => '', # for get / delete
     product => { # for insert
       contentLanguage => 'en',
       targetCountry => 'US',
       channel => 'online',
       offerId => "$i",
       title => "item title $i",
       description => "The item description for $i",
       link => 'http://www.bing.com',
       imageLink => 'https://img-prod-cms-rt-microsoft-com.akamaized.net/cms/api/am/imageFileData/RE1Mu3b',
       availability => 'in stock',
       condition => 'new',
       price => {
         value => '10.95',
         currency => 'USD',
       },
       shipping => [
         {
           country => 'US',
           service => 'Standard Shipping',
           price => {
             value => '7.95',
             currency => 'USD',
           },
         },
       ],
       brand => 'Apple',
       gtin => "${i}67890",
       mpn => "$i",
       googleProductCategory => 'Home & Garden > Household Supplies > Household Paper Products > Paper Towels',
       productType => 'Home & Garden > Household Supplies > Household Paper Products > Paper Towels',
       customLabel1 => 'Paper Towels'
     }
   };
 }

 $result = $bing->post(
   resource => 'products',
   method   => 'batch',
   dryrun   => 1,
   body => { entries => $products }
 );
 print "$result->{code} ". ($result->{code} eq '200' ? 'success' : 'failure') ."\n";

 # batch get

 $products = [];
 $batch_id = 0;
 foreach my $product_id ('211203'..'211209') {
   push @$products, {
     batchId    => ++$batch_id,
     merchantId => $bing->{merchant_id},
     method     => 'get', # insert / get / delete
     productId  => 'online:en:US:'. $product_id, # for get / delete
   };
 }

 $result = $bing->post(
   resource => 'products',
   method   => 'batch',
   dryrun   => 1,
   body => { entries => $products }
 );
 print "$result->{code} ". ($result->{code} eq '200' ? 'success' : 'failure') ."\n";
 print Dumper $result;

 # batch delete

 $products = [];
 $batch_id = 0;
 foreach my $product_id ('211203'..'211205') {
   push @$products, {
     batchId    => ++$batch_id,
     merchantId => $bing->{merchant_id},
     method     => 'delete', # insert / get / delete
     productId  => 'online:en:US:'. $product_id, # for get / delete
   };
 }

 $result = $bing->post(
   resource => 'products',
   method   => 'batch',
   dryrun   => 1,
   body => { entries => $products }
 );
 print "$result->{code} ". ($result->{code} eq '200' ? 'success' : 'failure') ."\n";

=head1 METHODS AND FUNCTIONS

=head2 new()

 Create a new Bing::ContentAPI object

=head3 debug

 Displays API debug information

=head3 merchant_id

 Merchant ID is the Bing Merchant Center Store ID
 https://bingads.microsoft.com/

=head3 developer_token

 Developer token from https://developers.bingads.microsoft.com/Account

=head3 client_id

 Client ID is the Application ID value in "Registering Your Application":
 https://docs.microsoft.com/bingads/guides/authentication-oauth#registerapplication

=head3 redirect_uri

 If you registered a native application, use "https://login.live.com/oauth20_desktop.srf"
 as the redirect URI. If you registered a web application, use the redirect URI you
 specified in "Registering Your Application".

=head3 refresh_token

 The current refresh token

=head2 refresh_access_token()

 Using the current refresh_token, obtain a new access and refresh token

=head3 access_token

 returns access_token obtained via refresh_access_token()

=head3 refresh_token

 returns refresh_token obtained via refresh_access_token()

=head2 PRODUCTS

=head3 batch

 Retrieves, inserts, and deletes multiple products in a single request.

=head3 insert

 Uploads a product to your Merchant Center account. If an item with the
 same channel, contentLanguage, offerId, and targetCountry already exists,
 this method updates that entry.

=head3 list

 Lists the products in your Merchant Center account.

=head3 get

 Retrieves a product from your Merchant Center account.

=head3 delete

 Deletes a product from your Merchant Center account.

=head2 CATALOGS

=head3 list

 Lists the catalogs in your Merchant Center Account.

=head3 status

 Lists the status and issues of products offers in your Merchant Center Account.

=head1 UNIMPLEMENTED FEATURES

 Certain API methods are not yet implemented (no current personal business need).

 A "custom" resource is available to perform methods that are not implemented by
 this module.

 $result = $bing->get(
   resource => 'custom',
   method   => 'merchantId/orders/orderId'
 );

=head1 PREREQUISITES

 JSON
 REST::Client
 HTML::Entities

=head1 AUTHOR

 Original Author
 Bill Gerrard <[email protected]>

=head1 COPYRIGHT AND LICENSE

 Copyright (C) 2018 Bill Gerrard

 This library is free software; you can redistribute it and/or modify
 it under the same terms as Perl itself, either Perl version 5.20.2 or,
 at your option, any later version of Perl 5 you may have available.
 Disclaimer of warranty: This program is provided by the copyright holder
 and contributors "As is" and without any express or implied warranties.
 The implied warranties of merchantability, fitness for a particular purpose,
 or non-infringement are disclaimed to the extent permitted by your local
 law. Unless required by law, no copyright holder or contributor will be
 liable for any direct, indirect, incidental, or consequential damages
 arising in any way out of the use of the package, even if advised of the
 possibility of such damage.


=cut