EC-CUBE4でポイント履歴を作成する方法

EC-CUBE Advent Calendar 2020 19日目の記事です。

EC-CUBE4ではEC-CUBE3系で無くなったポイント機能が標準で搭載されています。

このポイント機能ですが、ポイント利用とポイント付与しかなくポイント履歴機能は存在しません。

時々お客さんによってはポイント履歴が欲しいと言われることがありますので、今回はポイント履歴を作成します。

今回の変更は複雑な箇所も多いため慣れていない方はお気をつけください。

会員登録時と商品レビュー公開時にも自動ポイント付与及びポイント履歴として表示させるようにカスタマイズも可能ですが今回は省きます。

ポイント履歴テーブルの作成

ポイント履歴テーブルが無ければ何も始まらないため、ポイント履歴テーブル(dtb_pointo_hisotry)を作成します。

src/Eccube/Entity直下に以下の内容を保存します。

  • src/Eccube/Entity/PointHistory.php
<?php

namespace Eccube\Entity;

use Doctrine\ORM\Mapping as ORM;

if (!class_exists('\Eccube\Entity\PointHistory')) {
    /**
     * PointHistory
     *
     * @ORM\Table(name="dtb_point_history")
     * @ORM\InheritanceType("SINGLE_TABLE")
     * @ORM\HasLifecycleCallbacks()
     * @ORM\Entity(repositoryClass="Eccube\Repository\PointHistoryRepository")
     */
    class PointHistory extends \Eccube\Entity\AbstractEntity
    {
        const TYPE_NULL = 0;
        const TYPE_ADD = 1;
        const TYPE_USE = 2;
        const TYPE_EXPIRED = 3;
        const TYPE_MIGRATION = 4;

        const EVENT_NULL = 0;
        const EVENT_SHOPPING = 1;
        const EVENT_ENTRY = 2;
        const EVENT_ORDER_CANCEL = 3;
        const EVENT_MANUAL = 4;
        const EVENT_EXPIRED = 5;
        const EVENT_MIGRATION = 6;
        const EVENT_REVIEW = 7;

        /**
         * @var int
         *
         * @ORM\Column(name="id", type="integer", options={"unsigned":true})
         * @ORM\Id
         * @ORM\GeneratedValue(strategy="IDENTITY")
         */
        private $id;

        /**
         * @var int
         *
         * @ORM\Column(name="point", type="integer")
         */
        private $point = 0;

        /**
         * @var \Eccube\Entity\Customer
         *
         * @ORM\ManyToOne(targetEntity="Eccube\Entity\Customer", inversedBy="Orders")
         * @ORM\JoinColumns({
         *   @ORM\JoinColumn(name="customer_id", referencedColumnName="id")
         * })
         */
        private $Customer;

        /**
         * @var \Eccube\Entity\Order
         *
         * @ORM\ManyToOne(targetEntity="Eccube\Entity\Order", inversedBy="MailHistories")
         * @ORM\JoinColumns({
         *   @ORM\JoinColumn(name="order_id", referencedColumnName="id")
         * })
         */
        private $Order;

        /**
         * @var \DateTime
         *
         * @ORM\Column(name="create_date", type="datetimetz")
         */
        private $create_date;

        /**
         * @var int
         *
         * @ORM\Column(name="record_type", type="integer", nullable=true)
         */
        private $record_type = self::TYPE_NULL;

        /**
         * @var int
         *
         * @ORM\Column(name="record_event", type="integer", nullable=true)
         */
        private $record_event = self::EVENT_NULL;

        /**
         * Get id.
         *
         * @return int
         */
        public function getId()
        {
            return $this->id;
        }

        /**
         * Set point.
         *
         * @param int $point
         *
         * @return PointHistory
         */
        public function setPoint($point)
        {
            $this->point = $point;

            return $this;
        }

        /**
         * Get point.
         *
         * @return int
         */
        public function getPoint()
        {
            return $this->point;
        }

        /**
         * Set createDate.
         *
         * @param \DateTime $createDate
         *
         * @return PointHistory
         */
        public function setCreateDate($createDate)
        {
            $this->create_date = $createDate;

            return $this;
        }

        /**
         * Get createDate.
         *
         * @return \DateTime
         */
        public function getCreateDate()
        {
            return $this->create_date;
        }

        /**
         * Set recordType.
         *
         * @param int|null $recordType
         *
         * @return PointHistory
         */
        public function setRecordType($recordType = null)
        {
            $this->record_type = $recordType;

            return $this;
        }

        /**
         * Get recordType.
         *
         * @return int|null
         */
        public function getRecordType()
        {
            return $this->record_type;
        }

        /**
         * Set recordEvent.
         *
         * @param int|null $recordEvent
         *
         * @return PointHistory
         */
        public function setRecordEvent($recordEvent = null)
        {
            $this->record_event = $recordEvent;

            return $this;
        }

        /**
         * Get recordEvent.
         *
         * @return int|null
         */
        public function getRecordEvent()
        {
            return $this->record_event;
        }

        /**
         * Set customer.
         *
         * @param \Eccube\Entity\Customer|null $customer
         *
         * @return PointHistory
         */
        public function setCustomer(\Eccube\Entity\Customer $customer = null)
        {
            $this->Customer = $customer;

            return $this;
        }

        /**
         * Get customer.
         *
         * @return \Eccube\Entity\Customer|null
         */
        public function getCustomer()
        {
            return $this->Customer;
        }

        /**
         * Set order.
         *
         * @param \Eccube\Entity\Order|null $order
         *
         * @return PointHistory
         */
        public function setOrder(\Eccube\Entity\Order $order = null)
        {
            $this->Order = $order;

            return $this;
        }

        /**
         * Get order.
         *
         * @return \Eccube\Entity\Order|null
         */
        public function getOrder()
        {
            return $this->Order;
        }
    }
}

constで定義している箇所は後ほど利用します。

次にPointHistoryRepositoryクラスを作成します。

  • src/Eccube/Repository/PointHistoryRepository.php
<?php

namespace Eccube\Repository;

use Eccube\Entity\Customer;
use Eccube\Entity\PointHistory;
use Symfony\Bridge\Doctrine\RegistryInterface;

/**
 * PointHistoryRepository
 *
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
 */
class PointHistoryRepository extends AbstractRepository
{
    public function __construct(RegistryInterface $registry)
    {
        parent::__construct($registry, PointHistory::class);
    }

    public function getQueryBuilder(Customer $Customer)
    {
        $qb = $this->createQueryBuilder('ph')
            ->select('ph')
            ->where('ph.Customer = :Customer')
            ->setParameter('Customer', $Customer)
            ->addOrderBy('ph.create_date', 'DESC');

        return $qb;
    }
}

以上でポイント履歴テーブルの準備ができました。

ポイント履歴保存関数の作成

次に、ポイント履歴を保存するためのカスタマイズを行います。

OrderStateMachineクラスとPointHelperクラスの変更をします。変更内容は説明するのが面倒くさいので、それぞれの全ソースを載せます。

  • src/Eccube/Service/PointHelper.php
<?php

/*
 * This file is part of EC-CUBE
 *
 * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
 *
 * http://www.ec-cube.co.jp/
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Eccube\Service;

use Doctrine\ORM\EntityManagerInterface;
use Eccube\Entity\Customer;
use Eccube\Entity\ItemHolderInterface;
use Eccube\Entity\Master\OrderItemType;
use Eccube\Entity\Master\TaxDisplayType;
use Eccube\Entity\Master\TaxType;
use Eccube\Entity\OrderItem;
use Eccube\Entity\PointHistory;
use Eccube\Repository\BaseInfoRepository;
use Eccube\Service\PurchaseFlow\Processor\PointProcessor;

class PointHelper
{
    /**
     * @var BaseInfoRepository
     */
    protected $baseInfoRepository;

    /**
     * @var EntityManagerInterface
     */
    protected $entityManager;

    /**
     * PointHelper constructor.
     *
     * @param BaseInfoRepository $baseInfoRepository
     * @param EntityManagerInterface $entityManager
     */
    public function __construct(BaseInfoRepository $baseInfoRepository, EntityManagerInterface $entityManager)
    {
        $this->baseInfoRepository = $baseInfoRepository;
        $this->entityManager = $entityManager;
    }

    /**
     * ポイント設定が有効かどうか.
     *
     * @return bool
     *
     * @throws \Doctrine\ORM\NoResultException
     * @throws \Doctrine\ORM\NonUniqueResultException
     */
    public function isPointEnabled()
    {
        $BaseInfo = $this->baseInfoRepository->get();

        return $BaseInfo->isOptionPoint();
    }

    /**
     * ポイントを金額に変換する.
     *
     * @param $point ポイント
     *
     * @return float|int 金額
     *
     * @throws \Doctrine\ORM\NoResultException
     * @throws \Doctrine\ORM\NonUniqueResultException
     */
    public function pointToPrice($point)
    {
        $BaseInfo = $this->baseInfoRepository->get();

        return intval($point * $BaseInfo->getPointConversionRate());
    }

    /**
     * ポイントを値引き額に変換する. マイナス値を返す.
     *
     * @param $point ポイント
     *
     * @return float|int 金額
     *
     * @throws \Doctrine\ORM\NoResultException
     * @throws \Doctrine\ORM\NonUniqueResultException
     */
    public function pointToDiscount($point)
    {
        return $this->pointToPrice($point) * -1;
    }

    /**
     * 金額をポイントに変換する.
     *
     * @param $price
     *
     * @return float ポイント
     *
     * @throws \Doctrine\ORM\NoResultException
     * @throws \Doctrine\ORM\NonUniqueResultException
     */
    public function priceToPoint($price)
    {
        $BaseInfo = $this->baseInfoRepository->get();

        return floor($price / $BaseInfo->getPointConversionRate());
    }

    /**
     * 明細追加処理.
     *
     * @param ItemHolderInterface $itemHolder
     * @param integer $discount
     */
    public function addPointDiscountItem(ItemHolderInterface $itemHolder, $discount)
    {
        $DiscountType = $this->entityManager->find(OrderItemType::class, OrderItemType::POINT);
        $TaxInclude = $this->entityManager->find(TaxDisplayType::class, TaxDisplayType::INCLUDED);
        $Taxation = $this->entityManager->find(TaxType::class, TaxType::NON_TAXABLE);

        $OrderItem = new OrderItem();
        $OrderItem->setProductName($DiscountType->getName())
            ->setPrice($discount)
            ->setQuantity(1)
            ->setTax(0)
            ->setTaxRate(0)
            ->setRoundingType(null)
            ->setOrderItemType($DiscountType)
            ->setTaxDisplayType($TaxInclude)
            ->setTaxType($Taxation)
            ->setOrder($itemHolder)
            ->setProcessorName(PointProcessor::class);
        $itemHolder->addItem($OrderItem);
    }

    /**
     * 既存のポイント明細を削除する.
     *
     * @param ItemHolderInterface $itemHolder
     */
    public function removePointDiscountItem(ItemHolderInterface $itemHolder)
    {
        foreach ($itemHolder->getItems() as $item) {
            if ($item->getProcessorName() == PointProcessor::class) {
                $itemHolder->removeOrderItem($item);
                $this->entityManager->remove($item);
            }
        }
    }

    public function prepare(ItemHolderInterface $itemHolder, $point)
    {
        // ユーザの保有ポイントを減算
        $Customer = $itemHolder->getCustomer();

        // ユーザの保有ポイントを減算する履歴を追加
        $PointHistory = new PointHistory();
        $PointHistory->setRecordType(PointHistory::TYPE_USE);
        $PointHistory->setRecordEvent(PointHistory::EVENT_SHOPPING);
        $PointHistory->setPoint(-$point);
        $PointHistory->setCustomer($Customer);
        $PointHistory->setOrder($itemHolder);
        $this->entityManager->persist($PointHistory);
        $this->entityManager->flush();

        $Customer->setPoint($Customer->getPoint() - $point);
    }

    public function rollback(ItemHolderInterface $itemHolder, $point)
    {
        $Order = $itemHolder;
        $event = ($itemHolder->getOrderNo() == null) ? PointHistory::EVENT_SHOPPING : PointHistory::EVENT_ORDER_CANCEL;
        $Customer = $itemHolder->getCustomer();

        // ユーザの保有ポイントを復元する履歴を追加
        $PointHistory = new PointHistory();
        $PointHistory->setRecordType(PointHistory::TYPE_ADD);
        $PointHistory->setRecordEvent($event);
        $PointHistory->setPoint($point);
        $PointHistory->setCustomer($Customer);
        $PointHistory->setOrder($Order);
        $this->entityManager->persist($PointHistory);
        $this->entityManager->flush();

        // 利用したポイントをユーザに戻す.
        $Customer->setPoint($Customer->getPoint() + $point);
    }

}
  • src/Eccube/Service/OrderStateMachine.php
<?php

/*
 * This file is part of EC-CUBE
 *
 * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
 *
 * http://www.ec-cube.co.jp/
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Eccube\Service;

use Doctrine\ORM\EntityManagerInterface;
use Eccube\Entity\Master\OrderStatus;
use Eccube\Entity\Order;
use Eccube\Entity\PointHistory;
use Eccube\Repository\Master\OrderStatusRepository;
use Eccube\Service\PurchaseFlow\Processor\PointProcessor;
use Eccube\Service\PurchaseFlow\Processor\StockReduceProcessor;
use Eccube\Service\PurchaseFlow\PurchaseContext;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Workflow\Event\Event;
use Symfony\Component\Workflow\StateMachine;

class OrderStateMachine implements EventSubscriberInterface
{
    /**
     * @var StateMachine
     */
    private $machine;

    /**
     * @var OrderStatusRepository
     */
    private $orderStatusRepository;

    /**
     * @var PointProcessor
     */
    private $pointProcessor;
    /**
     * @var StockReduceProcessor
     */
    private $stockReduceProcessor;

    /**
     * @var EntityManagerInterface
     */
    protected $entityManager;

    public function __construct(StateMachine $_orderStateMachine, OrderStatusRepository $orderStatusRepository, PointProcessor $pointProcessor, StockReduceProcessor $stockReduceProcessor, EntityManagerInterface $entityManager)
    {
        $this->machine = $_orderStateMachine;
        $this->orderStatusRepository = $orderStatusRepository;
        $this->pointProcessor = $pointProcessor;
        $this->stockReduceProcessor = $stockReduceProcessor;
        $this->entityManager = $entityManager;
    }

    /**
     * 指定ステータスに遷移.
     *
     * @param Order $Order 受注
     * @param OrderStatus $OrderStatus 遷移先ステータス
     */
    public function apply(Order $Order, OrderStatus $OrderStatus)
    {
        $context = $this->newContext($Order);
        $transition = $this->getTransition($context, $OrderStatus);
        if ($transition) {
            $this->machine->apply($context, $transition->getName());
        } else {
            throw new \InvalidArgumentException();
        }
    }

    /**
     * 指定ステータスに遷移できるかどうかを判定.
     *
     * @param Order $Order 受注
     * @param OrderStatus $OrderStatus 遷移先ステータス
     *
     * @return boolean 指定ステータスに遷移できる場合はtrue
     */
    public function can(Order $Order, OrderStatus $OrderStatus)
    {
        return !is_null($this->getTransition($this->newContext($Order), $OrderStatus));
    }

    private function getTransition(OrderStateMachineContext $context, OrderStatus $OrderStatus)
    {
        $transitions = $this->machine->getEnabledTransitions($context);
        foreach ($transitions as $t) {
            if (in_array($OrderStatus->getId(), $t->getTos())) {
                return $t;
            }
        }

        return null;
    }

    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents()
    {
        return [
            'workflow.order.completed' => ['onCompleted'],
            'workflow.order.transition.pay' => ['updatePaymentDate'],
            'workflow.order.transition.cancel' => [['rollbackStock'], ['rollbackUsePoint']],
            'workflow.order.transition.back_to_in_progress' => [['commitStock'], ['commitUsePoint']],
            'workflow.order.transition.ship' => [['commitAddPoint']],
            'workflow.order.transition.return' => [['rollbackUsePoint'], ['rollbackAddPoint']],
            'workflow.order.transition.cancel_return' => [['commitUsePoint'], ['commitAddPoint']],
        ];
    }

    /*
     * Event handlers.
     */

    /**
     * 入金日を更新する.
     *
     * @param Event $event
     */
    public function updatePaymentDate(Event $event)
    {
        /* @var Order $Order */
        $Order = $event->getSubject()->getOrder();
        $Order->setPaymentDate(new \DateTime());
    }

    /**
     * 会員の保有ポイントを減らす.
     *
     * @param Event $event
     *
     * @throws PurchaseFlow\PurchaseException
     */
    public function commitUsePoint(Event $event)
    {
        /* @var Order $Order */
        $Order = $event->getSubject()->getOrder();
        $this->pointProcessor->prepare($Order, new PurchaseContext());
    }

    /**
     * 利用ポイントを会員に戻す.
     *
     * @param Event $event
     */
    public function rollbackUsePoint(Event $event)
    {
        /* @var Order $Order */
        $Order = $event->getSubject()->getOrder();
        $this->pointProcessor->rollback($Order, new PurchaseContext());
    }

    /**
     * 在庫を減らす.
     *
     * @param Event $event
     *
     * @throws PurchaseFlow\PurchaseException
     */
    public function commitStock(Event $event)
    {
        /* @var Order $Order */
        $Order = $event->getSubject()->getOrder();
        $this->stockReduceProcessor->prepare($Order, new PurchaseContext());
    }

    /**
     * 在庫を戻す.
     *
     * @param Event $event
     */
    public function rollbackStock(Event $event)
    {
        /* @var Order $Order */
        $Order = $event->getSubject()->getOrder();
        $this->stockReduceProcessor->rollback($Order, new PurchaseContext());
    }

    /**
     * 会員に加算ポイントを付与する.
     *
     * @param Event $event
     */
    public function commitAddPoint(Event $event)
    {
        /* @var Order $Order */
        $Order = $event->getSubject()->getOrder();
        $Customer = $Order->getCustomer();
        if ($Customer) {
            $addPoint = intval($Order->getAddPoint());

            // ポイント履歴を追加
            $PointHistory = new PointHistory();
            $PointHistory->setRecordType(PointHistory::TYPE_ADD);
            $PointHistory->setRecordEvent(PointHistory::EVENT_SHOPPING);
            $PointHistory->setPoint($addPoint);
            $PointHistory->setCustomer($Customer);
            $PointHistory->setOrder($Order);
            $this->entityManager->persist($PointHistory);
            $this->entityManager->flush();

            $Customer->setPoint(intval($Customer->getPoint()) + intval($Order->getAddPoint()));
        }
    }

    /**
     * 会員に付与した加算ポイントを取り消す.
     *
     * @param Event $event
     */
    public function rollbackAddPoint(Event $event)
    {
        /* @var Order $Order */
        $Order = $event->getSubject()->getOrder();
        $Customer = $Order->getCustomer();
        if ($Customer) {
            $addPoint = intval($Order->getAddPoint());

            // 加算ポイントを減算するポイント履歴を追加
            $PointHistory = new PointHistory();
            $PointHistory->setRecordType(PointHistory::TYPE_USE);
            $PointHistory->setRecordEvent(PointHistory::EVENT_ORDER_CANCEL);
            $PointHistory->setPoint(-$addPoint);
            $PointHistory->setCustomer($Customer);
            $PointHistory->setOrder($Order);
            $this->entityManager->persist($PointHistory);
            $this->entityManager->flush();

            $Customer->setPoint(intval($Customer->getPoint()) - intval($Order->getAddPoint()));
        }
    }

    /**
     * 受注ステータスを再設定.
     * {@link StateMachine}によって遷移が終了したときには{@link Order#OrderStatus}のidが変更されるだけなのでOrderStatusを設定し直す.
     *
     * @param Event $event
     */
    public function onCompleted(Event $event)
    {
        /** @var $context OrderStateMachineContext */
        $context = $event->getSubject();
        $Order = $context->getOrder();
        $CompletedOrderStatus = $this->orderStatusRepository->find($context->getStatus());
        $Order->setOrderStatus($CompletedOrderStatus);
    }

    private function newContext(Order $Order)
    {
        return new OrderStateMachineContext((string) $Order->getOrderStatus()->getId(), $Order);
    }
}

class OrderStateMachineContext
{
    /** @var string */
    private $status;

    /** @var Order */
    private $Order;

    /**
     * OrderStateMachineContext constructor.
     *
     * @param string $status
     * @param Order $Order
     */
    public function __construct($status, Order $Order)
    {
        $this->status = $status;
        $this->Order = $Order;
    }

    /**
     * @return string
     */
    public function getStatus()
    {
        return $this->status;
    }

    /**
     * @param string $status
     */
    public function setStatus($status)
    {
        $this->status = $status;
    }

    /**
     * @return Order
     */
    public function getOrder()
    {
        return $this->Order;
    }
}

この2つのクラスで何を行っているかというと、それぞれポイントの加算、減算時にポイント履歴テーブルへ保存を行う処理が追加されています。

ポイント履歴画面の作成

マイページにポイント履歴画面を作成していきます。

先ずはポイント履歴を表示するためのContorlleとtwigファイルを用意します。

  • src/Eccube/Contoroller/Mypage/MypageContoller.php
<?php
namespace Eccube\Controller\Mypage;

・
・
・
use Eccube\Repository\PointHistoryRepository;


class MypageController extends AbstractController
{
    /**
     * @var ProductRepository
     */
    protected $productRepository;

    /**
     * @var CustomerFavoriteProductRepository
     */
    protected $customerFavoriteProductRepository;

    /**
     * @var BaseInfo
     */
    protected $BaseInfo;

    /**
     * @var CartService
     */
    protected $cartService;

    /**
     * @var OrderRepository
     */
    protected $orderRepository;

    /**
     * @var PurchaseFlow
     */
    protected $purchaseFlow;

    /**
     * @var MailService
     */
    protected $mailService;

    /**
     * @var HolidayRepository
     */
    protected $holidayRepository;

    /**
     * @var PointHistoryRepository
     */
    protected $pointHistoryRepository;

    /**
     * MypageController constructor.
     *
     * @param OrderRepository $orderRepository
     * @param CustomerFavoriteProductRepository $customerFavoriteProductRepository
     * @param CartService $cartService
     * @param BaseInfoRepository $baseInfoRepository
     * @param PurchaseFlow $purchaseFlow
     * @param MailService $mailService
     * @param HolidayRepository $holidayRepository
     * @param PointHistoryRepository $pointHistoryRepository
     */
    public function __construct(
        OrderRepository $orderRepository,
        CustomerFavoriteProductRepository $customerFavoriteProductRepository,
        CartService $cartService,
        BaseInfoRepository $baseInfoRepository,
        PurchaseFlow $purchaseFlow,
        MailService $mailService,
        HolidayRepository $holidayRepository,
        PointHistoryRepository $pointHistoryRepository
    ) {
        $this->orderRepository = $orderRepository;
        $this->customerFavoriteProductRepository = $customerFavoriteProductRepository;
        $this->BaseInfo = $baseInfoRepository->get();
        $this->cartService = $cartService;
        $this->purchaseFlow = $purchaseFlow;
        $this->mailService = $mailService;
        $this->holidayRepository = $holidayRepository;
        $this->pointHistoryRepository = $pointHistoryRepository;
    }

・
・
・

    /**
     * ポイント履歴を表示する.
     *
     * @Route("/mypage/point", name="mypage_point")
     * @Template("Mypage/point.twig")
     */
    public function point(Request $request, Paginator $paginator)
    {
        $Customer = $this->getUser();

        // マイページ表示用のポイント履歴に、0ポイントの履歴は含めないようにする
        $qb = $this->pointHistoryRepository->getQueryBuilder($Customer);
        $qb->andWhere('ph.point != 0');

        $pagination = $paginator->paginate(
            $qb,
            $request->get('pageno', 1),
            $this->eccubeConfig['eccube_search_pmax']
        );

        return [
            'Customer' => $Customer,
            'pagination' => $pagination,
        ];
    }
}

必要な箇所を抜粋して載せていますが、ポイント履歴を取得するために新たにpoint関数を作成しました。

次にpoint.twigを作成します。

twigのタグは気にせず、ポイント履歴を表示させている箇所だけを載せます。サンプルではBootstrap4で作成しています。

  • app/template/default/Mypage/point.twig
{% extends 'default_frame.twig' %}

{% set mypageno = 'point' %}

{% block main %}

・
・
・
{% include 'Mypage/navi.twig' %}

<table class="table table-bordered">
    <thead class="thead-light">
    <tr class="text-center">
        <th class="text-nowrap">日付</th>
        <th class="text-nowrap">ご注文番号</th>
        <th class="text-nowrap">利用・獲得</th>
        <th class="text-nowrap">ポイント数</th>
    </tr>
    </thead>
    <tbody class="text-center">
    {% for history in pagination %}
        <tr>
            <td data-title="日付">
                {{ history.create_date|date_day }}
            </td>
            <td data-title="ご注文番号">
                {% if history.order is not null %}
                    <a href="{{ url('mypage_history', {'order_no': history.order.order_no}) }}">
                        {{ history.order.order_no }}
                    </a>
                {% endif %}
                &nbsp;
            </td>
            <td data-title="利用・獲得">
                {% if history.record_event == 1 %}
                    {% if history.record_type == 1 %}
                        獲得
                    {% elseif history.record_type == 2 %}
                        利用
                    {% endif %}
                {% elseif history.record_event == 2 %}
                    会員登録 特典
                {% elseif history.record_event == 3 %}
                    ご注文取り消し
                {% elseif history.record_event == 4 %}
                    調整
                {% elseif history.record_event == 5 %}
                    期限切れのため失効
                {% elseif history.record_event == 6 %}
                    ポイント移行
                {% elseif history.record_event == 7 %}
                    レビューポイント
                {% endif %}
            </td>
            <td data-title="ポイント数">{{ history.point }}</td>
        </tr>
    {% endfor %}

    {# ポイントの履歴がない場合はその旨を表示する #}
    {% if pagination.totalItemCount == 0 %}
        <tr>
            <td>ポイントの獲得・利用の履歴がありません。</td>
        </tr>
    {% endif %}
    </tbody>
</table>
<div class="text-center">
    {% include "pager.twig" with {'pages': pagination.paginationData} %}
</div>
・
・
・
・

{% endblock %}

これらを用意したら、画面を表示するためにdtb_pageテーブルとdtb_page_layoutへポイント履歴画面のレコードを作成する必要があります。

こちらは普通にinsert文を用意して登録してください。参考に載せておきます。

INSERT INTO `dtb_page` (`master_page_id`, `page_name`, `url`, `file_name`, `edit_type`, `author`, `description`, `keyword`, `create_date`, `update_date`, `meta_robots`, `meta_tags`, `discriminator_type`) VALUES
(NULL,  'Mypage/ポイント履歴',   'mypage_point',    'Mypage/point',    2, NULL,   NULL,   NULL,   '2017-03-07 10:14:52', '2017-03-07 10:14:52', NULL,   NULL,   'page');

dtb_page_layoutへの配置は管理画面のレイアウト管理から行ってください。

最後に、マイページ画面のナビ部分にポイント履歴画面へのリンクを作成します。 リンクを貼る位置はどこでも構いません。

  • src/Eccube/Resource/template/default/Mypage/navi.twig
<li class="ec-navlistRole__item {% if mypageno|default('') == 'point' %}active{% endif %}">
    <a href="{{ url('mypage_point') }}">{{ 'ポイント履歴'|trans }}</a>
</li>

以上でポイント履歴を表示する準備が整いました。

実際に購入した後、ポイント履歴に獲得と表示されているかご確認ください。

管理画面の会員画面でポイント履歴を表示

管理画面でも特定会員のポイント履歴を表示させたいという要望もあると思いますので、会員の画面に表示されるよう作成してみます。

ポイント履歴を表示するためのTwig用関数を作成します。

Twigに関数を追加するのは、EccubeExtensionクラスに追加すれば良いです。

今回はgetPointHistories関数を作成し、twigからはget_point_historiesで呼び出せるようにします。 必要となる箇所だけ抜粋します。

  • src/Eccube/Twig/Extension/EccubeExtension.php
<?php
namespace Eccube\Twig\Extension;

・
・
・
use Eccube\Repository\PointHistoryRepository;
・
・
・

class EccubeExtension extends AbstractExtension
{
    /**
     * @var EccubeConfig
     */
    protected $eccubeConfig;

    /**
     * @var ProductRepository
     */
    private $productRepository;

    /**
     * @var PointHistoryRepository
     */
    private $pointHistoryRepository;

    /**
     * EccubeExtension constructor.
     *
     * @param EccubeConfig $eccubeConfig
     * @param ProductRepository $productRepository
     * @param PointHistoryRepository $pointHistoryRepository
     */
    public function __construct(EccubeConfig $eccubeConfig, ProductRepository $productRepository, PointHistoryRepository $pointHistoryRepository)
    {
        $this->eccubeConfig = $eccubeConfig;
        $this->productRepository = $productRepository;
        $this->pointHistoryRepository = $pointHistoryRepository;
    }

    /**
     * Returns a list of functions to add to the existing list.
     *
     * @return TwigFunction[] An array of functions
     */
    public function getFunctions()
    {
        return [
            new TwigFunction('has_errors', [$this, 'hasErrors']),
            new TwigFunction('active_menus', [$this, 'getActiveMenus']),
            new TwigFunction('class_categories_as_json', [$this, 'getClassCategoriesAsJson']),
            new TwigFunction('product', [$this, 'getProduct']),
            new TwigFunction('php_*', [$this, 'getPhpFunctions'], ['pre_escape' => 'html', 'is_safe' => ['html']]),
            new TwigFunction('currency_symbol', [$this, 'getCurrencySymbol']),
            new TwigFunction('get_point_histories', [$this, 'getPointHistories']),
        ];
    }

・
・
・
・

    /**
     * ポイント履歴を取得
     * (ただし0ポイントの履歴は含まない)
     *
     * @param Customer $Customer
     * @return array PointHistory
     */
    public function getPointHistories(Customer $Customer)
    {
        $qb = $this->pointHistoryRepository->getQueryBuilder($Customer);
        $qb->andWhere('ph.point != 0');

        return $qb->getQuery()->getResult();
    }
}

getPointHistoriesという関数でポイント履歴テーブルの情報を取得しています。これを会員画面から利用して表示させます。

表示させる場所はどこでもいいので、以下の内容を追加します。

  • src/Eccube/Resource/template/admin/Customer/edit.twig
{% if Customer.id %}
    <div class="card rounded border-0 mb-4">
        <div class="card-header">
            <div class="row">
                <div class="col-8">
                    <div class="d-inline-block" data-tooltip="true" data-placement="top">
                    <span class="card-title">
                        ポイント履歴
                    </span>
                    </div>
                </div>
                <div class="col-4 text-right">
                    <a data-toggle="collapse" href="#pointHistory" aria-expanded="false"
                       aria-controls="pointHistory">
                        <i class="fa fa-angle-up fa-lg"></i>
                    </a>
                </div>
            </div>
        </div>
        <div class="collapse show ec-cardCollapse" id="pointHistory">
            {% set point_histories = get_point_histories(Customer) %}
            {% if point_histories|length > 0 %}
                <div class="card-body">
                    <table class="table table-striped table-sm">
                        <thead class="table-active">
                        <tr>
                            <th class="align-middle pt-2 pb-2 pl-3">日時</th>
                            <th class="align-middle pt-2 pb-2">獲得・利用</th>
                            <th class="align-middle pt-2 pb-2">トリガ</th>
                            <th class="align-middle pt-2 pb-2 pr-3">ポイント数</th>
                            <th class="align-middle pt-2 pb-2 pr-3">注文番号</th>
                        </tr>
                        </thead>
                        <tbody>
                        {% for history in point_histories %}
                            <tr>
                                <td class="align-middle pl-3">{{ history.create_date|date_min }}</td>
                                <td class="align-middle">
                                    {% if history.record_type == 1 %}
                                        獲得
                                    {% elseif history.record_type == 2 %}
                                        利用
                                    {% elseif history.record_type == 3 %}
                                        失効
                                    {% elseif history.record_type == 4 %}
                                        ポイント移行
                                    {% endif %}
                                </td>
                                <td class="align-middle">
                                    {% if history.record_event == 1 %}
                                        商品購入
                                    {% elseif history.record_event == 2 %}
                                        会員登録 特典
                                    {% elseif history.record_event == 3 %}
                                        ご注文取り消し
                                    {% elseif history.record_event == 4 %}
                                        管理画面で編集
                                    {% elseif history.record_event == 5 %}
                                        期限切れ
                                    {% elseif history.record_event == 6 %}
                                        ポイント移行
                                    {% elseif history.record_event == 7 %}
                                        レビュー投稿
                                    {% endif %}
                                </td>
                                <td class="align-middle pr-3">
                                    {{ history.point }}
                                </td>
                                <td class="align-middle">
                                    {% if history.order is not null %}
                                        <a href="{{ url('admin_order_edit', { 'id' : history.order.id }) }}">
                                            {{ history.order.order_no }}
                                        </a>
                                    {% endif %}
                                </td>
                            </tr>
                        {% endfor %}
                        </tbody>
                    </table>
                </div>
            {% else %}
                <div class="card-body">
                    <div id="history_box" class="data-empty">
                    <span>
                        ポイントの履歴がありません
                    </span>
                    </div>
                </div>
            {% endif %}
        </div>
    </div>
{% endif %}

会員が存在していたらポイント履歴を表示するようにしています。

ポイント調整時の処理について

ある会員によっては手動で管理画面からポイントを加減算する時もあります。その場合もポイント履歴へ保存するように対応しておきます。

CustomerEditControllerクラスを修正することで対応可能で以下の内容となります。

行っている処理はコメントを参考にしてください。

  • src/Eccube/Controller/Admin/Customer/CustomerEditController.php
<?php

namespace Eccube\Controller\Admin\Customer;

use Eccube\Controller\AbstractController;
use Eccube\Entity\Master\CustomerStatus;
use Eccube\Entity\PointHistory;
use Eccube\Event\EccubeEvents;
use Eccube\Event\EventArgs;
use Eccube\Form\Type\Admin\CustomerType;
use Eccube\Repository\CustomerRepository;
use Eccube\Util\StringUtil;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;

class CustomerEditController extends AbstractController
{
    /**
     * @var CustomerRepository
     */
    protected $customerRepository;

    /**
     * @var EncoderFactoryInterface
     */
    protected $encoderFactory;

    public function __construct(
        CustomerRepository $customerRepository,
        EncoderFactoryInterface $encoderFactory
    )
    {
        $this->customerRepository = $customerRepository;
        $this->encoderFactory = $encoderFactory;
    }

    /**
     * @Route("/%eccube_admin_route%/customer/new", name="admin_customer_new")
     * @Route("/%eccube_admin_route%/customer/{id}/edit", requirements={"id" = "\d+"}, name="admin_customer_edit")
     * @Template("@admin/Customer/edit.twig")
     */
    public function index(Request $request, $id = null)
    {
        $this->entityManager->getFilters()->enable('incomplete_order_status_hidden');
        // 編集
        if ($id) {
            $Customer = $this->customerRepository
                ->find($id);

            if (is_null($Customer)) {
                throw new NotFoundHttpException();
            }

            $oldStatusId = $Customer->getStatus()->getId();
            // 編集用にデフォルトパスワードをセット
            $previous_password = $Customer->getPassword();
            $Customer->setPassword($this->eccubeConfig['eccube_default_password']);
            // ポイント
            $previous_point = $Customer->getPoint();
            // 新規登録
        } else {
            $Customer = $this->customerRepository->newCustomer();

            $oldStatusId = null;
            $previous_point = 0;
        }

        // 会員登録フォーム
        $builder = $this->formFactory
            ->createBuilder(CustomerType::class, $Customer);

        $event = new EventArgs(
            [
                'builder' => $builder,
                'Customer' => $Customer,
            ],
            $request
        );
        $this->eventDispatcher->dispatch(EccubeEvents::ADMIN_CUSTOMER_EDIT_INDEX_INITIALIZE, $event);

        $form = $builder->getForm();

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            log_info('会員登録開始', [$Customer->getId()]);

            $encoder = $this->encoderFactory->getEncoder($Customer);

            if ($Customer->getPassword() === $this->eccubeConfig['eccube_default_password']) {
                $Customer->setPassword($previous_password);
            } else {
                if ($Customer->getSalt() === null) {
                    $Customer->setSalt($encoder->createSalt());
                    $Customer->setSecretKey($this->customerRepository->getUniqueSecretKey());
                }
                $Customer->setPassword($encoder->encodePassword($Customer->getPassword(), $Customer->getSalt()));
            }

            // 退会ステータスに更新の場合、ダミーのアドレスに更新
            $newStatusId = $Customer->getStatus()->getId();
            if ($oldStatusId != $newStatusId && $newStatusId == CustomerStatus::WITHDRAWING) {
                $Customer->setEmail(StringUtil::random(60).'@dummy.dummy');
            }

            // ポイントの差分があれば調整ポイントとして履歴に保存
            if ($previous_point != $Customer->getPoint()) {
                $diff = $Customer->getPoint() - $previous_point;

                // ポイント履歴を追加
                $PointHistory = new PointHistory();
                $PointHistory->setRecordType(PointHistory::TYPE_NULL);
                $PointHistory->setRecordEvent(PointHistory::EVENT_MANUAL);
                $PointHistory->setPoint($diff);
                $PointHistory->setCustomer($Customer);
                $PointHistory->setOrder(null);
                $this->entityManager->persist($PointHistory);
                $this->entityManager->flush();
            }

            $this->entityManager->persist($Customer);
            $this->entityManager->flush();

            log_info('会員登録完了', [$Customer->getId()]);

            $event = new EventArgs(
                [
                    'form' => $form,
                    'Customer' => $Customer,
                ],
                $request
            );
            $this->eventDispatcher->dispatch(EccubeEvents::ADMIN_CUSTOMER_EDIT_INDEX_COMPLETE, $event);

            $this->addSuccess('admin.common.save_complete', 'admin');

            return $this->redirectToRoute('admin_customer_edit', [
                'id' => $Customer->getId(),
            ]);
        }

        return [
            'form' => $form->createView(),
            'Customer' => $Customer,
        ];
    }
}

以上でポイント履歴の説明がとなります。あれば便利かなという機能ですので、お客さんに要望された場合、是非参考にしてください。

なお、サイト運営途中からこの機能を導入された方は当然過去の履歴は存在していませんので、まとめてポイント履歴へ登録するinsert文か何かを作成して対応するようにしてください。

会員登録時と商品レビュー時のポイント付与については別の機会で説明します。