概要
今回は決済結果をMySQLに保存する機能を実装します。
Stripeを使った処理は無いです。
開発環境
OS:windows10
バックエンド側:
IDE:IntelliJ Community
spring-boot-starter-parent 2.75
java : 11
API検証ツール:POSTMAN
図
実装したい動き
Stripeでの決済が成功した場合実行される処理です。
手順としては3つの流れになっています。
1.注文情報(金額、日付、顧客Id)を保存する。(ordersテーブルに INSERT)
2.注文した商品情報を保存する。(order_itemsテーブルにINSERT)
3.顧客の買い物カゴを空にする、(cart_ItemsテーブルにDELETE処理)
データベース
今回の処理はテーブルを3つ使っています。
書き方はテキトーです。
クラス図
メインの処理はOrderServiceImplementで行っています。
※CartServiceImplemntは商品情報から総額を算出するために使っています。
コード部分(とても長いのでスキップ推奨、クラス図を先に見て気になる部分だけご覧ください)
Order(Entity)
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
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
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
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
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)
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
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)
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
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
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
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実行後の各テーブルの変化
まだできていない箇所
とりあえず動けばよしでの実装になっております。
私が把握している限りでも以下の対策がとれていません。
1.商品の値段が変化したときにorderItemの値段も変化し、 orderItemとCartItemで金額があわなくなる。
orderItemのパラメーターを修正する必要がある
2.着払い決済対応していない。
現状だとクレカの場合しか想定していません。着払いできません。
着払い対応にする場合はOrderItemService.finishOrderに分岐の処理を追加する必要があると考えます。
参考
github commit分