package WebGUI::Commerce::Transaction;

=head1 LEGAL

 -------------------------------------------------------------------
  WebGUI is Copyright 2001-2006 Plain Black Corporation.
 -------------------------------------------------------------------
  Please read the legal notices (docs/legal.txt) and the license
  (docs/license.txt) that came with this distribution before using
  this software.
 -------------------------------------------------------------------
  http://www.plainblack.com                     info@plainblack.com
 -------------------------------------------------------------------

=cut


use strict;
use WebGUI::SQL;
use WebGUI::Commerce::Payment;
use JSON;

#-------------------------------------------------------------------

=head2 addItem ( item, quantity )

Add's an item to the transaction.

=head3 item

An WebGUI::Commerce::Item object of the item you want to add.

=head3 quantity

The number of items that are tobe added.

=cut

sub addItem {
	my ($self, $item, $quantity, $lineItemAmount);
	$self = shift;
	$item = shift;
	$quantity = shift;
	$lineItemAmount = shift;
	
	# If we have a lineItemAmount specified, we have to override price*quantity and use the value passed in instead
	# We will however maintain the quantity passed and tell the user that the prices displayed are always line item totals.
	#
	# Again, prices are not per item but per line item.  Qty 3 Price 10.00 indicates 3 items for a total of 10.00, not 3 items at 10.00 each
	# It has to be this way to handle discounted items where all items in the quantity are not discounted.  For example, 2 items at 5.00 each and the third item is free.
	#
	# Hopefully the new commerce system will handle this better. (Maybe making discounted items a different item all together with their own line item to retain the detailed information)
	#
	my $totalPrice = ($lineItemAmount eq "") ? $item->price * $quantity : $lineItemAmount;
	$self->session->db->write("insert into transactionItem (transactionId, itemName, amount, quantity, itemId, itemType) values (?,?,?,?,?,?)",
			[$self->{_transactionId}, $item->name, $totalPrice, $quantity, $item->id, $item->type]);

	# Adjust total amount in the transaction table.
	$self->session->db->write("update transaction set amount=amount+? where transactionId=?",[$totalPrice,$self->{_transactionId}]);
	$self->{_properties}{amount} += $totalPrice;
	push @{$self->{_items}}, {
		transactionId	=> $self->{_transactionId},
		itemName	=> $item->name,
		amount		=> $totalPrice,
		quantity	=> $quantity,
		itemId		=> $item->id,
		itemType	=> $item->type,
	}
}

#-------------------------------------------------------------------

=head2 cancelTransaction ( )

Cancels a recurring transaction. This is done by trying to cancel the subscription at the gateway
using a Payment plugin. If this is succesfull the transaction is marked as canceled.

=cut

sub cancelTransaction {
	my ($self, $item, $plugin);
	$self = shift;

	return "Not a recurring transaction" unless ($self->isRecurring);
	
	# Recurring transactions can only have one item, so our items must be the first
	$item = $self->getItems->[0];

	$plugin = WebGUI::Commerce::Payment->load($self->session, $self->gateway);
	$plugin->cancelRecurringPayment({
		id => $self->gatewayId,
		transaction => $self,
		});
	return $plugin->resultMessage.' (code: '.$plugin->errorCode.')' if ($plugin->errorCode);

	$self->status('Canceled');

	return undef;
}

#-------------------------------------------------------------------

=head2 completeTransaction ( )

Sets the status of a transaction to 'Completed' and executes the handler for every item attached to
the transction.

=cut

sub completeTransaction {
	my ($self, $item);
	$self = shift;

	foreach (@{$self->getItems}) {
		$item = WebGUI::Commerce::Item->new($self->session,$_->{itemId}, $_->{itemType});
		if (ref($item) eq 'WebGUI::Commerce::Item::Event') {
			$item->handler($_->{transactionId});
		}
		else {
			$item->handler;
		}
		$self->session->errorHandler->warn(ref($item));
	}

	$self->status('Completed');	
}

#-------------------------------------------------------------------

=head2 delete ( )

Deletes the transaction from the database;

=cut

sub delete {
	my ($self) = shift;

	$self->session->db->write("delete from transaction where transactionId=".$self->session->db->quote($self->{_transactionId}));
	$self->session->db->write("delete from transactionItem where transactionId=".$self->session->db->quote($self->{_transactionId}));

	undef $self;
}

#-------------------------------------------------------------------

=head2 deleteItem ( itemId, itemType )

Deletes an item from a transaction. This will purge the record from the database, and
updates the amount of the transaction. It doesn't change the shipping cost however.

Also if you want to credit the user (you'll probably want to) for the amount of the 
removed items, you must do this yourself. 

=head3 itemId

The id of the item you want to remove.

=head3 itemType

The type of the item you want to remove.

=cut

#-------------------------------------------------------------------

sub deleteItem {
	my ($self, $itemId, $itemType, $amount, @items);
	$self = shift;
	$itemId = shift;
	$itemType = shift;

	$self->session->errorHandler->fatal('No itemId') unless ($itemId);
	$self->session->errorHandler->fatal('No itemType') unless ($itemType);
	
	$amount = $self->get('amount');
	
	foreach (@{$self->getItems}) {
		
		if (($_->{itemId} eq $itemId) && ($_->{itemType} eq $itemType)) {
			$amount = $amount - ($_->{quantity} * $_->{amount});
		} else {
			push(@items, $_);
		}			
	}
	
	$self->session->db->write("delete from transactionItem where transactionId=".$self->session->db->quote($self->get('transactionId')).
		" and itemId=".$self->session->db->quote($itemId)." and itemType=".$self->session->db->quote($itemType));

	$self->session->db->write("update transaction set amount=".$self->session->db->quote($amount)." where transactionId=".$self->session->db->quote($self->get('transactionId')));
	
	$self->{_properties}{amount} = $amount;

	$self->{_items} = \@items;
}

#-------------------------------------------------------------------

=head2 gateway ( gatewayName )

Returns the gateway connected to the transaction. If gatewayName is given the gateway property is set to that.

=head3 gatewayName

The name to which to set the gateway.

=cut

sub gateway {
	my ($self, $gateway);
	$self = shift;
	$gateway = shift;

	if ($gateway) {
		$self->{_properties}{gateway} = $gateway;
		$self->session->db->write("update transaction set gateway=".$self->session->db->quote($gateway)." where transactionId=".$self->session->db->quote($self->{_transactionId}));
	}

	return $self->{_properties}{gateway};
}

#-------------------------------------------------------------------

=head2 gatewayId ( id )

Returns the gateway ID of the transaction. If id is given the gateway ID is set to it.

=head3 id

The ID which to set the gatewayId to.

=cut

sub gatewayId {
	my ($self, $gatewayId);
	$self = shift;
	$gatewayId = shift;

	if ($gatewayId) {
		$self->{_properties}{gatewayId} = $gatewayId;
		$self->session->db->write("update transaction set gatewayId=".$self->session->db->quote($gatewayId)." where transactionId=".$self->session->db->quote($self->{_transactionId}));
	}

	return $self->{_properties}{gatewayId};
}

#-------------------------------------------------------------------

=head2 get ( property )

Returns the property requested. If no property is specified this method returns a hashref
containing all properties.

=head3 property

The name of the property you want.

=cut

sub get {
	my ($self, $key);
	$self = shift;
	$key = shift;
	
	return $self->{_properties}{$key} if ($key);
	return $self->{_properties};
}

#-------------------------------------------------------------------

=head2 getByGatewayId ( id, gateway )

Constructor. Return a transaction object that is identified by the given id and payment gateway.
Returns undef if no match is found.

=head3 id

The gateway ID of the transaction.

=head3 gateway

The payment gateway which the transaction is tied to.

=cut

sub getByGatewayId {
	my ($self, $gatewayId, $paymentGateway, $transactionId);
	$self = shift;
	$gatewayId = shift;
	$paymentGateway = shift;

	($transactionId) = $self->session->db->quickArray("select transactionId from transaction where gatewayId=".$self->session->db->quote($gatewayId).
		" and gateway=".$self->session->db->quote($paymentGateway));

	return WebGUI::Commerce::Transaction->new($self->session, $transactionId) if $transactionId;
	return undef;
}

#-------------------------------------------------------------------

=head2 getItems ( )

=cut

sub getItems {
	my ($self);
	$self = shift;
	
	return $self->{_items};
}

#-------------------------------------------------------------------

=head2 getTransactions ( constraints )

Returns an array consisting of WebGUI::Commerce::Transaction objects complying to
the passed constraints.

=head3 constraints

A hashref containing the contrains by which the transactions are selected. These can be:

	* initStart
		Epoch that specifies the lower bounds on the initialisation date.

	* initStop
		Epoch that specifies the upper bound on the initialisation date.

	* completionStart
		Epoch specifying the lower bound on the completion date.
	
	* completionStop
		Epoch specifying the upper bound on the completion date.

	* status
		The status of the transaction. Can be: Pending, Completed or Canceled

	* shippingStatus
		The shipping status of the transaction. Can be: NotShipped, Shipped or Delivered

=cut	

sub getTransactions {
	my ($self, $criteria, @constraints, $sql, @transactionIds, @transactions);
	
	$self = shift;
	$criteria = shift;
	
	push (@constraints, 'initDate >= '.$self->session->db->quote($criteria->{initStart})) if (defined $criteria->{initStart});
	push (@constraints, 'initDate <= '.$self->session->db->quote($criteria->{initStop})) if (defined $criteria->{initStop});
	push (@constraints, 'completionDate >= '.$self->session->db->quote($criteria->{completionStart})) if (defined $criteria->{completionStart});
	push (@constraints, 'completionDate <= '.$self->session->db->quote($criteria->{completionStop})) if (defined $criteria->{completionStop});
	push (@constraints, 'status='.$self->session->db->quote($criteria->{paymentStatus})) if (defined $criteria->{paymentStatus});
	push (@constraints, 'shippingStatus='.$self->session->db->quote($criteria->{shippingStatus})) if (defined $criteria->{shippingStatus});
	
	$sql = 'select transactionId from transaction';
	$sql .= ' where '.join(' and ', @constraints) if (@constraints);
	$sql .= ' order by initDate desc';
	@transactionIds = $self->session->db->buildArray($sql);

	foreach (@transactionIds) {
		push(@transactions, WebGUI::Commerce::Transaction->new($self->session, $_));
	}

	return @transactions;
}


#-------------------------------------------------------------------

=head2 isRecurring ( recurring )

Returns a boolean indcating whether the transaction is recurring. If recurring is given, the isRecurring flag
will be set to it.

=head3 recurring

A boolean which sets the transaction as recurring if true.

=cut

sub isRecurring {
	my ($self, $recurring);
	$self = shift;
	$recurring = shift;
	
	if (defined $recurring) {
		$self->{_properties}{recurring} = $recurring;
		$self->session->db->write("update transaction set recurring=".$self->session->db->quote($recurring)." where transactionId=".$self->session->db->quote($self->{_transactionId}));
	}
	
	return $self->{_properties}{recurring};
}

#-------------------------------------------------------------------

=head2 lastPayedTerm ( term )

Returns the last term number that has been paid. If term is given this number will be set to it.

-head3 term

The number which to set tha last payed term to.

=cut

sub lastPayedTerm {
	my ($self, $lastPayedTerm);
	$self = shift;
	$lastPayedTerm = shift;
	
	if (defined $lastPayedTerm) {
		$self->{_properties}{lastPayedTerm} = $lastPayedTerm;
		$self->session->db->write("update transaction set lastPayedTerm=".$self->session->db->quote($lastPayedTerm)." where transactionId=".$self->session->db->quote($self->{_transactionId}));
	}
	
	return $self->{_properties}{lastPayedTerm};
}

#-------------------------------------------------------------------

=head2 new ( transactionId, [ gateway, [ userId ] ] )

Constructor. Returns a transaction object. If transactionId is set to 'new' a new transaction is created.

=head3 transactionId

The transaction ID of the transaction you want. Set to 'new' for a new transaction.

=head3 gateway

The payment gateway to use for this transaction. Only needed for new transactions.

=head3 userId

The userId of the user for whom to create this transaction. Defaults to the current user. Only optional for
new transactions.

=cut

sub new {
	my ($class, $transactionId, $gatewayId, $userId, $properties, $sth, $row, @items);
	
	$class = shift;
	my $session = shift;
	$transactionId = shift;
	$gatewayId = shift;
	$userId = shift || $session->user->userId;
	
	if ($transactionId eq 'new') {
		$transactionId = $session->id->generate;

		$session->db->write("insert into transaction ".
			"(transactionId, userId, amount, gatewayId, initDate, completionDate, status) values ".
			"(".$session->db->quote($transactionId).",".$session->db->quote($userId).",0,".$session->db->quote($gatewayId).",".$session->db->quote(time).",NULL,'Pending')");
	}

	$properties = $session->db->quickHashRef("select * from transaction where transactionId=".$session->db->quote($transactionId));
	$sth = $session->db->read("select * from transactionItem where transactionId=".$session->db->quote($transactionId));
	while ($row = $sth->hashRef) {
		push(@items, $row);
	}

	bless {_session => $session, _transactionId => $transactionId, _properties => $properties, _items => \@items}, $class;
}

#-------------------------------------------------------------------

=head2 pendingTransactions ( )

Returns a reference to an array which contains transaction objects of all pending transactions.

=cut

sub pendingTransactions {
	my (@transactionIds, @transactions);
	my ($session) = @_;
	@transactionIds = $session->db->buildArray("select transactionId from transaction where status = 'Pending'");

	foreach (@transactionIds) {
		push(@transactions, WebGUI::Commerce::Transaction->new($session, $_));
	}

	return \@transactions;
}

#-------------------------------------------------------------------

=head2 session ( )

Returns the cached, local session variable.

=cut

sub session {
	my $self = shift;
	return $self->{_session};
}

#-------------------------------------------------------------------

=head2 shippingCost ( [amount] )

Returns the shipping cost for this transaction. If amount is supplied the sipping cost will
be set to that value.

=head3 amount
If supplied the shipping cost of the transaction will be set to this value.

=cut

sub shippingCost {
	my ($self, $shippingCost);
	$self = shift;
	$shippingCost = shift;

	if ($shippingCost) {
		$self->{_properties}{shippingCost} = $shippingCost;
		$self->session->db->write("update transaction set shippingCost=".$self->session->db->quote($shippingCost)." where transactionId=".$self->session->db->quote($self->{_transactionId}));
	}

	return $self->{_properties}{shippingCost};
}

#-------------------------------------------------------------------

=head2 shippingMethod ( [ method ] )

Returns the shipping method for this transaction. If amount is supplied the shipping method will
be set to it.

=head3 method
If supplied the shipping method of the transaction will be set to this value.

=cut

sub shippingMethod {
	my ($self, $shippingMethod);
	$self = shift;
	$shippingMethod = shift;

	if ($shippingMethod) {
		$self->{_properties}{shippingMethod} = $shippingMethod;
		$self->session->db->write("update transaction set shippingMethod=".$self->session->db->quote($shippingMethod)." where transactionId=".$self->session->db->quote($self->{_transactionId}));
	}

	return $self->{_properties}{shippingMethod};
}

#-------------------------------------------------------------------

=head2 shippingOptions ( [ options ] )

Returns the shipping options for this transaction. If options is supplied the shipping options will
be set to it.

=head3 options
If supplied the shipping options of the transaction will be set to this value. The value passed will
be serialized/un-serialized for you.

=cut

sub shippingOptions {
	my ($self, $shippingOptions);
	$self = shift;
	$shippingOptions = shift;

	if (scalar (keys %{$shippingOptions})) {
		$self->{_properties}{shippingOptions} = objToJson($shippingOptions);
		$self->session->db->write("update transaction set shippingOptions=? where transactionId=?",[$self->{_properties}{shippingOptions},$self->{_transactionId}]);
	}

	return jsonToObj($self->{_properties}{shippingOptions});
}

#-------------------------------------------------------------------

=head2 shippingStatus ( [ status ] )

Returns the shipping status for this transaction. If status is supplied the shipping status will
be set to it.

=head3 status
If supplied the shipping status of the transaction will be set to this value.

=cut

sub shippingStatus {
	my ($self, $shippingStatus);
	$self = shift;
	$shippingStatus = shift;

	if ($shippingStatus) {
		$self->{_properties}{shippingStatus} = $shippingStatus;
		$self->session->db->write("update transaction set shippingStatus=".$self->session->db->quote($shippingStatus)." where transactionId=".$self->session->db->quote($self->{_transactionId}));
	}

	return $self->{_properties}{shippingStatus};
}

#-------------------------------------------------------------------

=head2 status ( status )

Returns the status of the transaction. If status is given the transaction status will be set to it.

=head3 status

The value to set the transaction status to.

=cut

sub status {
	my ($self, $status);
	$self = shift;
	$status = shift;

	if ($status) {
		$self->{_properties}{status} = $status;
		$self->session->db->write("update transaction set status=".$self->session->db->quote($status)." where transactionId=".$self->session->db->quote($self->{_transactionId}));
	}

	return $self->{_properties}{status};
}


#-------------------------------------------------------------------

=head2 trackingNumber ( [ number ] )

Returns the tracking number of the shipped transaction. If numer is supplied the tracking number will
be set to it.

=head3 number
If supplied the tracking number of the transaction will be set to this value.

=cut

sub trackingNumber {
	my ($self, $trackingNumber);
	$self = shift;
	$trackingNumber = shift;

	if ($trackingNumber) {
		$self->{_properties}{trackingNumber} = $trackingNumber;
		$self->session->db->write("update transaction set trackingNumber=".$self->session->db->quote($trackingNumber)." where transactionId=".$self->session->db->quote($self->{_transactionId}));
	}

	return $self->{_properties}{trackingNumber};
}

#-------------------------------------------------------------------

=head2 transactionId ( )

Returns the transactionId of the transaction.

=cut

sub transactionId {
	my $self = shift;
	return $self->{_transactionId};
}

#-------------------------------------------------------------------

=head2 transactionsByUser ( userId )

Returns a reference to an array containing transaction objects of all tranactions by the user corresponding to userId.

=head3 userId

The ID of the user you want the transaction of.

=cut

sub transactionsByUser {
	my (@transactionIds, @transactions);
	my $self = shift;
	my $userId = shift;

	@transactionIds = $self->session->db->buildArray("select transactionId from transaction where userId =".$self->session->db->quote($userId));
	foreach (@transactionIds) {
		push (@transactions, WebGUI::Commerce::Transaction->new($self->session, $_));
	}

	return \@transactions;
}
	
1;

