1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-04 01:22:50 -05:00

Turn on file scoped namespaces (#2225)

This commit is contained in:
Justin Baur
2022-08-29 14:53:16 -04:00
committed by GitHub
parent 7c4521e0b4
commit 34fb4cca2a
1206 changed files with 73816 additions and 75022 deletions

View File

@ -4,59 +4,58 @@ using Bit.Core.Utilities;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace Bit.Billing.Controllers
{
[Route("apple")]
public class AppleController : Controller
{
private readonly BillingSettings _billingSettings;
private readonly ILogger<AppleController> _logger;
namespace Bit.Billing.Controllers;
public AppleController(
IOptions<BillingSettings> billingSettings,
ILogger<AppleController> logger)
[Route("apple")]
public class AppleController : Controller
{
private readonly BillingSettings _billingSettings;
private readonly ILogger<AppleController> _logger;
public AppleController(
IOptions<BillingSettings> billingSettings,
ILogger<AppleController> logger)
{
_billingSettings = billingSettings?.Value;
_logger = logger;
}
[HttpPost("iap")]
public async Task<IActionResult> PostIap()
{
if (HttpContext?.Request?.Query == null)
{
_billingSettings = billingSettings?.Value;
_logger = logger;
return new BadRequestResult();
}
[HttpPost("iap")]
public async Task<IActionResult> PostIap()
var key = HttpContext.Request.Query.ContainsKey("key") ?
HttpContext.Request.Query["key"].ToString() : null;
if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.AppleWebhookKey))
{
if (HttpContext?.Request?.Query == null)
{
return new BadRequestResult();
}
return new BadRequestResult();
}
var key = HttpContext.Request.Query.ContainsKey("key") ?
HttpContext.Request.Query["key"].ToString() : null;
if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.AppleWebhookKey))
{
return new BadRequestResult();
}
string body = null;
using (var reader = new StreamReader(HttpContext.Request.Body, Encoding.UTF8))
{
body = await reader.ReadToEndAsync();
}
string body = null;
using (var reader = new StreamReader(HttpContext.Request.Body, Encoding.UTF8))
{
body = await reader.ReadToEndAsync();
}
if (string.IsNullOrWhiteSpace(body))
{
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();
}
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();
}
}
}

View File

@ -9,200 +9,199 @@ using Bit.Core.Utilities;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace Bit.Billing.Controllers
{
[Route("bitpay")]
public class BitPayController : Controller
{
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;
namespace Bit.Billing.Controllers;
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
{
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)
{
_billingSettings = billingSettings?.Value;
_bitPayClient = bitPayClient;
_transactionRepository = transactionRepository;
_organizationRepository = organizationRepository;
_userRepository = userRepository;
_mailService = mailService;
_paymentService = paymentService;
_logger = logger;
}
[HttpPost("ipn")]
public async Task<IActionResult> PostIpn([FromBody] BitPayEventModel model, [FromQuery] string key)
{
if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.BitPayWebhookKey))
{
_billingSettings = billingSettings?.Value;
_bitPayClient = bitPayClient;
_transactionRepository = transactionRepository;
_organizationRepository = organizationRepository;
_userRepository = userRepository;
_mailService = mailService;
_paymentService = paymentService;
_logger = logger;
return new BadRequestResult();
}
if (model == null || string.IsNullOrWhiteSpace(model.Data?.Id) ||
string.IsNullOrWhiteSpace(model.Event?.Name))
{
return new BadRequestResult();
}
[HttpPost("ipn")]
public async Task<IActionResult> PostIpn([FromBody] BitPayEventModel model, [FromQuery] string key)
if (model.Event.Name != "invoice_confirmed")
{
if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.BitPayWebhookKey))
{
return new BadRequestResult();
}
if (model == null || string.IsNullOrWhiteSpace(model.Data?.Id) ||
string.IsNullOrWhiteSpace(model.Event?.Name))
{
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
{
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);
}
}
}
// Catch foreign key violations because user/org could have been deleted.
catch (SqlException e) when (e.Number == 547) { }
// Only processing confirmed invoice events for now.
return new OkResult();
}
private bool IsAccountCredit(BitPayLight.Models.Invoice.Invoice invoice)
var invoice = await _bitPayClient.GetInvoiceAsync(model.Data.Id);
if (invoice == null)
{
return invoice != null && invoice.PosData != null && invoice.PosData.Contains("accountCredit:1");
// Request forged...?
_logger.LogWarning("Invoice not found. #" + model.Data.Id);
return new BadRequestResult();
}
private DateTime GetTransactionDate(BitPayLight.Models.Invoice.Invoice invoice)
if (invoice.Status != "confirmed" && invoice.Status != "completed")
{
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);
_logger.LogWarning("Invoice status of '" + invoice.Status + "' is not acceptable. #" + invoice.Id);
return new BadRequestResult();
}
public Tuple<Guid?, Guid?> GetIdsFromPosData(BitPayLight.Models.Invoice.Invoice invoice)
if (invoice.Currency != "USD")
{
Guid? orgId = null;
Guid? userId = null;
// Only process USD payments
_logger.LogWarning("Non USD payment received. #" + invoice.Id);
return new OkResult();
}
if (invoice != null && !string.IsNullOrWhiteSpace(invoice.PosData) && invoice.PosData.Contains(":"))
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
{
var mainParts = invoice.PosData.Split(',');
foreach (var mainPart in mainParts)
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 parts = mainPart.Split(':');
if (parts.Length > 1 && Guid.TryParse(parts[1], out var id))
var org = await _organizationRepository.GetByIdAsync(tx.OrganizationId.Value);
if (org != null)
{
if (parts[0] == "userId")
billingEmail = org.BillingEmailAddress();
if (await _paymentService.CreditAccountAsync(org, tx.Amount))
{
userId = id;
}
else if (parts[0] == "organizationId")
{
orgId = id;
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);
}
}
}
}
return new Tuple<Guid?, Guid?>(orgId, userId);
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();
}
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);
}
}

View File

@ -8,166 +8,165 @@ using Bit.Core.Utilities;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace Bit.Billing.Controllers
namespace Bit.Billing.Controllers;
[Route("freshdesk")]
public class FreshdeskController : Controller
{
[Route("freshdesk")]
public class FreshdeskController : Controller
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)
{
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;
_billingSettings = billingSettings?.Value;
_userRepository = userRepository;
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
_logger = logger;
_globalSettings = globalSettings;
_httpClientFactory = httpClientFactory;
}
public FreshdeskController(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IOptions<BillingSettings> billingSettings,
ILogger<FreshdeskController> logger,
GlobalSettings globalSettings,
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))
{
_billingSettings = billingSettings?.Value;
_userRepository = userRepository;
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
_logger = logger;
_globalSettings = globalSettings;
_httpClientFactory = httpClientFactory;
return new BadRequestResult();
}
[HttpPost("webhook")]
public async Task<IActionResult> PostWebhook([FromQuery, Required] string key,
[FromBody, Required] FreshdeskWebhookModel model)
try
{
if (string.IsNullOrWhiteSpace(key) || !CoreHelpers.FixedTimeEquals(key, _billingSettings.FreshdeskWebhookKey))
var ticketId = model.TicketId;
var ticketContactEmail = model.TicketContactEmail;
var ticketTags = model.TicketTags;
if (string.IsNullOrWhiteSpace(ticketId) || string.IsNullOrWhiteSpace(ticketContactEmail))
{
return new BadRequestResult();
}
try
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 ticketId = model.TicketId;
var ticketContactEmail = model.TicketContactEmail;
var ticketTags = model.TicketTags;
if (string.IsNullOrWhiteSpace(ticketId) || string.IsNullOrWhiteSpace(ticketContactEmail))
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)
{
return new BadRequestResult();
tags.Add("Premium");
}
var orgs = await _organizationRepository.GetManyByUserIdAsync(user.Id);
foreach (var org in orgs)
{
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"))
{
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));
}
}
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);
}
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)
if (customFields.Any())
{
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)
{
tags.Add("Premium");
}
var orgs = await _organizationRepository.GetManyByUserIdAsync(user.Id);
foreach (var org in orgs)
{
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"))
{
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));
}
}
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);
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);
return new OkResult();
}
catch (Exception e)
{
_logger.LogError(e, "Error processing freshdesk webhook.");
return new BadRequestResult();
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();
}
private async Task<HttpResponseMessage> CallFreshdeskApiAsync(HttpRequestMessage request, int retriedCount = 0)
catch (Exception e)
{
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)
{
return response;
}
}
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>();
_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)
{
return response;
}
}
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>();
}
}

View File

@ -7,229 +7,228 @@ using Bit.Core.Utilities;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace Bit.Billing.Controllers
namespace Bit.Billing.Controllers;
[Route("freshsales")]
public class FreshsalesController : Controller
{
[Route("freshsales")]
public class FreshsalesController : Controller
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)
{
private readonly IUserRepository _userRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly ILogger _logger;
private readonly GlobalSettings _globalSettings;
_userRepository = userRepository;
_organizationRepository = organizationRepository;
_logger = logger;
_globalSettings = globalSettings;
private readonly string _freshsalesApiKey;
private readonly HttpClient _httpClient;
public FreshsalesController(IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOptions<BillingSettings> billingSettings,
ILogger<FreshsalesController> logger,
GlobalSettings globalSettings)
_httpClient = new HttpClient
{
_userRepository = userRepository;
_organizationRepository = organizationRepository;
_logger = logger;
_globalSettings = globalSettings;
BaseAddress = new Uri("https://bitwarden.freshsales.io/api/")
};
_httpClient = new HttpClient
{
BaseAddress = new Uri("https://bitwarden.freshsales.io/api/")
};
_freshsalesApiKey = billingSettings.Value.FreshsalesApiKey;
_freshsalesApiKey = billingSettings.Value.FreshsalesApiKey;
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Token",
$"token={_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();
}
[HttpPost("webhook")]
public async Task<IActionResult> PostWebhook([FromHeader(Name = "Authorization")] string key,
[FromBody] CustomWebhookRequestModel request,
CancellationToken cancellationToken)
try
{
if (string.IsNullOrWhiteSpace(key) || !CoreHelpers.FixedTimeEquals(_freshsalesApiKey, key))
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)
{
return Unauthorized();
return BadRequest(new { Message = "Lead has not primary email." });
}
try
var user = await _userRepository.GetByEmailAsync(primaryEmail.Value);
if (user == null)
{
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)
{
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();
}
catch (Exception ex)
var newTags = new HashSet<string>();
if (user.Premium)
{
Console.WriteLine(ex);
_logger.LogError(ex, "Error processing freshsales webhook");
return BadRequest(new { ex.Message });
newTags.Add("Premium");
}
}
private static LeadWrapper<T> CreateWrapper<T>(T lead)
{
return new LeadWrapper<T>
var noteItems = new List<string>
{
Lead = lead,
$"User, {user.Email}: {_globalSettings.BaseServiceUri.Admin}/users/edit/{user.Id}"
};
}
private static CreateNoteRequestModel CreateNoteRequestModel(long leadId, string content)
{
return new CreateNoteRequestModel
var orgs = await _organizationRepository.GetManyByUserIdAsync(user.Id);
foreach (var org in orgs)
{
Note = new EditNoteModel
noteItems.Add($"Org, {org.Name}: {_globalSettings.BaseServiceUri.Admin}/organizations/edit/{org.Id}");
if (TryGetPlanName(org.PlanType, out var planName))
{
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;
newTags.Add($"Org: {planName}");
}
}
}
}
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)
{
return new LeadWrapper<TItem>
if (newTags.Any())
{
Lead = lead,
};
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();
}
catch (Exception ex)
{
Console.WriteLine(ex);
_logger.LogError(ex, "Error processing freshsales webhook");
return BadRequest(new { ex.Message });
}
}
public class FreshsalesLeadModel
private static LeadWrapper<T> CreateWrapper<T>(T lead)
{
public string[] Tags { get; set; }
public FreshsalesEmailModel[] Emails { get; set; }
return new LeadWrapper<T>
{
Lead = lead,
};
}
public class FreshsalesEmailModel
private static CreateNoteRequestModel CreateNoteRequestModel(long leadId, string content)
{
[JsonPropertyName("value")]
public string Value { get; set; }
[JsonPropertyName("is_primary")]
public bool IsPrimary { get; set; }
return new CreateNoteRequestModel
{
Note = new EditNoteModel
{
Description = content,
TargetableType = "Lead",
TargetableId = leadId,
},
};
}
public class CreateNoteRequestModel
private static bool TryGetPlanName(PlanType planType, out string planName)
{
[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; }
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)
{
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; }
}

View File

@ -1,21 +1,20 @@
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Billing.Controllers
{
public class InfoController : Controller
{
[HttpGet("~/alive")]
[HttpGet("~/now")]
public DateTime GetAlive()
{
return DateTime.UtcNow;
}
namespace Bit.Billing.Controllers;
[HttpGet("~/version")]
public JsonResult GetVersion()
{
return Json(CoreHelpers.GetVersion());
}
public class InfoController : Controller
{
[HttpGet("~/alive")]
[HttpGet("~/now")]
public DateTime GetAlive()
{
return DateTime.UtcNow;
}
[HttpGet("~/version")]
public JsonResult GetVersion()
{
return Json(CoreHelpers.GetVersion());
}
}

View File

@ -1,54 +1,53 @@
using Microsoft.AspNetCore.Mvc;
namespace Billing.Controllers
namespace Billing.Controllers;
public class LoginController : Controller
{
public class LoginController : Controller
/*
private readonly PasswordlessSignInManager<IdentityUser> _signInManager;
public LoginController(
PasswordlessSignInManager<IdentityUser> signInManager)
{
/*
private readonly PasswordlessSignInManager<IdentityUser> _signInManager;
public LoginController(
PasswordlessSignInManager<IdentityUser> signInManager)
{
_signInManager = signInManager;
}
public IActionResult Index()
{
return View();
}
[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");
}
*/
_signInManager = signInManager;
}
public IActionResult Index()
{
return View();
}
[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");
}
*/
}

View File

@ -9,227 +9,226 @@ using Bit.Core.Utilities;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace Bit.Billing.Controllers
{
[Route("paypal")]
public class PayPalController : Controller
{
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;
namespace Bit.Billing.Controllers;
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
{
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)
{
_billingSettings = billingSettings?.Value;
_paypalIpnClient = paypalIpnClient;
_transactionRepository = transactionRepository;
_organizationRepository = organizationRepository;
_userRepository = userRepository;
_mailService = mailService;
_paymentService = paymentService;
_logger = logger;
}
[HttpPost("ipn")]
public async Task<IActionResult> PostIpn()
{
_logger.LogDebug("PayPal webhook has been hit.");
if (HttpContext?.Request?.Query == null)
{
_billingSettings = billingSettings?.Value;
_paypalIpnClient = paypalIpnClient;
_transactionRepository = transactionRepository;
_organizationRepository = organizationRepository;
_userRepository = userRepository;
_mailService = mailService;
_paymentService = paymentService;
_logger = logger;
return new BadRequestResult();
}
[HttpPost("ipn")]
public async Task<IActionResult> PostIpn()
var key = HttpContext.Request.Query.ContainsKey("key") ?
HttpContext.Request.Query["key"].ToString() : null;
if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.PayPal.WebhookKey))
{
_logger.LogDebug("PayPal webhook has been hit.");
if (HttpContext?.Request?.Query == null)
{
return new BadRequestResult();
}
_logger.LogWarning("PayPal webhook key is incorrect or does not exist.");
return new BadRequestResult();
}
var key = HttpContext.Request.Query.ContainsKey("key") ?
HttpContext.Request.Query["key"].ToString() : null;
if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.PayPal.WebhookKey))
{
_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();
}
string body = null;
using (var reader = new StreamReader(HttpContext.Request.Body, Encoding.UTF8))
{
body = await reader.ReadToEndAsync();
}
if (string.IsNullOrWhiteSpace(body))
{
return new BadRequestResult();
}
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.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)
{
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
});
}
}
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.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)
{
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