Обработка веб-крючка payment_intent.удалась, если он конкурирует с обратной записью клиента для создания сущности в базе данных

#c# #entity-framework-core #stripe-payments

Вопрос:

Мне нужны некоторые рекомендации по рабочему процессу для моего приложения при списании средств с кредитной карты с помощью Stripe.

Сценарий 1 — я, не используем webhook для payment_intent.succeeded поэтому, когда я называю полосой.confirmCardPayment на стороне клиента в JavaScript и получить paymentIntent назад, я тогда пост к моему серверу и создать учетную запись в «оплату» с учетом того, что метод под названием «SavePayment()», где все детали (идентификатор карты, годен месяц, сумма и т. д.) будут сохранены. После сохранения в БД я могу вернуть данные клиенту (заработанные баллы, сообщение об успешной оплате и т.д.). Тогда мы закончили!

Сценарий 2 Клиент(пользователь) закрывает браузер после вызова Stripe для списания средств с карты, но до того, как он сможет отправить сообщение обратно на мой сервер, чтобы добавить объект «Оплата». Поэтому теперь я использую веб-крючок payment_intent.succeeded , поскольку другие рекомендовали делать это для избыточности.

Проблема

Поскольку веб-крючок запускается немедленно, после того, как карта списана с помощью Stripe, мой сервер потенциально может получить две разные точки входа (отправку клиента обратно на сервер для сохранения платежа и событие запуска веб-крючка Stripes), чтобы создать объект «Оплата» в моей базе данных.

Теперь это не огромная проблема, потому что обе точки входа могут запрашивать объект «Оплата» на основе его уникального идентификатора (PaymentIntentId), чтобы узнать, существует ли он в базе данных.

Но предположим, что обе точки входа запрашивают и возвращают значение null, так что теперь обе точки входа продолжают создавать новую сущность «Оплата» и пытаются сохранить ее в базе данных. Один из них будет успешным, а другой теперь завершится неудачей, часто создавая исключение ограничения уникального идентификатора, создаваемое SQL Server.

Решение? — Это не похоже на идеальный рабочий процесс/сценарий, в котором часто может возникать несколько исключений, для создания сущности в моей базе данных. Есть ли для этого лучший рабочий процесс, или я застрял в его реализации таким образом?

Вот некоторые из моих кодов/кодов suedo, на которые можно посмотреть.

 public class Payment : BaseEntity
{
    public string PaymentIntentId { get; set; }
    public int Amount { get; set; }
    public string Currency { get; set; }
    public string CardBrand { get; set; }
    public string CardExpMonth { get; set; }
    public string CardExpYear { get; set; }
    public int CardFingerPrint { get; set; }
    public string CardLastFour { get; set; }
    public PaymentStatus Status { get; set; }
    public int StripeFee { get; set; }
    public int PointsAwarded { get; set; }
    public int PointsBefore { get; set; }
    public int PointsAfter { get; set; }
    public string StripeCustomer { get; set; }
    public int UserId { get; set; }
    public User User { get; set; }
}
 

Вот код от клиента, который нужно вызвать stripe, а затем отправить на мой сервер

 // submit button is pressed 
// do some work here then call Stripe

from(this.stripe.confirmCardPayment(this.paymentIntent.clientSecret, data)).subscribe((result: any) => {

  if (result.paymentIntent) {

    let payment = {
      paymentIntentId: result.paymentIntent.id,
      amount: result.paymentIntent.amount,
      currency: result.paymentIntent.currency,
      // fill in other fields
    };

    this.accountService.savePayment(payment).subscribe(response => {

      if (response.status === 'Success') {
        // do some stuff here
        this.alertService.success("You're purchase was successful");
        this.router.navigateByUrl('/somepage');
      }

      if (response.status === 'Failed') {
        this.alertService.danger("Failed to process card");
      }

    }, error => {
      console.log(error);
      this.alertService.danger("Oh no! Something happened, please contact the help desk.");
    }).add(() => {
      this.loadingPayment = false;
    });

  } else {
    this.loadingPayment = false;
    this.alertService.danger(result.error.message);
  }

}); 

Here is the server controller to save a «Payment» entity

         [HttpPost("savepayment")]
    public async Task<ActionResult> SavePayment(StripePaymentDto paymentDto)
    {
        var userFromRepo = await _userManager.FindByEmailFromClaimsPrinciple(HttpContext.User);
        
        if (userFromRepo == null) 
            return Unauthorized(new ApiResponse(401));
        // this calls the Stripe API to get the PaymentIntent (just incase the client changed it)
        var paymentIntent = await _paymentService.RetrievePaymentIntent(paymentDto.PaymentIntentId);
        if (paymentIntent == null) return BadRequest(new ApiResponse(400, "Problem Retrieving Payment Intent"));

        var payment = _mapper.Map<StripePaymentDto, StripePayment>(paymentDto);
        payment.UserId = userFromRepo.Id;

        if (paymentIntent.Status == "succeeded") {
           
            // fill in all the necessary fields
            // left out for brevity

        } else if (paymentIntent.Status == "requires_payment_method") {
            payment.Status = PaymentStatus.Failed;
            _logger.LogInformation("Payment Intent is not successful. Status: "   paymentIntent.Status   " PaymentIntentId: "   paymentIntent.PaymentIntentId);
            // send payment failure email
        } else {
            // don't know if this will be needed
            payment.Status = PaymentStatus.Pending;
        }

        _unitOfWork.Repository<StripePayment>().Add(payment);

        var success = await _unitOfWork.Complete();
        if (success > 0) {
            if (payment.Status == PaymentStatus.Success) {
                // send email
            }
            return Ok(_mapper.Map<StripePayment, StripePaymentDto>(payment));
        }
        
        return BadRequest(new ApiResponse(400, "Failed to save payment"));

    }
    
 

Вот веб-крючок в полоску

     [HttpPost("webhook")]
    public async Task<ActionResult> StripeWebhook()
    {
        var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync();

        // if this doesn't match we get an exception (sig with whSec) 
        var stripeEvent = EventUtility.ConstructEvent(json, Request.Headers["Stripe-Signature"], _whSecret);

        PaymentIntent intent; 

        switch (stripeEvent.Type)
        {
            case "payment_intent.succeeded":
                intent = (PaymentIntent)stripeEvent.Data.Object;
                _logger.LogInformation("Payment Succeeded: ", intent.Id);
                this.ProcessSuccess(intent);
                // order  = await _paymentService.UpdateOrderPaymentSucceeded(intent.Id);
                // _logger.LogInformation("Order updated to payment received: ", order.Id);
                break;
            case "payment_intent.payment_failed":
                intent = (PaymentIntent)stripeEvent.Data.Object;
                _logger.LogInformation("Payment Failed: ", intent.Id);
                // _logger.LogInformation("Payment Failed: ", order.Id);
                break;
        }

        return new EmptyResult();
    }

    private async void ProcessSuccess(PaymentIntent paymentIntent) {
        
        var spec = new PaymentsWithTypeSpecification(paymentIntent.Id);
        var paymentFromRepo = await _unitOfWork.Repository<StripePayment>().GetEntityWithSpec(spec);

        if (paymentFromRepo == null) {
            // create one and add it
            var payment = _mapper.Map<PaymentIntent, StripePayment>(paymentIntent);
            payment.UserId = Convert.ToInt32(paymentIntent.Metadata["userid"]);
        }

        // finish work here and then save to DB

    }
    
 

Комментарии:

1. опять я! как я ответил в другой теме, вам следует подумать о создании системы массового обслуживания, чтобы вы последовательно обрабатывали любые операции чтения/записи в свою базу данных как для входящего события webhook, так и для запросов на стороне клиента к вашему серверу.

2. Я не думаю, что создание очереди-хорошее решение.

3. Почему бы просто не обработать исключение в прослушивателе webhook — если вы получили ошибку, и запись теперь существует в базе данных, продолжайте в обычном режиме? Для временных ошибок веб — крючки stripes имеют встроенную логику повторных попыток, поэтому в любом случае должны быть достаточно надежными- stripe.com/docs/webhooks/best-practices#retry-logic

4. Привет, Сэм. Я думаю, что это может быть то, что мне нужно сделать, но мне придется иметь дело с исключением как на конце контроллера, так и на конце веб-крючка, так как оба будут участвовать в конкурсе на создание записи строки БД. Просто не кажется разумной логикой улавливать исключение для каждой записи строки такого типа?? Что и произойдет, когда каждая точка входа создаст попытку создать строку базы данных.

Ответ №1:

Отличный момент ниже. Я ценю вашу цель. После некоторых размышлений мой окончательный анализ таков: для предотвращения дублирования записей в базе данных из нескольких источников следует использовать уникальный индекс. (который вы используете)

Теперь, используя уникальный индекс, база данных выдаст исключение, которое код должен будет корректно обработать. Следовательно, ответ заключается в том, что вы делаете это так, как я и другие делали это в течение нескольких лет. К сожалению, я не знаю никаких других способов избежать исключения, как только вы перейдете на уровень базы данных.

Отличный вопрос, даже если ответ не тот, на который вы надеялись.

Комментарии:

1. Привет, Джон. Я только что прочитал ссылку, которую вы предоставили, но я немного сбит с толку, и у меня есть вопрос или 2. Мне не нужно обновлять эту строку, только добавьте ее один раз, когда совершена покупка. В моем сценарии пользователь(размещение клиента на контроллере) и пользователь(веб-крючок) будут бороться за создание и добавление его в таблицу, так будет ли эта реализация преобразования строк работать для моего сценария? Не похоже, чтобы это было так! Или преобразование строк обычно применяется для обновления одной и той же строки разными значениями?

2. Извините за задержку. Насколько я понимаю, rowversion предназначен для предотвращения обновления одной и той же строки разными значениями, поэтому он должен предотвращать конфликты из 2 разных источников, поскольку он поддерживается в базе данных.

3. Привет, Джон. Возможно, я вас неправильно понимаю, но вы говорите: «Насколько я понимаю, rowversion предназначен для предотвращения обновления одной и той же строки с разными значениями», но я никогда не обновляю эту строку после ее создания. Он создается один раз, и все. Разногласия возникают из-за создания строки, а не из-за какого-то обновления. Итак, если я неправильно не прочитал документацию, на которую вы оставили ссылку, я не думаю, что смогу ее использовать??