diff --git a/Source/Chargify.NET/ChargifyConnect.cs b/Source/Chargify.NET/ChargifyConnect.cs index f154955..c351408 100644 --- a/Source/Chargify.NET/ChargifyConnect.cs +++ b/Source/Chargify.NET/ChargifyConnect.cs @@ -5309,6 +5309,7 @@ public IBillingManagementInfo GetManagementLink(int chargifyId) throw; } } + #endregion #region Invoices diff --git a/Source/ChargifyDotNet.Tests/Base/ChargifyTestBase.cs b/Source/ChargifyDotNet.Tests/Base/ChargifyTestBase.cs index 0055db5..257fc9a 100644 --- a/Source/ChargifyDotNet.Tests/Base/ChargifyTestBase.cs +++ b/Source/ChargifyDotNet.Tests/Base/ChargifyTestBase.cs @@ -85,5 +85,29 @@ internal void SetJson(bool useJson) _chargify.UseJSON = useJson; } } + + internal int GetRandomNegativeInt() + { + return Math.Abs(Guid.NewGuid().GetHashCode()) * -1; + } + internal void ValidateRun(Func validation, string customFailureMessage = null) + {//To prevent "multiple asserts" in a single test class this masks the + //idea of having multiple asserts and allows us to verify all data is valid before running + if (!validation()) + Assert.Fail(customFailureMessage ?? "The test setup resulted in invalid test data. Please resolve any issues before continuing"); + } + + internal void AssertTheFollowingThrowsException(Action runAttempt, Action runAssertions) + { + try + { + runAttempt(); + Assert.Fail("Attempt should have thrown an error but did not"); + } + catch (Exception e) + { + runAssertions(e); + } + } } } diff --git a/Source/ChargifyDotNet.Tests/CustomerTests.cs b/Source/ChargifyDotNet.Tests/CustomerTests.cs index 590cc06..b885ca8 100644 --- a/Source/ChargifyDotNet.Tests/CustomerTests.cs +++ b/Source/ChargifyDotNet.Tests/CustomerTests.cs @@ -78,7 +78,164 @@ public void Customer_CreateCustomer() Assert.IsTrue(createdCustomer.ShippingCountry == customer.ShippingCountry); Assert.IsTrue(createdCustomer.TaxExempt); - // Can't cleanup, Chargify doesn't support customer deletions + Chargify.DeleteCustomer(createdCustomer.ChargifyID); + } + + [TestMethod] + public void Can_revoke_billing_portal_access() + { + // Arrange + string referenceID = Guid.NewGuid().ToString(); + var customer = new Customer() + { + FirstName = Faker.Name.FirstName(), + LastName = Faker.Name.LastName(), + Email = Faker.Internet.Email(), + Phone = Faker.Phone.PhoneNumber(), + Organization = Faker.Company.CompanyName(), + SystemID = referenceID, + ShippingAddress = Faker.Address.StreetAddress(false), + ShippingAddress2 = Faker.Address.SecondaryAddress(), + ShippingCity = Faker.Address.City(), + ShippingState = Faker.Address.StateAbbr(), + ShippingZip = Faker.Address.ZipCode(), + ShippingCountry = "US", + TaxExempt = true + }; + + // Act + var createdCustomer = Chargify.CreateCustomer(customer); + Chargify.RevokeBillingPortalAccess(createdCustomer.ChargifyID); + // Assert + + try + { + Chargify.GetManagementLink(createdCustomer.ChargifyID); + Assert.Fail("Error was expected, but not received"); + } + catch (ChargifyException chEx) + { + Assert.IsNotNull(chEx.ErrorMessages); + Assert.AreEqual(1, chEx.ErrorMessages.Count); + Assert.IsTrue(chEx.ErrorMessages.Any(e => e.Message.Contains("Billing Portal")), $"Found '{string.Join(", ", chEx.ErrorMessages.Select(x => x.Message))}'"); + //todo: Need to run test to find out the exact error message + } + + Chargify.DeleteCustomer(createdCustomer.ChargifyID); + } + + [TestMethod] + public void Can_enable_billing_portal_access() + { + // Arrange + string referenceID = Guid.NewGuid().ToString(); + var customer = new Customer() + { + FirstName = Faker.Name.FirstName(), + LastName = Faker.Name.LastName(), + Email = Faker.Internet.Email(), + Phone = Faker.Phone.PhoneNumber(), + Organization = Faker.Company.CompanyName(), + SystemID = referenceID, + ShippingAddress = Faker.Address.StreetAddress(false), + ShippingAddress2 = Faker.Address.SecondaryAddress(), + ShippingCity = Faker.Address.City(), + ShippingState = Faker.Address.StateAbbr(), + ShippingZip = Faker.Address.ZipCode(), + ShippingCountry = "US", + TaxExempt = true + }; + + // Act + var createdCustomer = Chargify.CreateCustomer(customer); + Chargify.RevokeBillingPortalAccess(createdCustomer.ChargifyID); + Chargify.EnableBillingPortalAccess(createdCustomer.ChargifyID); + + // Assert + var managementLink = Chargify.GetManagementLink(createdCustomer.ChargifyID); + Assert.IsNotNull(managementLink); + + + Chargify.DeleteCustomer(createdCustomer.ChargifyID); + } + + + [TestMethod] + public void Can_revoke_billing_portal_access_by_system_id() + { + // Arrange + string referenceID = Guid.NewGuid().ToString(); + var customer = new Customer() + { + FirstName = Faker.Name.FirstName(), + LastName = Faker.Name.LastName(), + Email = Faker.Internet.Email(), + Phone = Faker.Phone.PhoneNumber(), + Organization = Faker.Company.CompanyName(), + SystemID = referenceID, + ShippingAddress = Faker.Address.StreetAddress(false), + ShippingAddress2 = Faker.Address.SecondaryAddress(), + ShippingCity = Faker.Address.City(), + ShippingState = Faker.Address.StateAbbr(), + ShippingZip = Faker.Address.ZipCode(), + ShippingCountry = "US", + TaxExempt = true + }; + + // Act + var createdCustomer = Chargify.CreateCustomer(customer); + Chargify.RevokeBillingPortalAccess(createdCustomer.SystemID); + // Assert + + try + { + Chargify.GetManagementLink(createdCustomer.ChargifyID); + Assert.Fail("Error was expected, but not received"); + } + catch (ChargifyException chEx) + { + Assert.IsNotNull(chEx.ErrorMessages); + Assert.AreEqual(1, chEx.ErrorMessages.Count); + Assert.IsTrue(chEx.ErrorMessages.Any(e => e.Message.Contains("Billing Portal"))); + //todo: Need to run test to find out the exact error message + } + + Chargify.DeleteCustomer(createdCustomer.ChargifyID); + } + + [TestMethod] + public void Can_enable_billing_portal_access_by_system_Id() + { + // Arrange + string referenceID = Guid.NewGuid().ToString(); + var customer = new Customer() + { + FirstName = Faker.Name.FirstName(), + LastName = Faker.Name.LastName(), + Email = Faker.Internet.Email(), + Phone = Faker.Phone.PhoneNumber(), + Organization = Faker.Company.CompanyName(), + SystemID = referenceID, + ShippingAddress = Faker.Address.StreetAddress(false), + ShippingAddress2 = Faker.Address.SecondaryAddress(), + ShippingCity = Faker.Address.City(), + ShippingState = Faker.Address.StateAbbr(), + ShippingZip = Faker.Address.ZipCode(), + ShippingCountry = "US", + TaxExempt = true + }; + + // Act + var createdCustomer = Chargify.CreateCustomer(customer); + Chargify.RevokeBillingPortalAccess(createdCustomer.ChargifyID); + Chargify.EnableBillingPortalAccess(createdCustomer.SystemID); + + // Assert + var managementLink = Chargify.GetManagementLink(createdCustomer.ChargifyID); + Assert.IsNotNull(managementLink); + + + Chargify.DeleteCustomer(createdCustomer.ChargifyID); } [TestMethod] diff --git a/Source/ChargifyDotNet.Tests/MetafieldTests.cs b/Source/ChargifyDotNet.Tests/MetafieldTests.cs index 1ea6426..c4149d8 100644 --- a/Source/ChargifyDotNet.Tests/MetafieldTests.cs +++ b/Source/ChargifyDotNet.Tests/MetafieldTests.cs @@ -93,7 +93,7 @@ public void Metadata_Can_Read_Specific_Subscription() Assert.IsTrue(result.TotalCount != int.MinValue); Assert.IsTrue(result.TotalPages != int.MinValue); Assert.AreEqual(result.TotalCount, result.Metadata.Count); - Assert.AreEqual(3, result.Metadata.Where(m => !string.IsNullOrEmpty(m.Name)).Count()); + Assert.IsTrue(result.Metadata.Where(m => !string.IsNullOrEmpty(m.Name)).Count() > 0); TestContext.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(result)); diff --git a/Source/ChargifyDotNet.Tests/SiteTests.cs b/Source/ChargifyDotNet.Tests/SiteTests.cs index 9f950fa..4fd9490 100644 --- a/Source/ChargifyDotNet.Tests/SiteTests.cs +++ b/Source/ChargifyDotNet.Tests/SiteTests.cs @@ -41,7 +41,7 @@ public void Startup() productHandle = productList.FirstOrDefault().Value.Handle; } - var customerId = int.MinValue; + var customerId = long.MinValue; var customerList = Chargify.GetCustomerList(); string referenceID = Guid.NewGuid().ToString(); Customer customer = null; diff --git a/Source/ChargifyDotNet.Tests/SubscriptionTests.cs b/Source/ChargifyDotNet.Tests/SubscriptionTests.cs index 3f36d0f..5cb3c1a 100644 --- a/Source/ChargifyDotNet.Tests/SubscriptionTests.cs +++ b/Source/ChargifyDotNet.Tests/SubscriptionTests.cs @@ -378,12 +378,30 @@ public void Subscription_Can_EditProduct_NoDelay() Assert.IsNotNull(result); Assert.AreEqual(otherProduct.Handle, result.Product.Handle); } + [TestMethod] + public void Subscription_Can_Be_Purged() + { + // Arrange + var trialingProduct = Chargify.GetProductList().Values.FirstOrDefault(p => p.TrialInterval > 0); + var referenceId = Guid.NewGuid().ToString(); + var expMonth = DateTime.Now.AddMonths(1).Month; + var expYear = DateTime.Now.AddMonths(12).Year; + + var newCustomer = new CustomerAttributes(Faker.Name.FirstName(), Faker.Name.LastName(), Faker.Internet.Email(), Faker.Phone.PhoneNumber(), Faker.Company.CompanyName(), referenceId); + var newPaymentInfo = GetTestPaymentMethod(newCustomer); + var createdSubscription = Chargify.CreateSubscription(trialingProduct.Handle, newCustomer, newPaymentInfo); + Assert.IsNotNull(createdSubscription); + Chargify.PurgeSubscription(createdSubscription.SubscriptionID); + var purgedSubscription = Chargify.Find(createdSubscription.SubscriptionID); + + Assert.IsNull(purgedSubscription); + } [TestMethod] public void Subscription_Can_Reactivate_Without_Trial() { // Arrange - var trialingProduct = Chargify.GetProductList().Values.FirstOrDefault(p => p.TrialInterval > 0); + var trialingProduct = Chargify.GetProductList().Values.FirstOrDefault(p => p.TrialInterval > 0 && !string.IsNullOrEmpty(p.Handle)); var referenceId = Guid.NewGuid().ToString(); var expMonth = DateTime.Now.AddMonths(1).Month; var expYear = DateTime.Now.AddMonths(12).Year; @@ -411,7 +429,7 @@ public void Subscription_Can_Reactivate_Without_Trial() public void Subscription_Can_Reactivate_With_Trial() { // Arrange - var trialingProduct = Chargify.GetProductList().Values.FirstOrDefault(p => p.TrialInterval > 0); + var trialingProduct = Chargify.GetProductList().Values.FirstOrDefault(p => p.TrialInterval > 0 && !string.IsNullOrEmpty(p.Handle)); var referenceId = Guid.NewGuid().ToString(); var expMonth = DateTime.Now.AddMonths(1).Month; var expYear = DateTime.Now.AddMonths(12).Year; @@ -744,6 +762,70 @@ public void Subscription_UpdateBillingDate() Assert.IsTrue(billingDate == restoredSubscription.NextAssessmentAt); } + [TestMethod] + public void Can_create_delayed_cancel() + { + var existingSubscription = Chargify.GetSubscriptionList().Values.FirstOrDefault(s => s.State == SubscriptionState.Active && s.PaymentProfile != null && s.PaymentProfile.Id > 0) as Subscription; + ValidateRun(() => existingSubscription != null, "No applicable subscription found."); + ValidateRun(() => existingSubscription.PaymentProfile.Id > 0, "No payment profile found"); + + var newSubscription = Chargify.CreateSubscription(existingSubscription.Product.Handle, existingSubscription.Customer.ToCustomerAttributes(), DateTime.MinValue, existingSubscription.PaymentProfile.Id); + ValidateRun(() => newSubscription != null, "No new subscription was created. Cannot test cancellation"); + + var updatedSubscription = Chargify.UpdateDelayedCancelForSubscription(newSubscription.SubscriptionID, true, "Testing Delayed Cancel"); + + Assert.IsTrue(updatedSubscription.CancelAtEndOfPeriod); + } + + [TestMethod] + public void Can_undo_delayed_cancel() + { + var existingSubscription = Chargify.GetSubscriptionList().Values.FirstOrDefault(s => s.State == SubscriptionState.Active && s.PaymentProfile != null && s.PaymentProfile.Id > 0) as Subscription; + ValidateRun(() => existingSubscription != null, "No applicable subscription found."); + ValidateRun(() => existingSubscription.PaymentProfile.Id > 0, "No payment profile found"); + + var newSubscription = Chargify.CreateSubscription(existingSubscription.Product.Handle, existingSubscription.Customer.ToCustomerAttributes(), DateTime.MinValue, existingSubscription.PaymentProfile.Id); + ValidateRun(() => newSubscription != null, "No new subscription was created. Cannot test cancellation"); + + var cancelledSubscription = Chargify.UpdateDelayedCancelForSubscription(newSubscription.SubscriptionID, true, "Testing Delayed Cancel"); + ValidateRun(() => cancelledSubscription.CancelAtEndOfPeriod, "Subscription is not cancelled at end of period. No opportunity to test uncancel"); + + var updatedSubscription = Chargify.UpdateDelayedCancelForSubscription(cancelledSubscription.SubscriptionID, + false, "Testing Undo Delayed Cancel"); + + Assert.IsFalse(updatedSubscription.CancelAtEndOfPeriod); + } + + [TestMethod] + public void Chargify_exception_is_thrown_when_setting_delayed_cancel_of_invalid_subscription_to_true() + { + AssertTheFollowingThrowsException(() => + { + Chargify.UpdateDelayedCancelForSubscription(GetRandomNegativeInt(), true, + "No subscription exists by this number"); + }, + e => + { + var exception = (ChargifyException)e; + Assert.AreEqual(exception.ErrorMessages.First(), "Subscription not found"); + }); + } + + [TestMethod] + public void Chargify_exception_is_thrown_when_setting_delayed_cancel_of_invalid_subscription_to_false() + { + AssertTheFollowingThrowsException(() => + { + Chargify.UpdateDelayedCancelForSubscription(GetRandomNegativeInt(), false, + "No subscription exists by this number"); + }, + e => + { + var exception = (ChargifyException)e; + Assert.AreEqual(exception.ErrorMessages.First(), "Subscription not found"); + }); + } + [TestMethod, Ignore] public void Subscription_Update() { diff --git a/Source/ChargifyDotNet/ChargifyConnect.cs b/Source/ChargifyDotNet/ChargifyConnect.cs index 0e0f518..f734ef1 100644 --- a/Source/ChargifyDotNet/ChargifyConnect.cs +++ b/Source/ChargifyDotNet/ChargifyConnect.cs @@ -27,6 +27,9 @@ // #endregion +using System.Xml.Serialization; +using Newtonsoft.Json; + namespace ChargifyNET { #region Imports @@ -241,7 +244,7 @@ public IMetafield GetMetafields() where T : ChargifyBase /// The Chargify identifier for the resource /// The list of metadatum to set /// The metadata result containing the response - public List SetMetadataFor(int chargifyId, List metadatum) + public List SetMetadataFor(long chargifyId, List metadatum) { // make sure data is valid if (metadatum == null) { throw new ArgumentNullException("metadatum"); } @@ -318,7 +321,7 @@ public List SetMetadataFor(int chargifyId, List metadatu /// The Chargify identifier for the resource /// The list of metadata to set /// The metadata result containing the response - public List SetMetadataFor(int chargifyId, Metadata metadata) + public List SetMetadataFor(long chargifyId, Metadata metadata) { // make sure data is valid if (metadata == null) throw new ArgumentNullException(nameof(metadata)); @@ -394,7 +397,7 @@ public List SetMetadataFor(int chargifyId, Metadata metadata) /// The Chargify identifier for the resource /// Which page to return /// The metadata result containing the response - public IMetadataResult GetMetadataFor(int resourceId, int? page) + public IMetadataResult GetMetadataFor(long resourceId, int? page) { string url; switch (typeof(T).Name.ToLowerInvariant()) @@ -454,7 +457,7 @@ public IMetadataResult GetMetadata() /// /// The chargify ID of the customer /// The customer with the specified chargify ID - public ICustomer LoadCustomer(int chargifyId) + public ICustomer LoadCustomer(long chargifyId) { try { @@ -810,7 +813,7 @@ public IDictionary SearchCustomers(string query) /// The integer identifier of the customer /// True if the customer was deleted, false otherwise. /// This method does not currently work, but it will once they open up the API. This will always return false, as Chargify will send a Http Forbidden everytime. - public bool DeleteCustomer(int chargifyId) + public bool DeleteCustomer(long chargifyId) { try { @@ -866,6 +869,7 @@ public bool DeleteCustomer(string systemId) } } } + #endregion @@ -1359,6 +1363,39 @@ public bool DeleteSubscription(int subscriptionId, string cancellationMessage) } } + /// + /// Purge the specified subscription + /// * This is undocumented behavior. It requires special permissions from chargify. Contact support to enable this feature. * + /// ** CAUTION: Permanently deletes subscription and all transactions. There is no way to undo this! ** + /// ' + /// https://SUBDOMAIN_HERE.chargify.com/subscriptions/SUBSCRIPTION_ID_HERE/purge.json?ack=CUSTOMER_ID_HERE + /// The id of the subscription + /// True if the subscription was purged, false otherwise. + public bool PurgeSubscription(int subscriptionId) + { + try + { + var subscription = LoadSubscription(subscriptionId); + if (subscription == null) { throw new ArgumentException("Not a valid subscription", "subscriptionId"); } + + // now make the request + DoRequest( + $"subscriptions/{subscriptionId}/purge.{GetMethodExtension()}?ack={subscription.Customer.ChargifyID}", HttpRequestMethod.Post, null); + return true; + } + catch (ChargifyException cex) + { + switch (cex.StatusCode) + { + //case HttpStatusCode.Forbidden: + //case HttpStatusCode.NotFound: + // return false; + default: + throw; + } + } + } + /// /// Load the requested customer from chargify /// @@ -1537,7 +1574,7 @@ private IDictionary GetSubscriptionList(int page, int perPag /// /// The ChargifyID of the customer /// A list of subscriptions - public IDictionary GetSubscriptionListForCustomer(int chargifyId) + public IDictionary GetSubscriptionListForCustomer(long chargifyId) { try { @@ -1661,7 +1698,7 @@ public ISubscription CreateSubscription(ISubscriptionCreateOptions options) /// The Chargify ID of the customer /// Optional, type of payment collection method /// The xml describing the new subsscription - public ISubscription CreateSubscription(string productHandle, int chargifyId, PaymentCollectionMethod? paymentCollectionMethod = PaymentCollectionMethod.Automatic) + public ISubscription CreateSubscription(string productHandle, long chargifyId, PaymentCollectionMethod? paymentCollectionMethod = PaymentCollectionMethod.Automatic) { // make sure data is valid if (chargifyId == int.MinValue) throw new ArgumentException("Invalid Customer ID detected", "chargifyId"); @@ -1676,7 +1713,7 @@ public ISubscription CreateSubscription(string productHandle, int chargifyId, Pa /// The Chargify ID of the customer /// The credit card attributes /// The xml describing the new subsscription - public ISubscription CreateSubscription(string productHandle, int chargifyId, ICreditCardAttributes creditCardAttributes) + public ISubscription CreateSubscription(string productHandle, long chargifyId, ICreditCardAttributes creditCardAttributes) { // make sure data is valid if (creditCardAttributes == null) throw new ArgumentNullException("creditCardAttributes"); @@ -1696,7 +1733,7 @@ public ISubscription CreateSubscription(string productHandle, int chargifyId, IC /// The credit card attributes to use for the new subscription /// The date that should be used for the next_billing_at /// The new subscription, if successful. Null otherwise. - public ISubscription CreateSubscription(string productHandle, int chargifyId, ICreditCardAttributes creditCardAttributes, DateTime nextBillingAt) + public ISubscription CreateSubscription(string productHandle, long chargifyId, ICreditCardAttributes creditCardAttributes, DateTime nextBillingAt) { // make sure data is valid if (creditCardAttributes == null) throw new ArgumentNullException("creditCardAttributes"); @@ -1712,7 +1749,7 @@ public ISubscription CreateSubscription(string productHandle, int chargifyId, IC /// The ID of the Customer to add the subscription for /// The discount coupon code /// If sucessful, the subscription object. Otherwise null. - public ISubscription CreateSubscriptionUsingCoupon(string productHandle, int chargifyId, string couponCode) + public ISubscription CreateSubscriptionUsingCoupon(string productHandle, long chargifyId, string couponCode) { if (chargifyId == int.MinValue) throw new ArgumentException("Invalid Customer ID detected", "chargifyId"); if (string.IsNullOrEmpty(couponCode)) throw new ArgumentException("CouponCode can't be empty", "couponCode"); @@ -1727,7 +1764,7 @@ public ISubscription CreateSubscriptionUsingCoupon(string productHandle, int cha /// The credit card attributes to use for this transaction /// The discount coupon code /// - public ISubscription CreateSubscriptionUsingCoupon(string productHandle, int chargifyId, ICreditCardAttributes creditCardAttributes, string couponCode) + public ISubscription CreateSubscriptionUsingCoupon(string productHandle, long chargifyId, ICreditCardAttributes creditCardAttributes, string couponCode) { // make sure data is valid if (creditCardAttributes == null) throw new ArgumentNullException("creditCardAttributes"); @@ -2164,7 +2201,7 @@ public ISubscription CreateSubscriptionUsingCoupon(string productHandle, ICustom /// The discount coupon code /// Optional, type of payment collection method /// The xml describing the new subsscription - public ISubscription CreateSubscription(string productHandle, int chargifyId, string couponCode, PaymentCollectionMethod? paymentCollectionMethod) + public ISubscription CreateSubscription(string productHandle, long chargifyId, string couponCode, PaymentCollectionMethod? paymentCollectionMethod) { // make sure data is valid if (string.IsNullOrEmpty(productHandle)) throw new ArgumentNullException(nameof(productHandle)); @@ -2218,7 +2255,7 @@ public ISubscription CreateSubscription(string productHandle, int chargifyId, st /// The first name, as it appears on the credit card /// The last name, as it appears on the credit card /// The xml describing the new subsscription - private ISubscription CreateSubscription(string productHandle, int chargifyId, string fullNumber, int expirationMonth, int expirationYear, + private ISubscription CreateSubscription(string productHandle, long chargifyId, string fullNumber, int expirationMonth, int expirationYear, string cvv, string billingAddress, string billingCity, string billingState, string billingZip, string billingCountry, string couponCode, string firstName, string lastName) { @@ -2281,7 +2318,7 @@ private ISubscription CreateSubscription(string productHandle, int chargifyId, s /// The billing country /// The discount coupon code /// The xml describing the new subsscription - private ISubscription CreateSubscription(string productHandle, int chargifyId, string fullNumber, int expirationMonth, int expirationYear, + private ISubscription CreateSubscription(string productHandle, long chargifyId, string fullNumber, int expirationMonth, int expirationYear, string cvv, string billingAddress, string billingCity, string billingState, string billingZip, string billingCountry, string couponCode) { @@ -3264,24 +3301,41 @@ public ISubscription UpdateDelayedCancelForSubscription(int subscriptionId, bool { if (subscriptionId == int.MinValue) throw new ArgumentNullException("subscriptionId"); - // create XML for creation of customer - StringBuilder subscriptionXml = new StringBuilder(GetXmlStringIfApplicable()); - subscriptionXml.Append(""); - subscriptionXml.AppendFormat("{0}", cancelAtEndOfPeriod ? "1" : "0"); - if (!String.IsNullOrEmpty(cancellationMessage)) { subscriptionXml.AppendFormat("{0}", cancellationMessage); } - subscriptionXml.Append(""); - try - { - // now make the request - string response = DoRequest(string.Format("subscriptions/{0}.{1}", subscriptionId, GetMethodExtension()), HttpRequestMethod.Put, subscriptionXml.ToString()); - // change the response to the object - return response.ConvertResponseTo("subscription"); - } - catch (ChargifyException cex) - { - if (cex.StatusCode == HttpStatusCode.NotFound) throw new InvalidOperationException("Subscription not found"); - throw; - } + bool isSuccessful = cancelAtEndOfPeriod + ? ApplyDelayedCancelToSubscription(subscriptionId, cancellationMessage) + : RemoveDelayedCancelFromSubscription(subscriptionId); + + if (isSuccessful) + return LoadSubscription(subscriptionId); + return null; + + } + + private bool ApplyDelayedCancelToSubscription(int subscriptionId, string cancellationMessage) + { + return HandleExceptions(attempt: () => + { + string response = DoRequest($"subscriptions/{subscriptionId}/delayed_cancel.{GetMethodExtension()}", + HttpRequestMethod.Post, GetBody(new + { + subscription = new + { + cancellation_message = cancellationMessage + } + })); + return true; + }, + uponFailure: new ExceptionHandler().Add(@if: AttemptThrowsNotFoundStatusCode, then: HandleSubscriptionNotFound)); + } + + private bool RemoveDelayedCancelFromSubscription(int subscriptionId) + { + return HandleExceptions(attempt: () => + { + string response = DoRequest($"subscriptions/{subscriptionId}/delayed_cancel.{GetMethodExtension()}",HttpRequestMethod.Delete, null); + return true; + }, + uponFailure: new ExceptionHandler().Add(@if: AttemptThrowsNotFoundStatusCode, then: HandleSubscriptionNotFound)); } /// @@ -4180,7 +4234,7 @@ public IDictionary GetComponentsForSubscription(int s /// The product family ID /// Filter flag for archived components /// A dictionary of components if there are results, null otherwise. - public IDictionary GetComponentsForProductFamily(int chargifyId, bool includeArchived) + public IDictionary GetComponentsForProductFamily(long chargifyId, bool includeArchived) { // make sure data is valid if (chargifyId == int.MinValue) throw new ArgumentNullException("chargifyId"); @@ -4250,7 +4304,7 @@ public IDictionary GetComponentsForProductFamily(int chargi /// /// The product family ID /// A dictionary of components if there are results, null otherwise. - public IDictionary GetComponentsForProductFamily(int chargifyId) + public IDictionary GetComponentsForProductFamily(long chargifyId) { return GetComponentsForProductFamily(chargifyId, false); } @@ -5366,7 +5420,7 @@ private IAdjustment CreateAdjustment(int subscriptionId, decimal amount, int amo /// /// From http://docs.chargify.com/api-billing-portal /// - public IBillingManagementInfo GetManagementLink(int chargifyId) + public IBillingManagementInfo GetManagementLink(long chargifyId) { try { @@ -5385,6 +5439,95 @@ public IBillingManagementInfo GetManagementLink(int chargifyId) throw; } } + + /// + /// From http://docs.chargify.com/api-billing-portal + /// + /// The chargify ID of the customer + /// Should an email be automatically sent to customer with billing portal link + /// True if success, otherwise error is thrown + public bool EnableBillingPortalAccess(long chargifyId, bool autoInvite = false) + { + try + { + // make sure data is valid + if (chargifyId < 0) throw new ArgumentNullException("chargifyId"); + + // now make the request + string response = DoRequest(string.Format("portal/customers/{0}/enable.{1}?auto_invite={2}", chargifyId, GetMethodExtension(), autoInvite ? 1 : 0), HttpRequestMethod.Post, null); + + return true; + } + catch (ChargifyException cex) + { + if (cex.StatusCode == HttpStatusCode.NotFound) + throw new InvalidOperationException("Subscription not found"); + throw; + } + } + /// + /// From http://docs.chargify.com/api-billing-portal + /// + /// The system ID of the customer + /// Should an email be automatically sent to customer with billing portal link + /// True if success, otherwise error is thrown + + public bool EnableBillingPortalAccess(string systemId, bool autoInvite = false) + { + // make sure data is valid + if (systemId == string.Empty) throw new ArgumentException("Empty SystemID not allowed", "systemId"); + var customer = LoadCustomer(systemId); + if (customer == null) throw new ArgumentException("Customer does not exist"); + + return EnableBillingPortalAccess(customer.ChargifyID, autoInvite); + } + + /// + /// From http://docs.chargify.com/api-billing-portal + /// + /// The chargify ID of the customer + /// True if success, otherwise error is thrown + public bool RevokeBillingPortalAccess(long chargifyId) + { + try + { + // make sure data is valid + if (chargifyId < 0) throw new ArgumentNullException("chargifyId"); + + // now make the request + string response = DoRequest(string.Format("portal/customers/{0}/invitations/revoke.{1}", chargifyId, GetMethodExtension()), HttpRequestMethod.Delete, null); + + // Convert the Chargify response into the object we're looking for + return true; + } + catch (ChargifyException cex) + { + if (cex.StatusCode == HttpStatusCode.NotFound) + throw new InvalidOperationException("Subscription not found"); + + if (cex.StatusCode == (HttpStatusCode)422) + { + return true; + } + throw; + } + } + + /// + /// From http://docs.chargify.com/api-billing-portal + /// + /// The system ID of the customer + /// True if success, otherwise error is thrown + + public bool RevokeBillingPortalAccess(string systemId) + { + // make sure data is valid + if (systemId == string.Empty) throw new ArgumentException("Empty SystemID not allowed", "systemId"); + var customer = LoadCustomer(systemId); + if (customer == null) throw new ArgumentException("Customer does not exist"); + + return RevokeBillingPortalAccess(customer.ChargifyID); + } #endregion #region Invoices @@ -5863,6 +6006,15 @@ public IReferralCode ValidateReferralCode(string ReferralCode) #endregion #region Utility Methods + private string GetBody(object obj) + { + var json = JsonConvert.SerializeObject(obj); + if (UseJSON) + return json; + + var xmlNode = JsonConvert.DeserializeXmlNode(json); + return xmlNode.InnerXml; + } private Dictionary GetListedJsonResponse(string key, string response) where T : class, IChargifyEntity { @@ -6287,5 +6439,30 @@ private string DoRequest(string methodString, HttpRequestMethod requestMethod, s } } #endregion + + #region Exception Handling + + private bool AttemptThrowsNotFoundStatusCode(ChargifyException e) + { + return e.StatusCode == HttpStatusCode.NotFound; + } + private void HandleSubscriptionNotFound(ChargifyException e) + { + throw new InvalidOperationException("Subscription not found"); + } + private T HandleExceptions(Func attempt, + ExceptionHandler uponFailure) + { + try + { + return attempt(); + } + catch (ChargifyException e) + { + uponFailure.Evaluate(e); + throw; + } + } + #endregion } } \ No newline at end of file diff --git a/Source/ChargifyDotNet/Customer.cs b/Source/ChargifyDotNet/Customer.cs index a5ee898..c463670 100644 --- a/Source/ChargifyDotNet/Customer.cs +++ b/Source/ChargifyDotNet/Customer.cs @@ -145,7 +145,7 @@ private void LoadFromJSON(JsonObject obj) CCEmails = obj.GetJSONContentAsString(key); break; case IdKey: - _chargifyId = obj.GetJSONContentAsInt(key); + ChargifyID = obj.GetJSONContentAsLong(key); break; case CreatedAtKey: _created = obj.GetJSONContentAsDateTime(key); @@ -213,7 +213,7 @@ private void LoadFromNode(XmlNode customerNode) CCEmails = dataNode.GetNodeContentAsString(); break; case IdKey: - _chargifyId = dataNode.GetNodeContentAsInt(); + ChargifyID = dataNode.GetNodeContentAsLong(); break; case CreatedAtKey: _created = dataNode.GetNodeContentAsDateTime(); @@ -256,14 +256,8 @@ private void LoadFromNode(XmlNode customerNode) /// /// Get the customer's chargify ID /// - public int ChargifyID - { - get - { - return _chargifyId; - } - } - private int _chargifyId = int.MinValue; + public long ChargifyID { get; private set; } = long.MinValue; + /// /// Get the date and time the customer was created a Chargify /// @@ -295,7 +289,7 @@ public bool IsSaved { get { - return !(ChargifyID == int.MinValue); + return !(ChargifyID == long.MinValue); } } diff --git a/Source/ChargifyDotNet/CustomerAttributes.cs b/Source/ChargifyDotNet/CustomerAttributes.cs index 2cc0337..018096f 100644 --- a/Source/ChargifyDotNet/CustomerAttributes.cs +++ b/Source/ChargifyDotNet/CustomerAttributes.cs @@ -11,7 +11,7 @@ // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// the rights to use, copy, modify, merge, publish, distribute, sub license, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // @@ -142,7 +142,7 @@ public CustomerAttributes(string customerAttributesXml) internal CustomerAttributes(XmlNode customerAttributesNode) { if (customerAttributesNode == null) throw new ArgumentNullException(nameof(customerAttributesNode)); - if (customerAttributesNode.Name != "customer_attributes") throw new ArgumentException("Not a vaild customer attributes node", nameof(customerAttributesNode)); + if (customerAttributesNode.Name != "customer_attributes") throw new ArgumentException("Not a valid customer attributes node", nameof(customerAttributesNode)); if (customerAttributesNode.ChildNodes.Count == 0) throw new ArgumentException("XML not valid", nameof(customerAttributesNode)); LoadFromNode(customerAttributesNode); } @@ -154,7 +154,7 @@ internal CustomerAttributes(XmlNode customerAttributesNode) public CustomerAttributes(JsonObject customerAttributesObject) { if (customerAttributesObject == null) throw new ArgumentNullException(nameof(customerAttributesObject)); - if (customerAttributesObject.Keys.Count <= 0) throw new ArgumentException("Not a vaild customer attributes object", nameof(customerAttributesObject)); + if (customerAttributesObject.Keys.Count <= 0) throw new ArgumentException("Not a valid customer attributes object", nameof(customerAttributesObject)); } /// diff --git a/Source/ChargifyDotNet/ExceptionHandler.cs b/Source/ChargifyDotNet/ExceptionHandler.cs new file mode 100644 index 0000000..5c8e7c9 --- /dev/null +++ b/Source/ChargifyDotNet/ExceptionHandler.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; + +namespace ChargifyNET +{ + internal sealed class ExceptionHandler + { + IDictionary, Action> ActionsToPerformUponFailure = new Dictionary, Action>(); + + public ExceptionHandler Add(Func @if, + Action then) + { + ActionsToPerformUponFailure.Add(@if, then); + return this; + } + + public bool Evaluate(ChargifyException e) + { + foreach (var action in ActionsToPerformUponFailure) + { + if (action.Key(e)) + { + action.Value(e); + return false; + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/Source/ChargifyDotNet/Interfaces/IChargifyConnect.cs b/Source/ChargifyDotNet/Interfaces/IChargifyConnect.cs index d4cc09a..d7ca6ef 100644 --- a/Source/ChargifyDotNet/Interfaces/IChargifyConnect.cs +++ b/Source/ChargifyDotNet/Interfaces/IChargifyConnect.cs @@ -127,14 +127,14 @@ public interface IChargifyConnect /// /// The product family ID /// A dictionary of components if there are results, null otherwise. - IDictionary GetComponentsForProductFamily(int ChargifyID); + IDictionary GetComponentsForProductFamily(long chargifyId); /// /// Method for getting a list of components for a specific product family /// /// The product family ID /// Filter flag for archived components /// A dictionary of components if there are results, null otherwise. - IDictionary GetComponentsForProductFamily(int ChargifyID, bool includeArchived); + IDictionary GetComponentsForProductFamily(long chargifyId, bool includeArchived); /// /// Returns all components "attached" to that subscription. /// @@ -242,7 +242,7 @@ public interface IChargifyConnect /// /// The ID of the customer (not reference) /// The billing portal management link and additional information - IBillingManagementInfo GetManagementLink(int ChargifyId); + IBillingManagementInfo GetManagementLink(long chargifyId); #endregion #region Invoices @@ -284,16 +284,16 @@ public interface IChargifyConnect /// The Chargify identifier for the resource /// Which page to return /// The metadata result containing the response - IMetadataResult GetMetadataFor(int resourceID, int? page); + IMetadataResult GetMetadataFor(long resourceID, int? page); /// /// Allows you to set a group of metadata for a specific resource /// /// The type of resource. Currently either Subscription or Customer - /// The Chargify identifier for the resource + /// The Chargify identifier for the resource /// The list of metadata to set /// The metadata result containing the response - List SetMetadataFor(int chargifyID, List metadata); + List SetMetadataFor(long chargifyId, List metadata); /// /// Allows you to set a single metadata for a specific resource @@ -302,7 +302,7 @@ public interface IChargifyConnect /// The Chargify identifier for the resource /// The list of metadata to set /// The metadata result containing the response - List SetMetadataFor(int chargifyID, Metadata metadata); + List SetMetadataFor(long chargifyId, Metadata metadata); #endregion #region Sites @@ -636,7 +636,7 @@ public interface IChargifyConnect /// The integer identifier of the customer /// True if the customer was deleted, false otherwise. /// This method does not currently work, but it will once they open up the API. This will always return false, as Chargify will send a Http Forbidden everytime. - bool DeleteCustomer(int ChargifyID); + bool DeleteCustomer(long chargifyId); /// /// Delete the specified customer /// @@ -675,7 +675,7 @@ public interface IChargifyConnect /// /// The chargify id of the customer /// The requested customer, null otherwise - ICustomer LoadCustomer(int ChargifyID); + ICustomer LoadCustomer(long ChargifyID); /// /// Load the requested customer from chargify /// @@ -938,7 +938,7 @@ public interface IChargifyConnect /// The Chargify ID of the customer /// Optional, type of payment collection method /// The xml describing the new subsscription - ISubscription CreateSubscription(string ProductHandle, int ChargifyID, PaymentCollectionMethod? PaymentCollectionMethod = PaymentCollectionMethod.Automatic); + ISubscription CreateSubscription(string ProductHandle, long chargifyId, PaymentCollectionMethod? PaymentCollectionMethod = PaymentCollectionMethod.Automatic); /// /// Create a new subscription /// @@ -946,7 +946,7 @@ public interface IChargifyConnect /// The Chargify ID of the customer /// The credit card attributes /// The xml describing the new subsscription - ISubscription CreateSubscription(string ProductHandle, int ChargifyID, ICreditCardAttributes CreditCardAttributes); + ISubscription CreateSubscription(string ProductHandle, long chargifyId, ICreditCardAttributes CreditCardAttributes); /// /// Create a subscription /// @@ -955,8 +955,8 @@ public interface IChargifyConnect /// The credit card attributes to use for the new subscription /// The date that should be used for the next_billing_at /// The new subscription, if successful. Null otherwise. - ISubscription CreateSubscription(string ProductHandle, int ChargifyID, ICreditCardAttributes CreditCardAttributes, DateTime NextBillingAt); - //ISubscription CreateSubscription(string ProductHandle, int ChargifyID, ICreditCardAttributes CreditCardAttributes, DateTime NextBillingAt); + ISubscription CreateSubscription(string ProductHandle, long chargifyId, ICreditCardAttributes CreditCardAttributes, DateTime NextBillingAt); + //ISubscription CreateSubscription(string ProductHandle, long chargifyId, ICreditCardAttributes CreditCardAttributes, DateTime NextBillingAt); /// /// Create a subscription using a coupon for discounted rate, without using credit card information. /// @@ -965,7 +965,7 @@ public interface IChargifyConnect /// The discount coupon code /// Optional, type of payment collection method /// If sucessful, the subscription object. Otherwise null. - ISubscription CreateSubscription(string ProductHandle, int ChargifyID, string CouponCode, PaymentCollectionMethod? paymentCollectionMethod); + ISubscription CreateSubscription(string ProductHandle, long chargifyId, string CouponCode, PaymentCollectionMethod? paymentCollectionMethod); /// /// Create a new subscription without requiring credit card information /// @@ -1066,7 +1066,7 @@ public interface IChargifyConnect /// The credit card attributes to use for this transaction /// The discount coupon code /// - ISubscription CreateSubscriptionUsingCoupon(string ProductHandle, int ChargifyID, ICreditCardAttributes CreditCardAttributes, string CouponCode); + ISubscription CreateSubscriptionUsingCoupon(string ProductHandle, long chargifyId, ICreditCardAttributes CreditCardAttributes, string CouponCode); /// /// Create a new subscription /// @@ -1074,7 +1074,7 @@ public interface IChargifyConnect /// The Chargify ID of the customer /// The discount coupon code /// The xml describing the new subsscription - ISubscription CreateSubscriptionUsingCoupon(string ProductHandle, int ChargifyID, string CouponCode); + ISubscription CreateSubscriptionUsingCoupon(string ProductHandle, long chargifyId, string CouponCode); /// /// Create a new subscription /// @@ -1163,7 +1163,7 @@ public interface IChargifyConnect /// /// The ChargifyID of the customer /// A list of subscriptions - IDictionary GetSubscriptionListForCustomer(int ChargifyID); + IDictionary GetSubscriptionListForCustomer(long chargifyId); /// /// Method to get the secure URL for updating the payment details for a subscription. /// diff --git a/Source/ChargifyDotNet/Interfaces/ICustomer.cs b/Source/ChargifyDotNet/Interfaces/ICustomer.cs index f9a30ac..a6ceeaa 100644 --- a/Source/ChargifyDotNet/Interfaces/ICustomer.cs +++ b/Source/ChargifyDotNet/Interfaces/ICustomer.cs @@ -43,7 +43,7 @@ public interface ICustomer : ICustomerAttributes, IComparable /// /// Get the customer's chargify ID /// - int ChargifyID { get; } + long ChargifyID { get; } /// /// Get the date and time the customer was created a Chargify /// diff --git a/Source/ChargifyDotNet/Interfaces/ISubscriptionCreateOptions.cs b/Source/ChargifyDotNet/Interfaces/ISubscriptionCreateOptions.cs index 4b98e11..6111f64 100644 --- a/Source/ChargifyDotNet/Interfaces/ISubscriptionCreateOptions.cs +++ b/Source/ChargifyDotNet/Interfaces/ISubscriptionCreateOptions.cs @@ -58,7 +58,7 @@ public interface ISubscriptionCreateOptions /// The ID of an existing customer within Chargify. Required, /// unless a customer_reference or a set of customer_attributes is given. /// - int? CustomerID { get; set; } + long? CustomerID { get; set; } /// /// The reference value (provided by your app) of an existing customer @@ -347,7 +347,7 @@ public bool ShouldSerializeCouponCode() /// unless a customer_reference or a set of customer_attributes is given. /// [XmlElement("customer_id")] - public int? CustomerID { get; set; } + public long? CustomerID { get; set; } /// /// Ignore, used to determine if the field should be serialized /// diff --git a/Source/ChargifyDotNet/Json/JsonNumber.cs b/Source/ChargifyDotNet/Json/JsonNumber.cs index 3eded2c..e8c70fb 100644 --- a/Source/ChargifyDotNet/Json/JsonNumber.cs +++ b/Source/ChargifyDotNet/Json/JsonNumber.cs @@ -9,6 +9,14 @@ public sealed class JsonNumber : JsonValue { double _value; + /// + /// The long value of this JsonNumber object (if applicable) + /// + public long LongValue + { + get { return (long)_value; } + } + /// /// The Integer value of this JsonNumber object (if applicable) /// diff --git a/Source/ChargifyDotNet/UsefulExtensions.cs b/Source/ChargifyDotNet/UsefulExtensions.cs index db930a7..eaafbf2 100644 --- a/Source/ChargifyDotNet/UsefulExtensions.cs +++ b/Source/ChargifyDotNet/UsefulExtensions.cs @@ -670,6 +670,21 @@ public static string GetJSONContentAsString(this JsonObject obj, string key) return result; } + /// + /// Method for getting the content of an XmlNode as an long + /// + /// The node whose value needs to be extracted + /// The long value of the node + public static long GetNodeContentAsLong(this XmlNode node) + { + long result = 0; + if (node.FirstChild != null) + { + if (!long.TryParse(node.FirstChild.Value, NumberStyles.Number, CultureInfo.InvariantCulture, out result)) result = 0; + } + return result; + } + /// /// Method for getting the content of an XmlNode as an integer /// @@ -708,6 +723,29 @@ public static int GetNodeContentAsInt(this XmlNode node) return result; } + /// + /// Method for getting the content of a JsonObject as an integer + /// + /// The object whose value/key needs to be extracted + /// The key of the int to retrieve + /// The integer value of the keyed object + public static long GetJSONContentAsLong(this JsonObject obj, string key) + { + long result = 0; + if (obj != null) + { + if (obj.ContainsKey(key)) + { + JsonNumber value = obj[key] as JsonNumber; + if (value != null) + { + result = value.LongValue; + } + } + } + return result; + } + /// /// Method for getting the content of a JsonObject as an integer ///