mirror of
https://github.com/bitwarden/server.git
synced 2025-07-04 01:22:50 -05:00
Revert filescoped (#2227)
* Revert "Add git blame entry (#2226)" This reverts commit239286737d
. * Revert "Turn on file scoped namespaces (#2225)" This reverts commit34fb4cca2a
.
This commit is contained in:
@ -4,58 +4,59 @@ using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bit.Billing.Controllers;
|
||||
|
||||
[Route("apple")]
|
||||
public class AppleController : Controller
|
||||
namespace Bit.Billing.Controllers
|
||||
{
|
||||
private readonly BillingSettings _billingSettings;
|
||||
private readonly ILogger<AppleController> _logger;
|
||||
|
||||
public AppleController(
|
||||
IOptions<BillingSettings> billingSettings,
|
||||
ILogger<AppleController> logger)
|
||||
[Route("apple")]
|
||||
public class AppleController : Controller
|
||||
{
|
||||
_billingSettings = billingSettings?.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
private readonly BillingSettings _billingSettings;
|
||||
private readonly ILogger<AppleController> _logger;
|
||||
|
||||
[HttpPost("iap")]
|
||||
public async Task<IActionResult> PostIap()
|
||||
{
|
||||
if (HttpContext?.Request?.Query == null)
|
||||
public AppleController(
|
||||
IOptions<BillingSettings> billingSettings,
|
||||
ILogger<AppleController> logger)
|
||||
{
|
||||
return new BadRequestResult();
|
||||
_billingSettings = billingSettings?.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
var key = HttpContext.Request.Query.ContainsKey("key") ?
|
||||
HttpContext.Request.Query["key"].ToString() : null;
|
||||
if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.AppleWebhookKey))
|
||||
[HttpPost("iap")]
|
||||
public async Task<IActionResult> PostIap()
|
||||
{
|
||||
return new BadRequestResult();
|
||||
}
|
||||
if (HttpContext?.Request?.Query == null)
|
||||
{
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
string body = null;
|
||||
using (var reader = new StreamReader(HttpContext.Request.Body, Encoding.UTF8))
|
||||
{
|
||||
body = await reader.ReadToEndAsync();
|
||||
}
|
||||
var key = HttpContext.Request.Query.ContainsKey("key") ?
|
||||
HttpContext.Request.Query["key"].ToString() : null;
|
||||
if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.AppleWebhookKey))
|
||||
{
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
return new BadRequestResult();
|
||||
}
|
||||
string body = null;
|
||||
using (var reader = new StreamReader(HttpContext.Request.Body, Encoding.UTF8))
|
||||
{
|
||||
body = await reader.ReadToEndAsync();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(JsonSerializer.Deserialize<JsonDocument>(body), JsonHelpers.Indented);
|
||||
_logger.LogInformation(Bit.Core.Constants.BypassFiltersEventId, "Apple IAP Notification:\n\n{0}", json);
|
||||
return new OkResult();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error processing IAP status notification.");
|
||||
return new BadRequestResult();
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(JsonSerializer.Deserialize<JsonDocument>(body), JsonHelpers.Indented);
|
||||
_logger.LogInformation(Bit.Core.Constants.BypassFiltersEventId, "Apple IAP Notification:\n\n{0}", json);
|
||||
return new OkResult();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error processing IAP status notification.");
|
||||
return new BadRequestResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,199 +9,200 @@ using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bit.Billing.Controllers;
|
||||
|
||||
[Route("bitpay")]
|
||||
public class BitPayController : Controller
|
||||
namespace Bit.Billing.Controllers
|
||||
{
|
||||
private readonly BillingSettings _billingSettings;
|
||||
private readonly BitPayClient _bitPayClient;
|
||||
private readonly ITransactionRepository _transactionRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly ILogger<BitPayController> _logger;
|
||||
|
||||
public BitPayController(
|
||||
IOptions<BillingSettings> billingSettings,
|
||||
BitPayClient bitPayClient,
|
||||
ITransactionRepository transactionRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IUserRepository userRepository,
|
||||
IMailService mailService,
|
||||
IPaymentService paymentService,
|
||||
ILogger<BitPayController> logger)
|
||||
[Route("bitpay")]
|
||||
public class BitPayController : Controller
|
||||
{
|
||||
_billingSettings = billingSettings?.Value;
|
||||
_bitPayClient = bitPayClient;
|
||||
_transactionRepository = transactionRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_userRepository = userRepository;
|
||||
_mailService = mailService;
|
||||
_paymentService = paymentService;
|
||||
_logger = logger;
|
||||
}
|
||||
private readonly BillingSettings _billingSettings;
|
||||
private readonly BitPayClient _bitPayClient;
|
||||
private readonly ITransactionRepository _transactionRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly ILogger<BitPayController> _logger;
|
||||
|
||||
[HttpPost("ipn")]
|
||||
public async Task<IActionResult> PostIpn([FromBody] BitPayEventModel model, [FromQuery] string key)
|
||||
{
|
||||
if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.BitPayWebhookKey))
|
||||
public BitPayController(
|
||||
IOptions<BillingSettings> billingSettings,
|
||||
BitPayClient bitPayClient,
|
||||
ITransactionRepository transactionRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IUserRepository userRepository,
|
||||
IMailService mailService,
|
||||
IPaymentService paymentService,
|
||||
ILogger<BitPayController> logger)
|
||||
{
|
||||
return new BadRequestResult();
|
||||
}
|
||||
if (model == null || string.IsNullOrWhiteSpace(model.Data?.Id) ||
|
||||
string.IsNullOrWhiteSpace(model.Event?.Name))
|
||||
{
|
||||
return new BadRequestResult();
|
||||
_billingSettings = billingSettings?.Value;
|
||||
_bitPayClient = bitPayClient;
|
||||
_transactionRepository = transactionRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_userRepository = userRepository;
|
||||
_mailService = mailService;
|
||||
_paymentService = paymentService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
if (model.Event.Name != "invoice_confirmed")
|
||||
[HttpPost("ipn")]
|
||||
public async Task<IActionResult> PostIpn([FromBody] BitPayEventModel model, [FromQuery] string key)
|
||||
{
|
||||
// Only processing confirmed invoice events for now.
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
var invoice = await _bitPayClient.GetInvoiceAsync(model.Data.Id);
|
||||
if (invoice == null)
|
||||
{
|
||||
// Request forged...?
|
||||
_logger.LogWarning("Invoice not found. #" + model.Data.Id);
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
if (invoice.Status != "confirmed" && invoice.Status != "completed")
|
||||
{
|
||||
_logger.LogWarning("Invoice status of '" + invoice.Status + "' is not acceptable. #" + invoice.Id);
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
if (invoice.Currency != "USD")
|
||||
{
|
||||
// Only process USD payments
|
||||
_logger.LogWarning("Non USD payment received. #" + invoice.Id);
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
var ids = GetIdsFromPosData(invoice);
|
||||
if (!ids.Item1.HasValue && !ids.Item2.HasValue)
|
||||
{
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
var isAccountCredit = IsAccountCredit(invoice);
|
||||
if (!isAccountCredit)
|
||||
{
|
||||
// Only processing credits
|
||||
_logger.LogWarning("Non-credit payment received. #" + invoice.Id);
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
var transaction = await _transactionRepository.GetByGatewayIdAsync(GatewayType.BitPay, invoice.Id);
|
||||
if (transaction != null)
|
||||
{
|
||||
_logger.LogWarning("Already processed this invoice. #" + invoice.Id);
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var tx = new Transaction
|
||||
if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.BitPayWebhookKey))
|
||||
{
|
||||
Amount = Convert.ToDecimal(invoice.Price),
|
||||
CreationDate = GetTransactionDate(invoice),
|
||||
OrganizationId = ids.Item1,
|
||||
UserId = ids.Item2,
|
||||
Type = TransactionType.Credit,
|
||||
Gateway = GatewayType.BitPay,
|
||||
GatewayId = invoice.Id,
|
||||
PaymentMethodType = PaymentMethodType.BitPay,
|
||||
Details = $"{invoice.Currency}, BitPay {invoice.Id}"
|
||||
};
|
||||
await _transactionRepository.CreateAsync(tx);
|
||||
|
||||
if (isAccountCredit)
|
||||
{
|
||||
string billingEmail = null;
|
||||
if (tx.OrganizationId.HasValue)
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(tx.OrganizationId.Value);
|
||||
if (org != null)
|
||||
{
|
||||
billingEmail = org.BillingEmailAddress();
|
||||
if (await _paymentService.CreditAccountAsync(org, tx.Amount))
|
||||
{
|
||||
await _organizationRepository.ReplaceAsync(org);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(tx.UserId.Value);
|
||||
if (user != null)
|
||||
{
|
||||
billingEmail = user.BillingEmailAddress();
|
||||
if (await _paymentService.CreditAccountAsync(user, tx.Amount))
|
||||
{
|
||||
await _userRepository.ReplaceAsync(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(billingEmail))
|
||||
{
|
||||
await _mailService.SendAddedCreditAsync(billingEmail, tx.Amount);
|
||||
}
|
||||
return new BadRequestResult();
|
||||
}
|
||||
}
|
||||
// Catch foreign key violations because user/org could have been deleted.
|
||||
catch (SqlException e) when (e.Number == 547) { }
|
||||
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
private bool IsAccountCredit(BitPayLight.Models.Invoice.Invoice invoice)
|
||||
{
|
||||
return invoice != null && invoice.PosData != null && invoice.PosData.Contains("accountCredit:1");
|
||||
}
|
||||
|
||||
private DateTime GetTransactionDate(BitPayLight.Models.Invoice.Invoice invoice)
|
||||
{
|
||||
var transactions = invoice.Transactions?.Where(t => t.Type == null &&
|
||||
!string.IsNullOrWhiteSpace(t.Confirmations) && t.Confirmations != "0");
|
||||
if (transactions != null && transactions.Count() == 1)
|
||||
{
|
||||
return DateTime.Parse(transactions.First().ReceivedTime, CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.RoundtripKind);
|
||||
}
|
||||
return CoreHelpers.FromEpocMilliseconds(invoice.CurrentTime);
|
||||
}
|
||||
|
||||
public Tuple<Guid?, Guid?> GetIdsFromPosData(BitPayLight.Models.Invoice.Invoice invoice)
|
||||
{
|
||||
Guid? orgId = null;
|
||||
Guid? userId = null;
|
||||
|
||||
if (invoice != null && !string.IsNullOrWhiteSpace(invoice.PosData) && invoice.PosData.Contains(":"))
|
||||
{
|
||||
var mainParts = invoice.PosData.Split(',');
|
||||
foreach (var mainPart in mainParts)
|
||||
if (model == null || string.IsNullOrWhiteSpace(model.Data?.Id) ||
|
||||
string.IsNullOrWhiteSpace(model.Event?.Name))
|
||||
{
|
||||
var parts = mainPart.Split(':');
|
||||
if (parts.Length > 1 && Guid.TryParse(parts[1], out var id))
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
if (model.Event.Name != "invoice_confirmed")
|
||||
{
|
||||
// Only processing confirmed invoice events for now.
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
var invoice = await _bitPayClient.GetInvoiceAsync(model.Data.Id);
|
||||
if (invoice == null)
|
||||
{
|
||||
// Request forged...?
|
||||
_logger.LogWarning("Invoice not found. #" + model.Data.Id);
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
if (invoice.Status != "confirmed" && invoice.Status != "completed")
|
||||
{
|
||||
_logger.LogWarning("Invoice status of '" + invoice.Status + "' is not acceptable. #" + invoice.Id);
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
if (invoice.Currency != "USD")
|
||||
{
|
||||
// Only process USD payments
|
||||
_logger.LogWarning("Non USD payment received. #" + invoice.Id);
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
var ids = GetIdsFromPosData(invoice);
|
||||
if (!ids.Item1.HasValue && !ids.Item2.HasValue)
|
||||
{
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
var isAccountCredit = IsAccountCredit(invoice);
|
||||
if (!isAccountCredit)
|
||||
{
|
||||
// Only processing credits
|
||||
_logger.LogWarning("Non-credit payment received. #" + invoice.Id);
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
var transaction = await _transactionRepository.GetByGatewayIdAsync(GatewayType.BitPay, invoice.Id);
|
||||
if (transaction != null)
|
||||
{
|
||||
_logger.LogWarning("Already processed this invoice. #" + invoice.Id);
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var tx = new Transaction
|
||||
{
|
||||
if (parts[0] == "userId")
|
||||
Amount = Convert.ToDecimal(invoice.Price),
|
||||
CreationDate = GetTransactionDate(invoice),
|
||||
OrganizationId = ids.Item1,
|
||||
UserId = ids.Item2,
|
||||
Type = TransactionType.Credit,
|
||||
Gateway = GatewayType.BitPay,
|
||||
GatewayId = invoice.Id,
|
||||
PaymentMethodType = PaymentMethodType.BitPay,
|
||||
Details = $"{invoice.Currency}, BitPay {invoice.Id}"
|
||||
};
|
||||
await _transactionRepository.CreateAsync(tx);
|
||||
|
||||
if (isAccountCredit)
|
||||
{
|
||||
string billingEmail = null;
|
||||
if (tx.OrganizationId.HasValue)
|
||||
{
|
||||
userId = id;
|
||||
var org = await _organizationRepository.GetByIdAsync(tx.OrganizationId.Value);
|
||||
if (org != null)
|
||||
{
|
||||
billingEmail = org.BillingEmailAddress();
|
||||
if (await _paymentService.CreditAccountAsync(org, tx.Amount))
|
||||
{
|
||||
await _organizationRepository.ReplaceAsync(org);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (parts[0] == "organizationId")
|
||||
else
|
||||
{
|
||||
orgId = id;
|
||||
var user = await _userRepository.GetByIdAsync(tx.UserId.Value);
|
||||
if (user != null)
|
||||
{
|
||||
billingEmail = user.BillingEmailAddress();
|
||||
if (await _paymentService.CreditAccountAsync(user, tx.Amount))
|
||||
{
|
||||
await _userRepository.ReplaceAsync(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(billingEmail))
|
||||
{
|
||||
await _mailService.SendAddedCreditAsync(billingEmail, tx.Amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Catch foreign key violations because user/org could have been deleted.
|
||||
catch (SqlException e) when (e.Number == 547) { }
|
||||
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
return new Tuple<Guid?, Guid?>(orgId, userId);
|
||||
private bool IsAccountCredit(BitPayLight.Models.Invoice.Invoice invoice)
|
||||
{
|
||||
return invoice != null && invoice.PosData != null && invoice.PosData.Contains("accountCredit:1");
|
||||
}
|
||||
|
||||
private DateTime GetTransactionDate(BitPayLight.Models.Invoice.Invoice invoice)
|
||||
{
|
||||
var transactions = invoice.Transactions?.Where(t => t.Type == null &&
|
||||
!string.IsNullOrWhiteSpace(t.Confirmations) && t.Confirmations != "0");
|
||||
if (transactions != null && transactions.Count() == 1)
|
||||
{
|
||||
return DateTime.Parse(transactions.First().ReceivedTime, CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.RoundtripKind);
|
||||
}
|
||||
return CoreHelpers.FromEpocMilliseconds(invoice.CurrentTime);
|
||||
}
|
||||
|
||||
public Tuple<Guid?, Guid?> GetIdsFromPosData(BitPayLight.Models.Invoice.Invoice invoice)
|
||||
{
|
||||
Guid? orgId = null;
|
||||
Guid? userId = null;
|
||||
|
||||
if (invoice != null && !string.IsNullOrWhiteSpace(invoice.PosData) && invoice.PosData.Contains(":"))
|
||||
{
|
||||
var mainParts = invoice.PosData.Split(',');
|
||||
foreach (var mainPart in mainParts)
|
||||
{
|
||||
var parts = mainPart.Split(':');
|
||||
if (parts.Length > 1 && Guid.TryParse(parts[1], out var id))
|
||||
{
|
||||
if (parts[0] == "userId")
|
||||
{
|
||||
userId = id;
|
||||
}
|
||||
else if (parts[0] == "organizationId")
|
||||
{
|
||||
orgId = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Tuple<Guid?, Guid?>(orgId, userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,165 +8,166 @@ using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bit.Billing.Controllers;
|
||||
|
||||
[Route("freshdesk")]
|
||||
public class FreshdeskController : Controller
|
||||
namespace Bit.Billing.Controllers
|
||||
{
|
||||
private readonly BillingSettings _billingSettings;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly ILogger<FreshdeskController> _logger;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public FreshdeskController(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOptions<BillingSettings> billingSettings,
|
||||
ILogger<FreshdeskController> logger,
|
||||
GlobalSettings globalSettings,
|
||||
IHttpClientFactory httpClientFactory)
|
||||
[Route("freshdesk")]
|
||||
public class FreshdeskController : Controller
|
||||
{
|
||||
_billingSettings = billingSettings?.Value;
|
||||
_userRepository = userRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
private readonly BillingSettings _billingSettings;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly ILogger<FreshdeskController> _logger;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
[HttpPost("webhook")]
|
||||
public async Task<IActionResult> PostWebhook([FromQuery, Required] string key,
|
||||
[FromBody, Required] FreshdeskWebhookModel model)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key) || !CoreHelpers.FixedTimeEquals(key, _billingSettings.FreshdeskWebhookKey))
|
||||
public FreshdeskController(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOptions<BillingSettings> billingSettings,
|
||||
ILogger<FreshdeskController> logger,
|
||||
GlobalSettings globalSettings,
|
||||
IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
return new BadRequestResult();
|
||||
_billingSettings = billingSettings?.Value;
|
||||
_userRepository = userRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
try
|
||||
[HttpPost("webhook")]
|
||||
public async Task<IActionResult> PostWebhook([FromQuery, Required] string key,
|
||||
[FromBody, Required] FreshdeskWebhookModel model)
|
||||
{
|
||||
var ticketId = model.TicketId;
|
||||
var ticketContactEmail = model.TicketContactEmail;
|
||||
var ticketTags = model.TicketTags;
|
||||
if (string.IsNullOrWhiteSpace(ticketId) || string.IsNullOrWhiteSpace(ticketContactEmail))
|
||||
if (string.IsNullOrWhiteSpace(key) || !CoreHelpers.FixedTimeEquals(key, _billingSettings.FreshdeskWebhookKey))
|
||||
{
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
var updateBody = new Dictionary<string, object>();
|
||||
var note = string.Empty;
|
||||
var customFields = new Dictionary<string, object>();
|
||||
var user = await _userRepository.GetByEmailAsync(ticketContactEmail);
|
||||
if (user != null)
|
||||
try
|
||||
{
|
||||
var userLink = $"{_globalSettings.BaseServiceUri.Admin}/users/edit/{user.Id}";
|
||||
note += $"<li>User, {user.Email}: {userLink}</li>";
|
||||
customFields.Add("cf_user", userLink);
|
||||
var tags = new HashSet<string>();
|
||||
if (user.Premium)
|
||||
var ticketId = model.TicketId;
|
||||
var ticketContactEmail = model.TicketContactEmail;
|
||||
var ticketTags = model.TicketTags;
|
||||
if (string.IsNullOrWhiteSpace(ticketId) || string.IsNullOrWhiteSpace(ticketContactEmail))
|
||||
{
|
||||
tags.Add("Premium");
|
||||
return new BadRequestResult();
|
||||
}
|
||||
var orgs = await _organizationRepository.GetManyByUserIdAsync(user.Id);
|
||||
|
||||
foreach (var org in orgs)
|
||||
var updateBody = new Dictionary<string, object>();
|
||||
var note = string.Empty;
|
||||
var customFields = new Dictionary<string, object>();
|
||||
var user = await _userRepository.GetByEmailAsync(ticketContactEmail);
|
||||
if (user != null)
|
||||
{
|
||||
var orgNote = $"{org.Name} ({org.Seats.GetValueOrDefault()}): " +
|
||||
$"{_globalSettings.BaseServiceUri.Admin}/organizations/edit/{org.Id}";
|
||||
note += $"<li>Org, {orgNote}</li>";
|
||||
if (!customFields.Any(kvp => kvp.Key == "cf_org"))
|
||||
var userLink = $"{_globalSettings.BaseServiceUri.Admin}/users/edit/{user.Id}";
|
||||
note += $"<li>User, {user.Email}: {userLink}</li>";
|
||||
customFields.Add("cf_user", userLink);
|
||||
var tags = new HashSet<string>();
|
||||
if (user.Premium)
|
||||
{
|
||||
customFields.Add("cf_org", orgNote);
|
||||
}
|
||||
else
|
||||
{
|
||||
customFields["cf_org"] += $"\n{orgNote}";
|
||||
tags.Add("Premium");
|
||||
}
|
||||
var orgs = await _organizationRepository.GetManyByUserIdAsync(user.Id);
|
||||
|
||||
var planName = GetAttribute<DisplayAttribute>(org.PlanType).Name.Split(" ").FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(planName))
|
||||
foreach (var org in orgs)
|
||||
{
|
||||
tags.Add(string.Format("Org: {0}", planName));
|
||||
}
|
||||
}
|
||||
if (tags.Any())
|
||||
{
|
||||
var tagsToUpdate = tags.ToList();
|
||||
if (!string.IsNullOrWhiteSpace(ticketTags))
|
||||
{
|
||||
var splitTicketTags = ticketTags.Split(',');
|
||||
for (var i = 0; i < splitTicketTags.Length; i++)
|
||||
var orgNote = $"{org.Name} ({org.Seats.GetValueOrDefault()}): " +
|
||||
$"{_globalSettings.BaseServiceUri.Admin}/organizations/edit/{org.Id}";
|
||||
note += $"<li>Org, {orgNote}</li>";
|
||||
if (!customFields.Any(kvp => kvp.Key == "cf_org"))
|
||||
{
|
||||
tagsToUpdate.Insert(i, splitTicketTags[i]);
|
||||
customFields.Add("cf_org", orgNote);
|
||||
}
|
||||
else
|
||||
{
|
||||
customFields["cf_org"] += $"\n{orgNote}";
|
||||
}
|
||||
|
||||
var planName = GetAttribute<DisplayAttribute>(org.PlanType).Name.Split(" ").FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(planName))
|
||||
{
|
||||
tags.Add(string.Format("Org: {0}", planName));
|
||||
}
|
||||
}
|
||||
updateBody.Add("tags", tagsToUpdate);
|
||||
if (tags.Any())
|
||||
{
|
||||
var tagsToUpdate = tags.ToList();
|
||||
if (!string.IsNullOrWhiteSpace(ticketTags))
|
||||
{
|
||||
var splitTicketTags = ticketTags.Split(',');
|
||||
for (var i = 0; i < splitTicketTags.Length; i++)
|
||||
{
|
||||
tagsToUpdate.Insert(i, splitTicketTags[i]);
|
||||
}
|
||||
}
|
||||
updateBody.Add("tags", tagsToUpdate);
|
||||
}
|
||||
|
||||
if (customFields.Any())
|
||||
{
|
||||
updateBody.Add("custom_fields", customFields);
|
||||
}
|
||||
var updateRequest = new HttpRequestMessage(HttpMethod.Put,
|
||||
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}", ticketId))
|
||||
{
|
||||
Content = JsonContent.Create(updateBody),
|
||||
};
|
||||
await CallFreshdeskApiAsync(updateRequest);
|
||||
|
||||
var noteBody = new Dictionary<string, object>
|
||||
{
|
||||
{ "body", $"<ul>{note}</ul>" },
|
||||
{ "private", true }
|
||||
};
|
||||
var noteRequest = new HttpRequestMessage(HttpMethod.Post,
|
||||
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/notes", ticketId))
|
||||
{
|
||||
Content = JsonContent.Create(noteBody),
|
||||
};
|
||||
await CallFreshdeskApiAsync(noteRequest);
|
||||
}
|
||||
|
||||
if (customFields.Any())
|
||||
return new OkResult();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error processing freshdesk webhook.");
|
||||
return new BadRequestResult();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> CallFreshdeskApiAsync(HttpRequestMessage request, int retriedCount = 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
var freshdeskAuthkey = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_billingSettings.FreshdeskApiKey}:X"));
|
||||
var httpClient = _httpClientFactory.CreateClient("FreshdeskApi");
|
||||
request.Headers.Add("Authorization", freshdeskAuthkey);
|
||||
var response = await httpClient.SendAsync(request);
|
||||
if (response.StatusCode != System.Net.HttpStatusCode.TooManyRequests || retriedCount > 3)
|
||||
{
|
||||
updateBody.Add("custom_fields", customFields);
|
||||
return response;
|
||||
}
|
||||
var updateRequest = new HttpRequestMessage(HttpMethod.Put,
|
||||
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}", ticketId))
|
||||
{
|
||||
Content = JsonContent.Create(updateBody),
|
||||
};
|
||||
await CallFreshdeskApiAsync(updateRequest);
|
||||
|
||||
var noteBody = new Dictionary<string, object>
|
||||
{
|
||||
{ "body", $"<ul>{note}</ul>" },
|
||||
{ "private", true }
|
||||
};
|
||||
var noteRequest = new HttpRequestMessage(HttpMethod.Post,
|
||||
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/notes", ticketId))
|
||||
{
|
||||
Content = JsonContent.Create(noteBody),
|
||||
};
|
||||
await CallFreshdeskApiAsync(noteRequest);
|
||||
}
|
||||
|
||||
return new OkResult();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error processing freshdesk webhook.");
|
||||
return new BadRequestResult();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> CallFreshdeskApiAsync(HttpRequestMessage request, int retriedCount = 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
var freshdeskAuthkey = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_billingSettings.FreshdeskApiKey}:X"));
|
||||
var httpClient = _httpClientFactory.CreateClient("FreshdeskApi");
|
||||
request.Headers.Add("Authorization", freshdeskAuthkey);
|
||||
var response = await httpClient.SendAsync(request);
|
||||
if (response.StatusCode != System.Net.HttpStatusCode.TooManyRequests || retriedCount > 3)
|
||||
catch
|
||||
{
|
||||
return response;
|
||||
if (retriedCount > 3)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
await Task.Delay(30000 * (retriedCount + 1));
|
||||
return await CallFreshdeskApiAsync(request, retriedCount++);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (retriedCount > 3)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
await Task.Delay(30000 * (retriedCount + 1));
|
||||
return await CallFreshdeskApiAsync(request, retriedCount++);
|
||||
}
|
||||
|
||||
private TAttribute GetAttribute<TAttribute>(Enum enumValue) where TAttribute : Attribute
|
||||
{
|
||||
return enumValue.GetType().GetMember(enumValue.ToString()).First().GetCustomAttribute<TAttribute>();
|
||||
private TAttribute GetAttribute<TAttribute>(Enum enumValue) where TAttribute : Attribute
|
||||
{
|
||||
return enumValue.GetType().GetMember(enumValue.ToString()).First().GetCustomAttribute<TAttribute>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,228 +7,229 @@ using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bit.Billing.Controllers;
|
||||
|
||||
[Route("freshsales")]
|
||||
public class FreshsalesController : Controller
|
||||
namespace Bit.Billing.Controllers
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly ILogger _logger;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
private readonly string _freshsalesApiKey;
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public FreshsalesController(IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOptions<BillingSettings> billingSettings,
|
||||
ILogger<FreshsalesController> logger,
|
||||
GlobalSettings globalSettings)
|
||||
[Route("freshsales")]
|
||||
public class FreshsalesController : Controller
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly ILogger _logger;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
_httpClient = new HttpClient
|
||||
private readonly string _freshsalesApiKey;
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public FreshsalesController(IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOptions<BillingSettings> billingSettings,
|
||||
ILogger<FreshsalesController> logger,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
BaseAddress = new Uri("https://bitwarden.freshsales.io/api/")
|
||||
};
|
||||
_userRepository = userRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
|
||||
_freshsalesApiKey = billingSettings.Value.FreshsalesApiKey;
|
||||
|
||||
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
|
||||
"Token",
|
||||
$"token={_freshsalesApiKey}");
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("webhook")]
|
||||
public async Task<IActionResult> PostWebhook([FromHeader(Name = "Authorization")] string key,
|
||||
[FromBody] CustomWebhookRequestModel request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key) || !CoreHelpers.FixedTimeEquals(_freshsalesApiKey, key))
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var leadResponse = await _httpClient.GetFromJsonAsync<LeadWrapper<FreshsalesLeadModel>>(
|
||||
$"leads/{request.LeadId}",
|
||||
cancellationToken);
|
||||
|
||||
var lead = leadResponse.Lead;
|
||||
|
||||
var primaryEmail = lead.Emails
|
||||
.Where(e => e.IsPrimary)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (primaryEmail == null)
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
return BadRequest(new { Message = "Lead has not primary email." });
|
||||
}
|
||||
|
||||
var user = await _userRepository.GetByEmailAsync(primaryEmail.Value);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
var newTags = new HashSet<string>();
|
||||
|
||||
if (user.Premium)
|
||||
{
|
||||
newTags.Add("Premium");
|
||||
}
|
||||
|
||||
var noteItems = new List<string>
|
||||
{
|
||||
$"User, {user.Email}: {_globalSettings.BaseServiceUri.Admin}/users/edit/{user.Id}"
|
||||
BaseAddress = new Uri("https://bitwarden.freshsales.io/api/")
|
||||
};
|
||||
|
||||
var orgs = await _organizationRepository.GetManyByUserIdAsync(user.Id);
|
||||
_freshsalesApiKey = billingSettings.Value.FreshsalesApiKey;
|
||||
|
||||
foreach (var org in orgs)
|
||||
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
|
||||
"Token",
|
||||
$"token={_freshsalesApiKey}");
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("webhook")]
|
||||
public async Task<IActionResult> PostWebhook([FromHeader(Name = "Authorization")] string key,
|
||||
[FromBody] CustomWebhookRequestModel request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key) || !CoreHelpers.FixedTimeEquals(_freshsalesApiKey, key))
|
||||
{
|
||||
noteItems.Add($"Org, {org.Name}: {_globalSettings.BaseServiceUri.Admin}/organizations/edit/{org.Id}");
|
||||
if (TryGetPlanName(org.PlanType, out var planName))
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var leadResponse = await _httpClient.GetFromJsonAsync<LeadWrapper<FreshsalesLeadModel>>(
|
||||
$"leads/{request.LeadId}",
|
||||
cancellationToken);
|
||||
|
||||
var lead = leadResponse.Lead;
|
||||
|
||||
var primaryEmail = lead.Emails
|
||||
.Where(e => e.IsPrimary)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (primaryEmail == null)
|
||||
{
|
||||
newTags.Add($"Org: {planName}");
|
||||
return BadRequest(new { Message = "Lead has not primary email." });
|
||||
}
|
||||
|
||||
var user = await _userRepository.GetByEmailAsync(primaryEmail.Value);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
var newTags = new HashSet<string>();
|
||||
|
||||
if (user.Premium)
|
||||
{
|
||||
newTags.Add("Premium");
|
||||
}
|
||||
|
||||
var noteItems = new List<string>
|
||||
{
|
||||
$"User, {user.Email}: {_globalSettings.BaseServiceUri.Admin}/users/edit/{user.Id}"
|
||||
};
|
||||
|
||||
var orgs = await _organizationRepository.GetManyByUserIdAsync(user.Id);
|
||||
|
||||
foreach (var org in orgs)
|
||||
{
|
||||
noteItems.Add($"Org, {org.Name}: {_globalSettings.BaseServiceUri.Admin}/organizations/edit/{org.Id}");
|
||||
if (TryGetPlanName(org.PlanType, out var planName))
|
||||
{
|
||||
newTags.Add($"Org: {planName}");
|
||||
}
|
||||
}
|
||||
|
||||
if (newTags.Any())
|
||||
{
|
||||
var allTags = newTags.Concat(lead.Tags);
|
||||
var updateLeadResponse = await _httpClient.PutAsJsonAsync(
|
||||
$"leads/{request.LeadId}",
|
||||
CreateWrapper(new { tags = allTags }),
|
||||
cancellationToken);
|
||||
updateLeadResponse.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
var createNoteResponse = await _httpClient.PostAsJsonAsync(
|
||||
"notes",
|
||||
CreateNoteRequestModel(request.LeadId, string.Join('\n', noteItems)), cancellationToken);
|
||||
createNoteResponse.EnsureSuccessStatusCode();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
if (newTags.Any())
|
||||
catch (Exception ex)
|
||||
{
|
||||
var allTags = newTags.Concat(lead.Tags);
|
||||
var updateLeadResponse = await _httpClient.PutAsJsonAsync(
|
||||
$"leads/{request.LeadId}",
|
||||
CreateWrapper(new { tags = allTags }),
|
||||
cancellationToken);
|
||||
updateLeadResponse.EnsureSuccessStatusCode();
|
||||
Console.WriteLine(ex);
|
||||
_logger.LogError(ex, "Error processing freshsales webhook");
|
||||
return BadRequest(new { ex.Message });
|
||||
}
|
||||
|
||||
var createNoteResponse = await _httpClient.PostAsJsonAsync(
|
||||
"notes",
|
||||
CreateNoteRequestModel(request.LeadId, string.Join('\n', noteItems)), cancellationToken);
|
||||
createNoteResponse.EnsureSuccessStatusCode();
|
||||
return NoContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
_logger.LogError(ex, "Error processing freshsales webhook");
|
||||
return BadRequest(new { ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static LeadWrapper<T> CreateWrapper<T>(T lead)
|
||||
{
|
||||
return new LeadWrapper<T>
|
||||
private static LeadWrapper<T> CreateWrapper<T>(T lead)
|
||||
{
|
||||
Lead = lead,
|
||||
};
|
||||
}
|
||||
|
||||
private static CreateNoteRequestModel CreateNoteRequestModel(long leadId, string content)
|
||||
{
|
||||
return new CreateNoteRequestModel
|
||||
{
|
||||
Note = new EditNoteModel
|
||||
return new LeadWrapper<T>
|
||||
{
|
||||
Description = content,
|
||||
TargetableType = "Lead",
|
||||
TargetableId = leadId,
|
||||
},
|
||||
};
|
||||
}
|
||||
Lead = lead,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool TryGetPlanName(PlanType planType, out string planName)
|
||||
{
|
||||
switch (planType)
|
||||
private static CreateNoteRequestModel CreateNoteRequestModel(long leadId, string content)
|
||||
{
|
||||
case PlanType.Free:
|
||||
planName = "Free";
|
||||
return true;
|
||||
case PlanType.FamiliesAnnually:
|
||||
case PlanType.FamiliesAnnually2019:
|
||||
planName = "Families";
|
||||
return true;
|
||||
case PlanType.TeamsAnnually:
|
||||
case PlanType.TeamsAnnually2019:
|
||||
case PlanType.TeamsMonthly:
|
||||
case PlanType.TeamsMonthly2019:
|
||||
planName = "Teams";
|
||||
return true;
|
||||
case PlanType.EnterpriseAnnually:
|
||||
case PlanType.EnterpriseAnnually2019:
|
||||
case PlanType.EnterpriseMonthly:
|
||||
case PlanType.EnterpriseMonthly2019:
|
||||
planName = "Enterprise";
|
||||
return true;
|
||||
case PlanType.Custom:
|
||||
planName = "Custom";
|
||||
return true;
|
||||
default:
|
||||
planName = null;
|
||||
return false;
|
||||
return new CreateNoteRequestModel
|
||||
{
|
||||
Note = new EditNoteModel
|
||||
{
|
||||
Description = content,
|
||||
TargetableType = "Lead",
|
||||
TargetableId = leadId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static bool TryGetPlanName(PlanType planType, out string planName)
|
||||
{
|
||||
switch (planType)
|
||||
{
|
||||
case PlanType.Free:
|
||||
planName = "Free";
|
||||
return true;
|
||||
case PlanType.FamiliesAnnually:
|
||||
case PlanType.FamiliesAnnually2019:
|
||||
planName = "Families";
|
||||
return true;
|
||||
case PlanType.TeamsAnnually:
|
||||
case PlanType.TeamsAnnually2019:
|
||||
case PlanType.TeamsMonthly:
|
||||
case PlanType.TeamsMonthly2019:
|
||||
planName = "Teams";
|
||||
return true;
|
||||
case PlanType.EnterpriseAnnually:
|
||||
case PlanType.EnterpriseAnnually2019:
|
||||
case PlanType.EnterpriseMonthly:
|
||||
case PlanType.EnterpriseMonthly2019:
|
||||
planName = "Enterprise";
|
||||
return true;
|
||||
case PlanType.Custom:
|
||||
planName = "Custom";
|
||||
return true;
|
||||
default:
|
||||
planName = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class CustomWebhookRequestModel
|
||||
{
|
||||
[JsonPropertyName("leadId")]
|
||||
public long LeadId { get; set; }
|
||||
}
|
||||
|
||||
public class LeadWrapper<T>
|
||||
{
|
||||
[JsonPropertyName("lead")]
|
||||
public T Lead { get; set; }
|
||||
|
||||
public static LeadWrapper<TItem> Create<TItem>(TItem lead)
|
||||
public class CustomWebhookRequestModel
|
||||
{
|
||||
return new LeadWrapper<TItem>
|
||||
[JsonPropertyName("leadId")]
|
||||
public long LeadId { get; set; }
|
||||
}
|
||||
|
||||
public class LeadWrapper<T>
|
||||
{
|
||||
[JsonPropertyName("lead")]
|
||||
public T Lead { get; set; }
|
||||
|
||||
public static LeadWrapper<TItem> Create<TItem>(TItem lead)
|
||||
{
|
||||
Lead = lead,
|
||||
};
|
||||
return new LeadWrapper<TItem>
|
||||
{
|
||||
Lead = lead,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class FreshsalesLeadModel
|
||||
{
|
||||
public string[] Tags { get; set; }
|
||||
public FreshsalesEmailModel[] Emails { get; set; }
|
||||
}
|
||||
|
||||
public class FreshsalesEmailModel
|
||||
{
|
||||
[JsonPropertyName("value")]
|
||||
public string Value { get; set; }
|
||||
|
||||
[JsonPropertyName("is_primary")]
|
||||
public bool IsPrimary { get; set; }
|
||||
}
|
||||
|
||||
public class CreateNoteRequestModel
|
||||
{
|
||||
[JsonPropertyName("note")]
|
||||
public EditNoteModel Note { get; set; }
|
||||
}
|
||||
|
||||
public class EditNoteModel
|
||||
{
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
[JsonPropertyName("targetable_type")]
|
||||
public string TargetableType { get; set; }
|
||||
|
||||
[JsonPropertyName("targetable_id")]
|
||||
public long TargetableId { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
public class FreshsalesLeadModel
|
||||
{
|
||||
public string[] Tags { get; set; }
|
||||
public FreshsalesEmailModel[] Emails { get; set; }
|
||||
}
|
||||
|
||||
public class FreshsalesEmailModel
|
||||
{
|
||||
[JsonPropertyName("value")]
|
||||
public string Value { get; set; }
|
||||
|
||||
[JsonPropertyName("is_primary")]
|
||||
public bool IsPrimary { get; set; }
|
||||
}
|
||||
|
||||
public class CreateNoteRequestModel
|
||||
{
|
||||
[JsonPropertyName("note")]
|
||||
public EditNoteModel Note { get; set; }
|
||||
}
|
||||
|
||||
public class EditNoteModel
|
||||
{
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
[JsonPropertyName("targetable_type")]
|
||||
public string TargetableType { get; set; }
|
||||
|
||||
[JsonPropertyName("targetable_id")]
|
||||
public long TargetableId { get; set; }
|
||||
}
|
||||
|
@ -1,20 +1,21 @@
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Billing.Controllers;
|
||||
|
||||
public class InfoController : Controller
|
||||
namespace Bit.Billing.Controllers
|
||||
{
|
||||
[HttpGet("~/alive")]
|
||||
[HttpGet("~/now")]
|
||||
public DateTime GetAlive()
|
||||
public class InfoController : Controller
|
||||
{
|
||||
return DateTime.UtcNow;
|
||||
}
|
||||
[HttpGet("~/alive")]
|
||||
[HttpGet("~/now")]
|
||||
public DateTime GetAlive()
|
||||
{
|
||||
return DateTime.UtcNow;
|
||||
}
|
||||
|
||||
[HttpGet("~/version")]
|
||||
public JsonResult GetVersion()
|
||||
{
|
||||
return Json(CoreHelpers.GetVersion());
|
||||
[HttpGet("~/version")]
|
||||
public JsonResult GetVersion()
|
||||
{
|
||||
return Json(CoreHelpers.GetVersion());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,53 +1,54 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Billing.Controllers;
|
||||
|
||||
public class LoginController : Controller
|
||||
namespace Billing.Controllers
|
||||
{
|
||||
/*
|
||||
private readonly PasswordlessSignInManager<IdentityUser> _signInManager;
|
||||
|
||||
public LoginController(
|
||||
PasswordlessSignInManager<IdentityUser> signInManager)
|
||||
public class LoginController : Controller
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
}
|
||||
/*
|
||||
private readonly PasswordlessSignInManager<IdentityUser> _signInManager;
|
||||
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Index(LoginModel model)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
public LoginController(
|
||||
PasswordlessSignInManager<IdentityUser> signInManager)
|
||||
{
|
||||
var result = await _signInManager.PasswordlessSignInAsync(model.Email,
|
||||
Url.Action("Confirm", "Login", null, Request.Scheme));
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Account not found.");
|
||||
}
|
||||
_signInManager = signInManager;
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Confirm(string email, string token)
|
||||
{
|
||||
var result = await _signInManager.PasswordlessSignInAsync(email, token, false);
|
||||
if (!result.Succeeded)
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View("Error");
|
||||
return View();
|
||||
}
|
||||
|
||||
return RedirectToAction("Index", "Home");
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Index(LoginModel model)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var result = await _signInManager.PasswordlessSignInAsync(model.Email,
|
||||
Url.Action("Confirm", "Login", null, Request.Scheme));
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Account not found.");
|
||||
}
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Confirm(string email, string token)
|
||||
{
|
||||
var result = await _signInManager.PasswordlessSignInAsync(email, token, false);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return View("Error");
|
||||
}
|
||||
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
*/
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
@ -9,226 +9,227 @@ using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bit.Billing.Controllers;
|
||||
|
||||
[Route("paypal")]
|
||||
public class PayPalController : Controller
|
||||
namespace Bit.Billing.Controllers
|
||||
{
|
||||
private readonly BillingSettings _billingSettings;
|
||||
private readonly PayPalIpnClient _paypalIpnClient;
|
||||
private readonly ITransactionRepository _transactionRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly ILogger<PayPalController> _logger;
|
||||
|
||||
public PayPalController(
|
||||
IOptions<BillingSettings> billingSettings,
|
||||
PayPalIpnClient paypalIpnClient,
|
||||
ITransactionRepository transactionRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IUserRepository userRepository,
|
||||
IMailService mailService,
|
||||
IPaymentService paymentService,
|
||||
ILogger<PayPalController> logger)
|
||||
[Route("paypal")]
|
||||
public class PayPalController : Controller
|
||||
{
|
||||
_billingSettings = billingSettings?.Value;
|
||||
_paypalIpnClient = paypalIpnClient;
|
||||
_transactionRepository = transactionRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_userRepository = userRepository;
|
||||
_mailService = mailService;
|
||||
_paymentService = paymentService;
|
||||
_logger = logger;
|
||||
}
|
||||
private readonly BillingSettings _billingSettings;
|
||||
private readonly PayPalIpnClient _paypalIpnClient;
|
||||
private readonly ITransactionRepository _transactionRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly ILogger<PayPalController> _logger;
|
||||
|
||||
[HttpPost("ipn")]
|
||||
public async Task<IActionResult> PostIpn()
|
||||
{
|
||||
_logger.LogDebug("PayPal webhook has been hit.");
|
||||
if (HttpContext?.Request?.Query == null)
|
||||
public PayPalController(
|
||||
IOptions<BillingSettings> billingSettings,
|
||||
PayPalIpnClient paypalIpnClient,
|
||||
ITransactionRepository transactionRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IUserRepository userRepository,
|
||||
IMailService mailService,
|
||||
IPaymentService paymentService,
|
||||
ILogger<PayPalController> logger)
|
||||
{
|
||||
return new BadRequestResult();
|
||||
_billingSettings = billingSettings?.Value;
|
||||
_paypalIpnClient = paypalIpnClient;
|
||||
_transactionRepository = transactionRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_userRepository = userRepository;
|
||||
_mailService = mailService;
|
||||
_paymentService = paymentService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
var key = HttpContext.Request.Query.ContainsKey("key") ?
|
||||
HttpContext.Request.Query["key"].ToString() : null;
|
||||
if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.PayPal.WebhookKey))
|
||||
[HttpPost("ipn")]
|
||||
public async Task<IActionResult> PostIpn()
|
||||
{
|
||||
_logger.LogWarning("PayPal webhook key is incorrect or does not exist.");
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
string body = null;
|
||||
using (var reader = new StreamReader(HttpContext.Request.Body, Encoding.UTF8))
|
||||
{
|
||||
body = await reader.ReadToEndAsync();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
var verified = await _paypalIpnClient.VerifyIpnAsync(body);
|
||||
if (!verified)
|
||||
{
|
||||
_logger.LogWarning("Unverified IPN received.");
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
var ipnTransaction = new PayPalIpnClient.IpnTransaction(body);
|
||||
if (ipnTransaction.TxnType != "web_accept" && ipnTransaction.TxnType != "merch_pmt" &&
|
||||
ipnTransaction.PaymentStatus != "Refunded")
|
||||
{
|
||||
// Only processing billing agreement payments, buy now button payments, and refunds for now.
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
if (ipnTransaction.ReceiverId != _billingSettings.PayPal.BusinessId)
|
||||
{
|
||||
_logger.LogWarning("Receiver was not proper business id. " + ipnTransaction.ReceiverId);
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
if (ipnTransaction.PaymentStatus == "Refunded" && ipnTransaction.ParentTxnId == null)
|
||||
{
|
||||
// Refunds require parent transaction
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
if (ipnTransaction.PaymentType == "echeck" && ipnTransaction.PaymentStatus != "Refunded")
|
||||
{
|
||||
// Not accepting eChecks, unless it is a refund
|
||||
_logger.LogWarning("Got an eCheck payment. " + ipnTransaction.TxnId);
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
if (ipnTransaction.McCurrency != "USD")
|
||||
{
|
||||
// Only process USD payments
|
||||
_logger.LogWarning("Received a payment not in USD. " + ipnTransaction.TxnId);
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
var ids = ipnTransaction.GetIdsFromCustom();
|
||||
if (!ids.Item1.HasValue && !ids.Item2.HasValue)
|
||||
{
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
if (ipnTransaction.PaymentStatus == "Completed")
|
||||
{
|
||||
var transaction = await _transactionRepository.GetByGatewayIdAsync(
|
||||
GatewayType.PayPal, ipnTransaction.TxnId);
|
||||
if (transaction != null)
|
||||
_logger.LogDebug("PayPal webhook has been hit.");
|
||||
if (HttpContext?.Request?.Query == null)
|
||||
{
|
||||
_logger.LogWarning("Already processed this completed transaction. #" + ipnTransaction.TxnId);
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
var isAccountCredit = ipnTransaction.IsAccountCredit();
|
||||
try
|
||||
{
|
||||
var tx = new Transaction
|
||||
{
|
||||
Amount = ipnTransaction.McGross,
|
||||
CreationDate = ipnTransaction.PaymentDate,
|
||||
OrganizationId = ids.Item1,
|
||||
UserId = ids.Item2,
|
||||
Type = isAccountCredit ? TransactionType.Credit : TransactionType.Charge,
|
||||
Gateway = GatewayType.PayPal,
|
||||
GatewayId = ipnTransaction.TxnId,
|
||||
PaymentMethodType = PaymentMethodType.PayPal,
|
||||
Details = ipnTransaction.TxnId
|
||||
};
|
||||
await _transactionRepository.CreateAsync(tx);
|
||||
|
||||
if (isAccountCredit)
|
||||
{
|
||||
string billingEmail = null;
|
||||
if (tx.OrganizationId.HasValue)
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(tx.OrganizationId.Value);
|
||||
if (org != null)
|
||||
{
|
||||
billingEmail = org.BillingEmailAddress();
|
||||
if (await _paymentService.CreditAccountAsync(org, tx.Amount))
|
||||
{
|
||||
await _organizationRepository.ReplaceAsync(org);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(tx.UserId.Value);
|
||||
if (user != null)
|
||||
{
|
||||
billingEmail = user.BillingEmailAddress();
|
||||
if (await _paymentService.CreditAccountAsync(user, tx.Amount))
|
||||
{
|
||||
await _userRepository.ReplaceAsync(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(billingEmail))
|
||||
{
|
||||
await _mailService.SendAddedCreditAsync(billingEmail, tx.Amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Catch foreign key violations because user/org could have been deleted.
|
||||
catch (SqlException e) when (e.Number == 547) { }
|
||||
}
|
||||
else if (ipnTransaction.PaymentStatus == "Refunded" || ipnTransaction.PaymentStatus == "Reversed")
|
||||
{
|
||||
var refundTransaction = await _transactionRepository.GetByGatewayIdAsync(
|
||||
GatewayType.PayPal, ipnTransaction.TxnId);
|
||||
if (refundTransaction != null)
|
||||
{
|
||||
_logger.LogWarning("Already processed this refunded transaction. #" + ipnTransaction.TxnId);
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
var parentTransaction = await _transactionRepository.GetByGatewayIdAsync(
|
||||
GatewayType.PayPal, ipnTransaction.ParentTxnId);
|
||||
if (parentTransaction == null)
|
||||
{
|
||||
_logger.LogWarning("Parent transaction was not found. " + ipnTransaction.TxnId);
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
var refundAmount = System.Math.Abs(ipnTransaction.McGross);
|
||||
var remainingAmount = parentTransaction.Amount -
|
||||
parentTransaction.RefundedAmount.GetValueOrDefault();
|
||||
if (refundAmount > 0 && !parentTransaction.Refunded.GetValueOrDefault() &&
|
||||
remainingAmount >= refundAmount)
|
||||
var key = HttpContext.Request.Query.ContainsKey("key") ?
|
||||
HttpContext.Request.Query["key"].ToString() : null;
|
||||
if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.PayPal.WebhookKey))
|
||||
{
|
||||
parentTransaction.RefundedAmount =
|
||||
parentTransaction.RefundedAmount.GetValueOrDefault() + refundAmount;
|
||||
if (parentTransaction.RefundedAmount == parentTransaction.Amount)
|
||||
_logger.LogWarning("PayPal webhook key is incorrect or does not exist.");
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
string body = null;
|
||||
using (var reader = new StreamReader(HttpContext.Request.Body, Encoding.UTF8))
|
||||
{
|
||||
body = await reader.ReadToEndAsync();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
var verified = await _paypalIpnClient.VerifyIpnAsync(body);
|
||||
if (!verified)
|
||||
{
|
||||
_logger.LogWarning("Unverified IPN received.");
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
var ipnTransaction = new PayPalIpnClient.IpnTransaction(body);
|
||||
if (ipnTransaction.TxnType != "web_accept" && ipnTransaction.TxnType != "merch_pmt" &&
|
||||
ipnTransaction.PaymentStatus != "Refunded")
|
||||
{
|
||||
// Only processing billing agreement payments, buy now button payments, and refunds for now.
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
if (ipnTransaction.ReceiverId != _billingSettings.PayPal.BusinessId)
|
||||
{
|
||||
_logger.LogWarning("Receiver was not proper business id. " + ipnTransaction.ReceiverId);
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
if (ipnTransaction.PaymentStatus == "Refunded" && ipnTransaction.ParentTxnId == null)
|
||||
{
|
||||
// Refunds require parent transaction
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
if (ipnTransaction.PaymentType == "echeck" && ipnTransaction.PaymentStatus != "Refunded")
|
||||
{
|
||||
// Not accepting eChecks, unless it is a refund
|
||||
_logger.LogWarning("Got an eCheck payment. " + ipnTransaction.TxnId);
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
if (ipnTransaction.McCurrency != "USD")
|
||||
{
|
||||
// Only process USD payments
|
||||
_logger.LogWarning("Received a payment not in USD. " + ipnTransaction.TxnId);
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
var ids = ipnTransaction.GetIdsFromCustom();
|
||||
if (!ids.Item1.HasValue && !ids.Item2.HasValue)
|
||||
{
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
if (ipnTransaction.PaymentStatus == "Completed")
|
||||
{
|
||||
var transaction = await _transactionRepository.GetByGatewayIdAsync(
|
||||
GatewayType.PayPal, ipnTransaction.TxnId);
|
||||
if (transaction != null)
|
||||
{
|
||||
parentTransaction.Refunded = true;
|
||||
_logger.LogWarning("Already processed this completed transaction. #" + ipnTransaction.TxnId);
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
await _transactionRepository.ReplaceAsync(parentTransaction);
|
||||
await _transactionRepository.CreateAsync(new Transaction
|
||||
var isAccountCredit = ipnTransaction.IsAccountCredit();
|
||||
try
|
||||
{
|
||||
Amount = refundAmount,
|
||||
CreationDate = ipnTransaction.PaymentDate,
|
||||
OrganizationId = ids.Item1,
|
||||
UserId = ids.Item2,
|
||||
Type = TransactionType.Refund,
|
||||
Gateway = GatewayType.PayPal,
|
||||
GatewayId = ipnTransaction.TxnId,
|
||||
PaymentMethodType = PaymentMethodType.PayPal,
|
||||
Details = ipnTransaction.TxnId
|
||||
});
|
||||
}
|
||||
}
|
||||
var tx = new Transaction
|
||||
{
|
||||
Amount = ipnTransaction.McGross,
|
||||
CreationDate = ipnTransaction.PaymentDate,
|
||||
OrganizationId = ids.Item1,
|
||||
UserId = ids.Item2,
|
||||
Type = isAccountCredit ? TransactionType.Credit : TransactionType.Charge,
|
||||
Gateway = GatewayType.PayPal,
|
||||
GatewayId = ipnTransaction.TxnId,
|
||||
PaymentMethodType = PaymentMethodType.PayPal,
|
||||
Details = ipnTransaction.TxnId
|
||||
};
|
||||
await _transactionRepository.CreateAsync(tx);
|
||||
|
||||
return new OkResult();
|
||||
if (isAccountCredit)
|
||||
{
|
||||
string billingEmail = null;
|
||||
if (tx.OrganizationId.HasValue)
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(tx.OrganizationId.Value);
|
||||
if (org != null)
|
||||
{
|
||||
billingEmail = org.BillingEmailAddress();
|
||||
if (await _paymentService.CreditAccountAsync(org, tx.Amount))
|
||||
{
|
||||
await _organizationRepository.ReplaceAsync(org);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(tx.UserId.Value);
|
||||
if (user != null)
|
||||
{
|
||||
billingEmail = user.BillingEmailAddress();
|
||||
if (await _paymentService.CreditAccountAsync(user, tx.Amount))
|
||||
{
|
||||
await _userRepository.ReplaceAsync(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(billingEmail))
|
||||
{
|
||||
await _mailService.SendAddedCreditAsync(billingEmail, tx.Amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Catch foreign key violations because user/org could have been deleted.
|
||||
catch (SqlException e) when (e.Number == 547) { }
|
||||
}
|
||||
else if (ipnTransaction.PaymentStatus == "Refunded" || ipnTransaction.PaymentStatus == "Reversed")
|
||||
{
|
||||
var refundTransaction = await _transactionRepository.GetByGatewayIdAsync(
|
||||
GatewayType.PayPal, ipnTransaction.TxnId);
|
||||
if (refundTransaction != null)
|
||||
{
|
||||
_logger.LogWarning("Already processed this refunded transaction. #" + ipnTransaction.TxnId);
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
var parentTransaction = await _transactionRepository.GetByGatewayIdAsync(
|
||||
GatewayType.PayPal, ipnTransaction.ParentTxnId);
|
||||
if (parentTransaction == null)
|
||||
{
|
||||
_logger.LogWarning("Parent transaction was not found. " + ipnTransaction.TxnId);
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
var refundAmount = System.Math.Abs(ipnTransaction.McGross);
|
||||
var remainingAmount = parentTransaction.Amount -
|
||||
parentTransaction.RefundedAmount.GetValueOrDefault();
|
||||
if (refundAmount > 0 && !parentTransaction.Refunded.GetValueOrDefault() &&
|
||||
remainingAmount >= refundAmount)
|
||||
{
|
||||
parentTransaction.RefundedAmount =
|
||||
parentTransaction.RefundedAmount.GetValueOrDefault() + refundAmount;
|
||||
if (parentTransaction.RefundedAmount == parentTransaction.Amount)
|
||||
{
|
||||
parentTransaction.Refunded = true;
|
||||
}
|
||||
|
||||
await _transactionRepository.ReplaceAsync(parentTransaction);
|
||||
await _transactionRepository.CreateAsync(new Transaction
|
||||
{
|
||||
Amount = refundAmount,
|
||||
CreationDate = ipnTransaction.PaymentDate,
|
||||
OrganizationId = ids.Item1,
|
||||
UserId = ids.Item2,
|
||||
Type = TransactionType.Refund,
|
||||
Gateway = GatewayType.PayPal,
|
||||
GatewayId = ipnTransaction.TxnId,
|
||||
PaymentMethodType = PaymentMethodType.PayPal,
|
||||
Details = ipnTransaction.TxnId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new OkResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user