From 194534c6248d5212a5bd3abb6feae7775eba314e Mon Sep 17 00:00:00 2001
From: "takumi.tokoro"
Date: Mon, 15 Jun 2026 16:22:19 +0900
Subject: [PATCH 01/24] =?UTF-8?q?feat:=20=E8=BF=94=E5=93=81=E7=94=B3?=
=?UTF-8?q?=E8=AB=8B=E3=81=AE=E3=83=87=E3=83=BC=E3=82=BF=E5=B1=A4=E3=82=92?=
=?UTF-8?q?=E8=BF=BD=E5=8A=A0=EF=BC=88Entity/Repository/=E5=88=9D=E6=9C=9F?=
=?UTF-8?q?=E3=83=87=E3=83=BC=E3=82=BF/=E3=83=9E=E3=82=A4=E3=82=B0?=
=?UTF-8?q?=E3=83=AC=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=EF=BC=89=20(#6820?=
=?UTF-8?q?)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- RefundRequest / RefundRequestFile / Master\RefundRequestStatus エンティティを追加
- Product に返品可否フラグ refund_allowed (default true) を追加
- 各 Repository(検索・注文明細別件数の一括取得=N+1回避)を追加
- 新規インストール用 CSV(mtb_refund_request_status, dtb_mail_template id=10, dtb_csv refund_allowed)
- 既存環境アップデート用 INSERT マイグレーション(冪等・down 実装)
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.../Version20260615000000.php | 94 ++++++++
.../Entity/Master/RefundRequestStatus.php | 42 ++++
src/Eccube/Entity/Product.php | 21 ++
src/Eccube/Entity/RefundRequest.php | 227 ++++++++++++++++++
src/Eccube/Entity/RefundRequestFile.php | 153 ++++++++++++
.../Master/RefundRequestStatusRepository.php | 31 +++
.../RefundRequestFileRepository.php | 30 +++
.../Repository/RefundRequestRepository.php | 139 +++++++++++
.../doctrine/import_csv/en/dtb_csv.csv | 3 +-
.../import_csv/en/dtb_mail_template.csv | 1 +
.../en/mtb_refund_request_status.csv | 6 +
.../doctrine/import_csv/ja/dtb_csv.csv | 1 +
.../import_csv/ja/dtb_mail_template.csv | 1 +
.../ja/mtb_refund_request_status.csv | 6 +
14 files changed, 754 insertions(+), 1 deletion(-)
create mode 100644 app/DoctrineMigrations/Version20260615000000.php
create mode 100644 src/Eccube/Entity/Master/RefundRequestStatus.php
create mode 100644 src/Eccube/Entity/RefundRequest.php
create mode 100644 src/Eccube/Entity/RefundRequestFile.php
create mode 100644 src/Eccube/Repository/Master/RefundRequestStatusRepository.php
create mode 100644 src/Eccube/Repository/RefundRequestFileRepository.php
create mode 100644 src/Eccube/Repository/RefundRequestRepository.php
create mode 100644 src/Eccube/Resource/doctrine/import_csv/en/mtb_refund_request_status.csv
create mode 100644 src/Eccube/Resource/doctrine/import_csv/ja/mtb_refund_request_status.csv
diff --git a/app/DoctrineMigrations/Version20260615000000.php b/app/DoctrineMigrations/Version20260615000000.php
new file mode 100644
index 00000000000..7232252a6a8
--- /dev/null
+++ b/app/DoctrineMigrations/Version20260615000000.php
@@ -0,0 +1,94 @@
+ $lang === 'en' ? 'New' : '新規申請',
+ RefundRequestStatus::PROCESSING => $lang === 'en' ? 'Processing' : '処理中',
+ RefundRequestStatus::ACCEPTED => $lang === 'en' ? 'Accepted' : '承認済',
+ RefundRequestStatus::DECLINED => $lang === 'en' ? 'Declined' : '却下',
+ RefundRequestStatus::INFO_REQUESTED => $lang === 'en' ? 'Information Requested' : '追加情報依頼',
+ ];
+ $sortNo = 0;
+ foreach ($statuses as $id => $name) {
+ $exists = $this->connection->fetchOne(
+ 'SELECT COUNT(*) FROM mtb_refund_request_status WHERE id = :id',
+ ['id' => $id]
+ );
+ if ($exists == 0) {
+ $this->addSql(
+ "INSERT INTO mtb_refund_request_status (id, name, sort_no, discriminator_type) VALUES (?, ?, ?, 'refundrequeststatus')",
+ [$id, $name, $sortNo]
+ );
+ }
+ $sortNo++;
+ }
+
+ // dtb_mail_template(管理者向け返品申請通知メール)
+ $mailExists = $this->connection->fetchOne(
+ 'SELECT COUNT(*) FROM dtb_mail_template WHERE id = 10'
+ );
+ if ($mailExists == 0) {
+ $name = $lang === 'en' ? 'Refund Request Notification' : '返品申請通知メール';
+ $subject = $lang === 'en' ? 'A refund request has been submitted' : '返品申請を受け付けました';
+ $this->addSql(
+ 'INSERT INTO dtb_mail_template (id, creator_id, name, file_name, mail_subject, deletable, create_date, update_date, discriminator_type) '
+ ."VALUES (10, null, ?, 'Mail/refund_request_notify.twig', ?, false, '2017-03-07 10:14:52', '2017-03-07 10:14:52', 'mailtemplate')",
+ [$name, $subject]
+ );
+ }
+
+ // dtb_csv(商品CSV出力項目 refund_allowed)
+ $csvExists = $this->connection->fetchOne(
+ 'SELECT COUNT(*) FROM dtb_csv WHERE id = 215'
+ );
+ if ($csvExists == 0) {
+ $dispName = $lang === 'en' ? 'Refund Allowed Flag' : '返品許可フラグ';
+ $this->addSql(
+ 'INSERT INTO dtb_csv (id, csv_type_id, creator_id, entity_name, field_name, reference_field_name, disp_name, sort_no, enabled, create_date, update_date, discriminator_type) '
+ ."VALUES (215, 1, null, 'Eccube\\Entity\\Product', 'refund_allowed', null, ?, 33, false, '2017-03-07 10:14:00', '2017-03-07 10:14:00', 'csv')",
+ [$dispName]
+ );
+ }
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->addSql('DELETE FROM dtb_csv WHERE id = 215');
+ $this->addSql('DELETE FROM dtb_mail_template WHERE id = 10');
+ $this->addSql('DELETE FROM mtb_refund_request_status WHERE id IN (1, 2, 3, 4, 5)');
+ }
+}
diff --git a/src/Eccube/Entity/Master/RefundRequestStatus.php b/src/Eccube/Entity/Master/RefundRequestStatus.php
new file mode 100644
index 00000000000..b32bb649f48
--- /dev/null
+++ b/src/Eccube/Entity/Master/RefundRequestStatus.php
@@ -0,0 +1,42 @@
+ true])]
+ private bool $refund_allowed = true;
+
/**
* @var \DateTime
*/
@@ -703,6 +706,24 @@ public function getFreeArea(): ?string
return $this->free_area;
}
+ /**
+ * 返品申請を許可するかどうかを設定する.
+ */
+ public function setRefundAllowed(bool $refundAllowed): Product
+ {
+ $this->refund_allowed = $refundAllowed;
+
+ return $this;
+ }
+
+ /**
+ * 返品申請を許可するかどうかを取得する.
+ */
+ public function isRefundAllowed(): bool
+ {
+ return $this->refund_allowed;
+ }
+
/**
* Set createDate.
*/
diff --git a/src/Eccube/Entity/RefundRequest.php b/src/Eccube/Entity/RefundRequest.php
new file mode 100644
index 00000000000..c1964dc6030
--- /dev/null
+++ b/src/Eccube/Entity/RefundRequest.php
@@ -0,0 +1,227 @@
+ true])]
+ #[ORM\Id]
+ #[ORM\GeneratedValue(strategy: 'IDENTITY')]
+ private ?int $id = null;
+
+ #[ORM\Column(name: 'quantity', type: Types::DECIMAL, precision: 10, scale: 0, options: ['default' => 0])]
+ private ?string $quantity = '0';
+
+ #[ORM\Column(name: 'reason', type: Types::TEXT)]
+ private ?string $reason = null;
+
+ #[ORM\Column(name: 'admin_note', type: Types::TEXT, nullable: true)]
+ private ?string $admin_note = null;
+
+ /**
+ * @var \DateTime
+ */
+ #[ORM\Column(name: 'create_date', type: Types::DATETIMETZ_MUTABLE)]
+ private $create_date;
+
+ /**
+ * @var \DateTime
+ */
+ #[ORM\Column(name: 'update_date', type: Types::DATETIMETZ_MUTABLE)]
+ private $update_date;
+
+ #[ORM\ManyToOne(targetEntity: Order::class)]
+ #[ORM\JoinColumn(name: 'order_id', referencedColumnName: 'id')]
+ private ?Order $Order = null;
+
+ #[ORM\ManyToOne(targetEntity: OrderItem::class)]
+ #[ORM\JoinColumn(name: 'order_item_id', referencedColumnName: 'id')]
+ private ?OrderItem $OrderItem = null;
+
+ #[ORM\ManyToOne(targetEntity: Customer::class)]
+ #[ORM\JoinColumn(name: 'customer_id', referencedColumnName: 'id')]
+ private ?Customer $Customer = null;
+
+ #[ORM\ManyToOne(targetEntity: RefundRequestStatus::class)]
+ #[ORM\JoinColumn(name: 'refund_request_status_id', referencedColumnName: 'id')]
+ private ?RefundRequestStatus $RefundRequestStatus = null;
+
+ /**
+ * @var Collection
+ */
+ #[ORM\OneToMany(targetEntity: RefundRequestFile::class, mappedBy: 'RefundRequest', cascade: ['persist', 'remove'])]
+ #[ORM\OrderBy(['sort_no' => 'ASC'])]
+ private Collection $RefundRequestFiles;
+
+ public function __construct()
+ {
+ $this->RefundRequestFiles = new ArrayCollection();
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function setQuantity(string $quantity): self
+ {
+ $this->quantity = $quantity;
+
+ return $this;
+ }
+
+ public function getQuantity(): ?string
+ {
+ return $this->quantity;
+ }
+
+ public function setReason(?string $reason): self
+ {
+ $this->reason = $reason;
+
+ return $this;
+ }
+
+ public function getReason(): ?string
+ {
+ return $this->reason;
+ }
+
+ public function setAdminNote(?string $adminNote): self
+ {
+ $this->admin_note = $adminNote;
+
+ return $this;
+ }
+
+ public function getAdminNote(): ?string
+ {
+ return $this->admin_note;
+ }
+
+ public function setCreateDate(\DateTime $createDate): self
+ {
+ $this->create_date = $createDate;
+
+ return $this;
+ }
+
+ public function getCreateDate(): ?\DateTime
+ {
+ return $this->create_date;
+ }
+
+ public function setUpdateDate(\DateTime $updateDate): self
+ {
+ $this->update_date = $updateDate;
+
+ return $this;
+ }
+
+ public function getUpdateDate(): ?\DateTime
+ {
+ return $this->update_date;
+ }
+
+ public function setOrder(?Order $order = null): self
+ {
+ $this->Order = $order;
+
+ return $this;
+ }
+
+ public function getOrder(): ?Order
+ {
+ return $this->Order;
+ }
+
+ public function setOrderItem(?OrderItem $orderItem = null): self
+ {
+ $this->OrderItem = $orderItem;
+
+ return $this;
+ }
+
+ public function getOrderItem(): ?OrderItem
+ {
+ return $this->OrderItem;
+ }
+
+ public function setCustomer(?Customer $customer = null): self
+ {
+ $this->Customer = $customer;
+
+ return $this;
+ }
+
+ public function getCustomer(): ?Customer
+ {
+ return $this->Customer;
+ }
+
+ public function setRefundRequestStatus(?RefundRequestStatus $refundRequestStatus = null): self
+ {
+ $this->RefundRequestStatus = $refundRequestStatus;
+
+ return $this;
+ }
+
+ public function getRefundRequestStatus(): ?RefundRequestStatus
+ {
+ return $this->RefundRequestStatus;
+ }
+
+ /**
+ * @return Collection
+ */
+ public function getRefundRequestFiles(): Collection
+ {
+ return $this->RefundRequestFiles;
+ }
+
+ public function addRefundRequestFile(RefundRequestFile $refundRequestFile): self
+ {
+ if (!$this->RefundRequestFiles->contains($refundRequestFile)) {
+ $this->RefundRequestFiles[] = $refundRequestFile;
+ $refundRequestFile->setRefundRequest($this);
+ }
+
+ return $this;
+ }
+
+ public function removeRefundRequestFile(RefundRequestFile $refundRequestFile): bool
+ {
+ return $this->RefundRequestFiles->removeElement($refundRequestFile);
+ }
+ }
+}
diff --git a/src/Eccube/Entity/RefundRequestFile.php b/src/Eccube/Entity/RefundRequestFile.php
new file mode 100644
index 00000000000..8de075ca1d0
--- /dev/null
+++ b/src/Eccube/Entity/RefundRequestFile.php
@@ -0,0 +1,153 @@
+ true])]
+ #[ORM\Id]
+ #[ORM\GeneratedValue(strategy: 'IDENTITY')]
+ private ?int $id = null;
+
+ #[ORM\Column(name: 'file_name', type: Types::STRING, length: 255)]
+ private ?string $file_name = null;
+
+ #[ORM\Column(name: 'mime_type', type: Types::STRING, length: 100)]
+ private ?string $mime_type = null;
+
+ #[ORM\Column(name: 'file_size', type: Types::INTEGER, options: ['unsigned' => true])]
+ private ?int $file_size = null;
+
+ #[ORM\Column(name: 'sort_no', type: Types::SMALLINT, options: ['unsigned' => true, 'default' => 0])]
+ private ?int $sort_no = 0;
+
+ /**
+ * @var \DateTime
+ */
+ #[ORM\Column(name: 'create_date', type: Types::DATETIMETZ_MUTABLE)]
+ private $create_date;
+
+ #[ORM\ManyToOne(targetEntity: RefundRequest::class, inversedBy: 'RefundRequestFiles')]
+ #[ORM\JoinColumn(name: 'refund_request_id', referencedColumnName: 'id')]
+ private ?RefundRequest $RefundRequest = null;
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function setFileName(?string $fileName): self
+ {
+ $this->file_name = $fileName;
+
+ return $this;
+ }
+
+ public function getFileName(): ?string
+ {
+ return $this->file_name;
+ }
+
+ public function setMimeType(?string $mimeType): self
+ {
+ $this->mime_type = $mimeType;
+
+ return $this;
+ }
+
+ public function getMimeType(): ?string
+ {
+ return $this->mime_type;
+ }
+
+ public function setFileSize(?int $fileSize): self
+ {
+ $this->file_size = $fileSize;
+
+ return $this;
+ }
+
+ public function getFileSize(): ?int
+ {
+ return $this->file_size;
+ }
+
+ public function setSortNo(?int $sortNo): self
+ {
+ $this->sort_no = $sortNo;
+
+ return $this;
+ }
+
+ public function getSortNo(): ?int
+ {
+ return $this->sort_no;
+ }
+
+ public function setCreateDate(\DateTime $createDate): self
+ {
+ $this->create_date = $createDate;
+
+ return $this;
+ }
+
+ public function getCreateDate(): ?\DateTime
+ {
+ return $this->create_date;
+ }
+
+ public function setRefundRequest(?RefundRequest $refundRequest = null): self
+ {
+ $this->RefundRequest = $refundRequest;
+
+ return $this;
+ }
+
+ public function getRefundRequest(): ?RefundRequest
+ {
+ return $this->RefundRequest;
+ }
+
+ /**
+ * ファイルが画像かどうか.
+ */
+ public function isImage(): bool
+ {
+ return str_starts_with((string) $this->mime_type, 'image/');
+ }
+
+ /**
+ * ファイルが動画かどうか.
+ */
+ public function isVideo(): bool
+ {
+ return str_starts_with((string) $this->mime_type, 'video/');
+ }
+ }
+}
diff --git a/src/Eccube/Repository/Master/RefundRequestStatusRepository.php b/src/Eccube/Repository/Master/RefundRequestStatusRepository.php
new file mode 100644
index 00000000000..38d8df3d70d
--- /dev/null
+++ b/src/Eccube/Repository/Master/RefundRequestStatusRepository.php
@@ -0,0 +1,31 @@
+
+ */
+class RefundRequestStatusRepository extends AbstractRepository
+{
+ public function __construct(RegistryInterface $registry)
+ {
+ parent::__construct($registry, RefundRequestStatus::class);
+ }
+}
diff --git a/src/Eccube/Repository/RefundRequestFileRepository.php b/src/Eccube/Repository/RefundRequestFileRepository.php
new file mode 100644
index 00000000000..0b27db8496d
--- /dev/null
+++ b/src/Eccube/Repository/RefundRequestFileRepository.php
@@ -0,0 +1,30 @@
+
+ */
+class RefundRequestFileRepository extends AbstractRepository
+{
+ public function __construct(RegistryInterface $registry)
+ {
+ parent::__construct($registry, RefundRequestFile::class);
+ }
+}
diff --git a/src/Eccube/Repository/RefundRequestRepository.php b/src/Eccube/Repository/RefundRequestRepository.php
new file mode 100644
index 00000000000..4dd1e7a979c
--- /dev/null
+++ b/src/Eccube/Repository/RefundRequestRepository.php
@@ -0,0 +1,139 @@
+
+ */
+class RefundRequestRepository extends AbstractRepository
+{
+ public function __construct(RegistryInterface $registry)
+ {
+ parent::__construct($registry, RefundRequest::class);
+ }
+
+ /**
+ * 管理画面の検索条件から QueryBuilder を生成する.
+ *
+ * @param array $searchData
+ */
+ public function getQueryBuilderBySearchData(array $searchData): QueryBuilder
+ {
+ $qb = $this->createQueryBuilder('rr')
+ ->innerJoin('rr.Order', 'o')
+ ->innerJoin('rr.OrderItem', 'oi')
+ ->innerJoin('rr.Customer', 'c')
+ ->innerJoin('rr.RefundRequestStatus', 's');
+
+ // 複合検索(申請ID・注文番号・会員ID・会員名)
+ if (isset($searchData['multi']) && StringUtil::isNotBlank($searchData['multi'])) {
+ $multi = preg_match('/^\d{0,10}$/', $searchData['multi']) ? $searchData['multi'] : null;
+ $qb->andWhere('rr.id = :multi_id OR o.order_no LIKE :multi_like OR c.id = :multi_id '
+ .'OR CONCAT(c.name01, c.name02) LIKE :multi_like')
+ ->setParameter('multi_id', $multi)
+ ->setParameter('multi_like', '%'.$this->escapeLike($searchData['multi']).'%');
+ }
+
+ // ステータス(複数選択)
+ if (!empty($searchData['status']) && is_iterable($searchData['status'])) {
+ $qb->andWhere($qb->expr()->in('rr.RefundRequestStatus', ':status'))
+ ->setParameter('status', $searchData['status']);
+ }
+
+ // 申請日時
+ if (!empty($searchData['create_date_start'])) {
+ $qb->andWhere('rr.create_date >= :create_date_start')
+ ->setParameter('create_date_start', $searchData['create_date_start']);
+ }
+ if (!empty($searchData['create_date_end'])) {
+ $date = clone $searchData['create_date_end'];
+ $date = $date->modify('+1 days');
+ $qb->andWhere('rr.create_date < :create_date_end')
+ ->setParameter('create_date_end', $date);
+ }
+
+ // 更新日時
+ if (!empty($searchData['update_date_start'])) {
+ $qb->andWhere('rr.update_date >= :update_date_start')
+ ->setParameter('update_date_start', $searchData['update_date_start']);
+ }
+ if (!empty($searchData['update_date_end'])) {
+ $date = clone $searchData['update_date_end'];
+ $date = $date->modify('+1 days');
+ $qb->andWhere('rr.update_date < :update_date_end')
+ ->setParameter('update_date_end', $date);
+ }
+
+ $qb->orderBy('rr.update_date', 'DESC');
+
+ return $qb;
+ }
+
+ /**
+ * 指定した注文明細に対する会員の返品申請を時系列で取得する.
+ *
+ * @return RefundRequest[]
+ */
+ public function findByOrderItemAndCustomer(OrderItem $OrderItem, Customer $Customer): array
+ {
+ return $this->createQueryBuilder('rr')
+ ->innerJoin('rr.RefundRequestStatus', 's')
+ ->leftJoin('rr.RefundRequestFiles', 'f')
+ ->addSelect('s', 'f')
+ ->where('rr.OrderItem = :OrderItem')
+ ->andWhere('rr.Customer = :Customer')
+ ->setParameter('OrderItem', $OrderItem)
+ ->setParameter('Customer', $Customer)
+ ->orderBy('rr.create_date', 'ASC')
+ ->getQuery()
+ ->getResult();
+ }
+
+ /**
+ * 会員の返品申請件数を、注文明細ID別に一括取得する(N+1回避).
+ *
+ * @return array [order_item_id => count]
+ */
+ public function getRefundRequestCountsByCustomer(Customer $Customer): array
+ {
+ $rows = $this->createQueryBuilder('rr')
+ ->select('IDENTITY(rr.OrderItem) AS order_item_id', 'COUNT(rr.id) AS cnt')
+ ->where('rr.Customer = :Customer')
+ ->setParameter('Customer', $Customer)
+ ->groupBy('rr.OrderItem')
+ ->getQuery()
+ ->getResult();
+
+ $counts = [];
+ foreach ($rows as $row) {
+ $counts[(int) $row['order_item_id']] = (int) $row['cnt'];
+ }
+
+ return $counts;
+ }
+
+ private function escapeLike(string $value): string
+ {
+ return str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $value);
+ }
+}
diff --git a/src/Eccube/Resource/doctrine/import_csv/en/dtb_csv.csv b/src/Eccube/Resource/doctrine/import_csv/en/dtb_csv.csv
index 08c4a5cbf69..1cd93d82184 100644
--- a/src/Eccube/Resource/doctrine/import_csv/en/dtb_csv.csv
+++ b/src/Eccube/Resource/doctrine/import_csv/en/dtb_csv.csv
@@ -209,4 +209,5 @@ id,csv_type_id,creator_id,entity_name,field_name,reference_field_name,disp_name,
210,7,,Eccube\\Entity\\ClassCategory,id,,Class Category ID,1,1,2021-05-18 01:26:41,2021-05-18 01:26:41,csv
211,7,,Eccube\\Entity\\ClassCategory,ClassName,id,Class NameID,2,1,2021-05-18 01:26:41,2021-05-18 01:26:41,csv
212,7,,Eccube\\Entity\\ClassCategory,name,,Class Category Name,3,1,2021-05-18 01:26:41,2021-05-18 01:26:41,csv
-213,7,,Eccube\\Entity\\ClassCategory,backend_name,,Class Category Backend Name,4,1,2021-05-18 01:26:41,2021-05-18 01:26:41,csv
\ No newline at end of file
+213,7,,Eccube\\Entity\\ClassCategory,backend_name,,Class Category Backend Name,4,1,2021-05-18 01:26:41,2021-05-18 01:26:41,csv
+215,1,,Eccube\\Entity\\Product,refund_allowed,,Refund Allowed Flag,33,0,2017-03-07 10:14:00,2017-03-07 10:14:00,csv
\ No newline at end of file
diff --git a/src/Eccube/Resource/doctrine/import_csv/en/dtb_mail_template.csv b/src/Eccube/Resource/doctrine/import_csv/en/dtb_mail_template.csv
index 9d899a2ecca..91c81797fb7 100644
--- a/src/Eccube/Resource/doctrine/import_csv/en/dtb_mail_template.csv
+++ b/src/Eccube/Resource/doctrine/import_csv/en/dtb_mail_template.csv
@@ -8,3 +8,4 @@ id,creator_id,name,file_name,mail_subject,deletable,create_date,update_date,disc
7,,Password Reminder,Mail/reset_complete_mail.twig,Your password has been changed,0,2017-03-07 10:14:52,2017-03-07 10:14:52,mailtemplate
8,,Shipping Notice,Mail/shipping_notify.twig,Your order has been shipped,0,2017-03-07 10:14:52,2017-03-07 10:14:52,mailtemplate
9,,Notification email,Mail/customer_change_notify.twig,Your account information has been changed.,0,2017-03-07 10:14:52,2017-03-07 10:14:52,mailtemplate
+10,,Refund Request Notification,Mail/refund_request_notify.twig,A refund request has been submitted,0,2017-03-07 10:14:52,2017-03-07 10:14:52,mailtemplate
diff --git a/src/Eccube/Resource/doctrine/import_csv/en/mtb_refund_request_status.csv b/src/Eccube/Resource/doctrine/import_csv/en/mtb_refund_request_status.csv
new file mode 100644
index 00000000000..1ef09b41afb
--- /dev/null
+++ b/src/Eccube/Resource/doctrine/import_csv/en/mtb_refund_request_status.csv
@@ -0,0 +1,6 @@
+id,name,sort_no,discriminator_type
+1,New,0,refundrequeststatus
+2,Processing,1,refundrequeststatus
+3,Accepted,2,refundrequeststatus
+4,Declined,3,refundrequeststatus
+5,Information Requested,4,refundrequeststatus
diff --git a/src/Eccube/Resource/doctrine/import_csv/ja/dtb_csv.csv b/src/Eccube/Resource/doctrine/import_csv/ja/dtb_csv.csv
index a91a0f717fa..cf24babe5b6 100644
--- a/src/Eccube/Resource/doctrine/import_csv/ja/dtb_csv.csv
+++ b/src/Eccube/Resource/doctrine/import_csv/ja/dtb_csv.csv
@@ -211,3 +211,4 @@
"212","7",,"Eccube\\Entity\\ClassCategory","name",,"規格分類名","3","1","2021-05-18 01:26:41","2021-05-18 01:26:41","csv"
"213","7",,"Eccube\\Entity\\ClassCategory","backend_name",,"分類管理名","4","1","2021-05-18 01:26:41","2021-05-18 01:26:41","csv"
"214","2",,"Eccube\\Entity\\Customer","buy_total",,"購入金額","34","0","2017-03-07 10:14:00","2017-03-07 10:14:00","csv"
+"215","1",,"Eccube\\Entity\\Product","refund_allowed",,"返品許可フラグ","33","0","2017-03-07 10:14:00","2017-03-07 10:14:00","csv"
diff --git a/src/Eccube/Resource/doctrine/import_csv/ja/dtb_mail_template.csv b/src/Eccube/Resource/doctrine/import_csv/ja/dtb_mail_template.csv
index 8c4591668ef..552c39c4ce1 100644
--- a/src/Eccube/Resource/doctrine/import_csv/ja/dtb_mail_template.csv
+++ b/src/Eccube/Resource/doctrine/import_csv/ja/dtb_mail_template.csv
@@ -8,3 +8,4 @@ id,creator_id,name,file_name,mail_subject,deletable,create_date,update_date,disc
"7",,"パスワードリマインダー","Mail/reset_complete_mail.twig","パスワード変更のお知らせ",0,"2017-03-07 10:14:52","2017-03-07 10:14:52","mailtemplate"
"8",,"出荷通知メール","Mail/shipping_notify.twig","商品出荷のお知らせ",0,"2017-03-07 10:14:52","2017-03-07 10:14:52","mailtemplate"
"9",,"会員情報変更通知メール","Mail/customer_change_notify.twig","会員情報変更のお知らせ",0,"2017-03-07 10:14:52","2017-03-07 10:14:52","mailtemplate"
+"10",,"返品申請通知メール","Mail/refund_request_notify.twig","返品申請を受け付けました",0,"2017-03-07 10:14:52","2017-03-07 10:14:52","mailtemplate"
diff --git a/src/Eccube/Resource/doctrine/import_csv/ja/mtb_refund_request_status.csv b/src/Eccube/Resource/doctrine/import_csv/ja/mtb_refund_request_status.csv
new file mode 100644
index 00000000000..b869bab0b1f
--- /dev/null
+++ b/src/Eccube/Resource/doctrine/import_csv/ja/mtb_refund_request_status.csv
@@ -0,0 +1,6 @@
+id,name,sort_no,discriminator_type
+1,新規申請,0,refundrequeststatus
+2,処理中,1,refundrequeststatus
+3,承認済,2,refundrequeststatus
+4,却下,3,refundrequeststatus
+5,追加情報依頼,4,refundrequeststatus
From 605aaf107939da74ae533e293a412b57501fb454 Mon Sep 17 00:00:00 2001
From: "takumi.tokoro"
Date: Mon, 15 Jun 2026 16:50:23 +0900
Subject: [PATCH 02/24] =?UTF-8?q?feat:=20=E8=BF=94=E5=93=81=E7=94=B3?=
=?UTF-8?q?=E8=AB=8B=E3=81=AE=E3=82=A4=E3=83=99=E3=83=B3=E3=83=88=E3=82=92?=
=?UTF-8?q?=E8=BF=BD=E5=8A=A0=EF=BC=88EccubeEvents=20=E5=AE=9A=E6=95=B0=20?=
=?UTF-8?q?/=20RefundRequestEvent=EF=BC=89=20(#6820)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- EccubeEvents に返品申請関連の定数を追加(フロント/管理/ステータス変更/メール)
- RefundRequestEvent を追加(ステータス変更時に変更前後のステータスを保持)
Co-Authored-By: Claude Opus 4.8 (1M context)
---
src/Eccube/Event/EccubeEvents.php | 28 +++++++++++++++
src/Eccube/Event/RefundRequestEvent.php | 48 +++++++++++++++++++++++++
2 files changed, 76 insertions(+)
create mode 100644 src/Eccube/Event/RefundRequestEvent.php
diff --git a/src/Eccube/Event/EccubeEvents.php b/src/Eccube/Event/EccubeEvents.php
index 49dbaea2319..a73ce862cdd 100644
--- a/src/Eccube/Event/EccubeEvents.php
+++ b/src/Eccube/Event/EccubeEvents.php
@@ -594,4 +594,32 @@ final class EccubeEvents
public const MAIL_PASSWORD_RESET_COMPLETE = 'mail.password.reset.complete';
public const MAIL_SHIPPING_NOTIFY = 'mail.shipping.notify';
public const MAIL_CUSTOMER_CHANGE_NOTIFY = 'mail.customer.change.notify';
+ public const MAIL_REFUND_REQUEST = 'mail.refund_request';
+
+ /**
+ * Mypage RefundRequestController
+ */
+ // index(申請フォーム)
+ public const FRONT_MYPAGE_REFUND_REQUEST_INDEX_INITIALIZE = 'front.mypage.refund_request.index.initialize';
+ public const FRONT_MYPAGE_REFUND_REQUEST_INDEX_COMPLETE = 'front.mypage.refund_request.index.complete';
+ // confirm(確認画面)
+ public const FRONT_MYPAGE_REFUND_REQUEST_CONFIRM_INITIALIZE = 'front.mypage.refund_request.confirm.initialize';
+ public const FRONT_MYPAGE_REFUND_REQUEST_CONFIRM_COMPLETE = 'front.mypage.refund_request.confirm.complete';
+ // itemHistory(商品別申請履歴)
+ public const FRONT_MYPAGE_REFUND_REQUEST_ITEM_HISTORY_INITIALIZE = 'front.mypage.refund_request.item_history.initialize';
+
+ /**
+ * Admin RefundRequestController
+ */
+ // index(一覧・検索)
+ public const ADMIN_REFUND_REQUEST_INDEX_INITIALIZE = 'admin.refund_request.index.initialize';
+ public const ADMIN_REFUND_REQUEST_INDEX_SEARCH = 'admin.refund_request.index.search';
+ // edit(詳細・編集)
+ public const ADMIN_REFUND_REQUEST_EDIT_INITIALIZE = 'admin.refund_request.edit.initialize';
+ public const ADMIN_REFUND_REQUEST_EDIT_COMPLETE = 'admin.refund_request.edit.complete';
+
+ /**
+ * RefundRequest ステータス変更
+ */
+ public const REFUND_REQUEST_STATUS_CHANGE = 'refund_request.status.change';
}
diff --git a/src/Eccube/Event/RefundRequestEvent.php b/src/Eccube/Event/RefundRequestEvent.php
new file mode 100644
index 00000000000..4698763f3b3
--- /dev/null
+++ b/src/Eccube/Event/RefundRequestEvent.php
@@ -0,0 +1,48 @@
+refundRequest;
+ }
+
+ public function getPreviousStatus(): ?RefundRequestStatus
+ {
+ return $this->previousStatus;
+ }
+
+ public function getNewStatus(): ?RefundRequestStatus
+ {
+ return $this->newStatus;
+ }
+}
From 3df39df58c2559054740f5f5e6ec4c981a7287f9 Mon Sep 17 00:00:00 2001
From: "takumi.tokoro"
Date: Mon, 15 Jun 2026 16:50:35 +0900
Subject: [PATCH 03/24] =?UTF-8?q?feat:=20=E8=BF=94=E5=93=81=E7=94=B3?=
=?UTF-8?q?=E8=AB=8B=E3=81=AE=E3=82=B9=E3=83=86=E3=83=BC=E3=83=88=E3=83=9E?=
=?UTF-8?q?=E3=82=B7=E3=83=B3=E3=81=A8=E3=82=B5=E3=83=BC=E3=83=93=E3=82=B9?=
=?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0=20(#6820)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- refund_request ステートマシン設定(NEW→PROCESSING→ACCEPTED/DECLINED/INFO_REQUESTED)
- RefundRequestStateMachine(遷移可否判定・遷移実行・利用可能遷移の取得)
- RefundRequestService(申請作成+エビデンスファイル保存+ステータス遷移)
- MailService に管理者向け返品申請通知メール送信を追加
- エビデンスファイルは非公開の var/ 配下に保存(配信はコントローラ経由に限定)
- eccube.yaml に保存先・通知メールテンプレートIDの設定を追加
Co-Authored-By: Claude Opus 4.8 (1M context)
---
app/config/eccube/packages/eccube.yaml | 3 +
.../packages/refund_request_state_machine.php | 59 +++++++
src/Eccube/Service/MailService.php | 60 +++++++
src/Eccube/Service/RefundRequestService.php | 135 +++++++++++++++
.../Service/RefundRequestStateMachine.php | 156 ++++++++++++++++++
5 files changed, 413 insertions(+)
create mode 100644 app/config/eccube/packages/refund_request_state_machine.php
create mode 100644 src/Eccube/Service/RefundRequestService.php
create mode 100644 src/Eccube/Service/RefundRequestStateMachine.php
diff --git a/app/config/eccube/packages/eccube.yaml b/app/config/eccube/packages/eccube.yaml
index 0b4a38f45d5..a9f1910dc4c 100644
--- a/app/config/eccube/packages/eccube.yaml
+++ b/app/config/eccube/packages/eccube.yaml
@@ -52,6 +52,8 @@ parameters:
eccube_twig_block_templates: []
eccube_save_image_dir: '%kernel.project_dir%/html/upload/save_image'
eccube_temp_image_dir: '%kernel.project_dir%/html/upload/temp_image'
+ # 返品申請のエビデンスファイル保存先。公開ドキュメントルート外(var/配下)に置き、配信はコントローラ経由(所有者チェック付き)に限定する
+ eccube_save_refund_request_file_dir: '%kernel.project_dir%/var/refund_request'
eccube_csv_size: 5 # post_max_size, upload_max_filesize に任せればよい?
eccube_csv_temp_realdir: '%kernel.cache_dir%/%kernel.environment%/eccube' # upload_tmp_dir に任せればよい?
eccube_csv_split_lines: 100
@@ -126,6 +128,7 @@ parameters:
eccube_forgot_mail_template_id: 6 #パスワードリセット
eccube_reset_complete_mail_template_id: 7 #パスワードリマインダー
eccube_shipping_notify_mail_template_id: 8 #出荷通知メール
+ eccube_refund_request_notify_mail_template_id: 10 #返品申請通知メール(管理者向け)
# メールアドレスをRFC準拠でチェックするか true: チェックする、false: チェックしない
eccube_rfc_email_check: false
eccube_email_len: 254
diff --git a/app/config/eccube/packages/refund_request_state_machine.php b/app/config/eccube/packages/refund_request_state_machine.php
new file mode 100644
index 00000000000..9133fd5f07c
--- /dev/null
+++ b/app/config/eccube/packages/refund_request_state_machine.php
@@ -0,0 +1,59 @@
+loadFromExtension('framework', [
+ 'workflows' => [
+ 'refund_request' => [
+ 'type' => 'state_machine',
+ 'marking_store' => [
+ 'type' => 'method',
+ ],
+ 'supports' => [
+ RefundRequestStateMachineContext::class,
+ ],
+ 'initial_marking' => (string) Status::NEW,
+ 'places' => [
+ (string) Status::NEW,
+ (string) Status::PROCESSING,
+ (string) Status::ACCEPTED,
+ (string) Status::DECLINED,
+ (string) Status::INFO_REQUESTED,
+ ],
+ 'transitions' => [
+ 'start_processing' => [
+ 'from' => (string) Status::NEW,
+ 'to' => (string) Status::PROCESSING,
+ ],
+ 'accept' => [
+ 'from' => (string) Status::PROCESSING,
+ 'to' => (string) Status::ACCEPTED,
+ ],
+ 'decline' => [
+ 'from' => (string) Status::PROCESSING,
+ 'to' => (string) Status::DECLINED,
+ ],
+ 'request_info' => [
+ 'from' => (string) Status::PROCESSING,
+ 'to' => (string) Status::INFO_REQUESTED,
+ ],
+ 'resume_processing' => [
+ 'from' => (string) Status::INFO_REQUESTED,
+ 'to' => (string) Status::PROCESSING,
+ ],
+ ],
+ ],
+ ],
+]);
diff --git a/src/Eccube/Service/MailService.php b/src/Eccube/Service/MailService.php
index 5a5328fec2e..5d1843d0f5a 100644
--- a/src/Eccube/Service/MailService.php
+++ b/src/Eccube/Service/MailService.php
@@ -21,6 +21,7 @@
use Eccube\Entity\MailTemplate;
use Eccube\Entity\Order;
use Eccube\Entity\OrderItem;
+use Eccube\Entity\RefundRequest;
use Eccube\Entity\Shipping;
use Eccube\Event\EccubeEvents;
use Eccube\Event\EventArgs;
@@ -858,4 +859,63 @@ public function convertRFCViolatingEmail(string $email): Address
return new Address($email);
}
+
+ /**
+ * 管理者へ返品申請の通知メールを送信する.
+ */
+ public function sendRefundRequestNotifyMail(RefundRequest $RefundRequest): void
+ {
+ log_info('返品申請通知メール送信開始', ['id' => $RefundRequest->getId()]);
+
+ $MailTemplate = $this->mailTemplateRepository->find($this->eccubeConfig['eccube_refund_request_notify_mail_template_id']);
+
+ $body = $this->twig->render($MailTemplate->getFileName(), [
+ 'RefundRequest' => $RefundRequest,
+ ]);
+
+ $message = (new Email())
+ ->subject('['.$this->BaseInfo->getShopName().'] '.$MailTemplate->getMailSubject())
+ ->from(new Address($this->BaseInfo->getEmail01(), $this->BaseInfo->getShopName()))
+ ->to($this->BaseInfo->getEmail01())
+ ->replyTo($this->BaseInfo->getEmail03())
+ ->returnPath($this->BaseInfo->getEmail04());
+
+ $htmlFileName = $this->getHtmlTemplate($MailTemplate->getFileName());
+ if (!is_null($htmlFileName)) {
+ $htmlBody = $this->twig->render($htmlFileName, [
+ 'RefundRequest' => $RefundRequest,
+ ]);
+ $message
+ ->text($body)
+ ->html($htmlBody);
+ } else {
+ $message->text($body);
+ }
+
+ $event = new EventArgs(
+ [
+ 'message' => $message,
+ 'RefundRequest' => $RefundRequest,
+ 'MailTemplate' => $MailTemplate,
+ 'BaseInfo' => $this->BaseInfo,
+ ]
+ );
+ $this->eventDispatcher->dispatch($event, EccubeEvents::MAIL_REFUND_REQUEST);
+
+ $message = $event->getArgument('message');
+
+ try {
+ $this->mailer->send($message);
+
+ $MailHistory = new MailHistory();
+ $MailHistory->setMailSubject($message->getSubject())
+ ->setMailBody($message->getTextBody())
+ ->setMailTemplate($MailTemplate)
+ ->setOrder($RefundRequest->getOrder())
+ ->setSendDate(new \DateTime());
+ $this->mailHistoryRepository->save($MailHistory);
+ } catch (TransportExceptionInterface $e) {
+ log_error('返品申請通知メールの送信に失敗しました。', ['RefundRequest' => $RefundRequest->getId()]);
+ }
+ }
}
diff --git a/src/Eccube/Service/RefundRequestService.php b/src/Eccube/Service/RefundRequestService.php
new file mode 100644
index 00000000000..c7a0ec95c01
--- /dev/null
+++ b/src/Eccube/Service/RefundRequestService.php
@@ -0,0 +1,135 @@
+refundRequestStatusRepository->find(RefundRequestStatus::NEW);
+ $RefundRequest->setRefundRequestStatus($NewStatus);
+
+ $sortNo = 1;
+ foreach ($uploadedFiles as $uploadedFile) {
+ if ($uploadedFile instanceof UploadedFile) {
+ $RefundRequest->addRefundRequestFile($this->saveFile($uploadedFile, $sortNo++));
+ }
+ }
+
+ $this->entityManager->persist($RefundRequest);
+ $this->entityManager->flush();
+
+ $this->mailService->sendRefundRequestNotifyMail($RefundRequest);
+
+ return $RefundRequest;
+ }
+
+ /**
+ * ステータスを遷移させる.
+ *
+ * @throws \InvalidArgumentException 不正な遷移の場合
+ */
+ public function changeStatus(RefundRequest $RefundRequest, string $transition): void
+ {
+ $PreviousStatus = $RefundRequest->getRefundRequestStatus();
+
+ $this->refundRequestStateMachine->applyTransition($RefundRequest, $transition);
+ $this->entityManager->flush();
+
+ $event = new RefundRequestEvent($RefundRequest, $PreviousStatus, $RefundRequest->getRefundRequestStatus());
+ $this->eventDispatcher->dispatch($event, EccubeEvents::REFUND_REQUEST_STATUS_CHANGE);
+ }
+
+ /**
+ * 指定した遷移が実行可能かどうかを判定する.
+ */
+ public function canApplyTransition(RefundRequest $RefundRequest, string $transition): bool
+ {
+ return $this->refundRequestStateMachine->can($RefundRequest, $transition);
+ }
+
+ /**
+ * 現在のステータスから実行可能な遷移を取得する.
+ *
+ * @return array [遷移名 => 遷移先ステータス]
+ */
+ public function getAvailableTransitions(RefundRequest $RefundRequest): array
+ {
+ return $this->refundRequestStateMachine->getAvailableTransitions($RefundRequest);
+ }
+
+ /**
+ * エビデンスファイルを非公開ディレクトリ(var/配下)へ保存する.
+ *
+ * 保存名は推測困難なランダム名にする(実体は公開せず、配信はコントローラ経由に限定)。
+ */
+ private function saveFile(UploadedFile $uploadedFile, int $sortNo): RefundRequestFile
+ {
+ $dir = $this->eccubeConfig['eccube_save_refund_request_file_dir'];
+ if (!is_dir($dir)) {
+ mkdir($dir, 0755, true);
+ }
+
+ $mimeType = $uploadedFile->getMimeType();
+ $fileSize = $uploadedFile->getSize();
+ $extension = $uploadedFile->guessExtension() ?: $uploadedFile->getClientOriginalExtension();
+ $fileName = bin2hex(random_bytes(16)).'.'.$extension;
+
+ $uploadedFile->move($dir, $fileName);
+
+ $RefundRequestFile = new RefundRequestFile();
+ $RefundRequestFile->setFileName($fileName)
+ ->setMimeType($mimeType)
+ ->setFileSize($fileSize)
+ ->setSortNo($sortNo);
+
+ return $RefundRequestFile;
+ }
+}
diff --git a/src/Eccube/Service/RefundRequestStateMachine.php b/src/Eccube/Service/RefundRequestStateMachine.php
new file mode 100644
index 00000000000..75555659181
--- /dev/null
+++ b/src/Eccube/Service/RefundRequestStateMachine.php
@@ -0,0 +1,156 @@
+newContext($RefundRequest);
+ if (!$this->_refundRequestStateMachine->can($context, $transition)) {
+ throw new \InvalidArgumentException(sprintf('Cannot apply transition "%s".', $transition));
+ }
+ $this->_refundRequestStateMachine->apply($context, $transition);
+ }
+
+ /**
+ * 指定した遷移を実行できるかどうかを判定する.
+ */
+ public function can(RefundRequest $RefundRequest, string $transition): bool
+ {
+ if (!$RefundRequest->getRefundRequestStatus()) {
+ return false;
+ }
+
+ return $this->_refundRequestStateMachine->can($this->newContext($RefundRequest), $transition);
+ }
+
+ /**
+ * 現在のステータスから実行可能な遷移を取得する.
+ *
+ * @return array [遷移名 => 遷移先ステータス]
+ */
+ public function getAvailableTransitions(RefundRequest $RefundRequest): array
+ {
+ if (!$RefundRequest->getRefundRequestStatus()) {
+ return [];
+ }
+
+ $result = [];
+ foreach ($this->_refundRequestStateMachine->getEnabledTransitions($this->newContext($RefundRequest)) as $transition) {
+ $toId = (int) $transition->getTos()[0];
+ $Status = $this->refundRequestStatusRepository->find($toId);
+ if ($Status instanceof RefundRequestStatus) {
+ $result[$transition->getName()] = $Status;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ #[\Override]
+ public static function getSubscribedEvents(): array
+ {
+ return [
+ 'workflow.refund_request.completed' => ['onCompleted'],
+ ];
+ }
+
+ /**
+ * 申請ステータスを再設定する.
+ * StateMachine による遷移完了時には marking(id)が変更されるだけなので、
+ * RefundRequestStatus エンティティを設定し直す.
+ */
+ public function onCompleted(Event $event): void
+ {
+ /** @var RefundRequestStateMachineContext $context */
+ $context = $event->getSubject();
+ $RefundRequest = $context->getRefundRequest();
+ $CompletedStatus = $this->refundRequestStatusRepository->find((int) $context->getStatus());
+ $RefundRequest->setRefundRequestStatus($CompletedStatus);
+ }
+
+ private function newContext(RefundRequest $RefundRequest): RefundRequestStateMachineContext
+ {
+ $status = $RefundRequest->getRefundRequestStatus();
+ $statusId = $status ? (string) $status->getId() : '';
+
+ return new RefundRequestStateMachineContext($statusId, $RefundRequest);
+ }
+}
+
+class RefundRequestStateMachineContext
+{
+ public function __construct(private string $status, private readonly RefundRequest $RefundRequest)
+ {
+ }
+
+ public function getStatus(): string
+ {
+ return $this->status;
+ }
+
+ public function setStatus(string $status): void
+ {
+ $this->status = $status;
+ }
+
+ public function getRefundRequest(): RefundRequest
+ {
+ return $this->RefundRequest;
+ }
+
+ /**
+ * Alias of getStatus()
+ */
+ public function getMarking(): string
+ {
+ return $this->getStatus();
+ }
+
+ /**
+ * Alias of setStatus()
+ */
+ public function setMarking(string $status): void
+ {
+ $this->setStatus($status);
+ }
+}
From e3125cc475103aeb2f577555c1c21bddc8e45a88 Mon Sep 17 00:00:00 2001
From: "takumi.tokoro"
Date: Tue, 16 Jun 2026 07:42:50 +0900
Subject: [PATCH 04/24] =?UTF-8?q?feat:=20=E8=BF=94=E5=93=81=E7=94=B3?=
=?UTF-8?q?=E8=AB=8B=E3=81=AE=E3=83=95=E3=82=A9=E3=83=BC=E3=83=A0=E3=82=BF?=
=?UTF-8?q?=E3=82=A4=E3=83=97=E3=82=92=E8=BF=BD=E5=8A=A0=EF=BC=88Front/Adm?=
=?UTF-8?q?in=EF=BC=89=20(#6820)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Front/RefundRequestType: 数量・理由・エビデンスファイル(最大3件/15MB/MIME制限)
- Admin/SearchRefundRequestType: 複合検索・ステータス・作成日・更新日
- Admin/RefundRequestEditType: 管理者メモ・ステータス遷移(利用可能な遷移のみ表示)
- Master/RefundRequestStatusType: マスタセレクト
Co-Authored-By: Claude Opus 4.6
---
.../Form/Type/Admin/RefundRequestEditType.php | 78 ++++++++++++
.../Type/Admin/SearchRefundRequestType.php | 96 +++++++++++++++
.../Form/Type/Front/RefundRequestType.php | 114 ++++++++++++++++++
.../Type/Master/RefundRequestStatusType.php | 51 ++++++++
4 files changed, 339 insertions(+)
create mode 100644 src/Eccube/Form/Type/Admin/RefundRequestEditType.php
create mode 100644 src/Eccube/Form/Type/Admin/SearchRefundRequestType.php
create mode 100644 src/Eccube/Form/Type/Front/RefundRequestType.php
create mode 100644 src/Eccube/Form/Type/Master/RefundRequestStatusType.php
diff --git a/src/Eccube/Form/Type/Admin/RefundRequestEditType.php b/src/Eccube/Form/Type/Admin/RefundRequestEditType.php
new file mode 100644
index 00000000000..aab794a21d2
--- /dev/null
+++ b/src/Eccube/Form/Type/Admin/RefundRequestEditType.php
@@ -0,0 +1,78 @@
+ $options
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options): void
+ {
+ /** @var RefundRequest $RefundRequest */
+ $RefundRequest = $options['refund_request'];
+ $transitions = $this->refundRequestStateMachine->getAvailableTransitions($RefundRequest);
+
+ $choices = [];
+ foreach ($transitions as $transitionName => $Status) {
+ $choices[(string) $Status] = $transitionName;
+ }
+
+ $builder
+ ->add('admin_note', TextareaType::class, [
+ 'required' => false,
+ 'mapped' => false,
+ ])
+ ->add('transition', ChoiceType::class, [
+ 'required' => false,
+ 'mapped' => false,
+ 'choices' => $choices,
+ 'placeholder' => 'admin.order.refund_request.transition_placeholder',
+ ]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'refund_request' => null,
+ ]);
+ $resolver->setAllowedTypes('refund_request', [RefundRequest::class, 'null']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBlockPrefix(): string
+ {
+ return 'admin_refund_request_edit';
+ }
+}
diff --git a/src/Eccube/Form/Type/Admin/SearchRefundRequestType.php b/src/Eccube/Form/Type/Admin/SearchRefundRequestType.php
new file mode 100644
index 00000000000..fc5e72ad182
--- /dev/null
+++ b/src/Eccube/Form/Type/Admin/SearchRefundRequestType.php
@@ -0,0 +1,96 @@
+ $options
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options): void
+ {
+ $builder
+ // 複合検索(申請ID・注文番号・会員ID・会員名)
+ ->add('multi', TextType::class, [
+ 'label' => 'admin.order.refund_request.multi_search_label',
+ 'required' => false,
+ 'constraints' => [
+ new Assert\Length(['max' => $this->eccubeConfig['eccube_stext_len']]),
+ ],
+ ])
+ ->add('status', RefundRequestStatusType::class, [
+ 'label' => 'admin.order.refund_request.status',
+ 'required' => false,
+ 'expanded' => true,
+ 'multiple' => true,
+ ])
+ ->add('create_date_start', DateType::class, [
+ 'label' => 'admin.order.refund_request.create_date__start',
+ 'required' => false,
+ 'input' => 'datetime',
+ 'widget' => 'single_text',
+ ])
+ ->add('create_date_end', DateType::class, [
+ 'label' => 'admin.order.refund_request.create_date__end',
+ 'required' => false,
+ 'input' => 'datetime',
+ 'widget' => 'single_text',
+ ])
+ ->add('update_date_start', DateType::class, [
+ 'label' => 'admin.order.refund_request.update_date__start',
+ 'required' => false,
+ 'input' => 'datetime',
+ 'widget' => 'single_text',
+ ])
+ ->add('update_date_end', DateType::class, [
+ 'label' => 'admin.order.refund_request.update_date__end',
+ 'required' => false,
+ 'input' => 'datetime',
+ 'widget' => 'single_text',
+ ]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'csrf_protection' => false,
+ ]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBlockPrefix(): string
+ {
+ return 'admin_search_refund_request';
+ }
+}
diff --git a/src/Eccube/Form/Type/Front/RefundRequestType.php b/src/Eccube/Form/Type/Front/RefundRequestType.php
new file mode 100644
index 00000000000..d4ceeaa07cc
--- /dev/null
+++ b/src/Eccube/Form/Type/Front/RefundRequestType.php
@@ -0,0 +1,114 @@
+ $options
+ */
+ public function buildForm(FormBuilderInterface $builder, array $options): void
+ {
+ $maxQuantity = $options['max_quantity'];
+
+ $builder
+ // quantity は decimal(string) のため unmapped にし、Controller で文字列化してセットする
+ ->add('quantity', IntegerType::class, [
+ 'mapped' => false,
+ 'constraints' => [
+ new Assert\NotBlank(),
+ new Assert\GreaterThan(0),
+ new Assert\LessThanOrEqual([
+ 'value' => $maxQuantity,
+ 'message' => 'form_error.refund_request.quantity_exceed',
+ ]),
+ ],
+ ])
+ ->add('reason', TextareaType::class, [
+ 'constraints' => [
+ new Assert\NotBlank(),
+ new Assert\Length(['max' => 4000]),
+ ],
+ ])
+ ->add('files', FileType::class, [
+ 'mapped' => false,
+ 'multiple' => true,
+ 'required' => false,
+ 'constraints' => [
+ new Assert\Count([
+ 'max' => self::MAX_FILE_COUNT,
+ 'maxMessage' => 'form_error.refund_request.max_files',
+ ]),
+ new Assert\All([
+ new Assert\File([
+ 'maxSize' => self::MAX_FILE_SIZE,
+ 'maxSizeMessage' => 'form_error.refund_request.file_size',
+ 'mimeTypes' => self::ALLOWED_MIME_TYPES,
+ 'mimeTypesMessage' => 'form_error.refund_request.file_type',
+ ]),
+ ]),
+ ],
+ ]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'data_class' => RefundRequest::class,
+ 'max_quantity' => 1,
+ ]);
+ $resolver->setAllowedTypes('max_quantity', ['int', 'string']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBlockPrefix(): string
+ {
+ return 'refund_request';
+ }
+}
diff --git a/src/Eccube/Form/Type/Master/RefundRequestStatusType.php b/src/Eccube/Form/Type/Master/RefundRequestStatusType.php
new file mode 100644
index 00000000000..c660d071408
--- /dev/null
+++ b/src/Eccube/Form/Type/Master/RefundRequestStatusType.php
@@ -0,0 +1,51 @@
+setDefaults([
+ 'class' => RefundRequestStatus::class,
+ ]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ #[\Override]
+ public function getBlockPrefix(): string
+ {
+ return 'refund_request_status';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ #[\Override]
+ public function getParent(): string
+ {
+ return MasterType::class;
+ }
+}
From 86ff9e76057f37014d3f1458c4d2cb1258d144c7 Mon Sep 17 00:00:00 2001
From: "takumi.tokoro"
Date: Tue, 16 Jun 2026 08:00:24 +0900
Subject: [PATCH 05/24] =?UTF-8?q?feat:=20=E8=BF=94=E5=93=81=E7=94=B3?=
=?UTF-8?q?=E8=AB=8B=E3=81=AEController=E3=83=BB=E3=83=86=E3=83=B3?=
=?UTF-8?q?=E3=83=97=E3=83=AC=E3=83=BC=E3=83=88=E3=83=BB=E3=83=8A=E3=83=93?=
=?UTF-8?q?=E3=83=BB=E7=BF=BB=E8=A8=B3=E3=82=92=E8=BF=BD=E5=8A=A0=20(#6820?=
=?UTF-8?q?)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Mypage/RefundRequestController: 申請入力/確認/完了/商品別履歴/ファイル配信(所有チェック付き)
- Admin/Order/RefundRequestController: 一覧検索/詳細編集/ステータス変更API/ファイル配信
- テンプレート7ファイル(フロント4 + 管理2 + メール通知1)
- history.twig: 発送済み注文に返品申請・履歴リンクを追加
- ナビ: 受注管理配下に「返品申請管理」メニュー追加
- 翻訳: messages/validators の ja/en に返品申請関連キーを追加
Co-Authored-By: Claude Opus 4.6
---
app/config/eccube/packages/eccube_nav.yaml | 3 +
.../Admin/Order/RefundRequestController.php | 273 ++++++++++++++++++
.../Mypage/RefundRequestController.php | 272 +++++++++++++++++
src/Eccube/Resource/locale/messages.en.yaml | 52 ++++
src/Eccube/Resource/locale/messages.ja.yaml | 52 ++++
src/Eccube/Resource/locale/validators.en.yaml | 4 +
src/Eccube/Resource/locale/validators.ja.yaml | 4 +
.../template/admin/Order/refund_request.twig | 145 ++++++++++
.../admin/Order/refund_request_edit.twig | 139 +++++++++
.../default/Mail/refund_request_notify.twig | 39 +++
.../template/default/Mypage/history.twig | 6 +
.../default/Mypage/refund_request.twig | 96 ++++++
.../Mypage/refund_request_complete.twig | 41 +++
.../Mypage/refund_request_confirm.twig | 75 +++++
.../Mypage/refund_request_item_history.twig | 107 +++++++
15 files changed, 1308 insertions(+)
create mode 100644 src/Eccube/Controller/Admin/Order/RefundRequestController.php
create mode 100644 src/Eccube/Controller/Mypage/RefundRequestController.php
create mode 100644 src/Eccube/Resource/template/admin/Order/refund_request.twig
create mode 100644 src/Eccube/Resource/template/admin/Order/refund_request_edit.twig
create mode 100644 src/Eccube/Resource/template/default/Mail/refund_request_notify.twig
create mode 100644 src/Eccube/Resource/template/default/Mypage/refund_request.twig
create mode 100644 src/Eccube/Resource/template/default/Mypage/refund_request_complete.twig
create mode 100644 src/Eccube/Resource/template/default/Mypage/refund_request_confirm.twig
create mode 100644 src/Eccube/Resource/template/default/Mypage/refund_request_item_history.twig
diff --git a/app/config/eccube/packages/eccube_nav.yaml b/app/config/eccube/packages/eccube_nav.yaml
index e83df1b6ee3..54bf654f69d 100644
--- a/app/config/eccube/packages/eccube_nav.yaml
+++ b/app/config/eccube/packages/eccube_nav.yaml
@@ -44,6 +44,9 @@ parameters:
shipping_csv_import:
name: admin.order.shipping_csv_upload
url: admin_shipping_csv_import
+ refund_request:
+ name: admin.order.refund_request.list_title
+ url: admin_refund_request
customer:
name: admin.customer.customer_management
icon: fa-users
diff --git a/src/Eccube/Controller/Admin/Order/RefundRequestController.php b/src/Eccube/Controller/Admin/Order/RefundRequestController.php
new file mode 100644
index 00000000000..aa40c79fd35
--- /dev/null
+++ b/src/Eccube/Controller/Admin/Order/RefundRequestController.php
@@ -0,0 +1,273 @@
+
+ */
+ #[Route(path: '/%eccube_admin_route%/order/refund_request', name: 'admin_refund_request', methods: ['GET', 'POST'])]
+ #[Route(path: '/%eccube_admin_route%/order/refund_request/page/{page_no}', name: 'admin_refund_request_page', requirements: ['page_no' => '\d+'], methods: ['GET', 'POST'])]
+ #[Template(template: '@admin/Order/refund_request.twig')]
+ public function index(Request $request, ?int $page_no = null): array
+ {
+ $builder = $this->formFactory->createBuilder(SearchRefundRequestType::class);
+
+ $event = new EventArgs(
+ [
+ 'builder' => $builder,
+ ],
+ $request
+ );
+ $this->eventDispatcher->dispatch($event, EccubeEvents::ADMIN_REFUND_REQUEST_INDEX_INITIALIZE);
+
+ $searchForm = $builder->getForm();
+
+ $page_count = $this->session->get('eccube.admin.refund_request.search.page_count',
+ $this->eccubeConfig->get('eccube_default_page_count'));
+
+ $page_count_param = (int) $request->get('page_count');
+ $pageMaxis = $this->pageMaxRepository->findAll();
+
+ if ($page_count_param) {
+ foreach ($pageMaxis as $pageMax) {
+ if ($page_count_param == $pageMax->getName()) {
+ $page_count = $pageMax->getName();
+ $this->session->set('eccube.admin.refund_request.search.page_count', $page_count);
+ break;
+ }
+ }
+ }
+
+ if ('POST' === $request->getMethod()) {
+ $searchForm->handleRequest($request);
+
+ if ($searchForm->isValid()) {
+ $page_no = 1;
+ $searchData = $searchForm->getData();
+
+ $this->session->set('eccube.admin.refund_request.search', FormUtil::getViewData($searchForm));
+ $this->session->set('eccube.admin.refund_request.search.page_no', $page_no);
+ } else {
+ return [
+ 'searchForm' => $searchForm->createView(),
+ 'pagination' => [],
+ 'pageMaxis' => $pageMaxis,
+ 'page_no' => $page_no,
+ 'page_count' => $page_count,
+ 'has_errors' => true,
+ ];
+ }
+ } else {
+ if (null !== $page_no || $request->get('resume')) {
+ if ($page_no) {
+ $this->session->set('eccube.admin.refund_request.search.page_no', (int) $page_no);
+ } else {
+ $page_no = $this->session->get('eccube.admin.refund_request.search.page_no', 1);
+ }
+ $viewData = $this->session->get('eccube.admin.refund_request.search', []);
+ $searchData = FormUtil::submitAndGetData($searchForm, $viewData);
+ } else {
+ $page_no = 1;
+ $viewData = [];
+ $searchData = FormUtil::submitAndGetData($searchForm, $viewData);
+
+ $this->session->set('eccube.admin.refund_request.search', $viewData);
+ $this->session->set('eccube.admin.refund_request.search.page_no', $page_no);
+ }
+ }
+
+ $qb = $this->refundRequestRepository->getQueryBuilderBySearchData($searchData);
+
+ $event = new EventArgs(
+ [
+ 'qb' => $qb,
+ 'searchData' => $searchData,
+ ],
+ $request
+ );
+ $this->eventDispatcher->dispatch($event, EccubeEvents::ADMIN_REFUND_REQUEST_INDEX_SEARCH);
+
+ $pagination = $this->paginator->paginate(
+ $qb,
+ $page_no,
+ $page_count
+ );
+
+ return [
+ 'searchForm' => $searchForm->createView(),
+ 'pagination' => $pagination,
+ 'pageMaxis' => $pageMaxis,
+ 'page_no' => $page_no,
+ 'page_count' => $page_count,
+ 'has_errors' => false,
+ ];
+ }
+
+ /**
+ * 返品申請詳細画面.
+ *
+ * @return Response|array
+ */
+ #[Route(path: '/%eccube_admin_route%/order/refund_request/{id}/edit', name: 'admin_refund_request_edit', requirements: ['id' => '\d+'], methods: ['GET', 'POST'])]
+ #[Template(template: '@admin/Order/refund_request_edit.twig')]
+ public function edit(Request $request, RefundRequest $RefundRequest): Response|array
+ {
+ $form = $this->createForm(RefundRequestEditType::class, null, [
+ 'refund_request' => $RefundRequest,
+ ]);
+ $form->get('admin_note')->setData($RefundRequest->getAdminNote());
+
+ $event = new EventArgs(
+ [
+ 'form' => $form,
+ 'RefundRequest' => $RefundRequest,
+ ],
+ $request
+ );
+ $this->eventDispatcher->dispatch($event, EccubeEvents::ADMIN_REFUND_REQUEST_EDIT_INITIALIZE);
+
+ $form->handleRequest($request);
+
+ if ($form->isSubmitted() && $form->isValid()) {
+ $adminNote = $form->get('admin_note')->getData();
+ $RefundRequest->setAdminNote($adminNote);
+ $this->entityManager->flush();
+
+ $transition = $form->get('transition')->getData();
+ if ($transition) {
+ try {
+ $this->refundRequestService->changeStatus($RefundRequest, $transition);
+ $this->addSuccess('admin.common.save_complete', 'admin');
+ } catch (\InvalidArgumentException $e) {
+ $this->addError('admin.order.refund_request.transition_error', 'admin');
+ }
+ } else {
+ $this->addSuccess('admin.common.save_complete', 'admin');
+ }
+
+ $event = new EventArgs(
+ [
+ 'form' => $form,
+ 'RefundRequest' => $RefundRequest,
+ ],
+ $request
+ );
+ $this->eventDispatcher->dispatch($event, EccubeEvents::ADMIN_REFUND_REQUEST_EDIT_COMPLETE);
+
+ return $this->redirectToRoute('admin_refund_request_edit', ['id' => $RefundRequest->getId()]);
+ }
+
+ $availableTransitions = $this->refundRequestService->getAvailableTransitions($RefundRequest);
+
+ return [
+ 'form' => $form->createView(),
+ 'RefundRequest' => $RefundRequest,
+ 'availableTransitions' => $availableTransitions,
+ ];
+ }
+
+ /**
+ * ステータス変更 API.
+ */
+ #[Route(path: '/%eccube_admin_route%/order/refund_request/{id}/update_status', name: 'admin_refund_request_update_status', requirements: ['id' => '\d+'], methods: ['PUT'])]
+ public function updateStatus(Request $request, RefundRequest $RefundRequest): JsonResponse
+ {
+ $this->isTokenValid();
+
+ $transition = $request->get('transition');
+ if (!$transition) {
+ throw new BadRequestHttpException();
+ }
+
+ try {
+ $this->refundRequestService->changeStatus($RefundRequest, $transition);
+
+ return $this->json([
+ 'success' => true,
+ 'status' => (string) $RefundRequest->getRefundRequestStatus(),
+ ]);
+ } catch (\InvalidArgumentException $e) {
+ return $this->json([
+ 'success' => false,
+ 'message' => trans('admin.order.refund_request.transition_error'),
+ ], Response::HTTP_BAD_REQUEST);
+ }
+ }
+
+ /**
+ * エビデンスファイル配信(管理画面).
+ */
+ #[Route(path: '/%eccube_admin_route%/order/refund_request/{id}/file/{file_id}', name: 'admin_refund_request_file', requirements: ['id' => '\d+', 'file_id' => '\d+'], methods: ['GET'])]
+ public function downloadFile(RefundRequest $RefundRequest, int $file_id): BinaryFileResponse
+ {
+ $RefundRequestFile = null;
+ foreach ($RefundRequest->getRefundRequestFiles() as $File) {
+ if ($File->getId() === $file_id) {
+ $RefundRequestFile = $File;
+ break;
+ }
+ }
+ if (!$RefundRequestFile) {
+ throw new NotFoundHttpException();
+ }
+
+ $filePath = $this->eccubeConfig['eccube_save_refund_request_file_dir'].'/'.$RefundRequestFile->getFileName();
+ $topDir = $this->eccubeConfig['eccube_save_refund_request_file_dir'];
+
+ if (str_contains($filePath, '..')) {
+ throw new NotFoundHttpException();
+ }
+
+ $realPath = realpath($filePath);
+ $realTopDir = realpath($topDir);
+
+ if ($realPath === false || $realTopDir === false || !str_starts_with($realPath, $realTopDir)) {
+ throw new NotFoundHttpException();
+ }
+
+ return new BinaryFileResponse($realPath);
+ }
+}
diff --git a/src/Eccube/Controller/Mypage/RefundRequestController.php b/src/Eccube/Controller/Mypage/RefundRequestController.php
new file mode 100644
index 00000000000..f3d48fd45c3
--- /dev/null
+++ b/src/Eccube/Controller/Mypage/RefundRequestController.php
@@ -0,0 +1,272 @@
+
+ */
+ #[Route(path: '/mypage/refund_request/{order_no}/{order_item_id}', name: 'mypage_refund_request', requirements: ['order_item_id' => '\d+'], methods: ['GET', 'POST'])]
+ #[Template(template: 'Mypage/refund_request.twig')]
+ public function index(Request $request, string $order_no, int $order_item_id): Response|RedirectResponse|array
+ {
+ $Order = $this->getValidOrder($order_no);
+ $OrderItem = $this->getValidOrderItem($Order, $order_item_id);
+
+ /** @var Customer $Customer */
+ $Customer = $this->getUser();
+
+ $RefundRequest = new RefundRequest();
+ $RefundRequest->setOrder($Order);
+ $RefundRequest->setOrderItem($OrderItem);
+ $RefundRequest->setCustomer($Customer);
+
+ $maxQuantity = (int) $OrderItem->getQuantity();
+ $form = $this->createForm(RefundRequestType::class, $RefundRequest, [
+ 'max_quantity' => $maxQuantity,
+ ]);
+
+ $event = new EventArgs(
+ [
+ 'form' => $form,
+ 'Order' => $Order,
+ 'OrderItem' => $OrderItem,
+ ],
+ $request
+ );
+ $this->eventDispatcher->dispatch($event, EccubeEvents::FRONT_MYPAGE_REFUND_REQUEST_INDEX_INITIALIZE);
+
+ $form->handleRequest($request);
+
+ if ($form->isSubmitted() && $form->isValid()) {
+ $quantity = $form->get('quantity')->getData();
+ $RefundRequest->setQuantity((string) $quantity);
+
+ switch ($request->get('mode')) {
+ case 'confirm':
+ log_info('返品申請確認画面表示', ['order_no' => $order_no, 'order_item_id' => $order_item_id]);
+
+ return $this->render(
+ 'Mypage/refund_request_confirm.twig',
+ [
+ 'form' => $form->createView(),
+ 'Order' => $Order,
+ 'OrderItem' => $OrderItem,
+ 'RefundRequest' => $RefundRequest,
+ ]
+ );
+
+ case 'complete':
+ log_info('返品申請処理開始', ['order_no' => $order_no, 'order_item_id' => $order_item_id]);
+
+ $uploadedFiles = $form->get('files')->getData() ?? [];
+ $this->refundRequestService->createRefundRequest($RefundRequest, $uploadedFiles);
+
+ log_info('返品申請処理完了', ['id' => $RefundRequest->getId()]);
+
+ $event = new EventArgs(
+ [
+ 'form' => $form,
+ 'Order' => $Order,
+ 'OrderItem' => $OrderItem,
+ 'RefundRequest' => $RefundRequest,
+ ],
+ $request
+ );
+ $this->eventDispatcher->dispatch($event, EccubeEvents::FRONT_MYPAGE_REFUND_REQUEST_INDEX_COMPLETE);
+
+ return $this->redirectToRoute('mypage_refund_request_complete', [
+ 'order_no' => $order_no,
+ 'order_item_id' => $order_item_id,
+ ]);
+ }
+ }
+
+ return [
+ 'form' => $form->createView(),
+ 'Order' => $Order,
+ 'OrderItem' => $OrderItem,
+ 'max_quantity' => $maxQuantity,
+ ];
+ }
+
+ /**
+ * 返品申請完了画面.
+ *
+ * @return array
+ */
+ #[Route(path: '/mypage/refund_request/{order_no}/{order_item_id}/complete', name: 'mypage_refund_request_complete', requirements: ['order_item_id' => '\d+'], methods: ['GET'])]
+ #[Template(template: 'Mypage/refund_request_complete.twig')]
+ public function complete(string $order_no, int $order_item_id): array
+ {
+ return [
+ 'order_no' => $order_no,
+ 'order_item_id' => $order_item_id,
+ ];
+ }
+
+ /**
+ * 商品別返品申請履歴画面.
+ *
+ * @return array
+ */
+ #[Route(path: '/mypage/refund_request/{order_no}/{order_item_id}/history', name: 'mypage_refund_request_item_history', requirements: ['order_item_id' => '\d+'], methods: ['GET'])]
+ #[Template(template: 'Mypage/refund_request_item_history.twig')]
+ public function itemHistory(Request $request, string $order_no, int $order_item_id): array
+ {
+ $Order = $this->getValidOrder($order_no);
+ $OrderItem = $this->getValidOrderItem($Order, $order_item_id);
+
+ /** @var Customer $Customer */
+ $Customer = $this->getUser();
+ $RefundRequests = $this->refundRequestRepository->findByOrderItemAndCustomer($OrderItem, $Customer);
+
+ $event = new EventArgs(
+ [
+ 'Order' => $Order,
+ 'OrderItem' => $OrderItem,
+ 'RefundRequests' => $RefundRequests,
+ ],
+ $request
+ );
+ $this->eventDispatcher->dispatch($event, EccubeEvents::FRONT_MYPAGE_REFUND_REQUEST_ITEM_HISTORY_INITIALIZE);
+
+ return [
+ 'Order' => $Order,
+ 'OrderItem' => $OrderItem,
+ 'RefundRequests' => $RefundRequests,
+ ];
+ }
+
+ /**
+ * エビデンスファイル配信(会員所有チェック付き).
+ */
+ #[Route(path: '/mypage/refund_request/file/{refund_request_id}/{file_id}', name: 'mypage_refund_request_file', requirements: ['refund_request_id' => '\d+', 'file_id' => '\d+'], methods: ['GET'])]
+ public function downloadFile(int $refund_request_id, int $file_id): BinaryFileResponse
+ {
+ /** @var Customer $Customer */
+ $Customer = $this->getUser();
+
+ $RefundRequest = $this->refundRequestRepository->find($refund_request_id);
+ if (!$RefundRequest || $RefundRequest->getCustomer()?->getId() !== $Customer->getId()) {
+ throw new NotFoundHttpException();
+ }
+
+ $RefundRequestFile = null;
+ foreach ($RefundRequest->getRefundRequestFiles() as $File) {
+ if ($File->getId() === $file_id) {
+ $RefundRequestFile = $File;
+ break;
+ }
+ }
+ if (!$RefundRequestFile) {
+ throw new NotFoundHttpException();
+ }
+
+ $filePath = $this->eccubeConfig['eccube_save_refund_request_file_dir'].'/'.$RefundRequestFile->getFileName();
+ $topDir = $this->eccubeConfig['eccube_save_refund_request_file_dir'];
+
+ if (str_contains($filePath, '..')) {
+ throw new NotFoundHttpException();
+ }
+
+ $realPath = realpath($filePath);
+ $realTopDir = realpath($topDir);
+
+ if ($realPath === false || $realTopDir === false || !str_starts_with($realPath, $realTopDir)) {
+ throw new NotFoundHttpException();
+ }
+
+ return new BinaryFileResponse($realPath);
+ }
+
+ /**
+ * 注文の所有チェック + 発送済みステータス検証.
+ */
+ private function getValidOrder(string $order_no): Order
+ {
+ $this->entityManager->getFilters()->enable('incomplete_order_status_hidden');
+
+ /** @var Order|null $Order */
+ $Order = $this->orderRepository->findOneBy([
+ 'order_no' => $order_no,
+ 'Customer' => $this->getUser(),
+ ]);
+
+ if (!$Order) {
+ throw new NotFoundHttpException();
+ }
+
+ if ($Order->getOrderStatus()->getId() !== OrderStatus::DELIVERED) {
+ throw new AccessDeniedHttpException();
+ }
+
+ return $Order;
+ }
+
+ /**
+ * 注文明細の所有チェック + 商品明細かつ返品許可チェック.
+ */
+ private function getValidOrderItem(Order $Order, int $order_item_id): OrderItem
+ {
+ $OrderItem = null;
+ foreach ($Order->getOrderItems() as $item) {
+ if ($item->getId() === $order_item_id && $item->isProduct()) {
+ $OrderItem = $item;
+ break;
+ }
+ }
+
+ if (!$OrderItem) {
+ throw new NotFoundHttpException();
+ }
+
+ if ($OrderItem->getProduct() && !$OrderItem->getProduct()->isRefundAllowed()) {
+ throw new AccessDeniedHttpException();
+ }
+
+ return $OrderItem;
+ }
+}
diff --git a/src/Eccube/Resource/locale/messages.en.yaml b/src/Eccube/Resource/locale/messages.en.yaml
index 3807b8b6380..6b3433080cd 100644
--- a/src/Eccube/Resource/locale/messages.en.yaml
+++ b/src/Eccube/Resource/locale/messages.en.yaml
@@ -274,6 +274,30 @@ front.mypage.withdraw_complete_message__body: |
front.mypage.customer.notify_title: Change customer information
front.mypage.delivery.notify_title: Change delivery information
+#------------------------------------------------------------------------------------
+# Refund Request (Front)
+#------------------------------------------------------------------------------------
+front.mypage.refund_request.title: Refund Request
+front.mypage.refund_request.confirm_title: Confirm Refund Request
+front.mypage.refund_request.complete_title: Refund Request Complete
+front.mypage.refund_request.item_history_title: Refund Request History
+front.mypage.refund_request.quantity: Quantity
+front.mypage.refund_request.quantity_max: "Up to %max% items"
+front.mypage.refund_request.reason: Reason
+front.mypage.refund_request.reason_placeholder: Please enter the reason for refund
+front.mypage.refund_request.files: Evidence Files
+front.mypage.refund_request.files_help: "You can attach up to 3 files (JPEG, PNG, GIF, WebP, MP4, MOV, AVI). Max 15MB each."
+front.mypage.refund_request.confirm: Confirm
+front.mypage.refund_request.submit: Submit Request
+front.mypage.refund_request.complete_message: Your refund request has been submitted
+front.mypage.refund_request.complete_description: We will review your request and contact you shortly. Please wait.
+front.mypage.refund_request.back_to_mypage: Back to My Page
+front.mypage.refund_request.request_date: Request Date
+front.mypage.refund_request.status: Status
+front.mypage.refund_request.no_history: No refund request history.
+front.mypage.refund_request.apply: Refund Request
+front.mypage.refund_request.history: Request History
+
#------------------------------------------------------------------------------------
# Products
#------------------------------------------------------------------------------------
@@ -966,6 +990,34 @@ admin.order.shipping_csv.tracking_number_description: Enter alphanumeric charact
admin.order.shipping_csv.shipping_date_col: Shipping Date
admin.order.shipping_csv.shipping_date_description: Enter the shipping date in the format of MM/DD/YYYY
+#------------------------------------------------------------------------------------
+# Refund Request (Admin)
+#------------------------------------------------------------------------------------
+admin.order.refund_request.list_title: Refund Requests
+admin.order.refund_request.edit_title: Refund Request Detail
+admin.order.refund_request.detail: Request Detail
+admin.order.refund_request.operation: Operation
+admin.order.refund_request.id: Request ID
+admin.order.refund_request.order_no: Order No
+admin.order.refund_request.customer: Customer
+admin.order.refund_request.product: Product
+admin.order.refund_request.status: Status
+admin.order.refund_request.quantity: Quantity
+admin.order.refund_request.reason: Reason
+admin.order.refund_request.create_date: Request Date
+admin.order.refund_request.create_date__start: Request Date (from)
+admin.order.refund_request.create_date__end: Request Date (to)
+admin.order.refund_request.update_date: Updated
+admin.order.refund_request.update_date__start: Updated (from)
+admin.order.refund_request.update_date__end: Updated (to)
+admin.order.refund_request.files: Attached Files
+admin.order.refund_request.admin_note: Admin Note
+admin.order.refund_request.transition: Change Status
+admin.order.refund_request.transition_placeholder: -- No Change --
+admin.order.refund_request.transition_error: Failed to change status.
+admin.order.refund_request.back_to_list: Back to list
+admin.order.refund_request.multi_search_label: Search by Request ID, Order No, Customer ID, Customer Name
+
#------------------------------------------------------------------------------------
# Customer
#------------------------------------------------------------------------------------
diff --git a/src/Eccube/Resource/locale/messages.ja.yaml b/src/Eccube/Resource/locale/messages.ja.yaml
index 96445e787c0..c306375cf11 100644
--- a/src/Eccube/Resource/locale/messages.ja.yaml
+++ b/src/Eccube/Resource/locale/messages.ja.yaml
@@ -274,6 +274,30 @@ front.mypage.withdraw_complete_message__body: |
front.mypage.customer.notify_title: 会員情報編集
front.mypage.delivery.notify_title: お届け先情報編集
+#------------------------------------------------------------------------------------
+# 返品申請(フロント)
+#------------------------------------------------------------------------------------
+front.mypage.refund_request.title: 返品申請
+front.mypage.refund_request.confirm_title: 返品申請内容確認
+front.mypage.refund_request.complete_title: 返品申請完了
+front.mypage.refund_request.item_history_title: 返品申請履歴
+front.mypage.refund_request.quantity: 返品希望数量
+front.mypage.refund_request.quantity_max: "最大 %max% 個まで申請できます"
+front.mypage.refund_request.reason: 返品理由
+front.mypage.refund_request.reason_placeholder: 返品理由を入力してください
+front.mypage.refund_request.files: 証拠ファイル
+front.mypage.refund_request.files_help: "画像(JPEG, PNG, GIF, WebP)または動画(MP4, MOV, AVI)を最大3件まで添付できます。1ファイル15MBまで。"
+front.mypage.refund_request.confirm: 確認画面へ
+front.mypage.refund_request.submit: 申請する
+front.mypage.refund_request.complete_message: 返品申請を受け付けました
+front.mypage.refund_request.complete_description: 返品申請の内容を確認のうえ、ご連絡いたします。しばらくお待ちください。
+front.mypage.refund_request.back_to_mypage: マイページへ戻る
+front.mypage.refund_request.request_date: 申請日時
+front.mypage.refund_request.status: ステータス
+front.mypage.refund_request.no_history: 返品申請履歴はありません。
+front.mypage.refund_request.apply: 返品申請
+front.mypage.refund_request.history: 返品申請履歴
+
#------------------------------------------------------------------------------------
# 商品
#------------------------------------------------------------------------------------
@@ -965,6 +989,34 @@ admin.order.shipping_csv.tracking_number_description: 半角英数字かハイ
admin.order.shipping_csv.shipping_date_col: 出荷日
admin.order.shipping_csv.shipping_date_description: "出荷日を「YYYY-MM-DD」の形式で設定"
+#------------------------------------------------------------------------------------
+# 返品申請(管理画面)
+#------------------------------------------------------------------------------------
+admin.order.refund_request.list_title: 返品申請管理
+admin.order.refund_request.edit_title: 返品申請詳細
+admin.order.refund_request.detail: 申請内容
+admin.order.refund_request.operation: 操作
+admin.order.refund_request.id: 申請ID
+admin.order.refund_request.order_no: 注文番号
+admin.order.refund_request.customer: 会員
+admin.order.refund_request.product: 商品
+admin.order.refund_request.status: ステータス
+admin.order.refund_request.quantity: 返品数量
+admin.order.refund_request.reason: 返品理由
+admin.order.refund_request.create_date: 申請日時
+admin.order.refund_request.create_date__start: 申請日(開始)
+admin.order.refund_request.create_date__end: 申請日(終了)
+admin.order.refund_request.update_date: 更新日時
+admin.order.refund_request.update_date__start: 更新日(開始)
+admin.order.refund_request.update_date__end: 更新日(終了)
+admin.order.refund_request.files: 添付ファイル
+admin.order.refund_request.admin_note: 管理者メモ
+admin.order.refund_request.transition: ステータス変更
+admin.order.refund_request.transition_placeholder: -- 変更なし --
+admin.order.refund_request.transition_error: ステータスの変更に失敗しました。
+admin.order.refund_request.back_to_list: 返品申請一覧へ戻る
+admin.order.refund_request.multi_search_label: 申請ID・注文番号・会員ID・会員名で検索
+
#------------------------------------------------------------------------------------
# 会員
#------------------------------------------------------------------------------------
diff --git a/src/Eccube/Resource/locale/validators.en.yaml b/src/Eccube/Resource/locale/validators.en.yaml
index 188d0b50b08..7b0193f42f2 100644
--- a/src/Eccube/Resource/locale/validators.en.yaml
+++ b/src/Eccube/Resource/locale/validators.en.yaml
@@ -72,3 +72,7 @@ form.type.add.quantity: Please enter more than 1.
form.type.select.select_is_future_or_now_date: Invalid Date of Birth.
form.type.admin.nottrackingnumberstyle: Tracking No. entry must be alphanumeric chars and hypens.
form.type.float.invalid: Only numbers and decimal points are accepted.
+form_error.refund_request.quantity_exceed: Quantity must be {{ compared_value }} or less.
+form_error.refund_request.max_files: You can attach up to {{ limit }} files.
+form_error.refund_request.file_size: File is too large. Max {{ limit }}.
+form_error.refund_request.file_type: File type not allowed. Please use JPEG, PNG, GIF, WebP, MP4, MOV, or AVI.
diff --git a/src/Eccube/Resource/locale/validators.ja.yaml b/src/Eccube/Resource/locale/validators.ja.yaml
index 341700bddea..c57fd0b769f 100644
--- a/src/Eccube/Resource/locale/validators.ja.yaml
+++ b/src/Eccube/Resource/locale/validators.ja.yaml
@@ -75,3 +75,7 @@ form.type.add.quantity: 1以上で入力してください。
form.type.select.select_is_future_or_now_date: 生年月日が不正な日付です。
form.type.admin.nottrackingnumberstyle: 送り状番号は半角英数字かハイフンのみを入力してください。
form.type.float.invalid: 数字と小数点のみ入力できます。
+form_error.refund_request.quantity_exceed: 返品数量は{{ compared_value }}以下で指定してください。
+form_error.refund_request.max_files: ファイルは{{ limit }}件まで添付できます。
+form_error.refund_request.file_size: ファイルサイズが大きすぎます。{{ limit }}以内にしてください。
+form_error.refund_request.file_type: 許可されていないファイル形式です。画像(JPEG, PNG, GIF, WebP)または動画(MP4, MOV, AVI)を添付してください。
diff --git a/src/Eccube/Resource/template/admin/Order/refund_request.twig b/src/Eccube/Resource/template/admin/Order/refund_request.twig
new file mode 100644
index 00000000000..f32a41087e2
--- /dev/null
+++ b/src/Eccube/Resource/template/admin/Order/refund_request.twig
@@ -0,0 +1,145 @@
+{#
+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.
+#}
+{% extends '@admin/default_frame.twig' %}
+{% set menus = ['order', 'refund_request'] %}
+
+{% block title %}{{ 'admin.order.refund_request.list_title'|trans }}{% endblock %}
+{% block sub_title %}{{ 'admin.order.order_management'|trans }}{% endblock %}
+
+{% form_theme searchForm '@admin/Form/bootstrap_4_layout.html.twig' %}
+{% block javascript %}
+{% endblock %}
+
+{% block main %}
+
+
+ {% if pagination is not empty and pagination|length > 0 %}
+
+
+
+ {{ 'admin.common.search_result'|trans({ '%count%': pagination.getTotalItemCount }) }}
+
+
+
+
+
+
+
+
+ {{ 'admin.order.refund_request.id'|trans }}
+ {{ 'admin.order.refund_request.order_no'|trans }}
+ {{ 'admin.order.refund_request.customer'|trans }}
+ {{ 'admin.order.refund_request.product'|trans }}
+ {{ 'admin.order.refund_request.status'|trans }}
+ {{ 'admin.order.refund_request.create_date'|trans }}
+
+
+
+
+ {% for RefundRequest in pagination %}
+
+ {{ RefundRequest.id }}
+
+ {{ RefundRequest.Order.order_no }}
+
+
+ {% if RefundRequest.Customer %}
+ {{ RefundRequest.Customer.name01 }} {{ RefundRequest.Customer.name02 }}
+ {% endif %}
+
+ {{ RefundRequest.OrderItem.productName }}
+ {{ RefundRequest.RefundRequestStatus }}
+ {{ RefundRequest.create_date|date_sec }}
+
+
+
+
+
+
+ {% endfor %}
+
+
+
+
+
+ {% include "@admin/pager.twig" with { 'pages': pagination.getPaginationData } %}
+
+ {% elseif has_errors %}
+ {% else %}
+
+
{{ 'admin.common.search_no_result'|trans }}
+
+ {% endif %}
+{% endblock %}
diff --git a/src/Eccube/Resource/template/admin/Order/refund_request_edit.twig b/src/Eccube/Resource/template/admin/Order/refund_request_edit.twig
new file mode 100644
index 00000000000..66fb4bca679
--- /dev/null
+++ b/src/Eccube/Resource/template/admin/Order/refund_request_edit.twig
@@ -0,0 +1,139 @@
+{#
+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.
+#}
+{% extends '@admin/default_frame.twig' %}
+{% set menus = ['order', 'refund_request'] %}
+
+{% block title %}{{ 'admin.order.refund_request.edit_title'|trans }}{% endblock %}
+{% block sub_title %}{{ 'admin.order.order_management'|trans }}{% endblock %}
+
+{% form_theme form '@admin/Form/bootstrap_4_layout.html.twig' %}
+{% block javascript %}
+{% endblock %}
+
+{% block main %}
+
+
+
+
+
+
{{ 'admin.order.refund_request.id'|trans }}
+
{{ RefundRequest.id }}
+
+
+
{{ 'admin.order.refund_request.status'|trans }}
+
{{ RefundRequest.RefundRequestStatus }}
+
+
+
{{ 'admin.order.refund_request.order_no'|trans }}
+
+
+
+
{{ 'admin.order.refund_request.customer'|trans }}
+
+
+
+
{{ 'admin.order.refund_request.product'|trans }}
+
{{ RefundRequest.OrderItem.productName }}
+
+
+
{{ 'admin.order.refund_request.quantity'|trans }}
+
{{ RefundRequest.quantity }}
+
+
+
{{ 'admin.order.refund_request.reason'|trans }}
+
{{ RefundRequest.reason|nl2br }}
+
+
+
{{ 'admin.order.refund_request.create_date'|trans }}
+
{{ RefundRequest.create_date|date_sec }}
+
+
+
{{ 'admin.order.refund_request.update_date'|trans }}
+
{{ RefundRequest.update_date|date_sec }}
+
+
+ {% if RefundRequest.RefundRequestFiles is not empty %}
+
+
{{ 'admin.order.refund_request.files'|trans }}
+
+ {% for File in RefundRequest.RefundRequestFiles %}
+
+ {% if File.isImage %}
+
+
+
+ {% elseif File.isVideo %}
+
+
+
+ {% else %}
+
{{ File.file_name }}
+ {% endif %}
+
+ {% endfor %}
+
+
+ {% endif %}
+
+
+
+
+
+{% endblock %}
diff --git a/src/Eccube/Resource/template/default/Mail/refund_request_notify.twig b/src/Eccube/Resource/template/default/Mail/refund_request_notify.twig
new file mode 100644
index 00000000000..a079c42dc90
--- /dev/null
+++ b/src/Eccube/Resource/template/default/Mail/refund_request_notify.twig
@@ -0,0 +1,39 @@
+{#
+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.
+#}
+{% autoescape 'safe_textmail' %}
+返品申請を受け付けました。
+
+************************************************
+ 返品申請内容
+************************************************
+
+申請日時:{{ RefundRequest.create_date|date_sec }}
+注文番号:{{ RefundRequest.Order.order_no }}
+会員名:{{ RefundRequest.Customer.name01 }} {{ RefundRequest.Customer.name02 }}
+
+商品名:{{ RefundRequest.OrderItem.productName }}
+{% if RefundRequest.OrderItem.ProductClass is not null %}
+{% if RefundRequest.OrderItem.ProductClass.ClassCategory1 is not null %}規格1:{{ RefundRequest.OrderItem.ProductClass.ClassCategory1 }}
+{% endif %}
+{% if RefundRequest.OrderItem.ProductClass.ClassCategory2 is not null %}規格2:{{ RefundRequest.OrderItem.ProductClass.ClassCategory2 }}
+{% endif %}
+{% endif %}
+返品希望数量:{{ RefundRequest.quantity }}
+
+返品理由:
+{{ RefundRequest.reason }}
+
+{% if RefundRequest.RefundRequestFiles is not empty %}
+添付ファイル数:{{ RefundRequest.RefundRequestFiles|length }}件
+{% endif %}
+
+※ このメールは自動送信です。管理画面より返品申請の詳細をご確認ください。
+{% endautoescape %}
diff --git a/src/Eccube/Resource/template/default/Mypage/history.twig b/src/Eccube/Resource/template/default/Mypage/history.twig
index 7ad1b0fc34d..a6b585b6d7d 100644
--- a/src/Eccube/Resource/template/default/Mypage/history.twig
+++ b/src/Eccube/Resource/template/default/Mypage/history.twig
@@ -95,6 +95,12 @@ file that was distributed with this source code.
{{ 'front.mypage.current_price'|trans }}{{ orderItem.productClass.price02IncTax|price }}
{% set remessage = true %}
{% endif %}
+ {% if Order.OrderStatus.id == constant('Eccube\\Entity\\Master\\OrderStatus::DELIVERED') and orderItem.Product is not null and orderItem.Product.isRefundAllowed %}
+
+ {% endif %}
diff --git a/src/Eccube/Resource/template/default/Mypage/refund_request.twig b/src/Eccube/Resource/template/default/Mypage/refund_request.twig
new file mode 100644
index 00000000000..29b19ef438b
--- /dev/null
+++ b/src/Eccube/Resource/template/default/Mypage/refund_request.twig
@@ -0,0 +1,96 @@
+{#
+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.
+#}
+{% extends 'default_frame.twig' %}
+
+{% set mypageno = 'index' %}
+
+{% set body_class = 'mypage' %}
+
+{% block main %}
+
+
+
+
+
+
+
+
+ {% if OrderItem.Product is not null %}
+
+ {% else %}
+
+ {% endif %}
+
+
+
{{ OrderItem.productName }}
+ {% if OrderItem.ProductClass is not null %}
+ {% if OrderItem.ProductClass.ClassCategory1 is not null %}
+
{{ OrderItem.ProductClass.ClassCategory1.ClassName.name }}:{{ OrderItem.ProductClass.ClassCategory1 }}
+ {% endif %}
+ {% if OrderItem.ProductClass.ClassCategory2 is not null %}
+
{{ OrderItem.ProductClass.ClassCategory2.ClassName.name }}:{{ OrderItem.ProductClass.ClassCategory2 }}
+ {% endif %}
+ {% endif %}
+
{{ OrderItem.price_inc_tax|price }} × {{ OrderItem.quantity|number_format }}
+
+
+
+
+ {% form_theme form 'Form/form_div_layout.twig' %}
+
+
+
+
+{% endblock %}
diff --git a/src/Eccube/Resource/template/default/Mypage/refund_request_complete.twig b/src/Eccube/Resource/template/default/Mypage/refund_request_complete.twig
new file mode 100644
index 00000000000..5aec7fb8903
--- /dev/null
+++ b/src/Eccube/Resource/template/default/Mypage/refund_request_complete.twig
@@ -0,0 +1,41 @@
+{#
+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.
+#}
+{% extends 'default_frame.twig' %}
+
+{% set mypageno = 'index' %}
+
+{% set body_class = 'mypage' %}
+
+{% block main %}
+
+
+
+
+
+
+
{{ 'front.mypage.refund_request.complete_message'|trans }}
+
+
{{ 'front.mypage.refund_request.complete_description'|trans }}
+
+
+
+
+
+{% endblock %}
diff --git a/src/Eccube/Resource/template/default/Mypage/refund_request_confirm.twig b/src/Eccube/Resource/template/default/Mypage/refund_request_confirm.twig
new file mode 100644
index 00000000000..b7f1a001a52
--- /dev/null
+++ b/src/Eccube/Resource/template/default/Mypage/refund_request_confirm.twig
@@ -0,0 +1,75 @@
+{#
+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.
+#}
+{% extends 'default_frame.twig' %}
+
+{% set mypageno = 'index' %}
+
+{% set body_class = 'mypage' %}
+
+{% block main %}
+
+
+
+
+
+
+
+
+ {% if OrderItem.Product is not null %}
+
+ {% else %}
+
+ {% endif %}
+
+
+
{{ OrderItem.productName }}
+
+
+
+
+
+
+ {{ 'front.mypage.refund_request.quantity'|trans }}
+ {{ RefundRequest.quantity }}
+
+
+ {{ 'front.mypage.refund_request.reason'|trans }}
+ {{ RefundRequest.reason|nl2br }}
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/src/Eccube/Resource/template/default/Mypage/refund_request_item_history.twig b/src/Eccube/Resource/template/default/Mypage/refund_request_item_history.twig
new file mode 100644
index 00000000000..37a80aea949
--- /dev/null
+++ b/src/Eccube/Resource/template/default/Mypage/refund_request_item_history.twig
@@ -0,0 +1,107 @@
+{#
+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.
+#}
+{% extends 'default_frame.twig' %}
+
+{% set mypageno = 'index' %}
+
+{% set body_class = 'mypage' %}
+
+{% block main %}
+
+
+
+
+
+
+
+
+ {% if OrderItem.Product is not null %}
+
+ {% else %}
+
+ {% endif %}
+
+
+
{{ OrderItem.productName }}
+ {% if OrderItem.ProductClass is not null %}
+ {% if OrderItem.ProductClass.ClassCategory1 is not null %}
+
{{ OrderItem.ProductClass.ClassCategory1.ClassName.name }}:{{ OrderItem.ProductClass.ClassCategory1 }}
+ {% endif %}
+ {% if OrderItem.ProductClass.ClassCategory2 is not null %}
+
{{ OrderItem.ProductClass.ClassCategory2.ClassName.name }}:{{ OrderItem.ProductClass.ClassCategory2 }}
+ {% endif %}
+ {% endif %}
+
+
+
+
+ {% if RefundRequests is not empty %}
+ {% for RefundRequest in RefundRequests %}
+
+
+ {{ 'front.mypage.refund_request.request_date'|trans }}
+ {{ RefundRequest.create_date|date_sec }}
+
+
+ {{ 'front.mypage.refund_request.status'|trans }}
+ {{ RefundRequest.RefundRequestStatus }}
+
+
+ {{ 'front.mypage.refund_request.quantity'|trans }}
+ {{ RefundRequest.quantity }}
+
+
+ {{ 'front.mypage.refund_request.reason'|trans }}
+ {{ RefundRequest.reason|nl2br }}
+
+ {% if RefundRequest.RefundRequestFiles is not empty %}
+
+ {{ 'front.mypage.refund_request.files'|trans }}
+
+ {% for File in RefundRequest.RefundRequestFiles %}
+
+ {% if File.isImage %}
+
+
+
+ {% elseif File.isVideo %}
+
+
+
+ {% else %}
+
{{ File.file_name }}
+ {% endif %}
+
+ {% endfor %}
+
+
+ {% endif %}
+
+ {% endfor %}
+ {% else %}
+
{{ 'front.mypage.refund_request.no_history'|trans }}
+ {% endif %}
+
+
+
+
+
+{% endblock %}
From 2451340fa47d4cab8c3217a292a1a124a50c26ac Mon Sep 17 00:00:00 2001
From: "takumi.tokoro"
Date: Tue, 16 Jun 2026 08:04:51 +0900
Subject: [PATCH 06/24] =?UTF-8?q?fix:=20=E8=BF=94=E5=93=81=E7=94=B3?=
=?UTF-8?q?=E8=AB=8B=E7=AE=A1=E7=90=86=E3=83=86=E3=83=B3=E3=83=97=E3=83=AC?=
=?UTF-8?q?=E3=83=BC=E3=83=88=E3=81=AECSRF=E3=83=88=E3=83=BC=E3=82=AF?=
=?UTF-8?q?=E3=83=B3=E5=87=A6=E7=90=86=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
FormViewの`_token`プロパティへのアクセスが不正だったため、
form要素のnovalidate属性でCSRFトークンを自動注入するように修正。
これにより管理画面返品申請一覧が正常に表示されるようになった。
Co-Authored-By: Claude Haiku 4.5
---
src/Eccube/Resource/template/admin/Order/refund_request.twig | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/Eccube/Resource/template/admin/Order/refund_request.twig b/src/Eccube/Resource/template/admin/Order/refund_request.twig
index f32a41087e2..002fdac8630 100644
--- a/src/Eccube/Resource/template/admin/Order/refund_request.twig
+++ b/src/Eccube/Resource/template/admin/Order/refund_request.twig
@@ -20,8 +20,7 @@ file that was distributed with this source code.
{% block main %}