#!/usr/bin/perl -Ilibs -I../www2/libs
use strict;

package API::Order::Shipping::Package;

use Date::Format;

use API::Order::Shipping;

our $Config;
BEGIN {
	use API::Config;
	$Config = $API::Config::Config;
}
use lib ($Config->{'Website_libs'});

use ECS::SQL;
use ECS::Shipping;
use ECS::Time;
use ECS::Site;
use ECS::Order;
use Data::Dumper;


# not an api method
# given $p and $order return a site
# my $site = deriveSite( $p => $order );
sub _deriveSite { 
	my ($p, $order) = @_;
	my $site;
	if (Scalar::Util::blessed($order) && $order->isa('ECS::Order')) {
		$site = $order->site;
	} else {
		if ($order->{siteID}) {
			$site = fromID ECS::Site($order->{siteID});
		} else {
			if ($p->{site} || $p->{SITE} || $p->{SITE_NAME} || $p->{siteID}) {
				my $s = $p->{site} || $p->{SITE} || $p->{SITE_NAME} || $p->{siteID};
				if (Scalar::Util::looks_like_number($s)) {
					$site = fromID ECS::Site( $s );
				} else {
					$site = fromName ECS::Site( $s );
				}
			} else {
				$site = fromName ECS::Site($ECS::Site::PRIMARY_SITE);
			}
		}
	}
	return $site;
}

# Not an API method
# Used for retrieving package details when at the packing screen and packages have been previously created
# Requires
# - $p->{'order'}
# - $p->{'boxes'} (from API::Order::Shipping::getAvailableBoxes -> ECS::Shipping::Box::getBoxes)
# Sets
# - $p->{'order'}->{'existingPackages'}
sub getExistingPackages {
	my $p = shift;
	my $order = $p->{'order'};

	my $site = $p->{site} = _deriveSite( $p, $order );

	local $ECS::Config::c{shipping} = $site->localConfig( q{shipping} );
	local $ECS::Config::c{taxID} = $site->localConfig( q{taxID} );
	
	$order->{'existingPackages'} = [];
	
	my $sql = 'SELECT 
			package.packageID, 
			package.shippingBoxID, 
			shippingBox.barcode,
			package.length, 
			package.width, 
			package.height, 
			package.weight 
		FROM package 
		LEFT JOIN shippingBox USING(shippingBoxID)
		WHERE orderID = '.$order->{'orderID'};
	my $packages = ECS::SQL::query({ 'DBH' => $p->{'DBHr'}||$p->{DBH}, 'sql' => $sql });
	
	return 0 if (!$packages);
	
    # check that the existing packages are using available boxes
    for my $package (@{ $packages }) {
        my $packageShippingBoxID = $package->{'shippingBoxID'};

        # ignore custom boxes
        next if !$packageShippingBoxID or uc($package->{'barcode'}) eq 'CUSTOM';

        # this package is not in the list of valid boxes for this courier!
        # do not use any of this data. the packer will have to re-pack the boxes.
        return 1
            unless $p->{'courier'}->{'boxes'}->{$packageShippingBoxID};
    }

	my @packages = sort { $a->{'packageID'} <=> $b->{'packageID'} } @{ $packages };
	$packages = \@packages;
	
	$order->{'existingPackages'} = $packages;
	
	$sql = 'SELECT packageID, productID, qty FROM packageProduct WHERE';
	
	my $packages_by_ID = {};
	foreach my $package (@{ $packages }) {
	
		$package->{'products'} = {};
		
		if (
			defined $package->{'shippingBoxID'}
			&& $package->{'shippingBoxID'}
			&& defined $p->{'boxes'}->{$package->{'shippingBoxID'}}
		) {
			$package->{'barcode'} = $p->{'boxes'}->{$package->{'shippingBoxID'}}->{'barcode'};
		} else {
			$package->{'barcode'} = 'CUSTOM';
			$package->{'customBox'} = 1;
		}
		
		$packages_by_ID->{$package->{'packageID'}} = $package;
		
		$sql .= ' packageID = '.$package->{'packageID'}.' OR'
	}
	
	$sql = substr($sql, 0, -3);
	
	my $package_products = ECS::SQL::query({ 'DBH' => $p->{'DBHr'}||$p->{DBH}, 'sql' => $sql });
	
	if ($package_products) {
		foreach my $package_product (@{ $package_products }) {
			my $package = $packages_by_ID->{$package_product->{'packageID'}};
			$package->{'products'}->{$package_product->{'productID'}} = $package_product->{'qty'}
		}
	}
	
	return 1;
}

# Not an API method
# Requires
# - $p->{'order'}
# - $p->{'order'}->{'packages'}
# - $p->{'order'}->{'shippingBoxes'}
# Sets
# - $order->{'packagesVolume'}
sub savePackages {
	my $p = shift;
	my $order = $p->{'order'};

	my $site = $p->{site} = _deriveSite( $p, $order );

	local $ECS::Config::c{shipping} = $site->localConfig( q{shipping} );
	local $ECS::Config::c{taxID} = $site->localConfig( q{taxID} );

	my $total_volume = 0;
	my $found_custom_box = 0;
	my $box_count = 0;
	
	# Remove any existing package info.
	my $sql = '
		DELETE
			package,
			packageProduct,
			packageForm
		FROM package
			LEFT JOIN packageProduct USING (packageID)
			LEFT JOIN packageForm USING (packageID)
		WHERE orderID = '.$order->{'orderID'};
		
	ECS::SQL::query({ 'DBH' => $p->{'DBH'}, 'sql' => $sql });
	
	foreach my $package (@{ $order->{'packages'} }) {
		
		$package->{'orderID'} = $order->{'orderID'};
		
		$package->{'boxNum'} = ($box_count + 1);
		$box_count++;
		
		$total_volume += ($package->{'length'} * $package->{'width'} * $package->{'height'});
		
		
		# checking if the scanned box is not custom
		if (defined $order->{'shippingBoxes'}->{$package->{'barcode'}}) {
			
			my $shipping_box = $order->{'shippingBoxes'}->{$package->{'barcode'}};
			$package->{'shippingBoxID'} = $shipping_box->{'shippingBoxID'};
			
			# if the box was cut-down
			if (
				$package->{'height'} != $shipping_box->{'height'}
				&& $shipping_box->{'resizable'}
			) {
				# if the cut-down height is already in the database, use what's in the system already
				if (defined $shipping_box->{'resizedHeights'}->{$package->{'height'}+0}) {
					$package->{'shippingBoxID'} = $shipping_box->{'resizedHeights'}->{$package->{'height'}+0};
				} else {
					# otherwise insert a new cut-down box with the new height size
					$package->{'shippingBoxID'} = ECS::SQL::insert({
						'DBH'		=>	$p->{'DBH'},
						'table'		=>	'shippingBox',
						'columns'	=>	[qw(length width height resized)],
						'hash'		=>	{
							'length'	=>	$shipping_box->{'length'},
							'width'		=>	$shipping_box->{'width'},
							'height'	=>	$package->{'height'},
							'resized'	=>	1
						}
					});
					# once the new cut-down box is created, link it to the parent box
					if (defined $package->{'shippingBoxID'}) {
						ECS::SQL::insert({
							'DBH'		=>	$p->{'DBH'},
							'table'		=>	'shippingBoxComponent',
							'columns'	=>	[qw(shippingBoxID componentShippingBoxID length width height)],
							'hash'		=>	{
								'shippingBoxID'	=>	$package->{'shippingBoxID'},
								'componentShippingBoxID'	=>	$shipping_box->{'shippingBoxID'},
								'length'	=>	$shipping_box->{'length'},
								'width'		=>	$shipping_box->{'width'},
								'height'	=>	$package->{'height'}
							}
						});
					}
					
					$shipping_box->{'resizedHeights'}->{int($package->{'height'})} = $package->{'shippingBoxID'};
				}
			}
		
		} 
		# if the scanned box is custom
		else {
			$found_custom_box = 1;
		
			# get or create custom box
			$package->{'shippingBoxID'} = ECS::Shipping::Box::setCustomBox({
				'DBH'	=>	$p->{'DBH'},
				'DBHr'  =>  $p->{'DBHr'},
				'box'	=>	{
					'length'		=>	$package->{'length'},
					'width'			=>	$package->{'width'},
					'height'		=>	$package->{'height'},
					'boxComponents'	=>	$package->{'boxComponents'},
				}
			});
		}
		
		$package->{'packageID'} = ECS::SQL::insert({
			'DBH' => $p->{'DBH'},
			'table' => 'package',
			'columns' => [qw(orderID shippingBoxID length width height weight)],
			'hash' => $package
		});
		
		if ($package->{'packageID'}) {
			if ($package->{'products'}) {
			
				$sql = 'INSERT INTO packageProduct (packageID, productID, qty) VALUES';
				
				foreach my $productID (keys %{ $package->{'products'} }) {
					$sql .= ' ('.$package->{'packageID'}.', '.$productID.', '.$package->{'products'}->{$productID}.'),';
				}
				
				$sql = substr($sql, 0, -1);
				
				ECS::SQL::query({ 'DBH' => $p->{'DBH'}, 'sql' => $sql });
			} else {
				$package->{'products'} = {};
			}
		}
	}
	
	$order->{'hasCustomBox'} = $found_custom_box;
	$order->{'packagesVolume'} = $total_volume;
	
	return 1;
}

# Requires
# - $p->{'order'}
# - $p->{'order'}->{'packages'}
sub shipPackages {
	my $p = shift;
	my $order = $p->{'order'};
	my $packages = $order->{'packages'};
	
	my $site = $p->{site} = _deriveSite( $p, $order );
	ECS::Order::log_ptco($p->{'order'}->{'orderID'}, "Calling shipPackages in Package.pm");

	local $ECS::Config::c{shipping} = $site->localConfig( q{shipping} );
	local $ECS::Config::c{taxID} = $site->localConfig( q{taxID} );

	API::Order::Shipping::getCountryName($p);
	
	$order->{'shippingCostActual'} = 0;
	$order->{'shippingForms'} = [];
	
	my ($label_success, $label_error) = ECS::Shipping::Package::getLabels($p);
	ECS::Order::log_ptco($p->{'order'}->{'orderID'}, "sub shipPackages:Label success: $label_success, Label error: $label_error");
	
	if ($label_success) {

        if ( $p->{ 'tryingAlternate' } ) {
            _logAlternateService( $p );
        }
		
		if ($order->{'status'} eq 'Shipped') {
			$order->{'dtShipped'} = 'NOW()';
			delete $order->{'notes'}; # No notes required for Shipped order status
		}
		
		# If the courier has a custom way of printing labels, use it
		$p->{'courier'}->{'package'}->{'getPrintLabels'}($p) if (defined $p->{'courier'}->{'package'}->{'getPrintLabels'});
		
		# If the courier has a custom way of printing forms, use it
		$p->{'courier'}->{'package'}->{'getPrintForms'}($p) if (defined $p->{'courier'}->{'package'}->{'getPrintForms'});
		
	} else {
    
        if ( not $p->{ 'tryingAlternate' } ) {
            my $failed_service = $p->{ 'service' }->{ 'shippingServiceCode' };

            # failed to get label, try to optimize this using the
            # alternate shipping services for this service
            _getAlternateServicesForPackOptimization( $p );
            API::Order::Shipping::optimizeShipping( $p );
			ECS::Order::log_ptco($p->{'order'}->{'orderID'}, "sub shipPackages:shippingServiceCode: " . Dumper($p->{'order'}->{'shippingServiceCode'}));
			ECS::Order::log_ptco($p->{'order'}->{'orderID'}, "sub shipPackages:servicesForPackOptimization: " . Dumper($p->{'servicesForPackOptimization'}));
			ECS::Order::log_ptco($p->{'order'}->{'orderID'}, "sub shipPackages:shippingRates: " . Dumper($p->{shippingRates}));
            if ( $p->{ 'order' }->{ 'shippingServiceCode' } ne $failed_service ) {
                $p->{ 'order' }->{ 'failedServiceCode' } = $failed_service;
                $p->{ 'tryingAlternate' } = 1;
				ECS::Order::log_ptco($p->{'order'}->{'orderID'}, "sub shipPackages:failedServiceCode: " . Dumper($p->{'order'}->{'failedServiceCode'}));
                return shipPackages( $p );
            }
        }
		
		# was not shipped, so there can be no shipping cost
		delete $order->{'shippingCostActual'}; 
	
		$order->{'holdReason'} = $label_error;
		API::Order::Shipping::Package::holdPackages($p);
		
	}
	
	return $label_success;
}

# similar to API::Order::Shipping::getServicesForPackOptimization
# except we don't care about if the order is eligible for PTCO or
# the transit times for the services
sub _getAlternateServicesForPackOptimization {
    my ( $p ) = @_;

    my @shippingServiceCodes = @{ $p->{ 'service' }->{ 'alternates' } || [] };
    return unless @shippingServiceCodes;

    my $plh = join ", ", ( "?" ) x @shippingServiceCodes;
    my $sql = qq{
        SELECT
            shippingServiceCode,
            markdownPercent
        FROM shippingService
        WHERE
            shippingServiceCode IN ( $plh )
    };
	my $DBHr = $p->{DBHr}||$p->{DBH};
    my $sth = $DBHr->prepare( $sql );
    $sth->execute( @shippingServiceCodes );

    $p->{ 'servicesForPackOptimization' } = $sth->fetchall_hashref( "shippingServiceCode" );

    return;
}

sub _logAlternateService {
    my ( $p ) = @_;

    my $order = $p->{ 'order' } or return;

    my $sql = q{
        INSERT INTO
            alternateServiceLog
        SET
            orderID    = ?,
            oldService = ?,
            oldCost    = ?,
            newService = ?,
            newCost    = ?
    };
    my $sth = $p->{ 'DBH' }->prepare( $sql );
    $sth->execute(
        $order->{ 'orderID' },
        $order->{ 'failedServiceCode' },
        $order->{ 'shipping' }->{ 'shippingCostQuoted' },
        $order->{ 'shippingServiceCode' },
        $order->{ 'shippingCostActual' },
    );

    return;
}

# Requires
# - $p->{'order'}
# - $p->{'order'}->{'packages'}
# - $p->{'order'}->{'holdReason'}
sub holdPackages {
	my $p = shift;
	my $order = $p->{'order'};
	my $packages = $order->{'packages'};

	my $site = $p->{site} = _deriveSite( $p, $order );

	local $ECS::Config::c{shipping} = $site->localConfig( q{shipping} );
	local $ECS::Config::c{taxID} = $site->localConfig( q{taxID} );

    $order->status("Holding Picked");
    $order->addNote($order->{'holdReason'});
	
	foreach my $package (@{ $p->{'order'}->{'packages'} }) {
		
		$p->{'label'} = {
			'type' => 'HOLD',
			'format' => 'summary',
			'message' => $order->{'holdReason'},
			'package' => $package
		};
		
		$package->{'shiplabel'} = ECS::Shipping::Package::getStatusLabel($p);
		$package->{internallabel} = 1;
	}

    # don't send the "Your order has shipped" email
    delete $p->{'template'};
	
	return 1;
}

sub getAddressLabel {
	my $p = shift;
	my $order = $p->{'order'};

	my $site = $p->{site} = _deriveSite( $p, $order );

	local $ECS::Config::c{shipping} = $site->localConfig( q{shipping} );
	local $ECS::Config::c{taxID} = $site->localConfig( q{taxID} );

	foreach my $package (@{ $p->{'order'}->{'packages'} }) {
		
		$p->{'label'} = {
			'type' => 'ADDRESS',
			'format' => 'address',
			'package' => $package
		};
		
		$package->{'shiplabel'} = ECS::Shipping::Package::getStatusLabel($p);
		$package->{internallabel} = 1;
	}
	
	return 1;
}

# Requires
# - $p->{'order'}
# Sets
# - $p->{'order'}->{'packages'}
sub getShippedPackages {
	my $p = shift;
	
	$p->{'order'}->{'packages'} = [];
	
	my $order = $p->{order};
	my $site = $p->{site} = _deriveSite( $p, $order );

	local $ECS::Config::c{shipping} = $site->localConfig( q{shipping} );
	local $ECS::Config::c{taxID} = $site->localConfig( q{taxID} );

	my $DBHr = $p->{DBHr}||$p->{DBH};
	my $sql = "SELECT * FROM package WHERE orderID = ".$p->{'order'}->{'orderID'};
	my $packages = ECS::SQL::query({ DBH => $DBHr, sql => $sql });
	
	#use Data::Dumper;
	#print Dumper($packages);
	
	return 0 if (!$packages);
	
	my $packageIDs = {};
	
	foreach my $package (@{ $packages }) {
	
		$package->{'dtModified'} = time2str('%L/%d/%y %l:%M%p', ECS::Time::datetimeToEpoch($package->{'timestamp'}));
		
		$package->{'trackingURL'} = 'javascript:';
		
		if (defined $package->{'trackingnumber'} && $package->{'trackingnumber'}) {
			$p->{'package'} = $package;
			ECS::Shipping::Package::getTrackingURL($p);
		}
		
		$package->{'products'} = {};
		
		$packageIDs->{$package->{'packageID'}} = $package;
	}
	
	my $sql_where = 'WHERE';
	foreach my $packageID (keys %{ $packageIDs }) {
		$sql_where .= ' packageID = '.$packageID.' OR';
	}
	$sql_where = substr($sql_where, 0, -3);
	
	$sql = 'SELECT * FROM packageProduct '.$sql_where;
	my $packageProducts = ECS::SQL::query({ 'DBH' => $p->{'DBH'}, 'sql' => $sql });
	
	if ($packageProducts) {
		foreach my $packageProduct (@{ $packageProducts }) {
			$packageIDs->{$packageProduct->{'packageID'}}->{'products'}->{$packageProduct->{'productID'}} = $packageProduct->{'qty'};
		}
	}
	
	$sql = 'SELECT * FROM packageForm '.$sql_where;
	my $packageForms = ECS::SQL::query({ 'DBH' => $DBHr, 'sql' => $sql });
	
	if ($packageForms) {
		foreach my $form (@{ $packageForms }) {
			my $package = $packageIDs->{$form->{'packageID'}};
			
			$package->{'forms'} = [] if (!defined $package->{'forms'});
			
			push @{ $package->{'forms'} }, $form;
		}
	}
	
	$p->{'order'}->{'packages'} = $packages;
	
	return 1;
}

# Requires
# - orderID
# Returns
# - numForms
# - packages
sub getShippedPackagesSummary {
	my $self = shift;
	my $input = shift;
	
	my $result = {
		'success' => 1,
		'error' => undef,
		'data' => {},
		'update_DB'	=> $Config->{'update_DB'}
	};
	
	my $p = {
		'DBH' => ECS::SQL::connect(),
		'DBHr' => ECS::SQL::connect('SLAVE'),
		'input' => $input
	};
	
	if (!defined $p->{'input'}->{'orderID'} || !$p->{'input'}->{'orderID'}) {
		$result->{'success'} = 0;
		$result->{'error'} = 'No order specified';
		return $result;
	}
	
	$p->{'orderID'} = $p->{'input'}->{'orderID'};
	
	if (!API::Order::Shipping::getShippingSummary($p)) {
		$result->{'success'} = 0;
		$result->{'error'} = 'Unable to retrieve order details';
		return $result;
	}

	my $order = $p->{order};
	my $site = $p->{site} = _deriveSite( $p, $order );

	local $ECS::Config::c{shipping} = $site->localConfig( q{shipping} );
	local $ECS::Config::c{taxID} = $site->localConfig( q{taxID} );
	
	foreach my $k (keys %{ $p->{'order'} }) {
		$p->{'input'}->{$k} = $p->{'order'}->{$k};
	}
	
	$p->{'input'}->{'typeID'} = 'orderID';
	
	API::Order::Shipping::getCourier($p);
	
	API::Order::Shipping::Package::getShippedPackages($p);
	
	$p->{'order'}->{'shippingForms'} = [];
	
	# If the courier has a custom way of printing forms, use it
	$p->{'courier'}->{'package'}->{'getPrintForms'}($p) if (defined $p->{'courier'}->{'package'}->{'getPrintForms'});
	
    $result->{'data'}->{'downloadableForms'} = grep { $_->{'contentType'} eq "pdf" }
        @{ $p->{'order'}->{'shippingForms'} };

    $result->{'data'}->{'printableForms'} = grep { $_->{'contentType'} eq "epl" }
        @{ $p->{'order'}->{'shippingForms'} };
	
	# This is a summary, so don't send the shipping label or forms data
	foreach my $package (@{ $p->{'order'}->{'packages'} }) {
		delete $package->{'shiplabel'};
		delete $package->{'forms'};
	}
	
	$result->{'data'}->{'packages'} = $p->{'order'}->{'packages'};
	
	return $result;
}

# Requires
# - orderID
# Returns
# - shippingLabels
# - shippingForms
sub getLabelsForms { 
	my $self = shift;
	my $input = shift;
	
	my $result = {
		'success' => 1,
		'error' => undef,
		'data' => {},
		'update_DB'	=> $Config->{'update_DB'}
	};
	
	my $p = {
		'DBH'  => ECS::SQL::connect(),
		'DBHr' => ECS::SQL::connect('SLAVE'),
		'input' => $input
	};
	
	if (!defined $p->{'input'}->{'orderID'} || !$p->{'input'}->{'orderID'}) {
		$result->{'success'} = 0;
		$result->{'error'} = 'No order specified';
		return $result;
	}
	
	$p->{'orderID'} = $p->{'input'}->{'orderID'};
	
	if (!API::Order::Shipping::getShippingSummary($p)) {
		$result->{'success'} = 0;
		$result->{'error'} = 'Unable to retrieve order details';
		return $result;
	}
	
	foreach my $k (keys %{ $p->{'order'} }) {
		$p->{'input'}->{$k} = $p->{'order'}->{$k};
	}
	
	my $order = $p->{order};
	my $site = $p->{site} = _deriveSite( $p, $order );

	local $ECS::Config::c{shipping} = $site->localConfig( q{shipping} );
	local $ECS::Config::c{taxID} = $site->localConfig( q{taxID} );

	$p->{'input'}->{'typeID'} = 'orderID';
	
	API::Order::Shipping::getCourier($p);
	
	API::Order::Shipping::getWarehouseShipment($p);
	
	API::Order::Shipping::Package::getShippedPackages($p);
	
	API::Order::Shipping::Package::buildPackageProductDetails($p);
	
	$p->{'order'}->{'shippingForms'} = [];
	
	# If the courier has a custom way of printing labels, use it
	$p->{'courier'}->{'package'}->{'getPrintLabels'}($p) if (defined $p->{'courier'}->{'package'}->{'getPrintLabels'});
	
	# If the courier has a custom way of printing forms, use it
	$p->{'courier'}->{'package'}->{'getPrintForms'}($p) if (defined $p->{'courier'}->{'package'}->{'getPrintForms'});
	
	my @shipping_labels;
	
	foreach my $package (@{ $p->{'order'}->{'packages'} }) {
		push(@shipping_labels, $package->{'shiplabel'}) if (defined $package->{'shiplabel'});
	}
	
	if (
		!scalar(@shipping_labels)
		&& !scalar(@{ $p->{'order'}->{'shippingForms'} })
	) {
		$result->{'success'} = 0;
		$result->{'error'} = 'No Shipping Labels or Forms to Print';
		return $result;
	}
	
	$result->{'data'}->{'shippingLabels'} = \@shipping_labels;
	$result->{'data'}->{'shippingForms'} = $p->{'order'}->{'shippingForms'};

	return $result;
}

# Not an API method
# Used for building $p->{'order'}->{'packages'}->[$i]->{'productDetails'} using $p->{'shipment'} data
# Requires
# - $p->{'shipment'} (warehouse shipment)
# - $p->{'order'}->{'packages'}
# Sets
# - $p->{'order'}->{'packages'}->[$i]->{'productDetails'}
sub buildPackageProductDetails { 
	my $p = shift;
	
	my %products_qty_remain;
	
	my $order = $p->{order};
	my $site = $p->{site} = _deriveSite( $p, $order );
    $p->{'packageProductKey'} ||= 'productDetails';

	local $ECS::Config::c{shipping} = $site->localConfig( q{shipping} );
	local $ECS::Config::c{taxID} = $site->localConfig( q{taxID} );

	for my $package (@{ $p->{'order'}->{'packages'} }) {
        my $packageProducts = $package->{'products'};
        delete $package->{'products'} if ($p->{'packageProductKey'} eq 'products');
        $package->{$p->{'packageProductKey'}} ||= [];
		
		for my $productID (keys %{ $packageProducts }) {
			my $package_product_qty = $packageProducts->{$productID};
			my $product_group = $p->{'shipment'}->{'productsGrouped'}->{$productID};
			
			for my $product (@{ $product_group }) {
				
				if (not defined $products_qty_remain{$product}) {
					$products_qty_remain{$product} = $product->{'qty'};
				}
				
				my $qty_remain = $products_qty_remain{$product};
				
				# Skip if this product has already been used for previous package product data
				next unless (
					$qty_remain > 0
					and $package_product_qty > 0
				);
				
				my $qty = $package_product_qty;
				if ($package_product_qty > $qty_remain) {
					$qty = $qty_remain;
				}
				
				$package_product_qty -= $qty;
				$products_qty_remain{$product} -= $qty;
				
				# create shallow clone of product
				my $c_product = { %{ $product } };
				
				# set the quantity packed
				$c_product->{'qty'} = $qty;
				
				push(@{ $package->{$p->{'packageProductKey'}} }, $c_product);
			}
		}
	}
	
	return 1;
}

1;
