支付优化-续订失败邮件通知

This commit is contained in:
2024-11-28 10:43:06 +08:00
parent 5019fbd3fc
commit 1b15aed6a2
9 changed files with 269 additions and 42 deletions

View File

@@ -10,43 +10,34 @@ public enum OrderStatusEnum {
* 未支付
*/
NOT_PAY("未支付"),
/**
* 支付成功
*/
SUCCESS("支付成功"),
/**
* 支付失败
*/
FAILURE("支付失败"),
/**
* 已关闭
*/
TIMEOUT_CLOSED("超时已关闭"),
/**
* 已取消
*/
CANCEL("用户已取消"),
/**
* 退款中
*/
REFUND_PROCESSING("退款中"),
/**
* 已退款
*/
REFUND_SUCCESS("已退款"),
/**
* 退款异常
*/
REFUND_ABNORMAL("退款异常"),
/**
* paypal订单状态为 APPROVED
*/

View File

@@ -781,6 +781,8 @@ public class SendEmailUtil {
private final static Long RENEWAL_REMINDER_USER_CN = 130728L;
private final static Long PAYMENT_FAILED_NEW_MERCHANT_EN = 131230L;
private final static Long PAYMENT_FAILED_RENEWAL_MERCHANT_EN = 131225L;
private final static Long PAYMENT_FAILED_RENEWAL_USER_EN = 131563L;
private final static Long PAYMENT_FAILED_RENEWAL_USER_CN = 131564L;
public static void subscriptionEmailReminder(String type, SubscriptionEmailParamsDTO subscriptionEmailParamsDTO, String language, String receiverAddress){
try{
@@ -816,6 +818,14 @@ public class SendEmailUtil {
case "fail_renewal":
merchant.setSubject("[Code-Create] Payment Failed : Renewal Order (" + subscriptionEmailParamsDTO.getOrderId() + ")");
templateMerchant.setTemplateID(PAYMENT_FAILED_RENEWAL_MERCHANT_EN);
// todo to user
if (language.equals("ENGLISH")){
user.setSubject("[Code-Create] Payment Failed : Renewal Order (" + subscriptionEmailParamsDTO.getOrderId() + ")");
templateUser.setTemplateID(PAYMENT_FAILED_RENEWAL_USER_EN);
}else {
user.setSubject("[Code-Create] 自动续费失败 (" + subscriptionEmailParamsDTO.getOrderId() + ")");
templateUser.setTemplateID(PAYMENT_FAILED_RENEWAL_USER_CN);
}
break;
case "new":
merchant.setSubject("[Code-Create] New Order(" + subscriptionEmailParamsDTO.getOrderId() + ")");
@@ -859,7 +869,7 @@ public class SendEmailUtil {
templateMerchant.setTemplateData(JSON.toJSONString(subscriptionEmailParamsDTO));
merchant.setTemplate(templateMerchant);
if (!type.equals("cancel") && !type.equals("fail_new") && !type.equals("fail_renewal") ){
if (!type.equals("cancel") && !type.equals("fail_new") ){
// 返回的resp是一个SendEmailResponse的实例与请求对象对应
SendEmailResponse respUser = client.SendEmail(user);
log.info("邮件主题:{}发送结果toUser###{}", user.getSubject(), SendEmailResponse.toJsonString(respUser));

View File

@@ -9,19 +9,22 @@ import com.stripe.exception.StripeException;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.simpleframework.xml.core.Validate;
import org.springframework.web.bind.annotation.*;
import springfox.documentation.annotations.ApiIgnore;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.util.List;
@Api(tags = "Stripe模块")
@Slf4j
@RestController
@RequestMapping("/api/stripe")
@ApiIgnore
public class StripeController {
@Resource
@@ -29,7 +32,7 @@ public class StripeController {
@ApiOperation("创建支付链接")
@PostMapping("/createOrder")
public Response<String> pay(@Validate @RequestBody ProductPurchaseDTO productPurchaseDTO) {
public Response<String> pay(@Valid @RequestBody ProductPurchaseDTO productPurchaseDTO) {
return Response.success(stripeService.pay(productPurchaseDTO));
}
@@ -45,7 +48,7 @@ public class StripeController {
}
@ApiOperation("申请退款")
@PostMapping("/trade/refund/{orderNo}/{reason}")
@GetMapping("/trade/refund/{orderNo}/{reason}")
public Response<HttpResponse<Refund>> refund(@PathVariable String orderNo, @PathVariable String reason) throws IOException {
String response = stripeService.refund(null,orderNo,reason);
if (response.equals("退款成功")){
@@ -56,19 +59,44 @@ public class StripeController {
}
@ApiOperation("获取订阅")
@PostMapping("/getSubscription")
public void getSubscription() {
@GetMapping("/getSubscription")
public Response<List<String>> getSubscription(@RequestParam String name, @RequestParam String email) {
try {
stripeService.getSubscription("xp", "xupei3360@163.com");
return Response.success(stripeService.getSubscriptionIds(name, email));
} catch (StripeException e) {
throw new RuntimeException(e);
}
}
@ApiOperation("取消订阅")
@PostMapping("/cancelSubscription")
@GetMapping("/cancelSubscription")
public Response<String> cancelSubscription(@RequestParam String subscriptionId) {
stripeService.cancelSubscription(subscriptionId);
return Response.success("success");
}
@ApiOperation("临时 取消订阅")
@GetMapping("/cancelSubscriptionTemp")
public Response<String> cancelSubscriptionTemp(@RequestParam String subscriptionId) {
stripeService.cancelSubscriptionTemp(subscriptionId);
return Response.success("success");
}
@ApiOperation("创建订阅 临时")
@GetMapping("/createSubscriptionTemp")
public Response<String> createSubscriptionTemp(@RequestParam String name, @RequestParam String email) {
return Response.success(stripeService.createSubscriptionTemp(name, email));
}
@ApiOperation("修改用户默认支付方式 临时")
@GetMapping("/changeCustomerPayment")
public Response<String> changeCustomerPayment(@RequestParam String name, @RequestParam String email) {
return Response.success(stripeService.changeCustomerPayment(name, email));
}
@ApiOperation("临时 发送续订失败邮件")
@GetMapping("/sendRenewalFailEmail")
public Response<Boolean> sendRenewalFailEmail(@RequestParam String invoiceId, @RequestParam String subscriptionId, @RequestParam String orderNo) {
return Response.success(stripeService.sendRenewalFailEmail(invoiceId, subscriptionId,orderNo));
}
}

View File

@@ -30,4 +30,7 @@ public class PaymentInfo extends BaseEntity{
private String paymentMethod;
private String last4;
// 发票托管页面
private String hostedInvoiceUrl;
}

View File

@@ -27,6 +27,4 @@ public class ProductPurchaseDTO {
@ApiModelProperty("是否自动续订 one_time || recurring")
private Boolean autoRenewal;
private String refId;
}

View File

@@ -48,5 +48,7 @@ public class SubscriptionEmailParamsDTO {
// 付款失败原因
private String failMessage;
private String accountPageRef;
}

View File

@@ -2,7 +2,6 @@ package com.ai.da.service;
import com.ai.da.model.dto.ProductPurchaseDTO;
import com.stripe.exception.StripeException;
import com.stripe.model.Subscription;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
@@ -18,10 +17,12 @@ public interface StripeService {
void checkOrderStatus(String orderNo);
List<Subscription> getSubscription(String name, String userEmail) throws StripeException;
List<String> getSubscriptionIds(String name, String userEmail) throws StripeException;
void cancelSubscription(String orderNo);
void cancelSubscriptionTemp(String subscriptionId);
Map<String, String> getPaymentMethod(String paymentMethodId);
/*void updateSubscription(String subscriptionId);
@@ -32,4 +33,9 @@ public interface StripeService {
void checkSubscriptionExpiration();
String createSubscriptionTemp(String name, String email);
String changeCustomerPayment(String name, String email);
boolean sendRenewalFailEmail(String invoiceId, String subscriptionId, String orderNo);
}

View File

@@ -236,6 +236,7 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
paymentInfo.setNotified(0);
paymentInfo.setPaymentMethod(paymentMethod.get("paymentMethod"));
paymentInfo.setLast4(paymentMethod.get("last4"));
paymentInfo.setHostedInvoiceUrl(invoice.getHostedInvoiceUrl());
paymentInfo.setCreateTime(LocalDateTime.now());
baseMapper.insert(paymentInfo);

View File

@@ -284,19 +284,27 @@ public class StripeServiceImpl implements StripeService {
log.info("创建连续订阅");
} else if (event.getType().equals("customer.subscription.updated")){
// 更新订阅信息
updateSubscription(subscription);
SubscriptionInfo subscriptionInfo = updateSubscription(subscription);
log.info("订阅更新");
if (subscription.getStatus().equals("active")){
response = sendEmail(subscription.getId(), null);
}
// 续订支付失败,邮件通知用户
if (subscription.getStatus().equals("past_due")){
// 发送续订失败邮件
response = sendRenewalFailEmail(null, subscription.getId(), subscriptionInfo.getOrderNo());
}
} else if (event.getType().equals("customer.subscription.deleted")){
SubscriptionInfo subscriptionInfo = updateSubscription(subscription);
log.info("用户取消连续订阅");
log.info("用户 {} 取消连续订阅 {}", subscriptionInfo.getAccountId(), subscription.getId());
if (subscriptionInfo.getCancelNotified() == (byte)0){
log.info("取消订阅 邮件通知商家");
response = sendEmail(subscription.getId(), "cancel");
}
subscriptionInfo.setCancelNotified((byte)1);
subscriptionInfoMapper.updateById(subscriptionInfo);
}
}/* else if (event.getType().equals("customer.subscription.paused")){
updateSubscription(subscription);
} else if (event.getType().equals("customer.subscription.resumed")){
@@ -332,14 +340,13 @@ public class StripeServiceImpl implements StripeService {
String json = gson.toJson(invoice);
paymentInfo.setContent(json);
paymentInfo.setType(type);
paymentInfo.setHostedInvoiceUrl(invoice.getHostedInvoiceUrl());
paymentInfoService.updateById(paymentInfo);
// 发送续订失败邮件
response = sendRenewalFailEmail(invoice.getId(), null, paymentInfo.getOrderNo());
}
/*// 支付失败 通知商家的条件 1、会话过期 2、支付失败 3、这个用户在这个支付失败后再无支付成功的订阅
if (invoice.getBillingReason().equals("subscription_create")){
response = sendEmail(paymentInfo.getOrderNo());
} else if (invoice.getBillingReason().equals("subscription_cycle")){
response = sendEmail(invoice.getSubscription(), "fail_renewal");
}*/
}
}else if (stripeObject instanceof Charge) {
Charge charge = (Charge) stripeObject;
@@ -417,9 +424,12 @@ public class StripeServiceImpl implements StripeService {
qw.eq("order_no", orderNo);
SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qw);
// 发送邮件通知商家用户支付失败
if (Objects.isNull(subscriptionInfo) || subscriptionInfo.getStatus().equals("incomplete")) {
if (Objects.isNull(subscriptionInfo)
|| subscriptionInfo.getStatus().equals("incomplete")
|| subscriptionInfo.getStatus().equals("incomplete_expired")) {
sendEmail(orderNo);
}else {
// todo 续订失败 应该不会走这里
sendEmail(subscriptionInfo.getSubscriptionId(), "fail_renewal");
}
}
@@ -535,6 +545,18 @@ public class StripeServiceImpl implements StripeService {
});
}
public void cancelSubscriptionTemp(String subscriptionId) {
Stripe.apiKey = privateKey;
try {
log.info("申请取消连续订阅 {}", subscriptionId);
Subscription subscription = Subscription.retrieve(subscriptionId);
Subscription cancel = subscription.cancel();
cancel.getStatus();
} catch (StripeException e) {
throw new RuntimeException(e);
}
}
public String refund(String amount, String orderNo, String reason) {
Refund refund;
RefundInfo refundByOrderNo = refundInfoService.createRefundByOrderNo(orderNo, reason);
@@ -630,7 +652,6 @@ public class StripeServiceImpl implements StripeService {
}
// 获取所有订阅
public List<Subscription> getSubscription(String username, String userEmail) {
Stripe.apiKey = privateKey;
String customerId = null;
@@ -642,6 +663,25 @@ public class StripeServiceImpl implements StripeService {
} catch (StripeException e) {
throw new RuntimeException(e);
}
}
// 获取所有订阅
public List<String> getSubscriptionIds(String username, String userEmail) {
Stripe.apiKey = privateKey;
String customerId = null;
try {
customerId = getCustomer(username, userEmail);
SubscriptionCollection list = Subscription.list(SubscriptionListParams.builder()
.setCustomer(customerId).build());
List<Subscription> data = list.getData();
ArrayList<String> subscriptionIds = new ArrayList<>();
data.forEach(subscription -> {
subscriptionIds.add(subscription.getId());
});
return subscriptionIds;
} catch (StripeException e) {
throw new RuntimeException(e);
}
}
@@ -747,7 +787,6 @@ public class StripeServiceImpl implements StripeService {
}
public boolean sendEmail(String subscriptionId, String type) {
SubscriptionEmailParamsDTO emailParamsDTO = new SubscriptionEmailParamsDTO();
QueryWrapper<SubscriptionInfo> qwSI = new QueryWrapper<>();
qwSI.eq("subscription_id", subscriptionId);
@@ -784,14 +823,7 @@ public class StripeServiceImpl implements StripeService {
emailParamsDTO.setTotalFee(paymentInfo.getPayerTotal().toString());
emailParamsDTO.setLastOrderDate(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodStart(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy));
emailParamsDTO.setEndOfPrepaidTerm(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy));
emailParamsDTO.setPaymentMethod(paymentInfo.getPaymentMethod());
emailParamsDTO.setLast4(paymentInfo.getLast4());
emailParamsDTO.setSubscriptionId(subscriptionInfo.getId().toString());
emailParamsDTO.setFailMessage(orderByOrderNo.getNote());
emailParamsDTO.setSubscriptionType(subscriptionInfo.getType());
emailParamsDTO.setStartDate(changeTimeStampFormat(orderByOrderNo.getCreateTime()));
emailParamsDTO.setNextPayDate(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy));
emailParamsDTO.setRenewalTime(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy));
setSubscriptionParams(paymentInfo, subscriptionInfo, orderByOrderNo, emailParamsDTO);
SendEmailUtil.subscriptionEmailReminder(type, emailParamsDTO, language, account.getUserEmail());
@@ -840,6 +872,79 @@ public class StripeServiceImpl implements StripeService {
return true;
}
public boolean sendRenewalFailEmail(String invoiceId, String subscriptionId, String orderNo){
// 1、确认当前订单最后一笔支付为fail
// 更新支付信息
PaymentInfo paymentInfo;
QueryWrapper<PaymentInfo> queryWrapper = new QueryWrapper<>();
if (StringUtil.isNullOrEmpty(invoiceId)){
queryWrapper.eq("order_no", orderNo).orderByDesc("id");
List<PaymentInfo> paymentInfos = paymentInfoService.getBaseMapper().selectList(queryWrapper);
if (paymentInfos.isEmpty() || !paymentInfos.get(0).getTradeState().equals("failed")){
return false;
}else {
paymentInfo = paymentInfos.get(0);
}
}else {
queryWrapper.eq("transaction_id", invoiceId);
paymentInfo = paymentInfoService.getBaseMapper().selectOne(queryWrapper);
if (Objects.isNull(paymentInfo)
|| !paymentInfo.getTradeState().equals("failed")
|| paymentInfo.getNotified().equals(1)){
return false;
}
}
// 2、确认当前订阅的状态为past_due
SubscriptionInfo subscriptionInfo;
QueryWrapper<SubscriptionInfo> qwSI = new QueryWrapper<>();
if (StringUtil.isNullOrEmpty(subscriptionId)){
qwSI.eq("order_no", orderNo);
}else {
qwSI.eq("subscription_id", subscriptionId);
}
subscriptionInfo = subscriptionInfoMapper.selectOne(qwSI);
if (Objects.isNull(subscriptionInfo) || !subscriptionInfo.getStatus().equals("past_due")){
return false;
}
// 3、组参数
com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(subscriptionInfo.getAccountId());
String userName = account.getUserName();
String language = account.getLanguage();
OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(subscriptionInfo.getOrderNo());
SubscriptionEmailParamsDTO emailParamsDTO = new SubscriptionEmailParamsDTO();
emailParamsDTO.setUsername(userName);
emailParamsDTO.setOrderId(paymentInfo.getId().toString());
emailParamsDTO.setCreateDate(String.valueOf(paymentInfo.getCreateTime()).replace("T", " "));
emailParamsDTO.setQuantity(String.valueOf(1));
emailParamsDTO.setTotalFee(paymentInfo.getPayerTotal().toString());
setSubscriptionParams(paymentInfo, subscriptionInfo, orderByOrderNo, emailParamsDTO);
// todo
emailParamsDTO.setAccountPageRef("\"https://www.aida.com.hk/home/homePage\"");
// 4、发邮件
SendEmailUtil.subscriptionEmailReminder("fail_renewal", emailParamsDTO, language, account.getUserEmail());
PaymentInfo payment = new PaymentInfo();
payment.setId(paymentInfo.getId());
payment.setNotified(1);
payment.setUpdateTime(LocalDateTime.now());
paymentInfoMapper.updateById(payment);
return true;
}
private void setSubscriptionParams(PaymentInfo paymentInfo, SubscriptionInfo subscriptionInfo, OrderInfo orderByOrderNo, SubscriptionEmailParamsDTO emailParamsDTO) {
emailParamsDTO.setPaymentMethod(paymentInfo.getPaymentMethod());
emailParamsDTO.setLast4(paymentInfo.getLast4());
emailParamsDTO.setSubscriptionId(subscriptionInfo.getId().toString());
emailParamsDTO.setFailMessage(orderByOrderNo.getNote());
emailParamsDTO.setSubscriptionType(subscriptionInfo.getType());
emailParamsDTO.setStartDate(changeTimeStampFormat(orderByOrderNo.getCreateTime()));
emailParamsDTO.setNextPayDate(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy));
emailParamsDTO.setRenewalTime(changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy));
}
public void subscriptionReminder(){
// 提前7天的 00:00:00 和 23:59:59
LocalDateTime startOfDay = LocalDateTime.now().plusDays(7).toLocalDate().atStartOfDay();
@@ -876,4 +981,87 @@ public class StripeServiceImpl implements StripeService {
}
}
// todo 新建一个订阅 使用不会成功的付款方式
public String createSubscriptionTemp(String name, String email){
Stripe.apiKey = privateKey;
try {
OrderInfo orderInfo = orderInfoService.createOrderByProductId(1, PayTypeEnum.STRIPE.getType(), ProductEnum.DailySubscription);
String paymentMethodCode = "pm_card_mastercard";
PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentMethodCode);
String customerId = getCustomer(name, email);
log.info("customerId: {}", customerId);
PaymentMethodAttachParams attachParams = PaymentMethodAttachParams.builder()
.setCustomer(customerId)
.build();
paymentMethod.attach(attachParams);
// 设置默认付款方式
Customer updatedCustomer = Customer.retrieve(customerId);
CustomerUpdateParams params = CustomerUpdateParams.builder()
.setInvoiceSettings(
CustomerUpdateParams.InvoiceSettings.builder()
.setDefaultPaymentMethod(paymentMethod.getId())
.build()
)
.build();
updatedCustomer.update(params);
// 3. 创建订阅
SubscriptionCreateParams subscriptionParams = SubscriptionCreateParams.builder()
.setCustomer(customerId)
.addItem(
SubscriptionCreateParams.Item.builder()
.setPrice("price_1QFXkf02n1TEydyNtA4TQ3Yz") // 替换为实际的价格 ID
.build()
)
.setDescription("AiDA - " + orderInfo.getOrderNo())
.build();
Subscription subscription = Subscription.create(subscriptionParams);
// 打印订阅 ID
System.out.println("Subscription created: " + subscription.getId());
return subscription.getId();
} catch (StripeException e) {
throw new RuntimeException(e);
}
}
public String changeCustomerPayment(String name, String email){
Stripe.apiKey = privateKey;
String paymentMethodCode = "pm_card_chargeCustomerFail";
try {
PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentMethodCode);
String customerId = getCustomer(name, email);
log.info("customerId: {}", customerId);
// 附加支付方式到客户
PaymentMethodAttachParams attachParams = PaymentMethodAttachParams.builder()
.setCustomer(customerId)
.build();
paymentMethod.attach(attachParams);
// 更新客户的默认支付方式
CustomerUpdateParams params = CustomerUpdateParams.builder()
.setInvoiceSettings(
CustomerUpdateParams.InvoiceSettings.builder()
.setDefaultPaymentMethod(paymentMethod.getId())
.build()
)
.build();
// 更新客户信息
Customer customer = Customer.retrieve(customerId);
customer.update(params);
return paymentMethod.getId();
} catch (StripeException e) {
throw new RuntimeException(e);
}
}
}