Revision 802 (by ahitrov, 2020/08/18 18:06:27) Мерж корзины с учетом товаров

package webshop::Keeper;

use strict;
use warnings 'all';
use base qw(Contenido::Keeper);

use Data::Dumper;
use Contenido::Globals;
use webshop::Basket;
use webshop::SQL::Basket;

sub add_item {
  my $self = shift;
  my $object = shift;
  my (%options) = @_;

  return	unless ref $object;
  return	unless $object->item_id && $object->number;
  return	unless $object->uid || $object->session;


  my %opts;
  if ( $object->uid ) {
	$opts{uid} = $object->uid;
  } elsif ( $object->session ) {
	$opts{session} = $object->session;
  }
  my @items = $keeper->get_documents (
		class	=> 'webshop::Basket',
		status	=> 1,
		order_id=> 0,
		%opts,
	);
  my $total = 0;
  my $sum = 0;
  my $found = 0;
  if ( @items ) {
	foreach my $item ( @items ) {
		if ( $object->item_id == $item->item_id && $object->color_id == $item->color_id && 
		    ((!$object->size_id && !$object->size) || ($object->size_id && $object->size_id == $item->size_id) || ($object->size && $object->size eq $item->size)) ) {
			$item->number( $item->number + $object->number );
			$item->price( $object->price );
			$item->store;
			$found = 1;
		}
		$total += $item->number;
		$sum += $item->number * $item->price;
	}
  }
  unless ( $found ) {
	$total += $object->number;
	$sum += $object->number * $object->price;
	$object->order_id(0);
	$object->store;
  }
  my @plugins = split (/[\ |\t]+/, $state->{plugins});
  if ( grep { $_ eq 'session' } @plugins && ref $session ) {
	$session->set ( basket_total => $total, basket_sum => $sum );
  }
  return ($total, $sum);
}

sub add_wishlist {
  my $self = shift;
  my $object = shift;
  my (%options) = @_;

  return	unless ref $object;
  return	unless $object->item_id && $object->number;
  return	unless $object->uid || $object->session;


  my %opts;
  if ( $object->uid ) {
	$opts{uid} = $object->uid;
  } elsif ( $object->session ) {
	$opts{session} = $object->session;
  }
  my @items = $keeper->get_documents (
		class	=> 'webshop::Basket',
		status	=> 0,
		order_id=> 0,
		%opts,
	);
  my $total = 0;
  my $sum = 0;
  my $found = 0;
  if ( @items ) {
	foreach my $item ( @items ) {
		if ( $object->item_id == $item->item_id && $object->color_id == $item->color_id && 
		    ((!$object->size_id && !$object->size) || ($object->size_id && $object->size_id == $item->size_id) || ($object->size && $object->size eq $item->size)) ) {
			$item->number($item->number + $object->number);
			$item->store;
			$found = 1;
		}
		$total += $item->number;
		$sum += $item->number * $item->price;
	}
  }
  unless ( $found ) {
	$total += $object->number;
	$sum += $object->number * $object->price;
	$object->order_id(0);
	$object->store;
  }
  return ($total, $sum);
}

### Метод приведения корзины для пользователя в момент логина
#############################################################
sub merge_basket {
  my $self = shift;
  my (%opts) = @_;

  warn "Merge begin: ".Dumper(\%opts)."\n"		if $DEBUG;
  return	unless $opts{uid} && $opts{session};

  my @items_session = $keeper->get_documents (
		class	=> 'webshop::Basket',
		status	=> [1,0],
		session	=> $opts{session},
		order_id	=> 0,
	);
  my @items_user = $keeper->get_documents (
		class	=> 'webshop::Basket',
		status	=> [1,0],
		order_id=> 0,
		uid	=> $opts{uid},
	);
  my ($basket_total, $basket_sum, $wishlist_total, $wishlist_sum) = (0,0,0,0);
  my %p_ids = map { $_->item_id => 1 } (@items_user, @items_session);
  my @p_ids = keys %p_ids;
  my $products = @p_ids ? $keeper->get_documents(
		id	=> \@p_ids,
		class	=> $state->{webshop}->{item_document_class},
		return_mode	=> 'hash_ref',
	) : {};

  foreach my $item ( @items_user ) {
	my $product = exists $products->{$item->item_id} ? $products->{$item->item_id} : undef;
	my ($item_from_session) = grep { 
						$_->item_id == $item->item_id
						&& $_->status == $item->status
						&& ($_->color_id || 0) == ($item->color_id || 0)
						&& ($_->size_id || 0) == ($item->size_id || 0)
						&& ($_->colour || '') eq ($item->colour || '')
						&& ($_->size || '') eq ($item->size || '')
					} @items_session;
	if ( ref $item_from_session ) {
		warn "Merge basket: Found identical item id=".$item_from_session->id."\n"		if $DEBUG;
		$item->number( $item->number + $item_from_session->number );
		$item->price( ref $product ? $product->price : $item_from_session->price );
		$item->store;
		$item_from_session->status(-1);
		$item_from_session->delete;
	}
	if ( $item->status == 1 ) {
		$basket_total += $item->number;
		$basket_sum += $item->number * $item->price;
	} else {
		$wishlist_total += $item->number;
		$wishlist_sum += $item->number * $item->price;
	}
  }
  foreach my $item ( @items_session ) {
	my $product = exists $products->{$item->item_id} ? $products->{$item->item_id} : undef;
	if ( $item->status > 0 ) {
		$item->session(undef);
		$item->uid( $opts{uid} );
		if ( ref $product ) {
			$item->price( $product->price );
		}
		$item->store;
		if ( $item->status == 1 ) {
			$basket_total += $item->number;
			$basket_sum += $item->number * $item->price;
		} else {
			$wishlist_total += $item->number;
			$wishlist_sum += $item->number * $item->price;
		}
	}
  }
  warn "Merge end\n"					if $DEBUG;
  return ($basket_total, $basket_sum, $wishlist_total, $wishlist_sum);
}


sub get_basket {
  my $self = shift;
  my (%opts) = @_;

  return	unless $opts{uid} || $opts{session} || $opts{order_id};

  my $with_products = delete $opts{with_products} || 0;
  my $product_status = exists $opts{product_status} ? delete $opts{product_status} : 'positive';
  my $uid = delete $opts{uid};
  my $session_id = delete $opts{session};
  unless ( exists $opts{order_id} && $opts{order_id} ) {
	$opts{order_id} = 0;
	if ( $uid ) {
		$opts{uid} = $uid;
	} elsif ( $session_id ) {
		$opts{session} = $session_id;
	}
  }
  $opts{status} = 1	unless exists $opts{status} && defined $opts{status};
  my @basket = $keeper->get_documents (
		class	=> 'webshop::Basket',
		%opts,
	);
  my $total = 0;
  my $sum = 0;
  my $items;
  if ( $with_products ) {
	my %ids = map { $_->item_id => 1 } @basket;
	my @ids = keys %ids;
	$items = @ids ? $keeper->get_documents (
			id	=> \@ids,
			class	=> $state->{webshop}->{item_document_class},
			status	=> $product_status,
			return_mode	=> 'hash_ref',
		) : {};
  }
  foreach my $bi ( @basket ) {
	if ( $bi->status == 1 ) {
		$total += $bi->number;
		$sum += $bi->number * ($bi->price || 0);
	}
	if ( ref $items eq 'HASH' && exists $items->{$bi->item_id} ) {
		$bi->{item} = $items->{$bi->item_id};
	}
  }
  return \@basket;
}


sub basket_count {
    my $self = shift;
    my $basket = shift;
    return (0,0)	unless ref $basket eq 'ARRAY' && @$basket;
    my (%opts) = @_;
    my $with_products = delete $opts{with_products};

    my $total = 0;
    my $sum = 0;
    foreach my $item ( @$basket ) {
	if ( $item->status == 1 ) {
		$total += $item->number;
		if ( exists $item->{item} && $item->{item}->status && $item->{item}->price && $item->{item}->storage ) {
			$sum += $item->number * $item->{item}->price;
		} elsif ( !$with_products ) {
			$sum += $item->number * $item->price;
		}
	}
    }
    return ($total, $sum);
}


sub wishlist_count {
    my $self = shift;
    my $basket = shift;
    return (0,0)	unless ref $basket eq 'ARRAY' && @$basket;

    my $total = 0;
    my $sum = 0;
    foreach my $item ( @$basket ) {
	if ( $item->status == 0 ) {
		$total += $item->number;
		$sum += $item->number * $item->price;
	}
    }
    return ($total, $sum);
}


sub clear_basket {
  my $self = shift;
  my (%opts) = @_;

  return	unless exists $opts{uid} || exists $opts{session};

  my $table_name = webshop::SQL::Basket->db_table;
  my $request = "delete from $table_name where order_id = 0 AND status = 1 AND";
  my $dbh = $keeper->SQL;
  my @vals;
  if ( exists $opts{uid} && $opts{uid} ) {
	$request .= " uid in (?)";
	push @vals, $opts{uid};
  } elsif ( exists $opts{session} ) {
	$request .= " session in (?)";
	push @vals, $opts{session};
  }
  warn "CLEAR: [$request]. VALS: [".join(',',@vals)."]\n"	if $DEBUG;
  my $statement = $dbh->prepare ($request);
  $statement->execute( @vals ) || $log->error("DBI execute error on $request\n"."\ncalled with opts:\n".Data::Dumper::Dumper(\%opts));;
  $statement->finish;

  my @plugins = split (/[\ |\t]+/, $state->{plugins});
  if ( grep { $_ eq 'session' } @plugins && ref $session ) {
	$session->set ( basket_total => 0, basket_sum => 0 );
  }
}


sub clear_wishlist {
  my $self = shift;
  my (%opts) = @_;

  return	unless exists $opts{uid} || exists $opts{session};

  my $table_name = webshop::SQL::Basket->db_table;
  my $request = "delete from $table_name where order_id = 0 AND status = 0 AND";
  my $dbh = $keeper->SQL;
  my @vals;
  if ( exists $opts{uid} ) {
	$request .= " uid in (?)";
	push @vals, $opts{uid};
  } elsif ( exists $opts{session} ) {
	$request .= " session in (?)";
	push @vals, $opts{session};
  }
  my $statement = $dbh->prepare ($request);
  $statement->execute( @vals ) || $log->error("DBI execute error on $request\n"."\ncalled with opts:\n".Data::Dumper::Dumper(\%opts));;
  $statement->finish;
}


### Метод пересчета корзины
# Принимает на вход параметры:
#	session		=> session_id пользователя
#	uid		=> UID пользователя
#	delete		=> массив или отдельный item_id
#	renumber	=> ссылка на хеш вида item => number
#############################################################
sub recount {
  my $self = shift;
  my (%opts) = @_;

  warn "Recount Started!!!\n"			if $DEBUG;
  return	unless exists $opts{uid} || exists $opts{session} || exists $opts{order_id};
  my $basket = $self->get_basket ( %opts );
  return	unless ref $basket eq 'ARRAY' && @$basket;

  warn Dumper(\%opts)				if $DEBUG;

  my $total = 0;
  my $sum = 0;
  my @new_basket;
  my $session_no_store = delete $opts{session_no_store};
  foreach my $item ( @$basket ) {
	my $delete = 0;
	if ( exists $opts{renumber} && ref $opts{renumber} eq 'HASH' && exists $opts{renumber}{$item->id} && int($opts{renumber}{$item->id}) == 0 ) {
		$delete = 1;
	} elsif ( exists $opts{delete} && ref $opts{delete} eq 'ARRAY' ) {
		$delete = 1	if grep { $_ == $item->id } @{ $opts{delete} };
	} elsif ( exists $opts{delete} && $opts{delete} ) {
		$delete = 1	if $item->id == $opts{delete};
	}
	if ( $delete ) {
		warn "Item ID=".$item->id." DELETE\n"	if $DEBUG;
		$item->delete();
		next;
	} else {
		my $store = 0;
		if ( exists $opts{renumber} && ref $opts{renumber} eq 'HASH' && exists $opts{renumber}{$item->id} && $opts{renumber}{$item->id} != $item->number ) {
			$item->number( $opts{renumber}{$item->id} );
			$store = 1;
		}
		if ( exists $opts{price} && ref $opts{price} eq 'HASH' && exists $opts{price}{$item->id} && $opts{price}{$item->id} != $item->price ) {
			$item->price( $opts{price}{$item->id} );
			$store = 1;
		}
		$item->store	if $store;
		$total += $item->number;
		$sum += $item->number * $item->price;
		push @new_basket, $item;
	}
  }
  my @plugins = split (/[\ |\t]+/, $state->{plugins});
  if ( !$session_no_store && grep { $_ eq 'session' } @plugins && ref $session ) {
	$session->set ( basket_total => $total, basket_sum => $sum );
  }
  return ($total, $sum, \@new_basket);
}


sub get_orders {
  my $self = shift;
  my (%opts) = @_;

  my $list = delete $opts{list};
  my $count = delete $opts{count};
  if ( $count ) {
	delete  $opts{order_by};
	my $item_count = $keeper->get_documents (
			class	=> 'webshop::Order',
			count	=> 1,
			%opts,
		);
	return $item_count;
  } else {
	$opts{order_by} ||= 'status';
	my @items = $keeper->get_documents (
			class	=> 'webshop::Order',
			%opts,
		);
	if ( exists $opts{id} && defined $opts{id} && !ref $opts{id} ) {
		if ( $list ) {
			$items[0]->{list} = $self->get_order_list( order_id => $opts{id} );
		}
		return $items[0];
	} else {
		if ( $list ) {
			map { $_->{list} = $self->get_order_list( order_id => $_->id ) } @items;
		}
		return \@items;
	}
  }
}


sub get_order_list {
  my $self = shift;
  my (%opts) = @_;

  return	unless $opts{order_id};

  $opts{status} = 1;
  my @items = $keeper->get_documents (
		class	=> 'webshop::Basket',
		%opts,
	);
  my $total = 0;
  my $sum = 0;
  foreach my $item ( @items ) {
	my $Item = $item->class->new( $keeper, $item->id );
	$item->{item} = $Item;
	if ( $item->status == 1 ) {
		$total += $item->number;
		$sum += $item->number * $item->price;
	}
  }
  return \@items;
}


### Метод приведения купонов для пользователя в момент логина
#############################################################
sub merge_coupons {
    my $self = shift;
    my (%opts) = @_;

    warn "Merge (coupons) begin: ".Dumper(\%opts)."\n"		if $DEBUG;
    return	unless $opts{uid} && $opts{session};

    my @items = $keeper->get_links (
		class	=> 'webshop::OrderCouponLink',
		session	=> $opts{session},
		source_id	=> 0,
	);
    my $merge_to = $keeper->get_links (
		class	=> 'webshop::OrderCouponLink',
		uid	=> $opts{uid},
		source_id	=> 0,
		return_mode	=> 'array_ref',
	);
    my %merge_to;
    foreach my $link ( @$merge_to ) {
	if ( exists $merge_to{$link->dest_id} ) {
		$link->delete;
	} else {
		$merge_to{$link->dest_id} = $link;
	}
    }
    foreach my $item ( @items ) {
	if ( exists $merge_to{$item->dest_id} ) {
		$item->delete;
	} else {
		$item->session( undef );
		$item->uid( $opts{uid} );
		$item->store;
	}
    }
}


sub check_discount {
    my $self = shift;
    my (%opts) = @_;

    warn "Check discount begin:\n"		if $DEBUG;
    my %dopts;
    if ( exists $opts{uid} && $opts{uid} ) {
	$dopts{uid} = [0,1];
    } else {
	$dopts{uid} = 0;
    }
    my $basket = exists $opts{basket} ? $opts{basket} : $self->get_basket( %opts, with_products => 1 );
    return 0	unless ref $basket eq 'ARRAY' && @$basket;
    my @basket = grep { exists $_->{item} && $_->{item} } @$basket;
    return 0	unless @basket;

    my $payment = delete $opts{payment};
    if ( $payment && !ref $payment ) {
	if ( $payment =~ /^\d+$/ ) {
		$payment = $keeper->get_document_by_id($payment, class => 'webshop::Payment');
	}
    }

    my $now = Contenido::DateTime->new;
    my @discounts = $keeper->get_documents(
			class	=> 'webshop::Discount',
			status	=> 1,
			interval=> [$now, $now],
			%dopts,
		);
    my @summoned = sort { $b->min_sum <=> $a->min_sum } grep { $_->min_sum && $_->discount && $_->min_sum =~ /^\d+$/ } @discounts;
    my ($total, $sum) = $self->basket_count( \@basket );
    return 0	unless $sum;
    my $result = 0;
    foreach my $discount ( @summoned ) {
	if ( $sum > $discount->min_sum ) {
		my $res = $discount->get_discount( basket => \@basket );
		$result = $res	if $res > $result;
	}
    }

    my $summarize = 0;
    if ( ref $payment && $payment->discount && $payment->discount > 0 ) {
	my $payment_discount = $payment->discount / 100;
	my $payment_discount_sum = 0;
	if ( $payment->exclude_specials ) {
		foreach my $item ( @basket ) {
			next	if $item->special_price;
			$payment_discount_sum += $item->price;
		}
		$payment_discount_sum = int($payment_discount_sum * $payment_discount);
	} else {
		$payment_discount_sum = int($sum * $payment_discount);
	}
	if ( $payment->summarize ) {
		$summarize = 1;
		$result += $payment_discount_sum	if $payment_discount_sum + $result < $sum;
	} else {
		$result = $payment_discount_sum		if $payment_discount_sum > $result;
	}
    }

    $result = 0		if $result >= $sum;
    return ($result, $summarize);
}


sub check_coupons {
    my $self = shift;
    my (%opts) = @_;

    warn "Check coupons begin:\n"		if $DEBUG;
    my %dopts;
    if ( exists $opts{uid} && $opts{uid} ) {
	$dopts{luid} = $opts{uid};
    } else {
	$dopts{lsession} = $opts{session};
    }
    my $basket = exists $opts{basket} ? $opts{basket} : $self->get_basket( %opts, with_products => 1 );
    return (0, [])	unless ref $basket eq 'ARRAY' && @$basket;
    my @basket = grep { exists $_->{item} && $_->{item} } @$basket;
    return (0, [])	unless @basket;

    my $now = Contenido::DateTime->new;
    my @coupons = exists $opts{coupons} && ref $opts{coupons} eq 'ARRAY' ? @{$opts{coupons}} : $keeper->get_documents(
			class	=> 'webshop::Coupon',
			lclass  => 'webshop::OrderCouponLink',
			lsource	=> 0,
			status	=> 1,
			interval=> [$now->ymd, $now->ymd],
			%dopts,
		);
    return (0, [])	unless @coupons;
    my @usable = grep { $_->discount } @coupons;
    warn "We count on ".scalar(@usable)." coupons: [".join(', ', map { $_->id.'. '.$_->name } @usable)."]\n"	if $DEBUG;
    my ($total, $sum) = $self->basket_count( \@basket );
    return (0, \@usable)	unless $sum;
    my %summoned = ( coupons => [], discount => 0 );
    my @groups;
    foreach my $coupon ( @usable ) {
	if ( $coupon->summon ) {
		push @{$summoned{coupons}}, $coupon;
	} else {
		push @groups, { coupons => [$coupon], discount => 0 };
	}
    }
    my $result = 0;
    foreach my $coupon ( @usable ) {
	my $res = $coupon->get_discount( basket => \@basket );
	$coupon->{result} = $res;
    }
    foreach my $group ( @groups ) {
	$group->{discount} = $group->{coupons}->[0]->{result};
	$group->{coupons}->[0]->{afflict_order} = 1;
    }
    if ( @{$summoned{coupons}} ) {
	@{$summoned{coupons}} = sort { $b->{result} <=> $a->{result} } @{$summoned{coupons}};
	my $check = $sum;
	foreach my $coupon ( @{$summoned{coupons}} ) {
		if ( ($coupon->{result} > 0) && ($check - $coupon->{result} > 0) ) {
			$summoned{discount} += $coupon->{result};
			$check -= $coupon->{result};
			$coupon->{afflict_order} = 1;
		} else {
			$coupon->{afflict_order} = 0;
		}
	}
	unshift @groups, \%summoned;
    }

    @groups = sort { $b->{discount} <=> $a->{discount} } @groups;
    my %chosen = map { $_->id => $_ } @{$groups[0]->{coupons}};
    foreach my $coupon ( @usable ) {
	if ( !exists $chosen{$coupon->id} ) {
		$coupon->{afflict_order} = 0;
	}
    }
    warn "We choose ".scalar(@{$groups[0]->{coupons}})." coupons: [".join(', ', map { $_->id.'. '.$_->name } @{$groups[0]->{coupons}})."]\n"	if $DEBUG;
    return ($groups[0]->{discount}, \@usable);
}


sub register_coupon {
	my $self = shift;
	my ($code, $session) = @_;
	my %result;
	my %opts;

	if ( ref $session ) {
		if ( $code ) {
			$code =~ s/([%_\\])/\\$1/g;
			if ( $session->id ) {
				$opts{uid} = $session->{id};
			} else {
				$opts{session} = $session->_session_id;
			}
			my $basket = $self->get_basket (
					%opts,
					with_products   => 1
				);
			my $now = Contenido::DateTime->new;
			my @reglinks = $keeper->get_links (
						class	=> 'webshop::OrderCouponLink',
						source_id	=> 0,
						%opts,
					);
			my %cids = map { $_->dest_id => 1 } @reglinks;
			my @cids = keys %cids;
			my @registered = @cids ? $keeper->get_documents(
					id	=> \@cids,
					class	=> 'webshop::Coupon',
				) : ();
			my ($coupon) = grep { uc(Encode::decode('utf-8', $_->code)) eq uc(Encode::decode('utf-8', $code)) } @registered;
			@registered = grep {
				my $bt = Contenido::DateTime->new( postgres => $_->dtime );
				my $et = Contenido::DateTime->new( postgres => $_->etime );
				$now >= $bt && $now <= $et && $_->status == 1
			} @registered;
			$result{coupons} = \@registered;
			if ( ref $coupon ) {
				$result{error} = 'Такой купон уже зарегистрирован';
				$result{found} = $coupon;
			} else {
				($coupon) = $keeper->get_documents(
						class   => 'webshop::Coupon',
						code    => $code,
						uid     => 0,
						status  => [1,3],
						interval        => [$now, $now],
						ilike   => 1,
					);
				if ( $session->id && !ref $coupon ) {
					($coupon) = $keeper->get_documents(
							class   => 'webshop::Coupon',
							code    => $code,
							uid     => $session->id,
							status  => [1,3],
							interval        => [$now, $now],
							ilike   => 1,
						);
				}
				if ( ref $coupon ) {
					if ( $coupon->uid && $coupon->status == 3 ) {
						$result{error} = 'Купон уже использован';
					}
					unless ( exists $result{error} && $result{error} ) {
						my $coupon_link = webshop::OrderCouponLink->new( $keeper );
						$coupon_link->status( 0 );
						if ( $session->id ) {
							$coupon_link->uid( $session->id );
						} else {
							$coupon_link->uid( 0 );
							$coupon_link->session( $session->_session_id );
						}
						$coupon_link->dest_id( $coupon->id );
						$coupon_link->dest_class( $coupon->class );
						$coupon_link->source_id( 0 );
						$coupon_link->source_class( 'webshop::Order' );
						$coupon_link->store;
						$result{created} = $coupon;
						push @registered, $coupon;
					}
				} else {
					$result{error} = 'Купон не найден';
				}
			}
		} else {
			$result{error} = 'Вы не указали код купона';
		}
	} else {
		$result{error} = 'Фатальная ошибка. Не работают сессии! Обратитесь в службу поддержки магазина';
	}

	return \%result;
}


sub price_format {
	my $self = shift;
	my $price = shift;

	if ( defined $price ) {
		$price = reverse $price;
		$price =~ s/(\d{3})/$1\ /g;
		$price = reverse $price;
	}

	return $price;
}


1;