<?php
/*
* Amazon Pay V2 for EC-CUBE4.2
* Copyright(c) 2023 EC-CUBE CO.,LTD. all rights reserved.
*
* https://www.ec-cube.co.jp/
*
* This program is not free software.
* It applies to terms of service.
*
*/
namespace Plugin\AmazonPayV2_42\Service;
use Doctrine\ORM\EntityManagerInterface;
use Eccube\Common\EccubeConfig;
use Eccube\Entity\Master\OrderStatus;
use Eccube\Repository\BaseInfoRepository;
use Eccube\Repository\CustomerRepository;
use Eccube\Repository\Master\OrderStatusRepository;
use Eccube\Service\CartService;
use Eccube\Service\PurchaseFlow\PurchaseContext;
use Eccube\Service\PurchaseFlow\PurchaseFlow;
use Eccube\Service\PurchaseFlow\Processor\StockReduceProcessor;
use Eccube\Service\PurchaseFlow\Processor\PointProcessor;
use Plugin\AmazonPayV2_42\Entity\Master\AmazonStatus;
use Plugin\AmazonPayV2_42\Exception\AmazonException;
use Plugin\AmazonPayV2_42\Exception\AmazonPaymentException;
use Plugin\AmazonPayV2_42\Repository\ConfigRepository;
use Plugin\AmazonPayV2_42\Amazon\Pay\API\Client as AmazonPayClient;
use Plugin\AmazonPayV2_42\Repository\Master\AmazonStatusRepository;
use GuzzleHttp\Client;
use Guzzle\Http\Exception\BadResponseException;
use Guzzle\Http\Exception\CurlException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Psr\Container\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Carbon\Carbon;
class AmazonRequestService extends AbstractController
{
/**
* @var EntityManagerInterface
*/
protected $entityManager;
/**
* @var BaseInfoRepository
*/
protected $baseInfoRepository;
/**
* @var CustomerRepository
*/
protected $customerRepository;
/**
* @var CartService
*/
protected $cartService;
/**
* @var PurchaseFlow
*/
protected $purchaseFlow;
/**
* @var eccubeConfig
*/
protected $eccubeConfig;
/**
* @var ConfigRepository
*/
protected $configRepository;
/**
* @var Config
*/
protected $Config;
/**
* @var array
*/
protected $amazonApi;
/**
* @var array
*/
protected $amazonApiConfig;
/**
* @var Session
*/
protected $session;
/**
* @var TokenStorageInterface
*/
protected $tokenStorage;
/**
* @var ContainerInterface
*/
protected $container;
/**
* @var PointProcessor
*/
private $pointProcessor;
/**
* @var StockReduceProcessor
*/
private $stockReduceProcessor;
/**
* @var AmazonStatusRepository
*/
private $amazonStatusRepository;
/**
* @var OrderStatusRepository
*/
private $orderStatusRepository;
public function __construct(
EntityManagerInterface $entityManager,
BaseInfoRepository $baseInfoRepository,
CustomerRepository $customerRepository,
CartService $cartService,
PurchaseFlow $cartPurchaseFlow,
EccubeConfig $eccubeConfig,
ConfigRepository $configRepository,
RequestStack $requestStack,
TokenStorageInterface $tokenStorage,
OrderStatusRepository $orderStatusRepository,
AmazonStatusRepository $amazonStatusRepository,
StockReduceProcessor $stockReduceProcessor,
PointProcessor $pointProcessor,
ContainerInterface $container
) {
$this->entityManager = $entityManager;
$this->BaseInfo = $baseInfoRepository->get();
$this->customerRepository = $customerRepository;
$this->cartService = $cartService;
$this->purchaseFlow = $cartPurchaseFlow;
$this->eccubeConfig = $eccubeConfig;
$this->configRepository = $configRepository;
$this->session = $requestStack->getSession();
$this->tokenStorage = $tokenStorage;
$this->orderStatusRepository = $orderStatusRepository;
$this->amazonStatusRepository = $amazonStatusRepository;
$this->stockReduceProcessor = $stockReduceProcessor;
$this->pointProcessor = $pointProcessor;
$this->container = $container;
$this->Config = $this->configRepository->get();
if (
$this->Config->getAmazonAccountMode() == $this->eccubeConfig['amazon_pay_v2']['account_mode']['owned'] &&
$this->Config->getEnv() == $this->eccubeConfig['amazon_pay_v2']['env']['prod']
) {
$this->amazonApi = $this->eccubeConfig['amazon_pay_v2']['api']['prod'];
} else {
$this->amazonApi = $this->eccubeConfig['amazon_pay_v2']['api']['sandbox'];
}
$this->amazonApiConfig = $this->eccubeConfig['amazon_pay_v2']['api']['config'];
}
private function payoutSellerOrderId($orderId, $request_type = '')
{
$request_attr = $request_type === '' ? '' : strtoupper($request_type) . '_';
$prefix = '';
$iniFile = dirname(__FILE__) . '/../amazon_pay_config.ini';
if (file_exists($iniFile)) {
$arrInit = parse_ini_file($iniFile);
$prefix = $arrInit['prefix'];
}
$prefix = $prefix === '' ? '' : $prefix . '_';
$timestamp = '';
if ($this->Config->getAmazonAccountMode() === $this->eccubeConfig['amazon_pay_v2']['account_mode']['shared']) {
$timestamp = Carbon::now()->timestamp;
}
$timestamp = $timestamp === '' ? '' : $timestamp . '_';
return $timestamp . $prefix . $request_attr . $orderId;
}
protected function getAmazonPayConfig()
{
$Config = $this->configRepository->get();
$config = [
'public_key_id' => $Config->getPublicKeyId(),
'private_key' => $this->eccubeConfig->get('kernel.project_dir') . '/' . $Config->getPrivateKeyPath(),
'sandbox' => $Config->getEnv() == $this->eccubeConfig['amazon_pay_v2']['env']['sandbox'] ? true : false, // true (Sandbox) or false (Production) boolean
'region' => 'jp' // Must be one of: 'us', 'eu', 'jp'
];
return $config;
}
public function createCheckoutSessionPayload($cart_key)
{
$Config = $this->configRepository->get();
$router = $this->container->get('router');
$payload = [
'webCheckoutDetails' => [
'checkoutReviewReturnUrl' => $router->generate('amazon_checkout_review', ['cart' => $cart_key], UrlGeneratorInterface::ABSOLUTE_URL),
],
'paymentDetails' => [
'allowOvercharge' => true, //増額許可
],
'storeId' => $Config->getClientId(),
'deliverySpecifications' => [
'addressRestrictions' => [
'type' => 'Allowed',
'restrictions' => [
'JP' => [],
],
],
],
];
return json_encode($payload, JSON_FORCE_OBJECT);
}
public function createUpdateCheckoutSessionPayload($Order)
{
$router = $this->container->get('router');
// NOTE: AmazonPayAPI仕様上、0円の決済は許容しない.
if($Order->getPaymentTotal() == 0) {
throw AmazonPaymentException::create(AmazonPaymentException::ZERO_PAYMENT);
}
$config = $this->configRepository->get();
if ($config->getSale() == $this->eccubeConfig['amazon_pay_v2']['sale']['authori']) {
$paymentIntent = 'Authorize';
} elseif ($config->getSale() == $this->eccubeConfig['amazon_pay_v2']['sale']['capture']) {
$paymentIntent = 'AuthorizeWithCapture';
}
$payload = [
'webCheckoutDetails' => [
'checkoutResultReturnUrl' => $router->generate('amazon_pay_shopping_checkout_result', [], UrlGeneratorInterface::ABSOLUTE_URL),
],
'paymentDetails' => [
'paymentIntent' => $paymentIntent,
'canHandlePendingAuthorization' => false,
'chargeAmount' => [
'amount' => (int)$Order->getPaymentTotal(),
'currencyCode' => "JPY"
],
//"softDescriptor" => "softDescriptor"
],
'merchantMetadata' => [
'merchantReferenceId' => $this->payoutSellerOrderId($Order->getId()),
'noteToBuyer' => ''
// "customInformation" => "customInformation"
],
"platformId" => "A1LODGGQOBGE66"
];
// 店舗名が50文字を超えるとAmazon側でエラーとなるため考慮する
if (mb_strlen($this->BaseInfo->getShopName()) < 51) {
$payload['merchantMetadata']['merchantStoreName'] = $this->BaseInfo->getShopName();
}
return json_encode($payload, JSON_FORCE_OBJECT);
}
public function createCompleteCheckoutSessionPayload($Order)
{
$payload = [
'chargeAmount' => [
'amount' => (int)$Order->getPaymentTotal(),
'currencyCode' => 'JPY',
]
];
return json_encode($payload, JSON_FORCE_OBJECT);
}
public function createCaptureChargePayload($Order, $billingAmount = null)
{
$payload = [
'captureAmount' => [
'amount' => is_null($billingAmount) ? (int)$Order->getPaymentTotal() : $billingAmount,
'currencyCode' => 'JPY',
]
];
return json_encode($payload, JSON_FORCE_OBJECT);
}
public function createCancelChargePayload($cancellationReason = null)
{
$payload = [
'cancellationReason' => $cancellationReason
];
return json_encode($payload, JSON_FORCE_OBJECT);
}
public function createCloseChargePermissionPayload($closureReason = null, $cancelPendingCharges = null)
{
$payload = [
'closureReason' => $closureReason,
'cancelPendingCharges' => $cancelPendingCharges
];
return json_encode($payload, JSON_FORCE_OBJECT);
}
public function createCreateRefundPayload($chargeId, $refundAmount)
{
$payload = [
'chargeId' => $chargeId,
'refundAmount' => [
'amount' => $refundAmount,
'currencyCode' => $this->eccubeConfig['amazon_pay_v2']['api']['payload']['currency_code'],
]
];
return json_encode($payload, JSON_FORCE_OBJECT);
}
public function createCreateChargePayload($chargePermissionId, $paymentTotal, $CaptureNow = false, $canHandlePendingAuthorization = false)
{
$payload = [
'chargePermissionId' => $chargePermissionId,
'chargeAmount' => [
'amount' => $paymentTotal,
'currencyCode' => $this->eccubeConfig['amazon_pay_v2']['api']['payload']['currency_code']
],
'captureNow' => $CaptureNow,
'canHandlePendingAuthorization' => $canHandlePendingAuthorization
];
return json_encode($payload, JSON_FORCE_OBJECT);
}
public function updateCheckoutSession($Order, $amazonCheckoutSessionId)
{
$client = new AmazonPayClient($this->getAmazonPayConfig());
$result = $client->updateCheckoutSession($amazonCheckoutSessionId, $this->createUpdateCheckoutSessionPayload($Order));
return json_decode($result['response']);
}
public function signaturePayload($payload)
{
$client = new AmazonPayClient($this->getAmazonPayConfig());
$signature = $client->generateButtonSignature($payload);
return $signature;
}
public function getCheckoutSession($amazonCheckoutSessionId)
{
$client = new AmazonPayClient($this->getAmazonPayConfig());
$result = $client->getCheckoutSession($amazonCheckoutSessionId);
return json_decode($result['response']);
}
public function completeCheckoutSession($Order, $amazonCheckoutSessionId)
{
$client = new AmazonPayClient($this->getAmazonPayConfig());
$result = $client->completeCheckoutSession($amazonCheckoutSessionId, $this->createCompleteCheckoutSessionPayload($Order));
$response = json_decode($result['response']);
logs('amazon_pay_v2')->info('▼completeCheckoutSession http-status = ' . $result['status'] . ', order_id = ' . $Order->getId());
if ($result['status'] == 200 || $result['status'] == 202) {
if ($response->statusDetails->state == 'Completed') {
return $response;
}
} elseif (isset($response->reasonCode)) {
logs('amazon_pay_v2')->info('▼completeCheckoutSession reasonCode = ' . $response->reasonCode . ', order_id = ' . $Order->getId());
if ($response->reasonCode == 'CheckoutSessionCanceled') {
// チェックアウトセッションを再取得し理由コードを取得する
$checkoutSession = $this->getCheckoutSession($amazonCheckoutSessionId);
if ($checkoutSession && isset($checkoutSession->statusDetails->reasonCode)) {
$errorCode = AmazonPaymentException::getErrorCode($checkoutSession->statusDetails->reasonCode);
logs('amazon_pay_v2')->info('▼completeCheckoutSession statusDetails = ' . var_export($checkoutSession->statusDetails, true));
// 購入者が決済をキャンセルしたなら受注もキャンセルにする
$this->cancelOrder($Order);
logs('amazon_pay_v2')->info('▼completeCheckoutSession 受注をキャンセルしました' . 'order_id = ' . $Order->getId());
if ($errorCode) {
throw AmazonPaymentException::create($errorCode);
}
}
}
}
// 条件に合致しない場合は全てAmazonException()
throw new AmazonException();
}
/**
* 受注をキャンセルする
*
* @param $Order キャンセル対象の受注
*/
private function cancelOrder($Order)
{
// 在庫・使用ポイント戻しはexecutePurchaseFlowで実施されるため
// 自前実装不要
$OrderStatus = $this->orderStatusRepository->find($this->orderStatusRepository->find(OrderStatus::CANCEL));
$Order->setOrderStatus($OrderStatus);
$AmazonStatus = $this->amazonStatusRepository->find(AmazonStatus::CANCEL);
$Order->setAmazonPayV2AmazonStatus($AmazonStatus);
$this->entityManager->flush();
}
public function captureCharge($chargeId, $Order, $billingAmount = null)
{
$client = new AmazonPayClient($this->getAmazonPayConfig());
$headers = ['x-amz-pay-Idempotency-Key' => uniqid()];
$result = $client->captureCharge($chargeId, $this->createCaptureChargePayload($Order, $billingAmount), $headers);
return json_decode($result['response']);
}
/**
* 売上をキャンセル
*
* @param string $chargeId Amazon注文参照ID
* @return array or string リクエスト結果
*/
public function cancelCharge($chargeId, $cancellationReason = null)
{
$payload = $this->createCancelChargePayload($cancellationReason);
$client = new AmazonPayClient($this->getAmazonPayConfig());
$result = $client->cancelCharge($chargeId, $payload);
return json_decode($result['response']);
}
/**
* 注文取消
*/
public function closeChargePermission($chargePermissionId, $closureReason = null, $cancelPendingCharges = true)
{
$payload = $this->createCloseChargePermissionPayload($closureReason, $cancelPendingCharges);
$client = new AmazonPayClient($this->getAmazonPayConfig());
$result = $client->closeChargePermission($chargePermissionId, $payload);
return json_decode($result['response']);
}
/**
* 請求済み売り上げを返金
*
* @param string $amazonCaptureId Amazon取引ID
* @param string $chargeId 注文ID
* @param integer $refundAmoun 返金金額
* @return array or string リクエスト結果
*/
public function createRefund($chargeId, $refundAmount, $softDescriptor = null, $idempotencyKey = null)
{
$payload = $this->createCreateRefundPayload($chargeId, $refundAmount);
if (null != $softDescriptor) {
$payload = array_merge($payload, ["softDescriptor" => $softDescriptor]);
}
if ($idempotencyKey == null) {
$idempotencyKey = uniqid();
}
$headers = ['x-amz-pay-Idempotency-Key' => $idempotencyKey];
$client = new AmazonPayClient($this->getAmazonPayConfig());
$result = $client->createRefund($payload, $headers);
return json_decode($result['response']);
}
/**
* 購入を確定(オーソリのリクエスト)
*
* @param string $amazonOrderReferenceId Amazon注文参照ID
* @param integer $order_id 注文ID
* @param integer $payment_total 受注金額合計
* @return array or string リクエスト結果
*/
public function createCharge($chargePermissionId, $paymentTotal, $CaptureNow = false, $softDescriptor = null, $canHandlePendingAuthorization = false, $merchantMetadataMerchantReferenceId = null, $idempotencyKey = null)
{
$payload = $this->createCreateChargePayload($chargePermissionId, $paymentTotal, $CaptureNow, $canHandlePendingAuthorization);
if (null != $merchantMetadataMerchantReferenceId) {
$payload = array_merge($payload, [
"merchantMetadata" => [
"merchantReferenceId"=> $merchantMetadataMerchantReferenceId
]
]);
}
if (null != $softDescriptor) {
$payload = array_merge($payload, ["softDescriptor" => $softDescriptor]);
}
if ($idempotencyKey == null) {
$idempotencyKey = uniqid();
}
$headers = ['x-amz-pay-Idempotency-Key' => $idempotencyKey];
$client = new AmazonPayClient($this->getAmazonPayConfig());
$result = $client->createCharge($payload, $headers);
return json_decode($result['response']);
}
/**
* 請求情報取得
* @param string $chargeId Amazon請求情報参照ID
* @return array or string リクエスト結果
*/
public function getCharge($chargeId)
{
$client = new AmazonPayClient($this->getAmazonPayConfig());
$result = $client->getCharge($chargeId);
return json_decode($result['response']);
}
public function createSigninPayload($returnUrl)
{
$Config = $this->configRepository->get();
$payload = [
'signInReturnUrl' => $returnUrl,
'storeId' => $Config->getClientId(),
];
return json_encode($payload, JSON_FORCE_OBJECT);
}
/**
* 購入者情報取得
* @param string $buyerToken 購入者情報トークン
* @param array $headers ヘッダー
* @return array or string リクエスト結果
*/
public function getBuyer($buyerToken, $headers = null)
{
$client = new AmazonPayClient($this->getAmazonPayConfig());
$result = $client->getBuyer($buyerToken, $headers);
if ($result['status'] != 200) {
throw new AmazonException();
}
return json_decode($result['response']);
}
/**
* 取得したbuyerIdに一致した会員でログインを行う
*
* @param Request $request
* @param string $buyerId
* @return bool
*/
public function loginWithBuyerId(Request $request, $buyerId)
{
// buyerIdで会員を検索する
$Customers = $this->customerRepository->getNonWithdrawingCustomers(['v2_amazon_user_id' => $buyerId]);
if (empty($Customers[0]) || !$Customers[0] instanceof \Eccube\Entity\Customer) {
return false;
}
// ログイン処理
$token = new UsernamePasswordToken($Customers[0], 'customer', ['ROLE_USER']);
$this->tokenStorage->setToken($token);
$request->getSession()->migrate(true);
// 未ログインカートとログイン済みカートのマージ処理
$this->cartService->mergeFromPersistedCart();
foreach ($this->cartService->getCarts() as $Cart) {
$this->purchaseFlow->validate($Cart, new PurchaseContext($Cart, $Customers[0]));
}
$this->cartService->save();
return true;
}
}