#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 предназначен для предотвращения обновления одной и той же строки с разными значениями», но я никогда не обновляю эту строку после ее создания. Он создается один раз, и все. Разногласия возникают из-за создания строки, а не из-за какого-то обновления. Итак, если я неправильно не прочитал документацию, на которую вы оставили ссылку, я не думаю, что смогу ее использовать??