From 3beb716b8318e07ca963eef92b66b51f2e31177a Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Fri, 26 Sep 2025 09:32:07 +0500 Subject: [PATCH] common: add paddle billing api client --- .../Extensions/HttpClientExtensions.cs | 18 +- Streetwriters.Common/Interfaces/IResponse.cs | 3 + .../Models/GetSubscriptionResponse.cs | 254 +++++++-- .../Models/GetTransactionInvoiceResponse.cs | 25 + .../Models/GetTransactionResponse.cs | 18 + .../Models/ListTransactionsResponseV2.cs | 520 ++++++++++++++++++ Streetwriters.Common/Models/Response.cs | 3 + .../Models/SubscriptionPreviewResponse.cs | 60 ++ .../Services/PaddleBillingService.cs | 129 +++++ 9 files changed, 991 insertions(+), 39 deletions(-) create mode 100644 Streetwriters.Common/Models/GetTransactionInvoiceResponse.cs create mode 100644 Streetwriters.Common/Models/GetTransactionResponse.cs create mode 100644 Streetwriters.Common/Models/ListTransactionsResponseV2.cs create mode 100644 Streetwriters.Common/Models/SubscriptionPreviewResponse.cs create mode 100644 Streetwriters.Common/Services/PaddleBillingService.cs diff --git a/Streetwriters.Common/Extensions/HttpClientExtensions.cs b/Streetwriters.Common/Extensions/HttpClientExtensions.cs index 4a8c320..c293eee 100644 --- a/Streetwriters.Common/Extensions/HttpClientExtensions.cs +++ b/Streetwriters.Common/Extensions/HttpClientExtensions.cs @@ -37,19 +37,21 @@ namespace Streetwriters.Common.Extensions request.Content = content; } - foreach (var header in headers) + if (headers != null) { - if (header.Key == "Content-Type" || header.Key == "Content-Length") + foreach (var header in headers) { - if (request.Content != null) - request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable()); - continue; + if (header.Key == "Content-Type" || header.Key == "Content-Length") + { + request.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable()); + continue; + } + request.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable()); } - request.Headers.TryAddWithoutValidation(header.Key, header.Value.AsEnumerable()); } var response = await httpClient.SendAsync(request); - if (response.Content.Headers.ContentLength > 0) + if (response.Content.Headers.ContentLength > 0 && response.Content.Headers.ContentType.ToString().Contains("application/json")) { var res = await response.Content.ReadFromJsonAsync(); res.Success = response.IsSuccessStatusCode; @@ -58,7 +60,7 @@ namespace Streetwriters.Common.Extensions } else { - return new T { Success = response.IsSuccessStatusCode, StatusCode = (int)response.StatusCode }; + return new T { Success = response.IsSuccessStatusCode, StatusCode = (int)response.StatusCode, Content = response.Content }; } } diff --git a/Streetwriters.Common/Interfaces/IResponse.cs b/Streetwriters.Common/Interfaces/IResponse.cs index adb3006..bd8ca70 100644 --- a/Streetwriters.Common/Interfaces/IResponse.cs +++ b/Streetwriters.Common/Interfaces/IResponse.cs @@ -17,11 +17,14 @@ You should have received a copy of the Affero GNU General Public License along with this program. If not, see . */ +using System.Net.Http; + namespace Streetwriters.Common.Interfaces { public interface IResponse { bool Success { get; set; } int StatusCode { get; set; } + HttpContent Content { get; set; } } } \ No newline at end of file diff --git a/Streetwriters.Common/Models/GetSubscriptionResponse.cs b/Streetwriters.Common/Models/GetSubscriptionResponse.cs index 1fc459e..c718e1e 100644 --- a/Streetwriters.Common/Models/GetSubscriptionResponse.cs +++ b/Streetwriters.Common/Models/GetSubscriptionResponse.cs @@ -1,31 +1,223 @@ -/* -This file is part of the Notesnook Sync Server project (https://notesnook.com/) - -Copyright (C) 2023 Streetwriters (Private) Limited - -This program is free software: you can redistribute it and/or modify -it under the terms of the Affero GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -Affero GNU General Public License for more details. - -You should have received a copy of the Affero GNU General Public License -along with this program. If not, see . -*/ - -using System.Runtime.Serialization; -using System.Text.Json.Serialization; -using Streetwriters.Common.Interfaces; - -namespace Streetwriters.Common.Models -{ - public class SubscriptionResponse : Response - { - [JsonPropertyName("subscription")] - public ISubscription Subscription { get; set; } - } -} +namespace Streetwriters.Common.Models +{ + using System; + using System.Collections.Generic; + + using System.Text.Json; + using System.Text.Json.Serialization; + using System.Globalization; + + public partial class GetSubscriptionResponse + { + [JsonPropertyName("data")] + public Data Data { get; set; } + + [JsonPropertyName("meta")] + public Meta Meta { get; set; } + } + + public partial class Data + { + // [JsonPropertyName("id")] + // public string Id { get; set; } + + // [JsonPropertyName("status")] + // public string Status { get; set; } + + [JsonPropertyName("customer_id")] + public string CustomerId { get; set; } + + // [JsonPropertyName("address_id")] + // public string AddressId { get; set; } + + // [JsonPropertyName("business_id")] + // public object BusinessId { get; set; } + + // [JsonPropertyName("currency_code")] + // public string CurrencyCode { get; set; } + + // [JsonPropertyName("created_at")] + // public DateTimeOffset CreatedAt { get; set; } + + // [JsonPropertyName("updated_at")] + // public DateTimeOffset UpdatedAt { get; set; } + + // [JsonPropertyName("started_at")] + // public DateTimeOffset StartedAt { get; set; } + + [JsonPropertyName("first_billed_at")] + public DateTimeOffset? FirstBilledAt { get; set; } + + // [JsonPropertyName("next_billed_at")] + // public DateTimeOffset NextBilledAt { get; set; } + + // [JsonPropertyName("paused_at")] + // public object PausedAt { get; set; } + + // [JsonPropertyName("canceled_at")] + // public object CanceledAt { get; set; } + + // [JsonPropertyName("collection_mode")] + // public string CollectionMode { get; set; } + + // [JsonPropertyName("billing_details")] + // public object BillingDetails { get; set; } + + // [JsonPropertyName("current_billing_period")] + // public CurrentBillingPeriod CurrentBillingPeriod { get; set; } + + [JsonPropertyName("billing_cycle")] + public BillingCycle BillingCycle { get; set; } + + // [JsonPropertyName("scheduled_change")] + // public object ScheduledChange { get; set; } + + // [JsonPropertyName("items")] + // public Item[] Items { get; set; } + + // [JsonPropertyName("custom_data")] + // public object CustomData { get; set; } + + [JsonPropertyName("management_urls")] + public ManagementUrls ManagementUrls { get; set; } + + // [JsonPropertyName("discount")] + // public object Discount { get; set; } + + // [JsonPropertyName("import_meta")] + // public object ImportMeta { get; set; } + } + + public partial class BillingCycle + { + [JsonPropertyName("frequency")] + public long Frequency { get; set; } + + [JsonPropertyName("interval")] + public string Interval { get; set; } + } + + // public partial class CurrentBillingPeriod + // { + // [JsonPropertyName("starts_at")] + // public DateTimeOffset StartsAt { get; set; } + + // [JsonPropertyName("ends_at")] + // public DateTimeOffset EndsAt { get; set; } + // } + + // public partial class Item + // { + // [JsonPropertyName("status")] + // public string Status { get; set; } + + // [JsonPropertyName("quantity")] + // public long Quantity { get; set; } + + // [JsonPropertyName("recurring")] + // public bool Recurring { get; set; } + + // [JsonPropertyName("created_at")] + // public DateTimeOffset CreatedAt { get; set; } + + // [JsonPropertyName("updated_at")] + // public DateTimeOffset UpdatedAt { get; set; } + + // [JsonPropertyName("previously_billed_at")] + // public DateTimeOffset PreviouslyBilledAt { get; set; } + + // [JsonPropertyName("next_billed_at")] + // public DateTimeOffset NextBilledAt { get; set; } + + // [JsonPropertyName("trial_dates")] + // public object TrialDates { get; set; } + + // [JsonPropertyName("price")] + // public Price Price { get; set; } + // } + + // public partial class Price + // { + // [JsonPropertyName("id")] + // public string Id { get; set; } + + // [JsonPropertyName("product_id")] + // public string ProductId { get; set; } + + // [JsonPropertyName("type")] + // public string Type { get; set; } + + // [JsonPropertyName("description")] + // public string Description { get; set; } + + // [JsonPropertyName("name")] + // public string Name { get; set; } + + // [JsonPropertyName("tax_mode")] + // public string TaxMode { get; set; } + + // [JsonPropertyName("billing_cycle")] + // public BillingCycle BillingCycle { get; set; } + + // [JsonPropertyName("trial_period")] + // public object TrialPeriod { get; set; } + + // [JsonPropertyName("unit_price")] + // public UnitPrice UnitPrice { get; set; } + + // [JsonPropertyName("unit_price_overrides")] + // public object[] UnitPriceOverrides { get; set; } + + // [JsonPropertyName("custom_data")] + // public object CustomData { get; set; } + + // [JsonPropertyName("status")] + // public string Status { get; set; } + + // [JsonPropertyName("quantity")] + // public Quantity Quantity { get; set; } + + // [JsonPropertyName("import_meta")] + // public object ImportMeta { get; set; } + + // [JsonPropertyName("created_at")] + // public DateTimeOffset CreatedAt { get; set; } + + // [JsonPropertyName("updated_at")] + // public DateTimeOffset UpdatedAt { get; set; } + // } + + // public partial class Quantity + // { + // [JsonPropertyName("minimum")] + // public long Minimum { get; set; } + + // [JsonPropertyName("maximum")] + // public long Maximum { get; set; } + // } + + // public partial class UnitPrice + // { + // [JsonPropertyName("amount")] + // [JsonConverter(typeof(ParseStringConverter))] + // public long Amount { get; set; } + + // [JsonPropertyName("currency_code")] + // public string CurrencyCode { get; set; } + // } + + public partial class ManagementUrls + { + [JsonPropertyName("update_payment_method")] + public Uri UpdatePaymentMethod { get; set; } + + [JsonPropertyName("cancel")] + public Uri Cancel { get; set; } + } + + public partial class Meta + { + [JsonPropertyName("request_id")] + public string RequestId { get; set; } + } +} diff --git a/Streetwriters.Common/Models/GetTransactionInvoiceResponse.cs b/Streetwriters.Common/Models/GetTransactionInvoiceResponse.cs new file mode 100644 index 0000000..c745319 --- /dev/null +++ b/Streetwriters.Common/Models/GetTransactionInvoiceResponse.cs @@ -0,0 +1,25 @@ +namespace Streetwriters.Common.Models +{ + using System; + using System.Collections.Generic; + + using System.Text.Json; + using System.Text.Json.Serialization; + using System.Globalization; + + public partial class GetTransactionInvoiceResponse + { + [JsonPropertyName("data")] + public Invoice Invoice { get; set; } + + [JsonPropertyName("meta")] + public Meta Meta { get; set; } + } + + public partial class Invoice + { + + [JsonPropertyName("url")] + public string Url { get; set; } + } +} diff --git a/Streetwriters.Common/Models/GetTransactionResponse.cs b/Streetwriters.Common/Models/GetTransactionResponse.cs new file mode 100644 index 0000000..c293dcf --- /dev/null +++ b/Streetwriters.Common/Models/GetTransactionResponse.cs @@ -0,0 +1,18 @@ +namespace Streetwriters.Common.Models +{ + using System; + using System.Collections.Generic; + + using System.Text.Json; + using System.Text.Json.Serialization; + using System.Globalization; + + public partial class GetTransactionResponse + { + [JsonPropertyName("data")] + public TransactionV2 Transaction { get; set; } + + [JsonPropertyName("meta")] + public Meta Meta { get; set; } + } +} diff --git a/Streetwriters.Common/Models/ListTransactionsResponseV2.cs b/Streetwriters.Common/Models/ListTransactionsResponseV2.cs new file mode 100644 index 0000000..f0da768 --- /dev/null +++ b/Streetwriters.Common/Models/ListTransactionsResponseV2.cs @@ -0,0 +1,520 @@ +namespace Streetwriters.Common.Models +{ + using System; + using System.Collections.Generic; + + using System.Text.Json; + using System.Text.Json.Serialization; + using System.Globalization; + + public partial class ListTransactionsResponseV2 + { + [JsonPropertyName("data")] + public TransactionV2[] Transactions { get; set; } + + [JsonPropertyName("meta")] + public Meta Meta { get; set; } + } + + public partial class TransactionV2 + { + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } + + [JsonPropertyName("customer_id")] + public string CustomerId { get; set; } + + // [JsonPropertyName("address_id")] + // public string AddressId { get; set; } + + // [JsonPropertyName("business_id")] + // public object BusinessId { get; set; } + + [JsonPropertyName("custom_data")] + public Dictionary CustomData { get; set; } + + [JsonPropertyName("origin")] + public string Origin { get; set; } + + // [JsonPropertyName("collection_mode")] + // public string CollectionMode { get; set; } + + // [JsonPropertyName("subscription_id")] + // public string SubscriptionId { get; set; } + + // [JsonPropertyName("invoice_id")] + // public string InvoiceId { get; set; } + + // [JsonPropertyName("invoice_number")] + // public string InvoiceNumber { get; set; } + + [JsonPropertyName("billing_details")] + public BillingDetails BillingDetails { get; set; } + + [JsonPropertyName("billing_period")] + public BillingPeriod BillingPeriod { get; set; } + + // [JsonPropertyName("currency_code")] + // public string CurrencyCode { get; set; } + + // [JsonPropertyName("discount_id")] + // public string DiscountId { get; set; } + + [JsonPropertyName("created_at")] + public DateTimeOffset CreatedAt { get; set; } + + // [JsonPropertyName("updated_at")] + // public DateTimeOffset UpdatedAt { get; set; } + + [JsonPropertyName("billed_at")] + public DateTimeOffset? BilledAt { get; set; } + + [JsonPropertyName("items")] + public Item[] Items { get; set; } + + [JsonPropertyName("details")] + public Details Details { get; set; } + + // [JsonPropertyName("payments")] + // public Payment[] Payments { get; set; } + + // [JsonPropertyName("checkout")] + // public Checkout Checkout { get; set; } + } + + public partial class BillingDetails + { + // [JsonPropertyName("enable_checkout")] + // public bool EnableCheckout { get; set; } + + [JsonPropertyName("payment_terms")] + public PaymentTerms PaymentTerms { get; set; } + + // [JsonPropertyName("purchase_order_number")] + // public string PurchaseOrderNumber { get; set; } + + // [JsonPropertyName("additional_information")] + // public object AdditionalInformation { get; set; } + } + + public partial class PaymentTerms + { + [JsonPropertyName("interval")] + public string Interval { get; set; } + + [JsonPropertyName("frequency")] + public long Frequency { get; set; } + } + + public partial class BillingPeriod + { + [JsonPropertyName("starts_at")] + public DateTimeOffset StartsAt { get; set; } + + [JsonPropertyName("ends_at")] + public DateTimeOffset EndsAt { get; set; } + } + + // public partial class Checkout + // { + // [JsonPropertyName("url")] + // public Uri Url { get; set; } + // } + + public partial class Details + { + // [JsonPropertyName("tax_rates_used")] + // public TaxRatesUsed[] TaxRatesUsed { get; set; } + + [JsonPropertyName("totals")] + public Totals Totals { get; set; } + + // [JsonPropertyName("adjusted_totals")] + // public AdjustedTotals AdjustedTotals { get; set; } + + // [JsonPropertyName("payout_totals")] + // public Dictionary PayoutTotals { get; set; } + + // [JsonPropertyName("adjusted_payout_totals")] + // public AdjustedTotals AdjustedPayoutTotals { get; set; } + + [JsonPropertyName("line_items")] + public LineItem[] LineItems { get; set; } + } + + public partial class Totals + { + [JsonPropertyName("subtotal")] + public long Subtotal { get; set; } + + [JsonPropertyName("tax")] + public long Tax { get; set; } + + [JsonPropertyName("discount")] + public long Discount { get; set; } + + [JsonPropertyName("total")] + public long Total { get; set; } + + [JsonPropertyName("grand_total")] + public long GrandTotal { get; set; } + + // [JsonPropertyName("fee")] + // public object Fee { get; set; } + + // [JsonPropertyName("credit")] + // public long Credit { get; set; } + + // [JsonPropertyName("credit_to_balance")] + // public long CreditToBalance { get; set; } + + [JsonPropertyName("balance")] + public long Balance { get; set; } + + // [JsonPropertyName("earnings")] + // public object Earnings { get; set; } + + [JsonPropertyName("currency_code")] + public string CurrencyCode { get; set; } + } + // public partial class AdjustedTotals + // { + // [JsonPropertyName("subtotal")] + // [JsonConverter(typeof(ParseStringConverter))] + // public long Subtotal { get; set; } + + // [JsonPropertyName("tax")] + // [JsonConverter(typeof(ParseStringConverter))] + // public long Tax { get; set; } + + // [JsonPropertyName("total")] + // [JsonConverter(typeof(ParseStringConverter))] + // public long Total { get; set; } + + // [JsonPropertyName("fee")] + // [JsonConverter(typeof(ParseStringConverter))] + // public long Fee { get; set; } + + // [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + // [JsonPropertyName("chargeback_fee")] + // public ChargebackFee ChargebackFee { get; set; } + + // [JsonPropertyName("earnings")] + // [JsonConverter(typeof(ParseStringConverter))] + // public long Earnings { get; set; } + + // [JsonPropertyName("currency_code")] + // public string CurrencyCode { get; set; } + + // [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + // [JsonPropertyName("grand_total")] + // [JsonConverter(typeof(ParseStringConverter))] + // public long? GrandTotal { get; set; } + // } + + // public partial class ChargebackFee + // { + // [JsonPropertyName("amount")] + // [JsonConverter(typeof(ParseStringConverter))] + // public long Amount { get; set; } + + // [JsonPropertyName("original")] + // public object Original { get; set; } + // } + + public partial class LineItem + { + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("price_id")] + public string PriceId { get; set; } + + // [JsonPropertyName("quantity")] + // public long Quantity { get; set; } + + // [JsonPropertyName("totals")] + // public Totals Totals { get; set; } + + // [JsonPropertyName("product")] + // public Product Product { get; set; } + + // [JsonPropertyName("tax_rate")] + // public string TaxRate { get; set; } + + // [JsonPropertyName("unit_totals")] + // public Totals UnitTotals { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("proration")] + public Proration Proration { get; set; } + } + + // public partial class Product + // { + // [JsonPropertyName("id")] + // public string Id { get; set; } + + // [JsonPropertyName("name")] + // public string Name { get; set; } + + // [JsonPropertyName("description")] + // public string Description { get; set; } + + // [JsonPropertyName("type")] + // public TypeEnum Type { get; set; } + + // [JsonPropertyName("tax_category")] + // public TypeEnum TaxCategory { get; set; } + + // [JsonPropertyName("image_url")] + // public Uri ImageUrl { get; set; } + + // [JsonPropertyName("custom_data")] + // public CustomData CustomData { get; set; } + + // [JsonPropertyName("status")] + // public Status Status { get; set; } + + // [JsonPropertyName("created_at")] + // public DateTimeOffset CreatedAt { get; set; } + + // [JsonPropertyName("updated_at")] + // public DateTimeOffset UpdatedAt { get; set; } + + // [JsonPropertyName("import_meta")] + // public object ImportMeta { get; set; } + // } + + // public partial class CustomData + // { + // [JsonPropertyName("features")] + // public Features Features { get; set; } + + // [JsonPropertyName("suggested_addons")] + // public string[] SuggestedAddons { get; set; } + + // [JsonPropertyName("upgrade_description")] + // public string UpgradeDescription { get; set; } + // } + + // public partial class Features + // { + // [JsonPropertyName("aircraft_performance")] + // public bool AircraftPerformance { get; set; } + + // [JsonPropertyName("compliance_monitoring")] + // public bool ComplianceMonitoring { get; set; } + + // [JsonPropertyName("flight_log_management")] + // public bool FlightLogManagement { get; set; } + + // [JsonPropertyName("payment_by_invoice")] + // public bool PaymentByInvoice { get; set; } + + // [JsonPropertyName("route_planning")] + // public bool RoutePlanning { get; set; } + + // [JsonPropertyName("sso")] + // public bool Sso { get; set; } + // } + + public partial class Proration + { + [JsonPropertyName("billing_period")] + public BillingPeriod BillingPeriod { get; set; } + } + + // public partial class Totals + // { + // [JsonPropertyName("subtotal")] + // [JsonConverter(typeof(ParseStringConverter))] + // public long Subtotal { get; set; } + + // [JsonPropertyName("discount")] + // [JsonConverter(typeof(ParseStringConverter))] + // public long Discount { get; set; } + + // [JsonPropertyName("tax")] + // [JsonConverter(typeof(ParseStringConverter))] + // public long Tax { get; set; } + + // [JsonPropertyName("total")] + // [JsonConverter(typeof(ParseStringConverter))] + // public long Total { get; set; } + // } + + // public partial class TaxRatesUsed + // { + // [JsonPropertyName("tax_rate")] + // public string TaxRate { get; set; } + + // [JsonPropertyName("totals")] + // public Totals Totals { get; set; } + // } + + public partial class Item + { + [JsonPropertyName("price")] + public Price Price { get; set; } + + [JsonPropertyName("quantity")] + public long Quantity { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("proration")] + public Proration Proration { get; set; } + } + + public partial class Price + { + [JsonPropertyName("id")] + public string Id { get; set; } + + // [JsonPropertyName("description")] + // public string Description { get; set; } + + // [JsonPropertyName("type")] + // public TypeEnum Type { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + // [JsonPropertyName("product_id")] + // public string ProductId { get; set; } + + // [JsonPropertyName("billing_cycle")] + // public PaymentTerms BillingCycle { get; set; } + + // [JsonPropertyName("trial_period")] + // public object TrialPeriod { get; set; } + + // [JsonPropertyName("tax_mode")] + // public TaxMode TaxMode { get; set; } + + // [JsonPropertyName("unit_price")] + // public UnitPrice UnitPrice { get; set; } + + // [JsonPropertyName("unit_price_overrides")] + // public object[] UnitPriceOverrides { get; set; } + + // [JsonPropertyName("custom_data")] + // public object CustomData { get; set; } + + // [JsonPropertyName("quantity")] + // public Quantity Quantity { get; set; } + + // [JsonPropertyName("status")] + // public Status Status { get; set; } + + // [JsonPropertyName("created_at")] + // public DateTimeOffset CreatedAt { get; set; } + + // [JsonPropertyName("updated_at")] + // public DateTimeOffset UpdatedAt { get; set; } + + // [JsonPropertyName("import_meta")] + // public object ImportMeta { get; set; } + } + + // public partial class Quantity + // { + // [JsonPropertyName("minimum")] + // public long Minimum { get; set; } + + // [JsonPropertyName("maximum")] + // public long Maximum { get; set; } + // } + + // public partial class UnitPrice + // { + // [JsonPropertyName("amount")] + // [JsonConverter(typeof(ParseStringConverter))] + // public long Amount { get; set; } + + // [JsonPropertyName("currency_code")] + // public CurrencyCode CurrencyCode { get; set; } + // } + + // public partial class Payment + // { + // [JsonPropertyName("payment_attempt_id")] + // public Guid PaymentAttemptId { get; set; } + + // [JsonPropertyName("stored_payment_method_id")] + // public Guid StoredPaymentMethodId { get; set; } + + // [JsonPropertyName("payment_method_id")] + // public string PaymentMethodId { get; set; } + + // [JsonPropertyName("amount")] + // [JsonConverter(typeof(ParseStringConverter))] + // public long Amount { get; set; } + + // [JsonPropertyName("status")] + // public string Status { get; set; } + + // [JsonPropertyName("error_code")] + // public string ErrorCode { get; set; } + + // [JsonPropertyName("method_details")] + // public MethodDetails MethodDetails { get; set; } + + // [JsonPropertyName("created_at")] + // public DateTimeOffset CreatedAt { get; set; } + + // [JsonPropertyName("captured_at")] + // public DateTimeOffset? CapturedAt { get; set; } + // } + + // public partial class MethodDetails + // { + // [JsonPropertyName("type")] + // public string Type { get; set; } + + // [JsonPropertyName("card")] + // public Card Card { get; set; } + // } + + // public partial class Card + // { + // [JsonPropertyName("type")] + // public string Type { get; set; } + + // [JsonPropertyName("last4")] + // public string Last4 { get; set; } + + // [JsonPropertyName("expiry_month")] + // public long ExpiryMonth { get; set; } + + // [JsonPropertyName("expiry_year")] + // public long ExpiryYear { get; set; } + + // [JsonPropertyName("cardholder_name")] + // public string CardholderName { get; set; } + // } + + public partial class Meta + { + [JsonPropertyName("pagination")] + public Pagination Pagination { get; set; } + } + + public partial class Pagination + { + [JsonPropertyName("per_page")] + public long PerPage { get; set; } + + [JsonPropertyName("next")] + public Uri Next { get; set; } + + [JsonPropertyName("has_more")] + public bool HasMore { get; set; } + + [JsonPropertyName("estimated_total")] + public long EstimatedTotal { get; set; } + } +} diff --git a/Streetwriters.Common/Models/Response.cs b/Streetwriters.Common/Models/Response.cs index a6184e8..b7f4f4f 100644 --- a/Streetwriters.Common/Models/Response.cs +++ b/Streetwriters.Common/Models/Response.cs @@ -17,6 +17,7 @@ You should have received a copy of the Affero GNU General Public License along with this program. If not, see . */ +using System.Net.Http; using System.Runtime.Serialization; using Newtonsoft.Json; using Streetwriters.Common.Interfaces; @@ -28,5 +29,7 @@ namespace Streetwriters.Common.Models [JsonIgnore] public bool Success { get; set; } public int StatusCode { get; set; } + [JsonIgnore] + public HttpContent Content { get; set; } } } diff --git a/Streetwriters.Common/Models/SubscriptionPreviewResponse.cs b/Streetwriters.Common/Models/SubscriptionPreviewResponse.cs new file mode 100644 index 0000000..444d82d --- /dev/null +++ b/Streetwriters.Common/Models/SubscriptionPreviewResponse.cs @@ -0,0 +1,60 @@ +namespace Streetwriters.Common.Models +{ + using System; + using System.Collections.Generic; + + using System.Text.Json; + using System.Text.Json.Serialization; + using System.Globalization; + + public partial class SubscriptionPreviewResponse + { + [JsonPropertyName("data")] + public SubscriptionPreviewData Data { get; set; } + + [JsonPropertyName("meta")] + public Meta Meta { get; set; } + } + + public partial class SubscriptionPreviewData + { + [JsonPropertyName("currency_code")] + public string CurrencyCode { get; set; } + + [JsonPropertyName("billing_cycle")] + public BillingCycle BillingCycle { get; set; } + + [JsonPropertyName("update_summary")] + public UpdateSummary UpdateSummary { get; set; } + + [JsonPropertyName("immediate_transaction")] + public TransactionV2 ImmediateTransaction { get; set; } + + [JsonPropertyName("next_transaction")] + public TransactionV2 NextTransaction { get; set; } + + [JsonPropertyName("recurring_transaction_details")] + public Details RecurringTransactionDetails { get; set; } + } + + public partial class UpdateSummary + { + [JsonPropertyName("charge")] + public UpdateSummaryItem Charge { get; set; } + + [JsonPropertyName("credit")] + public UpdateSummaryItem Credit { get; set; } + + [JsonPropertyName("result")] + public UpdateSummaryItem Result { get; set; } + } + + public partial class UpdateSummaryItem + { + [JsonPropertyName("amount")] + public long Amount { get; set; } + + [JsonPropertyName("action")] + public string? Action { get; set; } + } +} diff --git a/Streetwriters.Common/Services/PaddleBillingService.cs b/Streetwriters.Common/Services/PaddleBillingService.cs new file mode 100644 index 0000000..06e559e --- /dev/null +++ b/Streetwriters.Common/Services/PaddleBillingService.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.WebUtilities; +using Streetwriters.Common.Models; + +namespace Streetwriters.Common.Services +{ + public class PaddleBillingService + { +#if DEBUG + private const string PADDLE_BASE_URI = "https://sandbox-api.paddle.com"; +#else + private const string PADDLE_BASE_URI = "https://api.paddle.com"; +#endif + private readonly HttpClient httpClient = new(); + public PaddleBillingService(string paddleApiKey) + { + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", paddleApiKey); + } + + public async Task GetSubscriptionAsync(string subscriptionId) + { + var url = $"{PADDLE_BASE_URI}/subscriptions/{subscriptionId}"; + var response = await httpClient.GetAsync(url); + return await response.Content.ReadFromJsonAsync(); + } + + public async Task GetTransactionAsync(string transactionId) + { + var url = $"{PADDLE_BASE_URI}/transactions/{transactionId}"; + var response = await httpClient.GetAsync(url); + return await response.Content.ReadFromJsonAsync(); + } + + public async Task GetTransactionInvoiceAsync(string transactionId) + { + var url = $"{PADDLE_BASE_URI}/transactions/{transactionId}/invoice"; + var response = await httpClient.GetAsync(url); + return await response.Content.ReadFromJsonAsync(); + } + + public async Task ListTransactionsAsync(string? subscriptionId = null, string? customerId = null, string[]? status = null, string[]? origin = null) + { + var url = $"{PADDLE_BASE_URI}/transactions"; + var parameters = new Dictionary() + { + { "subscription_id", subscriptionId }, + { "customer_id", customerId }, + { "status", string.Join(',', status ?? ["billed","completed"]) }, + { "order_by", "billed_at[DESC]" } + }; + if (origin is not null) parameters.Add("origin", string.Join(',', origin)); + var response = await httpClient.GetAsync(QueryHelpers.AddQueryString(url, parameters)); + + return await response.Content.ReadFromJsonAsync(); + } + + public async Task RefundTransactionAsync(string transactionId, string transactionItemId, string reason = "") + { + var url = $"{PADDLE_BASE_URI}/adjustments"; + var response = await httpClient.PostAsync(url, JsonContent.Create(new Dictionary + { + { "action", "refund" }, + { + "items", + new object[] + { + new Dictionary { + {"item_id", transactionItemId}, + {"type", "full"} + } + } + }, + { "reason", reason }, + { "transaction_id", transactionId } + })); + return response.IsSuccessStatusCode; + } + + public async Task PreviewSubscriptionChangeAsync(string subscriptionId, string newProductId) + { + var url = $"{PADDLE_BASE_URI}/subscriptions/{subscriptionId}/preview"; + var response = await httpClient.PatchAsync(url, JsonContent.Create(new + { + proration_billing_mode = "prorated_immediately", + items = new[] { new { price_id = newProductId, quantity = 1 } } + })); + return await response.Content.ReadFromJsonAsync(); + } + + public async Task ChangeSubscriptionAsync(string subscriptionId, string newProductId) + { + var url = $"{PADDLE_BASE_URI}/subscriptions/{subscriptionId}"; + var response = await httpClient.PatchAsync(url, JsonContent.Create(new + { + proration_billing_mode = "prorated_immediately", + items = new[] { new { price_id = newProductId, quantity = 1 } } + })); + return response.IsSuccessStatusCode; + } + + public async Task CancelSubscriptionAsync(string subscriptionId) + { + var url = $"{PADDLE_BASE_URI}/subscriptions/{subscriptionId}/cancel"; + var response = await httpClient.PostAsync(url, JsonContent.Create(new { effective_from = "immediately" })); + return response.IsSuccessStatusCode; + } + + public async Task PauseSubscriptionAsync(string subscriptionId) + { + var url = $"{PADDLE_BASE_URI}/subscriptions/{subscriptionId}/pause"; + var response = await httpClient.PostAsync(url, JsonContent.Create(new { })); + return response.IsSuccessStatusCode; + } + + public async Task ResumeSubscriptionAsync(string subscriptionId) + { + var url = $"{PADDLE_BASE_URI}/subscriptions/{subscriptionId}"; + var response = await httpClient.PatchAsync(url, JsonContent.Create(new Dictionary + { + {"scheduled_change", null} + })); + return response.IsSuccessStatusCode; + } + } +} \ No newline at end of file