バック：SpringBoot で決済機能を実装する。Stripeでの決済結果をローカルのデータベース上に記録する  3/4

# 概要
今回は決済結果をMySQLに保存する機能を実装します。
Stripeを使った処理は無いです。

# 開発環境
OS:windows10

バックエンド側：
IDE:IntelliJ Community
spring-boot-starter-parent 2.75
java　：　11
API検証ツール：POSTMAN

# 図　
## 実装したい動き
下図の赤枠部分を実装します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/6755e153-a103-2b4d-3cf1-dd88534f30b1.png)



Stripeでの決済が成功した場合実行される処理です。
手順としては３つの流れになっています。
1.注文情報（金額、日付、顧客Id）を保存する。（ordersテーブルに INSERT）
2.注文した商品情報を保存する。(order_itemsテーブルにINSERT)
3.顧客の買い物カゴを空にする、(cart_ItemsテーブルにDELETE処理)

## データベース
今回の処理はテーブルを３つ使っています。
書き方はテキトーです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/dac0b5a5-5581-82c8-a6cd-bdf5fa0400f4.png)







## クラス図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/44c255fb-d7be-5549-3f53-4d8432477bd6.png)




メインの処理はOrderServiceImplementで行っています。

※CartServiceImplemntは商品情報から総額を算出するために使っています。

# コード部分(とても長いのでスキップ推奨、クラス図を先に見て気になる部分だけご覧ください)

## Order(Entity)
```java:Order.java
package com.example.restapi.domain.order;


import lombok.*;

import javax.persistence.*;
import java.util.Date;

@Data
@Entity
@Table(name = "orders")
@NoArgsConstructor
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id", nullable = false)
    private Integer id;

    // 顧客のId
    @JoinColumn(name ="customer_id")
    Integer customerId;

    // お客様が支払う金額
    @JoinColumn(name ="amount")
    Integer amount;

    @JoinColumn(name ="created_at")
    Date createdAt;

    // 支払い方法について　0だとクレカ　1だと現金着払い
    @JoinColumn(name ="type")
    Integer type;

    // 支払状況
    // クレカ決済の場合、データベースに追加時にtrueになる
    // 現金払いの場合、データベースに追加時にはfalseになる。
    @Column(name ="is_payment_finished", nullable = false)
    boolean isPaymentFinished;

    // 出荷状況
    // データベースに追加された時はfalseが入る
    // 客に商品を渡したときに発送担当者が、trueにする。
    @Column(name ="is_shipping_finished", nullable = false)
    boolean isShippingFinished;

    public Order(Integer customerId, Integer amount, Date createdAt, Integer type, boolean isPaymentFinished, boolean isShippingFinished) {
        this.customerId = customerId;
        this.amount = amount;
        this.createdAt = createdAt;
        this.type = type;
        this.isPaymentFinished = isPaymentFinished;
        this.isShippingFinished = isShippingFinished;
    }
}

```

## OrderRepository
```java:OrderRepository.java
package com.example.restapi.domain.order;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface OrderRepository extends CrudRepository<Order,Integer> {

    Order save(Order order);

}


```

## OrderService

```java_OrderService.java
package com.example.restapi.domain.order;

import com.example.restapi.implement.cartItem.CartItemDto;
import com.example.restapi.implement.payment.PaymentInfoRequest;
import com.stripe.exception.StripeException;
import com.stripe.model.PaymentIntent;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public interface OrderService {

    // クレジットカード決済をするために、Stripe側の処理を行う
    // mySQLに書き込む処理はなし
    PaymentIntent createPaymentIntent(PaymentInfoRequest paymentInfoRequest) throws StripeException;

    // 決済を行う
    // OrderとOrderItemのテーブルにデータを追加する。CartItemの情報を初期化する Stripe上の処理はなし
    void finishOrder(int customerId, int amount, int type, List<CartItemDto> cartItemDtos);

}


```


## OrderServiceImplemnt
```diff_java:OrderServiceImplemnt.java
package com.example.restapi.implement.order;

import com.example.restapi.domain.cartItem.CartItemRepository;
import com.example.restapi.domain.order.Order;
import com.example.restapi.domain.order.OrderRepository;
import com.example.restapi.domain.order.OrderService;
import com.example.restapi.domain.orderItem.OrderItem;
import com.example.restapi.domain.orderItem.OrderItemRepository;
import com.example.restapi.implement.cartItem.CartItemDto;
import com.example.restapi.implement.payment.PaymentInfoRequest;
import com.stripe.exception.StripeException;
import com.stripe.model.PaymentIntent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;

@Service
@Transactional
public class OrderServiceImplement implements OrderService {

    @Autowired
    CartItemRepository cartItemRepository;

    @Autowired
    OrderItemRepository orderItemRepository;

    @Autowired
    OrderRepository orderRepository;

    @Override
    public PaymentIntent createPaymentIntent(PaymentInfoRequest paymentInfoRequest) throws StripeException {
        List<String> paymentMethodTypes = new ArrayList<>();
        paymentMethodTypes.add("card");
        List<String> paymentMethod = new ArrayList<>();
        Map<String, Object> automaticPaymentMethods =
                new HashMap<>();
        automaticPaymentMethods.put("enabled", true);
        Map<String, Object> params = new HashMap<>();
        params.put("amount", paymentInfoRequest.getAmount());
        params.put("currency", paymentInfoRequest.getCurrency());
        params.put("payment_method_types", paymentMethodTypes);

        return PaymentIntent.create(params);
    }

+    @Override
+    public void finishOrder(int customerId,int amount,int type,List<CartItemDto> cartItemDtos) {
+        // Orderを作成する
+        Order order = new Order(customerId,amount, new Date(),0,true,false);
+        orderRepository.save(order);
+
+        // cartItemsとorderの情報を元に顧客の。
+        List<OrderItem> orderItems = new ArrayList<>();
+        for(CartItemDto cartItemDto :cartItemDtos) {
+           OrderItem aartItem  = new OrderItem();
+           aartItem = orderItemRepository.save(new OrderItem(order.getId(),cartItemDto.getProductId(),cartItemDto.getQuantity()));
+        }
+        // 顧客のcartItemを削除する
+        cartItemRepository.deleteByCustomerId(customerId);
+
+    }
}

```

## OrderRestController
```diff_java:OrderRestController.java
package com.example.restapi.implement.order;

import com.example.restapi.domain.cartItem.CartItemService;
import com.example.restapi.domain.customer.CustomerService;
import com.example.restapi.domain.order.OrderService;
import com.example.restapi.implement.cartItem.CartItemResponse;
import com.example.restapi.implement.payment.PaymentInfoRequest;
import com.stripe.exception.StripeException;
import com.stripe.model.PaymentIntent;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@CrossOrigin(origins = "http://127.0.0.1:3000")
@RestController
public class OrderRestController {

    @Autowired
    OrderService orderService;

    @Autowired
    CartItemService cartItemService;

    @Autowired
    CustomerService customerService;

    @PostMapping("/api/pay/preparePaymentIntent")
    public ResponseEntity<String> preparePayment(@RequestBody PaymentInfoRequest paymentInfoRequest)throws StripeException{
        PaymentIntent paymentIntent = orderService.createPaymentIntent(paymentInfoRequest);
        String paymentStr = paymentIntent.toJson();
        return new ResponseEntity<>(paymentStr, HttpStatus.OK);
    }

+    @PutMapping("/api/pay/finish")
+    public void finish( HttpServletRequest request){
+        Integer customerId = customerService.getIdfromJwtToken(request);
+        CartItemResponse cartItemResponse = cartItemService.listCartItems(customerId);
+        orderService.finishOrder(customerId,(int)cartItemResponse.getTotal(),0,cartItemResponse.getCartItemDtos());
+    }
}

```

## OrderItem(Entity)
```java:OrderItem.java
package com.example.restapi.domain.orderItem;

import lombok.Data;

import javax.persistence.*;

@Data
@Entity
@Table(name = "order_items")
public class OrderItem {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id", nullable = false)
    private Integer id;

    @JoinColumn(name ="order_id", nullable = false)
    Integer orderId;

    // 商品のId 商品名や商品価格などを参照するときに使う。
    // 値上げなどで商品情報が変化したときに対応できなくなる問題がある
    @JoinColumn(name ="product_id", nullable = false)
    Integer productId;

    // 購入した商品の量
    @JoinColumn(name ="quantity", nullable = false)
    Integer quantity;

    public OrderItem(Integer orderId, Integer productId, Integer quantity) {
        this.orderId = orderId;
        this.productId = productId;
        this.quantity = quantity;
    }

    public OrderItem() {
    }
}

```

## OrderItemRepository
```java:OrderItemRepository.java
package com.example.restapi.domain.orderItem;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface OrderItemRepository extends CrudRepository<OrderItem,Integer> {
    OrderItem save(OrderItem orderItem);

}


```


## CartItem(Entity)
```java:CartItem.java
package com.example.restapi.domain.cartItem;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;

import javax.persistence.*;

@Entity
@Getter
@Setter
@ToString
@RequiredArgsConstructor
@Table(name = "cart_items")
public class CartItem{
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id", nullable = false)
    private Integer id;

    @JoinColumn(name ="customer_id")
    Integer customerId;

    @JoinColumn(name ="product_id")
    Integer productId;

    @JoinColumn(name ="quantity")
    Integer quantity;

    public CartItem(int customerId, int productId, int quantity) {
        this.customerId = customerId;
        this.productId = productId;
        this.quantity = quantity;
    }
    public CartItem(Integer id, int customerId, int productId, int quantity) {
        this.id = id;
        this.customerId = customerId;
        this.productId = productId;
        this.quantity = quantity;
    }
}


```

## CartItemRepository
```java:CartItemRepository.java
package com.example.restapi.domain.cartItem;

import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface CartItemRepository extends CrudRepository<CartItem, Integer> {
    public List<CartItem> findByCustomerId(Integer customerId);
    public void deleteByCustomerId(Integer customerId);
}

```
## CartItemService

```java_CartItemService.java
package com.example.restapi.domain.cartItem;

import com.example.restapi.implement.cartItem.CartItemDto;
import com.example.restapi.implement.cartItem.CartItemResponse;
import org.springframework.stereotype.Service;

import java.util.List;

// todo クラスのJavadocを追加します。
@Service
public interface CartItemService {
    // customerIdの顧客が買い物カゴの情報を取得します。
    public CartItemResponse listCartItems(Integer customerId);
    // customerIdの顧客の買い物かごを全て削除する。
    public void deleteByCustomer(Integer customerId);
}



```


## CartItemServiceImplement
```diff_java:CartItemServiceImplement.java
package com.example.restapi.implement.cartItem;

import com.example.restapi.domain.cartItem.CartItem;
import com.example.restapi.domain.cartItem.CartItemRepository;
import com.example.restapi.domain.cartItem.CartItemService;
import com.example.restapi.domain.product.Product;
import com.example.restapi.domain.product.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Transactional
public class CartItemServiceImplement  implements CartItemService {

    @Autowired
    CartItemRepository cartItemRepository;

    @Autowired
    ProductRepository productRepository;

    @Override
    public CartItemResponse listCartItems(Integer customerId) {

        List<CartItem> cartItems = cartItemRepository.findByCustomerId(customerId);
        CartItemResponse cartItemResponse = new CartItemResponse();
        List<CartItemDto> cartItemDtos = new ArrayList<>() ;
        // setCartItemResponse
        for(var cartItem:cartItems){
            Product product = productRepository.getProductById(cartItem.getProductId());
            CartItemDto cartItemDto=new CartItemDto(cartItem.getId(),cartItem.getCustomerId(),cartItem.getProductId(),cartItem.getQuantity(),product.getName(),product.getPrice(),product.getPrice()*(1+product.getTaxRate()));
            cartItemDtos.add(cartItemDto);
        }
        cartItemResponse.setCartItemDtos(cartItemDtos);
        // cal productCost
        for(var cartItem :cartItems){
            float miniSum = cartItem.getQuantity()*productRepository.getProductById(cartItem.getProductId()).getPrice();
            cartItemResponse.setProductCost(cartItemResponse.getProductCost() + miniSum);
        }
        // cal shipping
        if( cartItemResponse.getProductCost() <= 4000f){
            cartItemResponse.setShippingCost(300);
        }else{
            cartItemResponse.setShippingCost(0);
        }

        // cal subtotal
        cartItemResponse.setSubTotal(cartItemResponse.getProductCost() + cartItemResponse.getShippingCost());

        // cal tax
        for(var cartItem :cartItems){
            float eachTax = cartItem.getQuantity()*productRepository.getProductById(cartItem.getProductId()).getPrice()*productRepository.getProductById(cartItem.getProductId()).getTaxRate();
            cartItemResponse.setTax(cartItemResponse.getTax() + eachTax);
        }
        // cal total;
        cartItemResponse.setTotal(cartItemResponse.getSubTotal() + cartItemResponse.getTax());
        return cartItemResponse;
//        return cartItemList.stream().map(cartItem -> mapToDTO(cartItem)).collect(Collectors.toList());
    }


    @Override
    public void deleteByCustomer(Integer customerId) {
        cartItemRepository.deleteByCustomerId(customerId);
    }

    private CartItemDto mapToDTO(CartItem cartItem){
        CartItemDto cartItemDto = new CartItemDto();
        cartItemDto.setId(cartItem.getId());
        cartItemDto.setProductId(cartItem.getProductId());
        cartItemDto.setCustomerId(cartItem.getCustomerId());
        cartItemDto.setQuantity(cartItem.getQuantity());
        return cartItemDto;
    }

    private CartItem mapToEntity(CartItemDto cartItemDto){
        CartItem cartItem = new CartItem();
        cartItemDto.setId(cartItem.getId());
        cartItemDto.setQuantity(cartItem.getQuantity());
        cartItemDto.setProductId(cartItem.getProductId());
        cartItemDto.setCustomerId(cartItem.getCustomerId());

        return cartItem;
    }
}



```




# 動かして確認
PUTメソッドで以下APIを叩きます。（POSTMANを使用）
以下画像から、決済情報が保存され顧客の買い物カゴが空になっていることを確認できます。
API実行後の各テーブルの変化
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/e6104e41-4641-38de-630d-44be9250c029.png)

# まだできていない箇所
とりあえず動けばよしでの実装になっております。
私が把握している限りでも以下の対策がとれていません。
１．商品の値段が変化したときにorderItemの値段も変化し、　orderItemとCartItemで金額があわなくなる。
　　orderItemのパラメーターを修正する必要がある

２．着払い決済対応していない。
　　現状だとクレカの場合しか想定していません。着払いできません。
　　着払い対応にする場合はOrderItemService.finishOrderに分岐の処理を追加する必要があると考えます。    


# 参考
github　commit分

https://github.com/RYA234/springboot-ecshop-rest-api/commit/147a0ad71c763f01ff3bfe9f38f5adcc24b15d46