From a31ae32b5cb2e32e9760ff335f96aec9ef2836d0 Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Wed, 8 Jan 2025 13:55:23 +0100 Subject: [PATCH 1/9] Add Billing Meter Events --- README.md | 1 + .../Billing/Meter Events/MeterEvent.swift | 44 +++++++++++ .../Meter Events/MeterEventRoutes.swift | 62 +++++++++++++++ Sources/StripeKit/StripeClient.swift | 76 +++++++++++-------- 4 files changed, 153 insertions(+), 30 deletions(-) create mode 100644 Sources/StripeKit/Billing/Meter Events/MeterEvent.swift create mode 100644 Sources/StripeKit/Billing/Meter Events/MeterEventRoutes.swift diff --git a/README.md b/README.md index ac376e93..fdc7b6d6 100644 --- a/README.md +++ b/README.md @@ -339,6 +339,7 @@ extension StripeSignatureError: AbortError { * [x] Subscription Schedule * [x] Test Clocks * [x] Usage Records +* [x] Meter Events --- ### Connect * [x] Account diff --git a/Sources/StripeKit/Billing/Meter Events/MeterEvent.swift b/Sources/StripeKit/Billing/Meter Events/MeterEvent.swift new file mode 100644 index 00000000..3477cfc5 --- /dev/null +++ b/Sources/StripeKit/Billing/Meter Events/MeterEvent.swift @@ -0,0 +1,44 @@ +// +// MeterEvent.swift +// stripe-kit +// +// Created by TelemetryDeck on 08.01.25. +// + +import Foundation + +/// The [Meter Even Object](https://docs.stripe.com/api/billing/meter-event/object). +public struct MeterEvent: Codable { + /// String representing the object’s type. Objects of the same type share the same value. + public var object: String + /// Time at which the object was created. Measured in seconds since the Unix epoch. + public var created: Date? + /// The name of the meter event. Corresponds with the `event_name` field on a meter. + public var eventName: String + /// A unique identifier for the event. + public var identifier: String + /// Has the value `true` if the object exists in live mode or the value `false` if the object exists in test mode. + public var livemode: Bool + /// The payload of the event. This contains the fields corresponding to a meter’s `customer_mapping.event_payload_key` (default is `stripe_customer_id`) and `value_settings.event_payload_key` (default is `value`). Read more about the [payload](https://docs.stripe.com/billing/subscriptions/usage-based/recording-usage#payload-key-overrides). + public var payload: [String: String] + /// The timestamp passed in when creating the event. Measured in seconds since the Unix epoch. + public var timestamp: Date + + public init( + object: String, + created: Date? = nil, + eventName: String, + identifier: String, + livemode: Bool, + payload: [String: String], + timestamp: Date + ) { + self.object = object + self.created = created + self.eventName = eventName + self.identifier = identifier + self.livemode = livemode + self.payload = payload + self.timestamp = timestamp + } +} diff --git a/Sources/StripeKit/Billing/Meter Events/MeterEventRoutes.swift b/Sources/StripeKit/Billing/Meter Events/MeterEventRoutes.swift new file mode 100644 index 00000000..6a00be90 --- /dev/null +++ b/Sources/StripeKit/Billing/Meter Events/MeterEventRoutes.swift @@ -0,0 +1,62 @@ +// +// MeterEventRoutes.swift +// stripe-kit +// +// Created by TelemetryDeck on 08.01.25. +// + +import Foundation +import NIO +import NIOHTTP1 + +public protocol MeterEventRoutes: StripeAPIRoute { + /// Creates a billing meter event. + /// + /// - Parameters: + /// - event_name: The name of the meter event. Corresponds with the `event_name` field on a meter. + /// - payload: The payload of the event. This contains the fields corresponding to a meter’s `customer_mapping.event_payload_key` (default is `stripe_customer_id`) and `value_settings.event_payload_key` (default is `value`). Read more about the [payload](https://docs.stripe.com/billing/subscriptions/usage-based/recording-usage#payload-key-overrides). + /// - identifier: A unique identifier for the event. + /// - timestamp: The timestamp passed in when creating the event. Measured in seconds since the Unix epoch. Must be within the past 35 calendar days or up to 5 minutes in the future. Defaults to current timestamp if not specified. + func create( + event_name: String, + payload: [String: String], + identifier: String?, + timestamp: Date?) async throws -> MeterEvent +} + +public struct StripeMeterEventRoutes: MeterEventRoutes { + public var headers: HTTPHeaders = [:] + + private let apiHandler: StripeAPIHandler + private let meterevents = APIBase + APIVersion + "billing/meter_events" + + init(apiHandler: StripeAPIHandler) { + self.apiHandler = apiHandler + } + + public func create( + event_name: String, + payload: [String: String], + identifier: String?, + timestamp: Date?) async throws -> MeterEvent + { + var body: [String: Any] = [ + "event_name": event_name, + "payload": payload + ] + + if let identifier { + body["identifier"] = identifier + } + + if let timestamp { + body["timestamp"] = Int(timestamp.timeIntervalSince1970) + } + + return try await apiHandler.send( + method: .POST, + path: meterevents, + body: .string(body.queryParameters), + headers: headers) + } +} diff --git a/Sources/StripeKit/StripeClient.swift b/Sources/StripeKit/StripeClient.swift index e71c3d70..d32a8a68 100644 --- a/Sources/StripeKit/StripeClient.swift +++ b/Sources/StripeKit/StripeClient.swift @@ -5,11 +5,12 @@ // Created by Andrew Edwards on 4/30/19. // -import NIO import AsyncHTTPClient +import NIO public final class StripeClient { // MARK: - CORE RESOURCES + public var balances: BalanceRoutes public var balanceTransactions: BalanceTransactionRoutes public var charges: ChargeRoutes @@ -26,21 +27,25 @@ public final class StripeClient { public var refunds: RefundRoutes public var tokens: TokenRoutes public var ephemeralKeys: EphemeralKeyRoutes - + // MARK: - PAYMENT METHODS + public var paymentMethods: PaymentMethodRoutes public var bankAccounts: BankAccountRoutes public var cashBalances: CashBalanceRoutes public var cards: CardRoutes // public var sources: SourceRoutes - + // MARK: - CHECKOUT + public var sessions: SessionRoutes - + // MARK: - PaymentLink + public var paymentLinks: PaymentLinkRoutes - + // MARK: - Products + public var products: ProductRoutes public var prices: PriceRoutes public var coupons: CouponRoutes @@ -49,8 +54,9 @@ public final class StripeClient { public var taxCodes: TaxCodeRoutes public var taxRates: TaxRateRoutes public var shippingRates: ShippingRateRoutes - + // MARK: - BILLING + public var creditNotes: CreditNoteRoutes public var customerBalanceTransactions: CustomerBalanceTransactionRoutes public var portalSession: PortalSessionRoutes @@ -66,8 +72,10 @@ public final class StripeClient { public var quoteLineItems: QuoteLineItemRoutes public var quotes: QuoteRoutes public var testClocks: TestClockRoutes - + public var meterEvents: MeterEventRoutes + // MARK: - CONNECT + public var connectAccounts: AccountRoutes public var accountSessions: AccountSessionRoutes public var accountLinks: AccountLinkRoutes @@ -81,22 +89,25 @@ public final class StripeClient { public var transfers: TransferRoutes public var transferReversals: TransferReversalRoutes public var secretManager: SecretRoutes - + // MARK: - FRAUD + public var earlyFraudWarnings: EarlyFraudWarningRoutes public var reviews: ReviewRoutes public var valueLists: ValueListRoutes public var valueListItems: ValueListItemRoutes - + // MARK: - ISSUING + public var authorizations: AuthorizationRoutes public var cardholders: CardholderRoutes public var issuingCards: IssuingCardRoutes public var issuingDisputes: IssuingDisputeRoutes public var fundingInstructions: FundingInstructionsRoutes public var transactions: TransactionRoutes - + // MARK: - TERMINAL + public var terminalConnectionTokens: TerminalConnectionTokenRoutes public var terminalLocations: TerminalLocationRoutes public var terminalReaders: TerminalReaderRoutes @@ -105,29 +116,33 @@ public final class StripeClient { public var terminalHardwareSkus: TerminalHardwareSKURoutes public var terminalHardwareShippingMethods: TerminalHardwareShippingMethodRoutes public var terminalConfiguration: TerminalConfigurationRoutes - + // MARK: - SIGMA + public var scheduledQueryRuns: ScheduledQueryRunRoutes // MARK: - REPORTING + public var reportRuns: ReportRunRoutes public var reportTypes: ReportTypeRoutes - + // MARK: - IDENTITY + public var verificationSessions: VerificationSessionRoutes public var verificationReports: VerificationReportRoutes - + // MARK: - WEBHOOKS + public var webhookEndpoints: WebhookEndpointRoutes - + var handler: StripeAPIHandler - + /// Returns a StripeClient used to interact with the Stripe APIs. /// - Parameter httpClient: An `HTTPClient`used to communicate wiith the Stripe API /// - Parameter apiKey: A Stripe API key. public init(httpClient: HTTPClient, apiKey: String) { handler = StripeAPIHandler(httpClient: httpClient, apiKey: apiKey) - + balances = StripeBalanceRoutes(apiHandler: handler) balanceTransactions = StripeBalanceTransactionRoutes(apiHandler: handler) charges = StripeChargeRoutes(apiHandler: handler) @@ -144,17 +159,17 @@ public final class StripeClient { refunds = StripeRefundRoutes(apiHandler: handler) tokens = StripeTokenRoutes(apiHandler: handler) ephemeralKeys = StripeEphemeralKeyRoutes(apiHandler: handler) - + paymentMethods = StripePaymentMethodRoutes(apiHandler: handler) bankAccounts = StripeBankAccountRoutes(apiHandler: handler) cashBalances = StripeCashBalanceRoutes(apiHandler: handler) cards = StripeCardRoutes(apiHandler: handler) // sources = StripeSourceRoutes(apiHandler: handler) - + sessions = StripeSessionRoutes(apiHandler: handler) - + paymentLinks = StripePaymentLinkRoutes(apiHandler: handler) - + products = StripeProductRoutes(apiHandler: handler) prices = StripePriceRoutes(apiHandler: handler) coupons = StripeCouponRoutes(apiHandler: handler) @@ -163,7 +178,7 @@ public final class StripeClient { taxCodes = StripeTaxCodeRoutes(apiHandler: handler) taxRates = StripeTaxRateRoutes(apiHandler: handler) shippingRates = StripeShippingRateRoutes(apiHandler: handler) - + creditNotes = StripeCreditNoteRoutes(apiHandler: handler) customerBalanceTransactions = StripeCustomerBalanceTransactionRoutes(apiHandler: handler) portalSession = StripePortalSessionRoutes(apiHandler: handler) @@ -179,9 +194,10 @@ public final class StripeClient { quoteLineItems = StripeQuoteLineItemRoutes(apiHandler: handler) quotes = StripeQuoteRoutes(apiHandler: handler) testClocks = StripeTestClockRoutes(apiHandler: handler) - + meterEvents = StripeMeterEventRoutes(apiHandler: handler) + connectAccounts = StripeConnectAccountRoutes(apiHandler: handler) - accountSessions = StripeAccountSessionsRoutes(apiHandler: handler) + accountSessions = StripeAccountSessionsRoutes(apiHandler: handler) accountLinks = StripeAccountLinkRoutes(apiHandler: handler) applicationFees = StripeApplicationFeeRoutes(apiHandler: handler) applicationFeeRefunds = StripeApplicationFeeRefundRoutes(apiHandler: handler) @@ -193,19 +209,19 @@ public final class StripeClient { transfers = StripeTransferRoutes(apiHandler: handler) transferReversals = StripeTransferReversalRoutes(apiHandler: handler) secretManager = StripeSecretRoutes(apiHandler: handler) - + earlyFraudWarnings = StripeEarlyFraudWarningRoutes(apiHandler: handler) reviews = StripeReviewRoutes(apiHandler: handler) valueLists = StripeValueListRoutes(apiHandler: handler) valueListItems = StripeValueListItemRoutes(apiHandler: handler) - + authorizations = StripeAuthorizationRoutes(apiHandler: handler) cardholders = StripeCardholderRoutes(apiHandler: handler) issuingCards = StripeIssuingCardRoutes(apiHandler: handler) issuingDisputes = StripeIssuingDisputeRoutes(apiHandler: handler) fundingInstructions = StripeFundingInstructionsRoutes(apiHandler: handler) transactions = StripeTransactionRoutes(apiHandler: handler) - + terminalConnectionTokens = StripeTerminalConnectionTokenRoutes(apiHandler: handler) terminalLocations = StripeTerminalLocationRoutes(apiHandler: handler) terminalReaders = StripeTerminalReaderRoutes(apiHandler: handler) @@ -214,15 +230,15 @@ public final class StripeClient { terminalHardwareSkus = StripeTerminalHardwareSKURoutes(apiHandler: handler) terminalHardwareShippingMethods = StripeTerminalHardwareShippingMethodRoutes(apiHandler: handler) terminalConfiguration = StripeTerminalConfigurationRoutes(apiHandler: handler) - + scheduledQueryRuns = StripeScheduledQueryRunRoutes(apiHandler: handler) - + reportRuns = StripeReportRunRoutes(apiHandler: handler) reportTypes = StripeReportTypeRoutes(apiHandler: handler) - + verificationSessions = StripeVerificationSessionRoutes(apiHandler: handler) verificationReports = StripeVerificationReportRoutes(apiHandler: handler) - + webhookEndpoints = StripeWebhookEndpointRoutes(apiHandler: handler) } } From 10861007910b4e0d640a4f134036e9fd48e906be Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Wed, 8 Jan 2025 15:28:51 +0100 Subject: [PATCH 2/9] Add price.recurring.meter field --- Sources/StripeKit/Products/Prices/Price.swift | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/Sources/StripeKit/Products/Prices/Price.swift b/Sources/StripeKit/Products/Prices/Price.swift index 3a956ce3..a6eeb61b 100644 --- a/Sources/StripeKit/Products/Prices/Price.swift +++ b/Sources/StripeKit/Products/Prices/Price.swift @@ -1,6 +1,6 @@ // // Price.swift -// +// // // Created by Andrew Edwards on 7/19/20. // @@ -51,11 +51,11 @@ public struct Price: Codable { public var transformQuantity: PriceTransformQuantity? /// The unit amount in cents to be charged, represented as a decimal string with at most 12 decimal places. public var unitAmountDecimal: String? - + public init(id: String, active: Bool? = nil, currency: Currency? = nil, - metadata: [String : String]? = nil, + metadata: [String: String]? = nil, nickname: String? = nil, product: String? = nil, recurring: PriceRecurring? = nil, @@ -72,7 +72,8 @@ public struct Price: Codable { tiers: [PriceTier]? = nil, tiersMode: PriceTierMode? = nil, transformQuantity: PriceTransformQuantity? = nil, - unitAmountDecimal: String? = nil) { + unitAmountDecimal: String? = nil) + { self.id = id self.active = active self.currency = currency @@ -106,18 +107,23 @@ public struct PriceRecurring: Codable { public var intervalCount: Int? /// Configures how the quantity per period should be determined. Can be either `metered` or `licensed`. `licensed` automatically bills the `quantity` set when adding it to a subscription. `metered` aggregates the total usage based on usage records. Defaults to `licensed`. public var usageType: PlanUsageType? - + /// The meter tracking the usage of a metered price + public var meter: String? + public init(aggregateUsage: PriceRecurringAggregateUsage? = nil, interval: PlanInterval? = nil, intervalCount: Int? = nil, - usageType: PlanUsageType? = nil) { + usageType: PlanUsageType? = nil, + meter: String? = nil) + { self.aggregateUsage = aggregateUsage self.interval = interval self.intervalCount = intervalCount self.usageType = usageType + self.meter = meter } } - + public enum PriceRecurringAggregateUsage: String, Codable { case sum case lastDuringPeriod = "last_during_period" @@ -146,12 +152,13 @@ public struct PriceCurrencyOption: Codable { public var unitAmount: Int? /// The unit amount in cents to be charged, represented as a decimal string with at most 12 decimal places. Only set if `billing_scheme=per_unit`. public var unitAmountDecimal: String? - + public init(customUnitAmount: PriceCurrencyOptionCustomUnitAmount? = nil, taxBehavior: PriceTaxBehavior? = nil, tiers: [PriceTier]? = nil, unitAmount: Int? = nil, - unitAmountDecimal: String? = nil) { + unitAmountDecimal: String? = nil) + { self.customUnitAmount = customUnitAmount self.taxBehavior = taxBehavior self.tiers = tiers @@ -167,10 +174,11 @@ public struct PriceCurrencyOptionCustomUnitAmount: Codable { public var minimum: Int? /// The starting unit amount which can be updated by the customer. public var preset: Int? - + public init(maximum: Int? = nil, minimum: Int? = nil, - preset: Int? = nil) { + preset: Int? = nil) + { self.maximum = maximum self.minimum = minimum self.preset = preset @@ -194,12 +202,13 @@ public struct PriceTier: Codable { public var unitAmountDecimal: String? /// Up to and including to this quantity will be contained in the tier. public var upTo: Int? - + public init(flatAmount: Int? = nil, flatAmountDecimal: String? = nil, unitAmount: Int? = nil, unitAmountDecimal: String? = nil, - upTo: Int? = nil) { + upTo: Int? = nil) + { self.flatAmount = flatAmount self.flatAmountDecimal = flatAmountDecimal self.unitAmount = unitAmount @@ -215,10 +224,11 @@ public struct PriceCustomUnitAmount: Codable { public var minimum: Int? /// The starting unit amount which can be updated by the customer. public var preset: Int? - + public init(maximum: Int? = nil, minimum: Int? = nil, - preset: Int? = nil) { + preset: Int? = nil) + { self.maximum = maximum self.minimum = minimum self.preset = preset @@ -235,9 +245,10 @@ public struct PriceTransformQuantity: Codable { public var divideBy: Int? /// After division, either round the result `up` or `down`. public var round: PriceTransformQuantityRound? - + public init(divideBy: Int? = nil, - round: PriceTransformQuantityRound? = nil) { + round: PriceTransformQuantityRound? = nil) + { self.divideBy = divideBy self.round = round } @@ -261,13 +272,14 @@ public struct PriceSearchResult: Codable { public var nextPage: String? /// The total count of entries in the search result, not just the current page. public var totalCount: Int? - + public init(object: String, data: [Price]? = nil, hasMore: Bool? = nil, url: String? = nil, nextPage: String? = nil, - totalCount: Int? = nil) { + totalCount: Int? = nil) + { self.object = object self.data = data self.hasMore = hasMore @@ -277,17 +289,17 @@ public struct PriceSearchResult: Codable { } } - public struct PriceList: Codable { public var object: String public var hasMore: Bool? public var url: String? public var data: [Price]? - + public init(object: String, hasMore: Bool? = nil, url: String? = nil, - data: [Price]? = nil) { + data: [Price]? = nil) + { self.object = object self.hasMore = hasMore self.url = url From c44df15932537b075bb6499d6c612e51b629d772 Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Wed, 8 Jan 2025 16:59:13 +0100 Subject: [PATCH 3/9] Add Meters --- README.md | 1 + Sources/StripeKit/Billing/Meters/Meter.swift | 83 ++++++++++ .../Billing/Meters/MeterRoutes.swift | 151 ++++++++++++++++++ Sources/StripeKit/StripeClient.swift | 2 + 4 files changed, 237 insertions(+) create mode 100644 Sources/StripeKit/Billing/Meters/Meter.swift create mode 100644 Sources/StripeKit/Billing/Meters/MeterRoutes.swift diff --git a/README.md b/README.md index fdc7b6d6..21f9d495 100644 --- a/README.md +++ b/README.md @@ -339,6 +339,7 @@ extension StripeSignatureError: AbortError { * [x] Subscription Schedule * [x] Test Clocks * [x] Usage Records +* [x] Meters * [x] Meter Events --- ### Connect diff --git a/Sources/StripeKit/Billing/Meters/Meter.swift b/Sources/StripeKit/Billing/Meters/Meter.swift new file mode 100644 index 00000000..7733137f --- /dev/null +++ b/Sources/StripeKit/Billing/Meters/Meter.swift @@ -0,0 +1,83 @@ +// +// Meter.swift +// stripe-kit +// +// Created by TelemetryDeck on 08.01.25. +// + +import Foundation + +/// The Meter Object +public struct Meter: Codable { + /// Unique identifier for the object. + public var id: String + /// String representing the object’s type. Objects of the same type share the same value. + public var object: String + /// Time at which the object was created. Measured in seconds since the Unix epoch. + public var created: Date? + /// Fields that specify how to map a meter event to a customer. + public var customerMapping: MeterCustomerMapping + /// The meter’s name. + public var displayName: String + /// The name of the meter event to record usage for. Corresponds with the `event_name` field on meter events. + public var eventName: String + /// The time window to pre-aggregate meter events for, if any. + public var eventTimeWindow: MeterEventTimeWindow? + /// Has the value `true` if the object exists in live mode or the value `false` if the object exists in test mode. + public var liveMode: Bool + /// The meter’s status. + public var status: MeterStatus + /// The timestamps at which the meter status changed. + public var meterStatusTransitions: MeterStatusTransitions + /// Time at which the object was last updated. Measured in seconds since the Unix epoch. + public var updated: Date + /// Fields that specify how to calculate a meter event’s value. + public var valueSettings: MeterValueSettings +} + +public struct MeterCustomerMapping: Codable { + /// The key in the meter event payload to use for mapping the event to a customer. + public var eventPayloadKey: String + /// The method for mapping a meter event to a customer. + public var type: MeterCustomerMappingType +} + +public enum MeterCustomerMappingType: String, Codable { + /// Map a meter event to a customer by passing a customer ID in the event’s payload. + case byID = "by_id" +} + +public enum MeterEventTimeWindow: String, Codable { + /// Events are pre-aggregated in daily buckets. + case day + /// Events are pre-aggregated in hourly buckets. + case hour +} + +public enum MeterStatus: String, Codable { + /// The meter is active. + case active + /// The meter is inactive. No more events for this meter will be accepted. The meter cannot be attached to a price. + case inactive +} + +public struct MeterStatusTransitions: Codable { + /// The time the meter was deactivated, if any. Measured in seconds since Unix epoch. + public var deactivatedAt: Date? +} + +public struct MeterValueSettings: Codable { + /// The key in the meter event payload to use as the value for this meter. + public var eventPayloadKey: String +} + +public struct MeterDefaultAggregation: Codable { + public var formula: MeterDefaultAggregationFormula +} + +public enum MeterDefaultAggregationFormula: String, Codable { + /// Count the number of events. + case count + /// Sum each event’s value. + case sum +} diff --git a/Sources/StripeKit/Billing/Meters/MeterRoutes.swift b/Sources/StripeKit/Billing/Meters/MeterRoutes.swift new file mode 100644 index 00000000..6b4dadc6 --- /dev/null +++ b/Sources/StripeKit/Billing/Meters/MeterRoutes.swift @@ -0,0 +1,151 @@ +// +// MeterRoutes.swift +// stripe-kit +// +// Created by TelemetryDeck on 08.01.25. +// + +import Foundation +import NIO +import NIOHTTP1 + +/// Meters specify how to aggregate meter events over a billing period. Meter events represent the actions that customers take in your system. Meters attach to prices and form the basis of the bill. +/// +/// Related guide: [Usage based billing](https://docs.stripe.com/billing/subscriptions/usage-based). +public protocol MeterRoutes: StripeAPIRoute { + /// Creates a billing meter. + /// + /// - Parameters: + /// - defaultAggregation: The default settings to aggregate a meter’s events with. + /// - displayName: The meter’s name. Not visible to the customer. + /// - eventName: The name of the meter event to record usage for. Corresponds with the `event_name` field on meter events. + /// - customerMapping: Fields that specify how to map a meter event to a customer. + func create( + defaultAggregation: MeterDefaultAggregation, + displayName: String, + eventName: String, + customerMapping: MeterCustomerMapping?, + eventTimeWindow: MeterEventTimeWindow?, + valueSettings: MeterValueSettings? + ) async throws -> Meter + + /// Updates a billing meter. + /// + /// - Parameters: + /// - id: Unique identifier for the object. + /// - displayName: The meter’s name. Not visible to the customer. + func update( + id: String, + displayName: String? + ) async throws -> Meter + + /// Retrieves a billing meter. + /// + /// - Parameters: + /// - id: Unique identifier for the object. + func retrieve(id: String) async throws -> Meter + + /// Returns a list of your billing meters. + func listAll() async throws -> [Meter] + + /// Deactivates a billing meter. + /// + /// - Parameters: + /// - id: Unique identifier for the object. + func deactivate(id: String) async throws -> Meter + + /// Reactivates a billing meter. + /// + /// - Parameters: + /// - id: Unique identifier for the object. + func reactivate(id: String) async throws -> Meter +} + +public struct StripeMeterRoutes: MeterRoutes { + public var headers: HTTPHeaders = [:] + + private let apiHandler: StripeAPIHandler + private let meters = APIBase + APIVersion + "billing/meters" + + init(apiHandler: StripeAPIHandler) { + self.apiHandler = apiHandler + } + + public func create( + defaultAggregation: MeterDefaultAggregation, + displayName: String, + eventName: String, + customerMapping: MeterCustomerMapping?, + eventTimeWindow: MeterEventTimeWindow?, + valueSettings: MeterValueSettings? + ) async throws -> Meter { + var body: [String: Any] = [ + "default_aggregation[formula]": defaultAggregation.formula.rawValue, + "display_name": displayName, + "event_name": eventName + ] + + if let customerMapping = customerMapping { + body["customer_mapping[event_payload_key]"] = customerMapping.eventPayloadKey + body["customer_mapping[type]"] = customerMapping.type.rawValue + } + + if let eventTimeWindow = eventTimeWindow { + body["event_time_window"] = eventTimeWindow.rawValue + } + + if let valueSettings = valueSettings { + body["value_settings[event_payload_key]"] = valueSettings.eventPayloadKey + } + + return try await apiHandler.send( + method: .POST, + path: meters, + body: .string(body.queryParameters), + headers: headers + ) + } + + public func update(id: String, displayName: String?) async throws -> Meter { + var body: [String: Any] = [:] + + if let displayName = displayName { + body["display_name"] = displayName + } + + return try await apiHandler.send( + method: .POST, + path: "\(meters)/\(id)", + body: .string(body.queryParameters), + headers: headers + ) + } + + public func retrieve(id: String) async throws -> Meter { + return try await apiHandler.send(method: .GET, path: "\(meters)/\(id)", headers: headers) + } + + public func listAll() async throws -> [Meter] { + return try await apiHandler.send( + method: .GET, + path: meters, + headers: headers + ) + } + + public func deactivate(id: String) async throws -> Meter { + return try await apiHandler.send( + method: .POST, + path: "\(meters)/\(id)/deactivate", + headers: headers + ) + } + + public func reactivate(id: String) async throws -> Meter { + return try await apiHandler.send( + method: .POST, + path: "\(meters)/\(id)/reactivate", + headers: headers + ) + } +} diff --git a/Sources/StripeKit/StripeClient.swift b/Sources/StripeKit/StripeClient.swift index d32a8a68..5c281e8b 100644 --- a/Sources/StripeKit/StripeClient.swift +++ b/Sources/StripeKit/StripeClient.swift @@ -72,6 +72,7 @@ public final class StripeClient { public var quoteLineItems: QuoteLineItemRoutes public var quotes: QuoteRoutes public var testClocks: TestClockRoutes + public var meters: MeterRoutes public var meterEvents: MeterEventRoutes // MARK: - CONNECT @@ -194,6 +195,7 @@ public final class StripeClient { quoteLineItems = StripeQuoteLineItemRoutes(apiHandler: handler) quotes = StripeQuoteRoutes(apiHandler: handler) testClocks = StripeTestClockRoutes(apiHandler: handler) + meters = StripeMeterRoutes(apiHandler: handler) meterEvents = StripeMeterEventRoutes(apiHandler: handler) connectAccounts = StripeConnectAccountRoutes(apiHandler: handler) From 83621dbcacc912ae9f458843b2ee6064ade3fc90 Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Wed, 8 Jan 2025 17:01:56 +0100 Subject: [PATCH 4/9] Meter.liveMode seems to be optional --- Sources/StripeKit/Billing/Meters/Meter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/StripeKit/Billing/Meters/Meter.swift b/Sources/StripeKit/Billing/Meters/Meter.swift index 7733137f..030c7970 100644 --- a/Sources/StripeKit/Billing/Meters/Meter.swift +++ b/Sources/StripeKit/Billing/Meters/Meter.swift @@ -24,7 +24,7 @@ public struct Meter: Codable { /// The time window to pre-aggregate meter events for, if any. public var eventTimeWindow: MeterEventTimeWindow? /// Has the value `true` if the object exists in live mode or the value `false` if the object exists in test mode. - public var liveMode: Bool + public var liveMode: Bool? /// The meter’s status. public var status: MeterStatus /// The timestamps at which the meter status changed. From 8274976cdd2ada5a8254865251fc6a345376de2e Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Wed, 8 Jan 2025 17:03:23 +0100 Subject: [PATCH 5/9] Meter.meterStatusTransitions seems to be optional --- Sources/StripeKit/Billing/Meters/Meter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/StripeKit/Billing/Meters/Meter.swift b/Sources/StripeKit/Billing/Meters/Meter.swift index 030c7970..e6afe8f9 100644 --- a/Sources/StripeKit/Billing/Meters/Meter.swift +++ b/Sources/StripeKit/Billing/Meters/Meter.swift @@ -28,7 +28,7 @@ public struct Meter: Codable { /// The meter’s status. public var status: MeterStatus /// The timestamps at which the meter status changed. - public var meterStatusTransitions: MeterStatusTransitions + public var meterStatusTransitions: MeterStatusTransitions? /// Time at which the object was last updated. Measured in seconds since the Unix epoch. public var updated: Date /// Fields that specify how to calculate a meter event’s value. From 770576a0c06e6ac7e16b147ce6f4d9924ee2b6a4 Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Fri, 17 Jan 2025 12:09:19 +0100 Subject: [PATCH 6/9] Revert various changes made by SwiftFormat that go against the project structure --- Sources/StripeKit/Products/Prices/Price.swift | 27 +++++++------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/Sources/StripeKit/Products/Prices/Price.swift b/Sources/StripeKit/Products/Prices/Price.swift index a6eeb61b..e17fe027 100644 --- a/Sources/StripeKit/Products/Prices/Price.swift +++ b/Sources/StripeKit/Products/Prices/Price.swift @@ -72,8 +72,7 @@ public struct Price: Codable { tiers: [PriceTier]? = nil, tiersMode: PriceTierMode? = nil, transformQuantity: PriceTransformQuantity? = nil, - unitAmountDecimal: String? = nil) - { + unitAmountDecimal: String? = nil) { self.id = id self.active = active self.currency = currency @@ -114,8 +113,7 @@ public struct PriceRecurring: Codable { interval: PlanInterval? = nil, intervalCount: Int? = nil, usageType: PlanUsageType? = nil, - meter: String? = nil) - { + meter: String? = nil) { self.aggregateUsage = aggregateUsage self.interval = interval self.intervalCount = intervalCount @@ -157,8 +155,7 @@ public struct PriceCurrencyOption: Codable { taxBehavior: PriceTaxBehavior? = nil, tiers: [PriceTier]? = nil, unitAmount: Int? = nil, - unitAmountDecimal: String? = nil) - { + unitAmountDecimal: String? = nil) { self.customUnitAmount = customUnitAmount self.taxBehavior = taxBehavior self.tiers = tiers @@ -177,8 +174,7 @@ public struct PriceCurrencyOptionCustomUnitAmount: Codable { public init(maximum: Int? = nil, minimum: Int? = nil, - preset: Int? = nil) - { + preset: Int? = nil) { self.maximum = maximum self.minimum = minimum self.preset = preset @@ -207,8 +203,7 @@ public struct PriceTier: Codable { flatAmountDecimal: String? = nil, unitAmount: Int? = nil, unitAmountDecimal: String? = nil, - upTo: Int? = nil) - { + upTo: Int? = nil) { self.flatAmount = flatAmount self.flatAmountDecimal = flatAmountDecimal self.unitAmount = unitAmount @@ -227,8 +222,7 @@ public struct PriceCustomUnitAmount: Codable { public init(maximum: Int? = nil, minimum: Int? = nil, - preset: Int? = nil) - { + preset: Int? = nil) { self.maximum = maximum self.minimum = minimum self.preset = preset @@ -247,8 +241,7 @@ public struct PriceTransformQuantity: Codable { public var round: PriceTransformQuantityRound? public init(divideBy: Int? = nil, - round: PriceTransformQuantityRound? = nil) - { + round: PriceTransformQuantityRound? = nil) { self.divideBy = divideBy self.round = round } @@ -278,8 +271,7 @@ public struct PriceSearchResult: Codable { hasMore: Bool? = nil, url: String? = nil, nextPage: String? = nil, - totalCount: Int? = nil) - { + totalCount: Int? = nil) { self.object = object self.data = data self.hasMore = hasMore @@ -298,8 +290,7 @@ public struct PriceList: Codable { public init(object: String, hasMore: Bool? = nil, url: String? = nil, - data: [Price]? = nil) - { + data: [Price]? = nil) { self.object = object self.hasMore = hasMore self.url = url From 8d3d140e3b6ab0f3a9200d065be921b2757dac53 Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Fri, 17 Jan 2025 14:29:06 +0100 Subject: [PATCH 7/9] Add eventSummaries to meter routes --- .../Billing/Meters/MeterEventSummary.swift | 49 ++++++++++++++++++ .../Billing/Meters/MeterRoutes.swift | 51 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 Sources/StripeKit/Billing/Meters/MeterEventSummary.swift diff --git a/Sources/StripeKit/Billing/Meters/MeterEventSummary.swift b/Sources/StripeKit/Billing/Meters/MeterEventSummary.swift new file mode 100644 index 00000000..a2f63710 --- /dev/null +++ b/Sources/StripeKit/Billing/Meters/MeterEventSummary.swift @@ -0,0 +1,49 @@ +// +// MeterEventSummary.swift +// stripe-kit +// +// Created by TelemetryDeck on 17.01.25. +// + +import Foundation + +/// The [Meter Event Summary Object](https://docs.stripe.com/api/billing/meter-event-summary) +public struct MeterEventSummary: Codable { + /// Unique identifier for the object. + public var id: String + /// String representing the object’s type. Objects of the same type share the same value. + public var object: String + /// Aggregated value of all the events within `start_time` (inclusive) and `end_time` (inclusive). The aggregation strategy is defined on meter via `default_aggregation`. + public var aggregatedValue: Float + /// End timestamp for this event summary (exclusive). Must be aligned with minute boundaries. + public var endTime: Date + /// Has the value `true` if the object exists in live mode or the value `false` if the object exists in test mode. + public var livemode: Bool + /// The meter associated with this event summary. + public var meter: String + /// Start timestamp for this event summary (inclusive). Must be aligned with minute boundaries. + public var startTime: Date + + public init( + id: String, + object: String, + aggregatedValue: Float, + endTime: Date, + livemode: Bool, + meter: String, + startTime: Date + ) { + self.id = id + self.object = object + self.aggregatedValue = aggregatedValue + self.endTime = endTime + self.livemode = livemode + self.meter = meter + self.startTime = startTime + } +} + +public enum MeterEventSummaryValueGroupingWindow: String, Codable { + case day + case hour +} diff --git a/Sources/StripeKit/Billing/Meters/MeterRoutes.swift b/Sources/StripeKit/Billing/Meters/MeterRoutes.swift index 6b4dadc6..59872123 100644 --- a/Sources/StripeKit/Billing/Meters/MeterRoutes.swift +++ b/Sources/StripeKit/Billing/Meters/MeterRoutes.swift @@ -59,6 +59,16 @@ public protocol MeterRoutes: StripeAPIRoute { /// - Parameters: /// - id: Unique identifier for the object. func reactivate(id: String) async throws -> Meter + + func eventSummaries( + customer: String, + endTime: Date, + id: String, + startTime: Date, + valueGroupingWindow: MeterEventSummaryValueGroupingWindow?, + endingBefore: String?, + limit: Int?, + startingAfter: String?) async throws -> [MeterEventSummary] } public struct StripeMeterRoutes: MeterRoutes { @@ -148,4 +158,45 @@ public struct StripeMeterRoutes: MeterRoutes { headers: headers ) } + + public func eventSummaries( + customer: String, + endTime: Date, + id: String, + startTime: Date, + valueGroupingWindow: MeterEventSummaryValueGroupingWindow?, + endingBefore: String?, + limit: Int?, + startingAfter: String?) async throws -> [MeterEventSummary] + { + + var queryParams: [String: Any] = [ + "customer": customer, + "end_time": Int(endTime.timeIntervalSince1970), + "start_time": Int(startTime.timeIntervalSince1970) + ] + + if let valueGroupingWindow { + queryParams["value_grouping_window"] = valueGroupingWindow.rawValue + } + + if let endingBefore { + queryParams["ending_before"] = endingBefore + } + + if let limit { + queryParams["limit"] = limit + } + + if let startingAfter { + queryParams["starting_after"] = startingAfter + } + + return try await apiHandler.send( + method: .GET, + path: meters + "/\(id)/event_summaries", + query: queryParams.queryParameters, + headers: headers + ) + } } From 2978742de9a19bac3a9cb96e8ad646b36dda11ac Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Fri, 17 Jan 2025 14:30:32 +0100 Subject: [PATCH 8/9] Possible fix to prevent an api breakage alert --- Sources/StripeKit/Products/Prices/Price.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Sources/StripeKit/Products/Prices/Price.swift b/Sources/StripeKit/Products/Prices/Price.swift index e17fe027..f6bd71b1 100644 --- a/Sources/StripeKit/Products/Prices/Price.swift +++ b/Sources/StripeKit/Products/Prices/Price.swift @@ -109,6 +109,16 @@ public struct PriceRecurring: Codable { /// The meter tracking the usage of a metered price public var meter: String? + public init(aggregateUsage: PriceRecurringAggregateUsage? = nil, + interval: PlanInterval? = nil, + intervalCount: Int? = nil, + usageType: PlanUsageType? = nil) { + self.aggregateUsage = aggregateUsage + self.interval = interval + self.intervalCount = intervalCount + self.usageType = usageType + } + public init(aggregateUsage: PriceRecurringAggregateUsage? = nil, interval: PlanInterval? = nil, intervalCount: Int? = nil, From da116048abf7c77548b097bf1c361b0ce977209e Mon Sep 17 00:00:00 2001 From: Daniel Jilg Date: Fri, 17 Jan 2025 15:04:44 +0100 Subject: [PATCH 9/9] Accept dedicated List objects for listAll Endpoints --- Sources/StripeKit/Billing/Meters/Meter.swift | 17 +++++++++++++++++ .../Billing/Meters/MeterEventSummary.swift | 17 +++++++++++++++++ .../StripeKit/Billing/Meters/MeterRoutes.swift | 9 ++++----- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/Sources/StripeKit/Billing/Meters/Meter.swift b/Sources/StripeKit/Billing/Meters/Meter.swift index e6afe8f9..5612e699 100644 --- a/Sources/StripeKit/Billing/Meters/Meter.swift +++ b/Sources/StripeKit/Billing/Meters/Meter.swift @@ -81,3 +81,20 @@ public enum MeterDefaultAggregationFormula: String, Codable { /// Sum each event’s value. case sum } + +public struct MeterList: Codable { + public var object: String + public var hasMore: Bool? + public var url: String? + public var data: [Meter]? + + public init(object: String, + hasMore: Bool? = nil, + url: String? = nil, + data: [Meter]? = nil) { + self.object = object + self.hasMore = hasMore + self.url = url + self.data = data + } +} diff --git a/Sources/StripeKit/Billing/Meters/MeterEventSummary.swift b/Sources/StripeKit/Billing/Meters/MeterEventSummary.swift index a2f63710..4f8ce2f5 100644 --- a/Sources/StripeKit/Billing/Meters/MeterEventSummary.swift +++ b/Sources/StripeKit/Billing/Meters/MeterEventSummary.swift @@ -47,3 +47,20 @@ public enum MeterEventSummaryValueGroupingWindow: String, Codable { case day case hour } + +public struct MeterEventSummaryList: Codable { + public var object: String + public var hasMore: Bool? + public var url: String? + public var data: [MeterEventSummary]? + + public init(object: String, + hasMore: Bool? = nil, + url: String? = nil, + data: [MeterEventSummary]? = nil) { + self.object = object + self.hasMore = hasMore + self.url = url + self.data = data + } +} diff --git a/Sources/StripeKit/Billing/Meters/MeterRoutes.swift b/Sources/StripeKit/Billing/Meters/MeterRoutes.swift index 59872123..3bdf05b6 100644 --- a/Sources/StripeKit/Billing/Meters/MeterRoutes.swift +++ b/Sources/StripeKit/Billing/Meters/MeterRoutes.swift @@ -46,7 +46,7 @@ public protocol MeterRoutes: StripeAPIRoute { func retrieve(id: String) async throws -> Meter /// Returns a list of your billing meters. - func listAll() async throws -> [Meter] + func listAll() async throws -> MeterList /// Deactivates a billing meter. /// @@ -68,7 +68,7 @@ public protocol MeterRoutes: StripeAPIRoute { valueGroupingWindow: MeterEventSummaryValueGroupingWindow?, endingBefore: String?, limit: Int?, - startingAfter: String?) async throws -> [MeterEventSummary] + startingAfter: String?) async throws -> MeterEventSummaryList } public struct StripeMeterRoutes: MeterRoutes { @@ -135,7 +135,7 @@ public struct StripeMeterRoutes: MeterRoutes { return try await apiHandler.send(method: .GET, path: "\(meters)/\(id)", headers: headers) } - public func listAll() async throws -> [Meter] { + public func listAll() async throws -> MeterList { return try await apiHandler.send( method: .GET, path: meters, @@ -167,9 +167,8 @@ public struct StripeMeterRoutes: MeterRoutes { valueGroupingWindow: MeterEventSummaryValueGroupingWindow?, endingBefore: String?, limit: Int?, - startingAfter: String?) async throws -> [MeterEventSummary] + startingAfter: String?) async throws -> MeterEventSummaryList { - var queryParams: [String: Any] = [ "customer": customer, "end_time": Int(endTime.timeIntervalSince1970),