diff --git a/src/Stripe.net/Constants/Events.cs b/src/Stripe.net/Constants/Events.cs index de90ce37f3..cf910c8fa6 100644 --- a/src/Stripe.net/Constants/Events.cs +++ b/src/Stripe.net/Constants/Events.cs @@ -149,6 +149,21 @@ public static class Events /// public static string CouponUpdated => "coupon.updated"; + /// + /// Occurs whenever a credit note is created. + /// + public static string CreditNoteCreated => "credit_note.created"; + + /// + /// Occurs whenever a credit note is updated. + /// + public static string CreditNoteUpdated => "credit_note.updated"; + + /// + /// Occurs whenever a credit note is voided. + /// + public static string CreditNoteVoided => "credit_note.voided"; + /// /// Occurs whenever a new customer is created. /// diff --git a/src/Stripe.net/Entities/CreditNote.cs b/src/Stripe.net/Entities/CreditNote.cs new file mode 100644 index 0000000000..51bb157122 --- /dev/null +++ b/src/Stripe.net/Entities/CreditNote.cs @@ -0,0 +1,170 @@ +namespace Stripe +{ + using System; + using System.Collections.Generic; + using Newtonsoft.Json; + using Stripe.Infrastructure; + + public class CreditNote : StripeEntity, IHasId, IHasMetadata, IHasObject + { + /// + /// Unique identifier for the object. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// String representing the object’s type. Objects of the same type share the same value. + /// + [JsonProperty("object")] + public string Object { get; set; } + + /// + /// Credit note amount. + /// + [JsonProperty("amount")] + public long Amount { get; set; } + + /// + /// Time at which the object was created. Measured in seconds since the Unix epoch. + /// + [JsonProperty("created")] + [JsonConverter(typeof(DateTimeConverter))] + public DateTime Created { get; set; } + + /// + /// Three-letter ISO currency code, in lowercase. Must be a supported currency. + /// + [JsonProperty("currency")] + public string Currency { get; set; } + + #region Expandable Customer + + /// + /// ID of the customer associated with that credit note. + /// + [JsonIgnore] + public string CustomerId { get; set; } + + [JsonIgnore] + public Customer Customer { get; set; } + + [JsonProperty("customer")] + internal object InternalCustomer + { + get + { + return this.Customer ?? (object)this.CustomerId; + } + + set + { + StringOrObject.Map(value, s => this.CustomerId = s, o => this.Customer = o); + } + } + #endregion + + #region Expandable Invoice + + /// + /// ID of the invoice associated with that credit note. + /// + [JsonIgnore] + public string InvoiceId { get; set; } + + [JsonIgnore] + public Invoice Invoice { get; set; } + + [JsonProperty("invoice")] + internal object InternalInvoice + { + get + { + return this.Invoice ?? (object)this.InvoiceId; + } + + set + { + StringOrObject.Map(value, s => this.InvoiceId = s, o => this.Invoice = o); + } + } + #endregion + + /// + /// Has the value true if the object exists in live mode or the value false + /// if the object exists in test mode. + /// + [JsonProperty("livemode")] + public bool Livemode { get; set; } + + /// + /// Credit note memo. + /// + [JsonProperty("memo")] + public string Memo { get; set; } + + /// + /// A set of key/value pairs that you can attach to an order object. It can be useful for + /// storing additional information about the order in a structured format. + /// + [JsonProperty("metadata")] + public Dictionary Metadata { get; set; } + + /// + /// Credit note number. + /// + [JsonProperty("number")] + public string Number { get; set; } + + /// + /// The link to download the PDF of the credit note. + /// + [JsonProperty("pdf")] + public string Pdf { get; set; } + + /// + /// Reason for issuing this credit note, one of duplicate, fraudulent, + /// order_change, or product_unsatisfactory. + /// + [JsonProperty("reason")] + public string Reason { get; set; } + + #region Expandable Refund + + /// + /// ID of the refund associated with that credit note. + /// + [JsonIgnore] + public string RefundId { get; set; } + + [JsonIgnore] + public Refund Refund { get; set; } + + [JsonProperty("refund")] + internal object InternalRefund + { + get + { + return this.Refund ?? (object)this.RefundId; + } + + set + { + StringOrObject.Map(value, s => this.RefundId = s, o => this.Refund = o); + } + } + #endregion + + /// + /// Status of this credit note, one of issued or void. + /// + [JsonProperty("status")] + public string Status { get; set; } + + /// + /// Type of this credit note, one of post_payment or pre_payment. + /// + [JsonProperty("type")] + public string Type { get; set; } + } +} diff --git a/src/Stripe.net/Entities/Invoices/Invoice.cs b/src/Stripe.net/Entities/Invoices/Invoice.cs index b370edc7e1..039016b68f 100644 --- a/src/Stripe.net/Entities/Invoices/Invoice.cs +++ b/src/Stripe.net/Entities/Invoices/Invoice.cs @@ -243,6 +243,18 @@ internal object InternalPaymentIntent [JsonConverter(typeof(DateTimeConverter))] public DateTime PeriodStart { get; set; } + /// + /// Total amount of all post-payment credit notes issued for this invoice. + /// + [JsonProperty("post_payment_credit_notes_amount")] + public long? PostPaymentCreditNotesAmount { get; set; } + + /// + /// Total amount of all pre-payment credit notes issued for this invoice. + /// + [JsonProperty("pre_payment_credit_notes_amount")] + public long? PrePaymentCreditNotesAmount { get; set; } + [JsonProperty("receipt_number")] public string ReceiptNumber { get; set; } diff --git a/src/Stripe.net/Infrastructure/StripeTypeRegistry.cs b/src/Stripe.net/Infrastructure/StripeTypeRegistry.cs index 946197ef19..c4913802b4 100644 --- a/src/Stripe.net/Infrastructure/StripeTypeRegistry.cs +++ b/src/Stripe.net/Infrastructure/StripeTypeRegistry.cs @@ -26,6 +26,7 @@ internal static class StripeTypeRegistry { "checkout.session", typeof(Checkout.Session) }, { "country_spec", typeof(CountrySpec) }, { "coupon", typeof(Coupon) }, + { "credit_note", typeof(CreditNote) }, { "customer", typeof(Customer) }, { "discount", typeof(Discount) }, { "dispute", typeof(Dispute) }, diff --git a/src/Stripe.net/Services/CreditNotes/CreditNoteCreateOptions.cs b/src/Stripe.net/Services/CreditNotes/CreditNoteCreateOptions.cs new file mode 100644 index 0000000000..5c30fe9609 --- /dev/null +++ b/src/Stripe.net/Services/CreditNotes/CreditNoteCreateOptions.cs @@ -0,0 +1,61 @@ +namespace Stripe +{ + using System; + using System.Collections.Generic; + using Newtonsoft.Json; + using Stripe.Infrastructure; + + public class CreditNoteCreateOptions : BaseOptions + { + /// + /// Credit note amount. + /// + [JsonProperty("amount")] + public long? Amount { get; set; } + + /// + /// Amount to credit the customer balance. + /// + [JsonProperty("credit_amount")] + public long? CreditAmount { get; set; } + + /// + /// ID of the invoice. + /// + [JsonProperty("invoice")] + public string InvoiceId { get; set; } + + /// + /// Credit note memo. + /// + [JsonProperty("memo")] + public string Memo { get; set; } + + /// + /// Set of key-value pairs that you can attach to an object. This can be useful for storing + /// additional information about the object in a structured format. + /// + [JsonProperty("metadata")] + public Dictionary Metadata { get; set; } + + /// + /// Reason for issuing this credit note, one of duplicate, fraudulent, + /// order_change, or product_unsatisfactory. + /// + [JsonProperty("reason")] + public string Reason { get; set; } + + /// + /// ID of an existing refund to link this credit note to. + /// + [JsonProperty("refund")] + public string RefundId { get; set; } + + /// + /// Amount to refund. If set, a refund will be created for the charge associated with the + /// invoice. + /// + [JsonProperty("refund_amount")] + public long? RefundAmount { get; set; } + } +} diff --git a/src/Stripe.net/Services/CreditNotes/CreditNoteListOptions.cs b/src/Stripe.net/Services/CreditNotes/CreditNoteListOptions.cs new file mode 100644 index 0000000000..dede1506fa --- /dev/null +++ b/src/Stripe.net/Services/CreditNotes/CreditNoteListOptions.cs @@ -0,0 +1,14 @@ +namespace Stripe +{ + using System; + using Newtonsoft.Json; + + public class CreditNoteListOptions : ListOptions + { + /// + /// ID of the invoice. + /// + [JsonProperty("invoice")] + public string InvoiceId { get; set; } + } +} diff --git a/src/Stripe.net/Services/CreditNotes/CreditNoteService.cs b/src/Stripe.net/Services/CreditNotes/CreditNoteService.cs new file mode 100644 index 0000000000..e7424e8fad --- /dev/null +++ b/src/Stripe.net/Services/CreditNotes/CreditNoteService.cs @@ -0,0 +1,80 @@ +namespace Stripe +{ + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + public class CreditNoteService : Service, + ICreatable, + IListable, + IRetrievable, + IUpdatable + { + public CreditNoteService() + : base(null) + { + } + + public CreditNoteService(string apiKey) + : base(apiKey) + { + } + + public override string BasePath => "/credit_notes"; + + public virtual CreditNote Create(CreditNoteCreateOptions options, RequestOptions requestOptions = null) + { + return this.CreateEntity(options, requestOptions); + } + + public virtual Task CreateAsync(CreditNoteCreateOptions options, RequestOptions requestOptions = null, CancellationToken cancellationToken = default(CancellationToken)) + { + return this.CreateEntityAsync(options, requestOptions, cancellationToken); + } + + public virtual CreditNote Get(string creditNoteId, RequestOptions requestOptions = null) + { + return this.GetEntity(creditNoteId, null, requestOptions); + } + + public virtual Task GetAsync(string creditNoteId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default(CancellationToken)) + { + return this.GetEntityAsync(creditNoteId, null, requestOptions, cancellationToken); + } + + public virtual StripeList List(CreditNoteListOptions options = null, RequestOptions requestOptions = null) + { + return this.ListEntities(options, requestOptions); + } + + public virtual Task> ListAsync(CreditNoteListOptions options = null, RequestOptions requestOptions = null, CancellationToken cancellationToken = default(CancellationToken)) + { + return this.ListEntitiesAsync(options, requestOptions, cancellationToken); + } + + public virtual IEnumerable ListAutoPaging(CreditNoteListOptions options = null, RequestOptions requestOptions = null) + { + return this.ListEntitiesAutoPaging(options, requestOptions); + } + + public virtual CreditNote Update(string creditNoteId, CreditNoteUpdateOptions options, RequestOptions requestOptions = null) + { + return this.UpdateEntity(creditNoteId, options, requestOptions); + } + + public virtual Task UpdateAsync(string creditNoteId, CreditNoteUpdateOptions options, RequestOptions requestOptions = null, CancellationToken cancellationToken = default(CancellationToken)) + { + return this.UpdateEntityAsync(creditNoteId, options, requestOptions, cancellationToken); + } + + public virtual CreditNote VoidCreditNote(string creditNoteId, CreditNoteVoidOptions options, RequestOptions requestOptions = null) + { + return this.PostRequest($"{this.InstanceUrl(creditNoteId)}/void", options, requestOptions); + } + + public virtual Task VoidCreditNoteAsync(string creditNoteId, CreditNoteVoidOptions options, RequestOptions requestOptions = null, CancellationToken cancellationToken = default(CancellationToken)) + { + return this.PostRequestAsync($"{this.InstanceUrl(creditNoteId)}/void", options, requestOptions, cancellationToken); + } + } +} diff --git a/src/Stripe.net/Services/CreditNotes/CreditNoteUpdateOptions.cs b/src/Stripe.net/Services/CreditNotes/CreditNoteUpdateOptions.cs new file mode 100644 index 0000000000..64357d1a85 --- /dev/null +++ b/src/Stripe.net/Services/CreditNotes/CreditNoteUpdateOptions.cs @@ -0,0 +1,23 @@ +namespace Stripe +{ + using System; + using System.Collections.Generic; + using Newtonsoft.Json; + using Stripe.Infrastructure; + + public class CreditNoteUpdateOptions : BaseOptions + { + /// + /// Credit note memo. + /// + [JsonProperty("memo")] + public string Memo { get; set; } + + /// + /// Set of key-value pairs that you can attach to an object. This can be useful for storing + /// additional information about the object in a structured format. + /// + [JsonProperty("metadata")] + public Dictionary Metadata { get; set; } + } +} diff --git a/src/Stripe.net/Services/CreditNotes/CreditNoteVoidOptions.cs b/src/Stripe.net/Services/CreditNotes/CreditNoteVoidOptions.cs new file mode 100644 index 0000000000..f668c88fea --- /dev/null +++ b/src/Stripe.net/Services/CreditNotes/CreditNoteVoidOptions.cs @@ -0,0 +1,11 @@ +namespace Stripe +{ + using System; + using System.Collections.Generic; + using Newtonsoft.Json; + using Stripe.Infrastructure; + + public class CreditNoteVoidOptions : BaseOptions + { + } +} diff --git a/src/StripeTests/Entities/CreditNotes/CreditNoteTest.cs b/src/StripeTests/Entities/CreditNotes/CreditNoteTest.cs new file mode 100644 index 0000000000..4b8b969605 --- /dev/null +++ b/src/StripeTests/Entities/CreditNotes/CreditNoteTest.cs @@ -0,0 +1,52 @@ +namespace StripeTests +{ + using Newtonsoft.Json; + using Stripe; + using Xunit; + + public class CreditNoteTest : BaseStripeTest + { + public CreditNoteTest(StripeMockFixture stripeMockFixture) + : base(stripeMockFixture) + { + } + + [Fact] + public void Deserialize() + { + string json = this.GetFixture("/v1/credit_notes/cn_123"); + var creditNote = JsonConvert.DeserializeObject(json); + Assert.NotNull(creditNote); + Assert.IsType(creditNote); + Assert.NotNull(creditNote.Id); + Assert.Equal("credit_note", creditNote.Object); + } + + [Fact] + public void DeserializeWithExpansions() + { + string[] expansions = + { + "customer", + "invoice", + "refund", + }; + + string json = this.GetFixture("/v1/credit_notes/cn_123", expansions); + var creditNote = JsonConvert.DeserializeObject(json); + Assert.NotNull(creditNote); + Assert.IsType(creditNote); + Assert.NotNull(creditNote.Id); + Assert.Equal("credit_note", creditNote.Object); + + Assert.NotNull(creditNote.Customer); + Assert.Equal("customer", creditNote.Customer.Object); + + Assert.NotNull(creditNote.Invoice); + Assert.Equal("invoice", creditNote.Invoice.Object); + + Assert.NotNull(creditNote.Refund); + Assert.Equal("refund", creditNote.Refund.Object); + } + } +} diff --git a/src/StripeTests/Services/CreditNotes/CreditNoteServiceTest.cs b/src/StripeTests/Services/CreditNotes/CreditNoteServiceTest.cs new file mode 100644 index 0000000000..66de8d585f --- /dev/null +++ b/src/StripeTests/Services/CreditNotes/CreditNoteServiceTest.cs @@ -0,0 +1,154 @@ +namespace StripeTests +{ + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + + using Stripe; + using Xunit; + + public class CreditNoteServiceTest : BaseStripeTest + { + private const string CreditNoteId = "cn_123"; + + private readonly CreditNoteService service; + private readonly CreditNoteCreateOptions createOptions; + private readonly CreditNoteUpdateOptions updateOptions; + private readonly CreditNoteListOptions listOptions; + private readonly CreditNoteVoidOptions voidOptions; + + public CreditNoteServiceTest(MockHttpClientFixture mockHttpClientFixture) + : base(mockHttpClientFixture) + { + this.service = new CreditNoteService(); + + this.createOptions = new CreditNoteCreateOptions + { + Amount = 100, + InvoiceId = "in_123", + Reason = "duplicate", + }; + + this.updateOptions = new CreditNoteUpdateOptions + { + Metadata = new Dictionary + { + { "key", "value" }, + }, + }; + + this.listOptions = new CreditNoteListOptions + { + Limit = 1, + InvoiceId = "in_123", + }; + + this.voidOptions = new CreditNoteVoidOptions + { + }; + } + + [Fact] + public void Create() + { + var creditNote = this.service.Create(this.createOptions); + this.AssertRequest(HttpMethod.Post, "/v1/credit_notes"); + Assert.NotNull(creditNote); + Assert.Equal("credit_note", creditNote.Object); + } + + [Fact] + public async Task CreateAsync() + { + var creditNote = await this.service.CreateAsync(this.createOptions); + this.AssertRequest(HttpMethod.Post, "/v1/credit_notes"); + Assert.NotNull(creditNote); + Assert.Equal("credit_note", creditNote.Object); + } + + [Fact] + public void Get() + { + var creditNote = this.service.Get(CreditNoteId); + this.AssertRequest(HttpMethod.Get, "/v1/credit_notes/cn_123"); + Assert.NotNull(creditNote); + Assert.Equal("credit_note", creditNote.Object); + } + + [Fact] + public async Task GetAsync() + { + var creditNote = await this.service.GetAsync(CreditNoteId); + this.AssertRequest(HttpMethod.Get, "/v1/credit_notes/cn_123"); + Assert.NotNull(creditNote); + Assert.Equal("credit_note", creditNote.Object); + } + + [Fact] + public void List() + { + var creditNotes = this.service.List(this.listOptions); + this.AssertRequest(HttpMethod.Get, "/v1/credit_notes"); + Assert.NotNull(creditNotes); + Assert.Equal("list", creditNotes.Object); + Assert.Single(creditNotes.Data); + Assert.Equal("credit_note", creditNotes.Data[0].Object); + } + + [Fact] + public async Task ListAsync() + { + var creditNotes = await this.service.ListAsync(this.listOptions); + this.AssertRequest(HttpMethod.Get, "/v1/credit_notes"); + Assert.NotNull(creditNotes); + Assert.Equal("list", creditNotes.Object); + Assert.Single(creditNotes.Data); + Assert.Equal("credit_note", creditNotes.Data[0].Object); + } + + [Fact] + public void ListAutoPaging() + { + var creditNotes = this.service.ListAutoPaging(this.listOptions).ToList(); + Assert.NotNull(creditNotes); + Assert.Equal("credit_note", creditNotes[0].Object); + } + + [Fact] + public void Update() + { + var creditNote = this.service.Update(CreditNoteId, this.updateOptions); + this.AssertRequest(HttpMethod.Post, "/v1/credit_notes/cn_123"); + Assert.NotNull(creditNote); + Assert.Equal("credit_note", creditNote.Object); + } + + [Fact] + public async Task UpdateAsync() + { + var creditNote = await this.service.UpdateAsync(CreditNoteId, this.updateOptions); + this.AssertRequest(HttpMethod.Post, "/v1/credit_notes/cn_123"); + Assert.NotNull(creditNote); + Assert.Equal("credit_note", creditNote.Object); + } + + [Fact] + public void VoidCreditNote() + { + var creditNote = this.service.VoidCreditNote(CreditNoteId, this.voidOptions); + this.AssertRequest(HttpMethod.Post, "/v1/credit_notes/cn_123/void"); + Assert.NotNull(creditNote); + Assert.Equal("credit_note", creditNote.Object); + } + + [Fact] + public async Task VoidCreditNoteAsync() + { + var creditNote = await this.service.VoidCreditNoteAsync(CreditNoteId, this.voidOptions); + this.AssertRequest(HttpMethod.Post, "/v1/credit_notes/cn_123/void"); + Assert.NotNull(creditNote); + Assert.Equal("credit_note", creditNote.Object); + } + } +}