1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-02 00: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

@ -1,23 +1,22 @@
namespace Bit.Billing
{
public class BillingSettings
{
public virtual string JobsKey { get; set; }
public virtual string StripeWebhookKey { get; set; }
public virtual string StripeWebhookSecret { get; set; }
public virtual bool StripeEventParseThrowMismatch { get; set; } = true;
public virtual string BitPayWebhookKey { get; set; }
public virtual string AppleWebhookKey { get; set; }
public virtual string FreshdeskWebhookKey { get; set; }
public virtual string FreshdeskApiKey { get; set; }
public virtual string FreshsalesApiKey { get; set; }
public virtual PayPalSettings PayPal { get; set; } = new PayPalSettings();
namespace Bit.Billing;
public class PayPalSettings
{
public virtual bool Production { get; set; }
public virtual string BusinessId { get; set; }
public virtual string WebhookKey { get; set; }
}
public class BillingSettings
{
public virtual string JobsKey { get; set; }
public virtual string StripeWebhookKey { get; set; }
public virtual string StripeWebhookSecret { get; set; }
public virtual bool StripeEventParseThrowMismatch { get; set; } = true;
public virtual string BitPayWebhookKey { get; set; }
public virtual string AppleWebhookKey { get; set; }
public virtual string FreshdeskWebhookKey { get; set; }
public virtual string FreshdeskApiKey { get; set; }
public virtual string FreshsalesApiKey { get; set; }
public virtual PayPalSettings PayPal { get; set; } = new PayPalSettings();
public class PayPalSettings
{
public virtual bool Production { get; set; }
public virtual string BusinessId { get; set; }
public virtual string WebhookKey { get; set; }
}
}

View File

@ -1,14 +1,13 @@
namespace Bit.Billing.Constants
namespace Bit.Billing.Constants;
public static class HandledStripeWebhook
{
public static class HandledStripeWebhook
{
public static string SubscriptionDeleted => "customer.subscription.deleted";
public static string SubscriptionUpdated => "customer.subscription.updated";
public static string UpcomingInvoice => "invoice.upcoming";
public static string ChargeSucceeded => "charge.succeeded";
public static string ChargeRefunded => "charge.refunded";
public static string PaymentSucceeded => "invoice.payment_succeeded";
public static string PaymentFailed => "invoice.payment_failed";
public static string InvoiceCreated => "invoice.created";
}
public static string SubscriptionDeleted => "customer.subscription.deleted";
public static string SubscriptionUpdated => "customer.subscription.updated";
public static string UpcomingInvoice => "invoice.upcoming";
public static string ChargeSucceeded => "charge.succeeded";
public static string ChargeRefunded => "charge.refunded";
public static string PaymentSucceeded => "invoice.payment_succeeded";
public static string PaymentFailed => "invoice.payment_failed";
public static string InvoiceCreated => "invoice.created";
}

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

View File

@ -3,43 +3,42 @@ using Bit.Core.Jobs;
using Bit.Core.Settings;
using Quartz;
namespace Bit.Billing.Jobs
namespace Bit.Billing.Jobs;
public class JobsHostedService : BaseJobsHostedService
{
public class JobsHostedService : BaseJobsHostedService
public JobsHostedService(
GlobalSettings globalSettings,
IServiceProvider serviceProvider,
ILogger<JobsHostedService> logger,
ILogger<JobListener> listenerLogger)
: base(globalSettings, serviceProvider, logger, listenerLogger) { }
public override async Task StartAsync(CancellationToken cancellationToken)
{
public JobsHostedService(
GlobalSettings globalSettings,
IServiceProvider serviceProvider,
ILogger<JobsHostedService> logger,
ILogger<JobListener> listenerLogger)
: base(globalSettings, serviceProvider, logger, listenerLogger) { }
public override async Task StartAsync(CancellationToken cancellationToken)
var timeZone = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time") :
TimeZoneInfo.FindSystemTimeZoneById("America/New_York");
if (_globalSettings.SelfHosted)
{
var timeZone = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time") :
TimeZoneInfo.FindSystemTimeZoneById("America/New_York");
if (_globalSettings.SelfHosted)
{
timeZone = TimeZoneInfo.Local;
}
var everyDayAtNinePmTrigger = TriggerBuilder.Create()
.WithIdentity("EveryDayAtNinePmTrigger")
.StartNow()
.WithCronSchedule("0 0 21 * * ?", x => x.InTimeZone(timeZone))
.Build();
Jobs = new List<Tuple<Type, ITrigger>>();
// Add jobs here
await base.StartAsync(cancellationToken);
timeZone = TimeZoneInfo.Local;
}
public static void AddJobsServices(IServiceCollection services)
{
// Register jobs here
}
var everyDayAtNinePmTrigger = TriggerBuilder.Create()
.WithIdentity("EveryDayAtNinePmTrigger")
.StartNow()
.WithCronSchedule("0 0 21 * * ?", x => x.InTimeZone(timeZone))
.Build();
Jobs = new List<Tuple<Type, ITrigger>>();
// Add jobs here
await base.StartAsync(cancellationToken);
}
public static void AddJobsServices(IServiceCollection services)
{
// Register jobs here
}
}

View File

@ -1,28 +1,27 @@
namespace Bit.Billing.Models
namespace Bit.Billing.Models;
public class BitPayEventModel
{
public class BitPayEventModel
public EventModel Event { get; set; }
public InvoiceDataModel Data { get; set; }
public class EventModel
{
public EventModel Event { get; set; }
public InvoiceDataModel Data { get; set; }
public int Code { get; set; }
public string Name { get; set; }
}
public class EventModel
{
public int Code { get; set; }
public string Name { get; set; }
}
public class InvoiceDataModel
{
public string Id { get; set; }
public string Url { get; set; }
public string Status { get; set; }
public string Currency { get; set; }
public decimal Price { get; set; }
public string PosData { get; set; }
public bool ExceptionStatus { get; set; }
public long CurrentTime { get; set; }
public long AmountPaid { get; set; }
public string TransactionCurrency { get; set; }
}
public class InvoiceDataModel
{
public string Id { get; set; }
public string Url { get; set; }
public string Status { get; set; }
public string Currency { get; set; }
public decimal Price { get; set; }
public string PosData { get; set; }
public bool ExceptionStatus { get; set; }
public long CurrentTime { get; set; }
public long AmountPaid { get; set; }
public string TransactionCurrency { get; set; }
}
}

View File

@ -1,16 +1,15 @@
using System.Text.Json.Serialization;
namespace Bit.Billing.Models
namespace Bit.Billing.Models;
public class FreshdeskWebhookModel
{
public class FreshdeskWebhookModel
{
[JsonPropertyName("ticket_id")]
public string TicketId { get; set; }
[JsonPropertyName("ticket_id")]
public string TicketId { get; set; }
[JsonPropertyName("ticket_contact_email")]
public string TicketContactEmail { get; set; }
[JsonPropertyName("ticket_contact_email")]
public string TicketContactEmail { get; set; }
[JsonPropertyName("ticket_tags")]
public string TicketTags { get; set; }
}
[JsonPropertyName("ticket_tags")]
public string TicketTags { get; set; }
}

View File

@ -1,11 +1,10 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Billing.Models
namespace Bit.Billing.Models;
public class LoginModel
{
public class LoginModel
{
[Required]
[EmailAddress]
public string Email { get; set; }
}
[Required]
[EmailAddress]
public string Email { get; set; }
}

View File

@ -1,39 +1,38 @@
using Bit.Core.Utilities;
using Serilog.Events;
namespace Bit.Billing
namespace Bit.Billing;
public class Program
{
public class Program
public static void Main(string[] args)
{
public static void Main(string[] args)
{
Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.ConfigureLogging((hostingContext, logging) =>
logging.AddSerilog(hostingContext, e =>
Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.ConfigureLogging((hostingContext, logging) =>
logging.AddSerilog(hostingContext, e =>
{
var context = e.Properties["SourceContext"].ToString();
if (e.Level == LogEventLevel.Information &&
(context.StartsWith("\"Bit.Billing.Jobs") || context.StartsWith("\"Bit.Core.Jobs")))
{
var context = e.Properties["SourceContext"].ToString();
if (e.Level == LogEventLevel.Information &&
(context.StartsWith("\"Bit.Billing.Jobs") || context.StartsWith("\"Bit.Core.Jobs")))
{
return true;
}
return true;
}
if (e.Properties.ContainsKey("RequestPath") &&
!string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) &&
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
{
return false;
}
if (e.Properties.ContainsKey("RequestPath") &&
!string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) &&
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
{
return false;
}
return e.Level >= LogEventLevel.Warning;
}));
})
.Build()
.Run();
}
return e.Level >= LogEventLevel.Warning;
}));
})
.Build()
.Run();
}
}

View File

@ -6,94 +6,93 @@ using Bit.SharedWeb.Utilities;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Stripe;
namespace Bit.Billing
namespace Bit.Billing;
public class Startup
{
public class Startup
public Startup(IWebHostEnvironment env, IConfiguration configuration)
{
public Startup(IWebHostEnvironment env, IConfiguration configuration)
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
Configuration = configuration;
Environment = env;
}
public IConfiguration Configuration { get; }
public IWebHostEnvironment Environment { get; set; }
public void ConfigureServices(IServiceCollection services)
{
// Options
services.AddOptions();
// Settings
var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);
services.Configure<BillingSettings>(Configuration.GetSection("BillingSettings"));
// Stripe Billing
StripeConfiguration.ApiKey = globalSettings.Stripe.ApiKey;
StripeConfiguration.MaxNetworkRetries = globalSettings.Stripe.MaxNetworkRetries;
// Repositories
services.AddSqlServerRepositories(globalSettings);
// PayPal Client
services.AddSingleton<Utilities.PayPalIpnClient>();
// BitPay Client
services.AddSingleton<BitPayClient>();
// Context
services.AddScoped<ICurrentContext, CurrentContext>();
// Identity
services.AddCustomIdentityServices(globalSettings);
//services.AddPasswordlessIdentityServices<ReadOnlyDatabaseIdentityUserStore>(globalSettings);
// Services
services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings);
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// Mvc
services.AddMvc(config =>
{
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
Configuration = configuration;
Environment = env;
config.Filters.Add(new LoggingExceptionHandlerFilterAttribute());
});
services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
// Authentication
services.AddAuthentication();
// Jobs service, uncomment when we have some jobs to run
// Jobs.JobsHostedService.AddJobsServices(services);
// services.AddHostedService<Jobs.JobsHostedService>();
// Set up HttpClients
services.AddHttpClient("FreshdeskApi");
}
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
IHostApplicationLifetime appLifetime,
GlobalSettings globalSettings)
{
app.UseSerilog(env, appLifetime, globalSettings);
// Add general security headers
app.UseMiddleware<SecurityHeadersMiddleware>();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
public IConfiguration Configuration { get; }
public IWebHostEnvironment Environment { get; set; }
public void ConfigureServices(IServiceCollection services)
{
// Options
services.AddOptions();
// Settings
var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);
services.Configure<BillingSettings>(Configuration.GetSection("BillingSettings"));
// Stripe Billing
StripeConfiguration.ApiKey = globalSettings.Stripe.ApiKey;
StripeConfiguration.MaxNetworkRetries = globalSettings.Stripe.MaxNetworkRetries;
// Repositories
services.AddSqlServerRepositories(globalSettings);
// PayPal Client
services.AddSingleton<Utilities.PayPalIpnClient>();
// BitPay Client
services.AddSingleton<BitPayClient>();
// Context
services.AddScoped<ICurrentContext, CurrentContext>();
// Identity
services.AddCustomIdentityServices(globalSettings);
//services.AddPasswordlessIdentityServices<ReadOnlyDatabaseIdentityUserStore>(globalSettings);
// Services
services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings);
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// Mvc
services.AddMvc(config =>
{
config.Filters.Add(new LoggingExceptionHandlerFilterAttribute());
});
services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
// Authentication
services.AddAuthentication();
// Jobs service, uncomment when we have some jobs to run
// Jobs.JobsHostedService.AddJobsServices(services);
// services.AddHostedService<Jobs.JobsHostedService>();
// Set up HttpClients
services.AddHttpClient("FreshdeskApi");
}
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
IHostApplicationLifetime appLifetime,
GlobalSettings globalSettings)
{
app.UseSerilog(env, appLifetime, globalSettings);
// Add general security headers
app.UseMiddleware<SecurityHeadersMiddleware>();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
}
}

View File

@ -4,171 +4,170 @@ using System.Text;
using System.Web;
using Microsoft.Extensions.Options;
namespace Bit.Billing.Utilities
namespace Bit.Billing.Utilities;
public class PayPalIpnClient
{
public class PayPalIpnClient
private readonly HttpClient _httpClient = new HttpClient();
private readonly Uri _ipnUri;
public PayPalIpnClient(IOptions<BillingSettings> billingSettings)
{
private readonly HttpClient _httpClient = new HttpClient();
private readonly Uri _ipnUri;
var bSettings = billingSettings?.Value;
_ipnUri = new Uri(bSettings.PayPal.Production ? "https://www.paypal.com/cgi-bin/webscr" :
"https://www.sandbox.paypal.com/cgi-bin/webscr");
}
public PayPalIpnClient(IOptions<BillingSettings> billingSettings)
public async Task<bool> VerifyIpnAsync(string ipnBody)
{
if (ipnBody == null)
{
var bSettings = billingSettings?.Value;
_ipnUri = new Uri(bSettings.PayPal.Production ? "https://www.paypal.com/cgi-bin/webscr" :
"https://www.sandbox.paypal.com/cgi-bin/webscr");
throw new ArgumentException("No IPN body.");
}
public async Task<bool> VerifyIpnAsync(string ipnBody)
var request = new HttpRequestMessage
{
if (ipnBody == null)
Method = HttpMethod.Post,
RequestUri = _ipnUri
};
var cmdIpnBody = string.Concat("cmd=_notify-validate&", ipnBody);
request.Content = new StringContent(cmdIpnBody, Encoding.UTF8, "application/x-www-form-urlencoded");
var response = await _httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
throw new Exception("Failed to verify IPN, status: " + response.StatusCode);
}
var responseContent = await response.Content.ReadAsStringAsync();
if (responseContent.Equals("VERIFIED"))
{
return true;
}
else if (responseContent.Equals("INVALID"))
{
return false;
}
else
{
throw new Exception("Failed to verify IPN.");
}
}
public class IpnTransaction
{
private string[] _dateFormats = new string[]
{
"HH:mm:ss dd MMM yyyy PDT", "HH:mm:ss dd MMM yyyy PST", "HH:mm:ss dd MMM, yyyy PST",
"HH:mm:ss dd MMM, yyyy PDT","HH:mm:ss MMM dd, yyyy PST", "HH:mm:ss MMM dd, yyyy PDT"
};
public IpnTransaction(string ipnFormData)
{
if (string.IsNullOrWhiteSpace(ipnFormData))
{
throw new ArgumentException("No IPN body.");
return;
}
var request = new HttpRequestMessage
var qsData = HttpUtility.ParseQueryString(ipnFormData);
var dataDict = qsData.Keys.Cast<string>().ToDictionary(k => k, v => qsData[v].ToString());
TxnId = GetDictValue(dataDict, "txn_id");
TxnType = GetDictValue(dataDict, "txn_type");
ParentTxnId = GetDictValue(dataDict, "parent_txn_id");
PaymentStatus = GetDictValue(dataDict, "payment_status");
PaymentType = GetDictValue(dataDict, "payment_type");
McCurrency = GetDictValue(dataDict, "mc_currency");
Custom = GetDictValue(dataDict, "custom");
ItemName = GetDictValue(dataDict, "item_name");
ItemNumber = GetDictValue(dataDict, "item_number");
PayerId = GetDictValue(dataDict, "payer_id");
PayerEmail = GetDictValue(dataDict, "payer_email");
ReceiverId = GetDictValue(dataDict, "receiver_id");
ReceiverEmail = GetDictValue(dataDict, "receiver_email");
PaymentDate = ConvertDate(GetDictValue(dataDict, "payment_date"));
var mcGrossString = GetDictValue(dataDict, "mc_gross");
if (!string.IsNullOrWhiteSpace(mcGrossString) && decimal.TryParse(mcGrossString, out var mcGross))
{
Method = HttpMethod.Post,
RequestUri = _ipnUri
};
var cmdIpnBody = string.Concat("cmd=_notify-validate&", ipnBody);
request.Content = new StringContent(cmdIpnBody, Encoding.UTF8, "application/x-www-form-urlencoded");
var response = await _httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
throw new Exception("Failed to verify IPN, status: " + response.StatusCode);
McGross = mcGross;
}
var responseContent = await response.Content.ReadAsStringAsync();
if (responseContent.Equals("VERIFIED"))
var mcFeeString = GetDictValue(dataDict, "mc_fee");
if (!string.IsNullOrWhiteSpace(mcFeeString) && decimal.TryParse(mcFeeString, out var mcFee))
{
return true;
}
else if (responseContent.Equals("INVALID"))
{
return false;
}
else
{
throw new Exception("Failed to verify IPN.");
McFee = mcFee;
}
}
public class IpnTransaction
public string TxnId { get; set; }
public string TxnType { get; set; }
public string ParentTxnId { get; set; }
public string PaymentStatus { get; set; }
public string PaymentType { get; set; }
public decimal McGross { get; set; }
public decimal McFee { get; set; }
public string McCurrency { get; set; }
public string Custom { get; set; }
public string ItemName { get; set; }
public string ItemNumber { get; set; }
public string PayerId { get; set; }
public string PayerEmail { get; set; }
public string ReceiverId { get; set; }
public string ReceiverEmail { get; set; }
public DateTime PaymentDate { get; set; }
public Tuple<Guid?, Guid?> GetIdsFromCustom()
{
private string[] _dateFormats = new string[]
Guid? orgId = null;
Guid? userId = null;
if (!string.IsNullOrWhiteSpace(Custom) && Custom.Contains(":"))
{
"HH:mm:ss dd MMM yyyy PDT", "HH:mm:ss dd MMM yyyy PST", "HH:mm:ss dd MMM, yyyy PST",
"HH:mm:ss dd MMM, yyyy PDT","HH:mm:ss MMM dd, yyyy PST", "HH:mm:ss MMM dd, yyyy PDT"
};
public IpnTransaction(string ipnFormData)
{
if (string.IsNullOrWhiteSpace(ipnFormData))
var mainParts = Custom.Split(',');
foreach (var mainPart in mainParts)
{
return;
}
var qsData = HttpUtility.ParseQueryString(ipnFormData);
var dataDict = qsData.Keys.Cast<string>().ToDictionary(k => k, v => qsData[v].ToString());
TxnId = GetDictValue(dataDict, "txn_id");
TxnType = GetDictValue(dataDict, "txn_type");
ParentTxnId = GetDictValue(dataDict, "parent_txn_id");
PaymentStatus = GetDictValue(dataDict, "payment_status");
PaymentType = GetDictValue(dataDict, "payment_type");
McCurrency = GetDictValue(dataDict, "mc_currency");
Custom = GetDictValue(dataDict, "custom");
ItemName = GetDictValue(dataDict, "item_name");
ItemNumber = GetDictValue(dataDict, "item_number");
PayerId = GetDictValue(dataDict, "payer_id");
PayerEmail = GetDictValue(dataDict, "payer_email");
ReceiverId = GetDictValue(dataDict, "receiver_id");
ReceiverEmail = GetDictValue(dataDict, "receiver_email");
PaymentDate = ConvertDate(GetDictValue(dataDict, "payment_date"));
var mcGrossString = GetDictValue(dataDict, "mc_gross");
if (!string.IsNullOrWhiteSpace(mcGrossString) && decimal.TryParse(mcGrossString, out var mcGross))
{
McGross = mcGross;
}
var mcFeeString = GetDictValue(dataDict, "mc_fee");
if (!string.IsNullOrWhiteSpace(mcFeeString) && decimal.TryParse(mcFeeString, out var mcFee))
{
McFee = mcFee;
}
}
public string TxnId { get; set; }
public string TxnType { get; set; }
public string ParentTxnId { get; set; }
public string PaymentStatus { get; set; }
public string PaymentType { get; set; }
public decimal McGross { get; set; }
public decimal McFee { get; set; }
public string McCurrency { get; set; }
public string Custom { get; set; }
public string ItemName { get; set; }
public string ItemNumber { get; set; }
public string PayerId { get; set; }
public string PayerEmail { get; set; }
public string ReceiverId { get; set; }
public string ReceiverEmail { get; set; }
public DateTime PaymentDate { get; set; }
public Tuple<Guid?, Guid?> GetIdsFromCustom()
{
Guid? orgId = null;
Guid? userId = null;
if (!string.IsNullOrWhiteSpace(Custom) && Custom.Contains(":"))
{
var mainParts = Custom.Split(',');
foreach (var mainPart in mainParts)
var parts = mainPart.Split(':');
if (parts.Length > 1 && Guid.TryParse(parts[1], out var id))
{
var parts = mainPart.Split(':');
if (parts.Length > 1 && Guid.TryParse(parts[1], out var id))
if (parts[0] == "user_id")
{
if (parts[0] == "user_id")
{
userId = id;
}
else if (parts[0] == "organization_id")
{
orgId = id;
}
userId = id;
}
else if (parts[0] == "organization_id")
{
orgId = id;
}
}
}
return new Tuple<Guid?, Guid?>(orgId, userId);
}
public bool IsAccountCredit()
{
return !string.IsNullOrWhiteSpace(Custom) && Custom.Contains("account_credit:1");
}
return new Tuple<Guid?, Guid?>(orgId, userId);
}
private string GetDictValue(IDictionary<string, string> dict, string key)
{
return dict.ContainsKey(key) ? dict[key] : null;
}
public bool IsAccountCredit()
{
return !string.IsNullOrWhiteSpace(Custom) && Custom.Contains("account_credit:1");
}
private DateTime ConvertDate(string dateString)
private string GetDictValue(IDictionary<string, string> dict, string key)
{
return dict.ContainsKey(key) ? dict[key] : null;
}
private DateTime ConvertDate(string dateString)
{
if (!string.IsNullOrWhiteSpace(dateString))
{
if (!string.IsNullOrWhiteSpace(dateString))
var parsed = DateTime.TryParseExact(dateString, _dateFormats,
CultureInfo.InvariantCulture, DateTimeStyles.None, out var paymentDate);
if (parsed)
{
var parsed = DateTime.TryParseExact(dateString, _dateFormats,
CultureInfo.InvariantCulture, DateTimeStyles.None, out var paymentDate);
if (parsed)
{
var pacificTime = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time") :
TimeZoneInfo.FindSystemTimeZoneById("America/Los_Angeles");
return TimeZoneInfo.ConvertTimeToUtc(paymentDate, pacificTime);
}
var pacificTime = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time") :
TimeZoneInfo.FindSystemTimeZoneById("America/Los_Angeles");
return TimeZoneInfo.ConvertTimeToUtc(paymentDate, pacificTime);
}
return default(DateTime);
}
return default(DateTime);
}
}
}