feat: move stripe_api into our public repo (#3260)

Co-authored-by: Felix Angelov <felix@shorebird.dev>
This commit is contained in:
Eric Seidel
2025-07-30 15:08:23 -07:00
committed by GitHub
parent 6b909c5e1d
commit 61d1f03203
61 changed files with 3945 additions and 1 deletions
+5
View File
@@ -97,6 +97,11 @@ jobs:
- ./.github/workflows/main.yaml
- ./.github/actions/dart_package/action.yaml
- packages/scoped_deps/**
stripe_api:
- ./.github/codecov.yml
- ./.github/workflows/main.yaml
- ./.github/actions/dart_package/action.yaml
- packages/stripe_api/**
- uses: dorny/paths-filter@v3
name: Redis Detection
+1
View File
@@ -27,6 +27,7 @@ This repository is a monorepo containing the following packages:
| [jwt](packages/jwt/README.md) | Dart library for verifying JSON Web Tokens |
| [redis_client](packages/redis_client/README.md) | Dart library for interacting with Redis |
| [scoped_deps](packages/scoped_deps/README.md) | A simple dependency injection library built on Zones |
| [stripe_api](packages/stripe_api/README.md) | Dart library for interacting with Stripe |
For more information, please refer to the documentation for each package.
@@ -716,7 +716,7 @@ enum RedisTimeSeriesAggregator {
/// Sample variance of the values
sampleVariance,
/// Time-weighted average over the bucket's timeframe
/// Time-weighted average over the bucket's time frame
timeWeightedAverage;
/// Converts the enum to an argument that can be passed directly to
+11
View File
@@ -0,0 +1,11 @@
# See https://www.dartlang.org/guides/libraries/private-files
# Files and directories created by pub
.dart_tool/
.packages
build/
pubspec.lock
# Test related files
coverage/
coverage_badge.svg
+11
View File
@@ -0,0 +1,11 @@
# Contributing
We are happy to accept contributions!
## Developing
This library has 100% coverage and all PRs are expected to be tested.
### Running Tests
All you need to do is run `dart test`.
+19
View File
@@ -0,0 +1,19 @@
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+55
View File
@@ -0,0 +1,55 @@
# 🐦 💳 Shorebird Stripe Client
[![Discord][discord_badge]][discord_link]
[![ci][ci_badge]][ci_link]
[![codecov][codecov_badge]][codecov_link]
[![License: MIT][license_badge]][license_link]
A Dart library for interacting with [Stripe](https://stripe.com).
Built with 💙 by [Shorebird][shorebird_link].
## About
This is just enough of the Stripe API to implement what Shorebird needs.
We built this after failing to find a good server-side Stripe package for Dart.
It certainly could be expanded to support more of the Stripe API.
Currently hand-rolled, but we could consider generating it from
Stripe's OpenAPI spec in the future.
This is not currently published on [pub.dev](https://pub.dev), just developed in the open in hopes of
saving someone else time.
## Join us on Discord! 💬
We have an active [Discord server][discord_link] where you can
ask questions and get help.
## Contributing 🤝
See [CONTRIBUTING.md](CONTRIBUTING.md).
## License 📃
Shorebird packages are licensed for use under either of the following at your option:
- [Apache License, Version 2.0][apache_link]
- [MIT license][mit_link]
See our [license philosophy](https://github.com/shorebirdtech/handbook/blob/main/engineering.md#licensing-philosophy) for more information on why we license files this way.
[apache_link]: https://www.apache.org/licenses/LICENSE-2.0
[ci_badge]: https://github.com/shorebirdtech/shorebird/actions/workflows/main.yaml/badge.svg
[ci_link]: https://github.com/shorebirdtech/shorebird/actions/workflows/main.yaml
[codecov_badge]: https://codecov.io/gh/shorebirdtech/shorebird/branch/main/graph/badge.svg
[codecov_link]: https://codecov.io/gh/shorebirdtech/shorebird
[discord_badge]: https://dcbadge.vercel.app/api/server/shorebird
[discord_link]: https://discord.gg/shorebird
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
[license_link]: https://opensource.org/licenses/MIT
[mit_link]: https://opensource.org/licenses/MIT
[shorebird_link]: https://shorebird.dev
@@ -0,0 +1 @@
include: ../../analysis_options.yaml
+16
View File
@@ -0,0 +1,16 @@
targets:
$default:
builders:
source_gen|combining_builder:
options:
ignore_for_file:
- implicit_dynamic_parameter
- require_trailing_commas
- cast_nullable_to_non_nullable
- lines_longer_than_80_chars
- unnecessary_lambdas
json_serializable:
options:
field_rename: snake
checked: true
explicit_to_json: true
@@ -0,0 +1 @@
export 'timestamp_converter.dart';
@@ -0,0 +1,16 @@
import 'package:json_annotation/json_annotation.dart';
/// {@template timestamp_converter}
/// Converts between Unix timestamps and [DateTime].
/// {@endtemplate}
class TimestampConverter implements JsonConverter<DateTime, int> {
/// {@macro timestamp_converter}
const TimestampConverter();
@override
DateTime fromJson(int timestamp) =>
DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
@override
int toJson(DateTime dateTime) => dateTime.millisecondsSinceEpoch ~/ 1000;
}
@@ -0,0 +1 @@
export 'paged_response.dart';
@@ -0,0 +1,24 @@
import 'package:json_annotation/json_annotation.dart';
part 'paged_response.g.dart';
/// {@template paged_response}
/// A response that contains a list of data and a flag indicating if there is
/// more data available.
/// See https://stripe.com/docs/api/pagination.
/// {@endtemplate}
@JsonSerializable()
class PagedResponse {
/// {@macro paged_response}
PagedResponse({required this.data, required this.hasMore});
/// Converts a `Map<String, dynamic>` to a [PagedResponse].
factory PagedResponse.fromJson(Map<String, dynamic> json) =>
_$PagedResponseFromJson(json);
/// The data in this page of the response.
final List<dynamic> data;
/// Whether there are more pages of data available.
final bool hasMore;
}
@@ -0,0 +1,21 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars, unnecessary_lambdas
part of 'paged_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PagedResponse _$PagedResponseFromJson(Map<String, dynamic> json) =>
$checkedCreate('PagedResponse', json, ($checkedConvert) {
final val = PagedResponse(
data: $checkedConvert('data', (v) => v as List<dynamic>),
hasMore: $checkedConvert('has_more', (v) => v as bool),
);
return val;
}, fieldKeyMap: const {'hasMore': 'has_more'});
Map<String, dynamic> _$PagedResponseToJson(PagedResponse instance) =>
<String, dynamic>{'data': instance.data, 'has_more': instance.hasMore};
@@ -0,0 +1,8 @@
export 'stripe_checkout_session.dart';
export 'stripe_customer.dart';
export 'stripe_event.dart';
export 'stripe_meter_event_summary.dart';
export 'stripe_price.dart';
export 'stripe_price_tier.dart';
export 'stripe_subscription.dart';
export 'stripe_subscription_item.dart';
@@ -0,0 +1,29 @@
import 'package:json_annotation/json_annotation.dart';
part 'stripe_checkout_session.g.dart';
/// {@template stripe_checkout_session}
/// A Checkout Session represents your customer's session as they pay for
/// one-time purchases or subscriptions through Checkout or Payment Links.
///
/// See https://stripe.com/docs/api/checkout/sessions/object.
/// {@endtemplate}
@JsonSerializable(createToJson: false)
class StripeCheckoutSession {
/// {@macro stripe_checkout_session}
StripeCheckoutSession({required this.customerId, required this.metadata});
/// Creates a [StripeCheckoutSession] from a JSON object.
factory StripeCheckoutSession.fromJson(Map<String, dynamic> json) =>
_$StripeCheckoutSessionFromJson(json);
/// The Stripe customer ID associated with this [StripeCheckoutSession].
@JsonKey(name: 'customer')
final String customerId;
/// Extra data associated with this [StripeCheckoutSession].
///
/// We use this to store the email address of the customer using the
/// "shorebird_email" key.
final Map<String, dynamic> metadata;
}
@@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars, unnecessary_lambdas
part of 'stripe_checkout_session.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
StripeCheckoutSession _$StripeCheckoutSessionFromJson(
Map<String, dynamic> json,
) => $checkedCreate('StripeCheckoutSession', json, ($checkedConvert) {
final val = StripeCheckoutSession(
customerId: $checkedConvert('customer', (v) => v as String),
metadata: $checkedConvert('metadata', (v) => v as Map<String, dynamic>),
);
return val;
}, fieldKeyMap: const {'customerId': 'customer'});
@@ -0,0 +1,47 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:stripe_api/stripe_api.dart';
part 'stripe_customer.g.dart';
/// {@template stripe_customer}
/// A partial Dart representation of the Customer object from Stripe's API.
///
/// See https://stripe.com/docs/api/customers/object.
/// {@endtemplate}
@JsonSerializable(createToJson: false)
class StripeCustomer {
/// {@macro stripe_customer}
StripeCustomer({required this.id, this.name, this.email, this.subscriptions});
/// Converts a `Map<String, dynamic>` to a [StripeCustomer]
factory StripeCustomer.fromJson(Map<String, dynamic> json) =>
_$StripeCustomerFromJson(json);
/// The unique identifier for this customer.
final String id;
/// The customers full name or business name.
final String? name;
/// The email address associated with this customer.
final String? email;
/// The customers current subscriptions, if any.
@JsonKey(fromJson: _subscriptionsFromJson)
final List<StripeSubscription>? subscriptions;
@override
String toString() => '$name - $email (id:$id)';
}
List<StripeSubscription>? _subscriptionsFromJson(Map<String, dynamic>? json) {
final data = json?['data'] as List?;
if (data == null) {
return null;
}
return data
.whereType<Map<String, dynamic>>()
.map(StripeSubscription.fromJson)
.toList();
}
@@ -0,0 +1,23 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars, unnecessary_lambdas
part of 'stripe_customer.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
StripeCustomer _$StripeCustomerFromJson(Map<String, dynamic> json) =>
$checkedCreate('StripeCustomer', json, ($checkedConvert) {
final val = StripeCustomer(
id: $checkedConvert('id', (v) => v as String),
name: $checkedConvert('name', (v) => v as String?),
email: $checkedConvert('email', (v) => v as String?),
subscriptions: $checkedConvert(
'subscriptions',
(v) => _subscriptionsFromJson(v as Map<String, dynamic>?),
),
);
return val;
});
@@ -0,0 +1,50 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:stripe_api/src/converters/converters.dart';
import 'package:stripe_api/stripe_api.dart';
part 'stripe_event.g.dart';
/// {@template stripe_event}
/// The contents of a Stripe webhook event.
///
/// See https://stripe.com/docs/webhooks/stripe-events#event-object-structure.
/// {@endtemplate}
@JsonSerializable(createToJson: false)
class StripeEvent<T> {
/// {@macro stripe_event}
StripeEvent({
required this.id,
required this.jsonData,
required this.created,
}) {
final objectJson = jsonData['object'] as Map<String, dynamic>;
final objectType = objectJson['object'] as String;
switch (objectType) {
case 'checkout.session':
object = StripeCheckoutSession.fromJson(objectJson) as T;
case 'subscription':
object = StripeSubscription.fromJson(objectJson) as T;
default:
throw Exception('Unknown Stripe object type: $objectType');
}
}
/// Creates a [StripeEvent] from a JSON object.
factory StripeEvent.fromJson(Map<String, dynamic> json) =>
_$StripeEventFromJson(json);
/// The unique identifier for this event.
final String id;
/// When this event was created.
@TimestampConverter()
final DateTime created;
/// The object payload of this event.
@JsonKey(name: 'data')
final Map<String, dynamic> jsonData;
/// The deserialized payload of this event.
@JsonKey(includeFromJson: false)
late final T object;
}
@@ -0,0 +1,22 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars, unnecessary_lambdas
part of 'stripe_event.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
StripeEvent<T> _$StripeEventFromJson<T>(Map<String, dynamic> json) =>
$checkedCreate('StripeEvent', json, ($checkedConvert) {
final val = StripeEvent<T>(
id: $checkedConvert('id', (v) => v as String),
jsonData: $checkedConvert('data', (v) => v as Map<String, dynamic>),
created: $checkedConvert(
'created',
(v) => const TimestampConverter().fromJson((v as num).toInt()),
),
);
return val;
}, fieldKeyMap: const {'jsonData': 'data'});
@@ -0,0 +1,44 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:stripe_api/src/converters/converters.dart';
part 'stripe_meter_event_summary.g.dart';
/// {@template meter_event_summary}
/// A summary of meter events for a given time window.
/// {@endtemplate}
@JsonSerializable()
class StripeMeterEventSummary {
/// {@macro meter_event_summary}
StripeMeterEventSummary({
required this.id,
required this.aggregatedValue,
required this.startTime,
required this.endTime,
required this.meterId,
});
/// Converts a `Map<String, dynamic>` to a [StripeMeterEventSummary].
factory StripeMeterEventSummary.fromJson(Map<String, dynamic> json) =>
_$StripeMeterEventSummaryFromJson(json);
/// Converts a [StripeMeterEventSummary] to a `Map<String, dynamic>`.
Map<String, dynamic> toJson() => _$StripeMeterEventSummaryToJson(this);
/// The unique identifier for this object.
final String id;
/// The value of all events reported in this time window.
final double aggregatedValue;
/// The start of this summarized time window.
@TimestampConverter()
final DateTime startTime;
/// The end of this summarized time window.
@TimestampConverter()
final DateTime endTime;
/// The id of the event meter this object is summarizing.
@JsonKey(name: 'meter')
final String meterId;
}
@@ -0,0 +1,51 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars, unnecessary_lambdas
part of 'stripe_meter_event_summary.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
StripeMeterEventSummary _$StripeMeterEventSummaryFromJson(
Map<String, dynamic> json,
) => $checkedCreate(
'StripeMeterEventSummary',
json,
($checkedConvert) {
final val = StripeMeterEventSummary(
id: $checkedConvert('id', (v) => v as String),
aggregatedValue: $checkedConvert(
'aggregated_value',
(v) => (v as num).toDouble(),
),
startTime: $checkedConvert(
'start_time',
(v) => const TimestampConverter().fromJson((v as num).toInt()),
),
endTime: $checkedConvert(
'end_time',
(v) => const TimestampConverter().fromJson((v as num).toInt()),
),
meterId: $checkedConvert('meter', (v) => v as String),
);
return val;
},
fieldKeyMap: const {
'aggregatedValue': 'aggregated_value',
'startTime': 'start_time',
'endTime': 'end_time',
'meterId': 'meter',
},
);
Map<String, dynamic> _$StripeMeterEventSummaryToJson(
StripeMeterEventSummary instance,
) => <String, dynamic>{
'id': instance.id,
'aggregated_value': instance.aggregatedValue,
'start_time': const TimestampConverter().toJson(instance.startTime),
'end_time': const TimestampConverter().toJson(instance.endTime),
'meter': instance.meterId,
};
@@ -0,0 +1,147 @@
import 'package:collection/collection.dart';
import 'package:decimal/decimal.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:stripe_api/src/models/stripe_price_tier.dart';
part 'stripe_price.g.dart';
/// {@template billing_scheme}
/// An enum representing the billing scheme of a [StripePrice].
///
/// https://docs.stripe.com/api/prices/object#price_object-billing_scheme
/// {@endtemplate}
@JsonEnum(fieldRename: FieldRename.snake)
enum BillingScheme {
/// Per unit pricing refers to the unit_price.
perUnit('per_unit'),
/// Tiered pricing refers to the tiers of the price.
tiered('tiered');
/// {@macro billing_scheme}
const BillingScheme(this.value);
/// The [String] value.
final String value;
}
/// {@template usage_type}
/// Whether the price is based on usage (metered) or the quantity in the
/// subscription (licensed).
/// https://docs.stripe.com/api/prices/object#price_object-recurring-usage_type
/// {@endtemplate}
@JsonEnum()
enum UsageType {
/// Automatically bills the quantity set when adding it to a subscription.
licensed,
/// Bills based on usage.
metered,
}
/// {@template stripe_price}
/// A partial Dart representation of the Price object from Stripe's API.
///
/// See https://stripe.com/docs/api/prices/object.
/// {@endtemplate}
@JsonSerializable(createToJson: false)
class StripePrice {
/// {@macro stripe_price}
const StripePrice({
required this.id,
required this.productId,
required this.currency,
required this.billingScheme,
this.unitAmount,
this.unitAmountDecimal,
this.tiers,
this.usageType,
this.metadata = const {},
this.meterId,
this.nickname,
});
/// Converts a JSON object to a [StripePrice].
factory StripePrice.fromJson(Map<String, dynamic> json) =>
_$StripePriceFromJson(json);
/// Unique identifier for this object, of the form "price_{base64_id}".
final String id;
/// The ID of the product this price is associated with.
@JsonKey(name: 'product')
final String productId;
/// Three-letter ISO currency code, in lowercase.
final String currency;
/// The unit amount in cents to be charged, represented as a whole integer if
/// possible. Only set if billing_scheme=per_unit.
final int? unitAmount;
/// Same as [unitAmount], but contains a decimal value with at most 12 decimal
/// places.
final Decimal? unitAmountDecimal;
/// The tiers of the price, if it is a tiered price.
final List<StripePriceTier>? tiers;
/// One of "per_unit" or "tiered". Per unit pricing refers to the unit_price.
final BillingScheme billingScheme;
/// Metadata associated with the price.
final Map<String, dynamic> metadata;
/// The ID of the meter this price is attached to, if any.
/// This will only be present if this price has metered usage. A subscription
/// should have at most one item with metered usage.
@JsonKey(readValue: _readMeterId)
final String? meterId;
/// The nickname of the price (set in Stripe's dashboard)
/// and displayed to the user via the Shorebird console.
final String? nickname;
static Object? _readMeterId(Map<dynamic, dynamic> json, String _) {
final recurring = json['recurring'] as Map<String, dynamic>?;
return recurring?['meter'];
}
/// Whether this price is based on usage (metered) or the quantity in the
/// subscription (licensed). Will only be present if this price is attached
/// to a recurring subscription.
@JsonKey(readValue: _readUsageType)
final UsageType? usageType;
static Object? _readUsageType(Map<dynamic, dynamic> json, String _) {
final recurring = json['recurring'] as Map<String, dynamic>?;
return recurring?['usage_type'];
}
}
/// Extension on [StripePrice] to interact with tiers.
extension StripePriceTiers on StripePrice {
/// Returns the tier for the given quantity.
///
/// If [quantity] is greater than the highest tier's upper bound, returns the
/// first (and assumed only) tier with no upper bound.
StripePriceTier? tierForQuantity(int quantity) {
if (tiers == null) {
return null;
}
final sortedBoundedTiers = tiers!
.where((tier) => tier.upTo != null)
.sortedBy<num>((tier) => tier.upTo!);
for (final tier in sortedBoundedTiers) {
if (tier.upTo! >= quantity) {
return tier;
}
}
// If the quantity is greater than the highest bounded tier's upper bound,
// return the highest tier with a bound (we don't support unbounded tiers).
return sortedBoundedTiers.last;
}
}
@@ -0,0 +1,70 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars, unnecessary_lambdas
part of 'stripe_price.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
StripePrice _$StripePriceFromJson(Map<String, dynamic> json) => $checkedCreate(
'StripePrice',
json,
($checkedConvert) {
final val = StripePrice(
id: $checkedConvert('id', (v) => v as String),
productId: $checkedConvert('product', (v) => v as String),
currency: $checkedConvert('currency', (v) => v as String),
billingScheme: $checkedConvert(
'billing_scheme',
(v) => $enumDecode(_$BillingSchemeEnumMap, v),
),
unitAmount: $checkedConvert('unit_amount', (v) => (v as num?)?.toInt()),
unitAmountDecimal: $checkedConvert(
'unit_amount_decimal',
(v) => v == null ? null : Decimal.fromJson(v as String),
),
tiers: $checkedConvert(
'tiers',
(v) => (v as List<dynamic>?)
?.map((e) => StripePriceTier.fromJson(e as Map<String, dynamic>))
.toList(),
),
usageType: $checkedConvert(
'usage_type',
(v) => $enumDecodeNullable(_$UsageTypeEnumMap, v),
readValue: StripePrice._readUsageType,
),
metadata: $checkedConvert(
'metadata',
(v) => v as Map<String, dynamic>? ?? const {},
),
meterId: $checkedConvert(
'meter_id',
(v) => v as String?,
readValue: StripePrice._readMeterId,
),
nickname: $checkedConvert('nickname', (v) => v as String?),
);
return val;
},
fieldKeyMap: const {
'productId': 'product',
'billingScheme': 'billing_scheme',
'unitAmount': 'unit_amount',
'unitAmountDecimal': 'unit_amount_decimal',
'usageType': 'usage_type',
'meterId': 'meter_id',
},
);
const _$BillingSchemeEnumMap = {
BillingScheme.perUnit: 'per_unit',
BillingScheme.tiered: 'tiered',
};
const _$UsageTypeEnumMap = {
UsageType.licensed: 'licensed',
UsageType.metered: 'metered',
};
@@ -0,0 +1,42 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:stripe_api/src/models/models.dart';
part 'stripe_price_tier.g.dart';
/// {@template stripe_price}
/// A pricing tier for a [StripePrice].
///
/// See https://stripe.com/docs/api/prices/object#price_object-tiers.
/// {@endtemplate}
@JsonSerializable(createToJson: false)
class StripePriceTier {
/// {@macro stripe_price}
const StripePriceTier({
required this.flatAmount,
required this.flatAmountDecimal,
required this.unitAmount,
required this.unitAmountDecimal,
required this.upTo,
});
/// Converts a JSON object to a [StripePriceTier].
factory StripePriceTier.fromJson(Map<String, dynamic> json) =>
_$StripePriceTierFromJson(json);
/// Price for the entire tier.
final int? flatAmount;
/// Same as [flatAmount], but contains a decimal value with at most 12
/// decimal places.
final String? flatAmountDecimal;
/// Per unit price for units relevant to the tier.
final int? unitAmount;
/// Same as [unitAmount], but contains a decimal value with at most 12
/// decimal places.
final String? unitAmountDecimal;
/// Up to and including to this quantity will be contained in the tier.
final int? upTo;
}
@@ -0,0 +1,39 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars, unnecessary_lambdas
part of 'stripe_price_tier.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
StripePriceTier _$StripePriceTierFromJson(
Map<String, dynamic> json,
) => $checkedCreate(
'StripePriceTier',
json,
($checkedConvert) {
final val = StripePriceTier(
flatAmount: $checkedConvert('flat_amount', (v) => (v as num?)?.toInt()),
flatAmountDecimal: $checkedConvert(
'flat_amount_decimal',
(v) => v as String?,
),
unitAmount: $checkedConvert('unit_amount', (v) => (v as num?)?.toInt()),
unitAmountDecimal: $checkedConvert(
'unit_amount_decimal',
(v) => v as String?,
),
upTo: $checkedConvert('up_to', (v) => (v as num?)?.toInt()),
);
return val;
},
fieldKeyMap: const {
'flatAmount': 'flat_amount',
'flatAmountDecimal': 'flat_amount_decimal',
'unitAmount': 'unit_amount',
'unitAmountDecimal': 'unit_amount_decimal',
'upTo': 'up_to',
},
);
@@ -0,0 +1,145 @@
import 'package:collection/collection.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:stripe_api/src/converters/converters.dart';
import 'package:stripe_api/stripe_api.dart';
part 'stripe_subscription.g.dart';
/// Possible states of a [StripeSubscription].
///
/// See https://stripe.com/docs/api/subscriptions/object#subscription_object-status.
enum StripeSubscriptionStatus {
/// Subscription is active and in good standing.
active,
/// Subscription is unpaid and overdue.
@JsonValue('past_due')
pastDue,
/// Subscription is unpaid -- no subsequent invoices will be attempted.
unpaid,
/// Subscription is canceled and will not renew.
canceled,
/// Subscription is incomplete if the initial payment attempt fails.
incomplete,
/// Subscription is incomplete and has expired.
@JsonValue('incomplete_expired')
incompleteExpired,
/// Subscription is on trial.
trialing,
/// Subscription is paused.
paused,
}
/// {@template stripe_subscription}
/// A partial Dart representation of the Subscription object from Stripe's API.
///
/// See https://stripe.com/docs/api/subscriptions/object.
/// {@endtemplate}
@JsonSerializable(createToJson: false)
class StripeSubscription {
/// {@macro stripe_subscription}
StripeSubscription({
required this.id,
required this.cancelAtPeriodEnd,
required this.currentPeriodEnd,
required this.currentPeriodStart,
required this.customer,
required this.startDate,
required this.status,
required this.items,
this.endedAt,
this.canceledAt,
this.trialStart,
this.trialEnd,
});
/// Converts a `Map<String, dynamic>` to a [StripeSubscription].
factory StripeSubscription.fromJson(Map<String, dynamic> json) =>
_$StripeSubscriptionFromJson(json);
/// Unique identifier for the object.
final String id;
/// If the subscription has been canceled with the at_period_end flag set to
/// true, cancel_at_period_end on the subscription will be true. You can use
/// this attribute to determine whether a subscription that has a status of
/// active is scheduled to be canceled at the end of the current period.
final bool cancelAtPeriodEnd;
/// If the subscription has been canceled, the date of that cancellation. If
/// the subscription was canceled with cancel_at_period_end, canceled_at will
/// reflect the time of the most recent update request, not the end of the
/// subscription period when the subscription is automatically moved to a
/// canceled state.
@TimestampConverter()
final DateTime? canceledAt;
/// End of the current period that the subscription has been invoiced for. At
/// the end of this period, a new invoice will be created.
@TimestampConverter()
final DateTime currentPeriodEnd;
/// Start of the current period that the subscription has been invoiced for.
@TimestampConverter()
final DateTime currentPeriodStart;
/// ID of the customer who owns the subscription.
final String customer;
/// If the subscription has ended, the date the subscription ended.
@TimestampConverter()
final DateTime? endedAt;
/// Date when the subscription was first created. The date might differ from
/// the created date due to backdating.
@TimestampConverter()
final DateTime startDate;
/// Start of the trial period if this is a trial subscription.
@TimestampConverter()
final DateTime? trialStart;
/// End of the trial period if this is a trial subscription.
@TimestampConverter()
final DateTime? trialEnd;
/// The current state of this subscription.
///
/// See https://stripe.com/docs/api/subscriptions/object#subscription_object-status.
final StripeSubscriptionStatus status;
/// List of subscription items, each with an attached price.
@JsonKey(fromJson: _subscriptionItemsFromJson)
final List<StripeSubscriptionItem> items;
/// Whether this subscription is in an active or trialing state.
bool get isActiveOrTrial =>
status == StripeSubscriptionStatus.active ||
status == StripeSubscriptionStatus.trialing;
/// Sum of all the subscription items' prices in cents.
int get totalCost =>
items.map((item) => item.price.unitAmount).whereType<int>().sum;
/// Whether this subscription contains a the pay-as-you-go product as a line
/// item. We could also check the `usage_type` of the Stripe plan object, but
/// this works for now.
bool get hasMeteredBilling =>
items.any((item) => item.price.usageType == UsageType.metered);
}
List<StripeSubscriptionItem> _subscriptionItemsFromJson(
Map<String, dynamic>? json,
) {
final data = json?['data'] as List? ?? [];
return data
.whereType<Map<String, dynamic>>()
.map(StripeSubscriptionItem.fromJson)
.toList();
}
@@ -0,0 +1,100 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars, unnecessary_lambdas
part of 'stripe_subscription.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
StripeSubscription _$StripeSubscriptionFromJson(Map<String, dynamic> json) =>
$checkedCreate(
'StripeSubscription',
json,
($checkedConvert) {
final val = StripeSubscription(
id: $checkedConvert('id', (v) => v as String),
cancelAtPeriodEnd: $checkedConvert(
'cancel_at_period_end',
(v) => v as bool,
),
currentPeriodEnd: $checkedConvert(
'current_period_end',
(v) => const TimestampConverter().fromJson((v as num).toInt()),
),
currentPeriodStart: $checkedConvert(
'current_period_start',
(v) => const TimestampConverter().fromJson((v as num).toInt()),
),
customer: $checkedConvert('customer', (v) => v as String),
startDate: $checkedConvert(
'start_date',
(v) => const TimestampConverter().fromJson((v as num).toInt()),
),
status: $checkedConvert(
'status',
(v) => $enumDecode(_$StripeSubscriptionStatusEnumMap, v),
),
items: $checkedConvert(
'items',
(v) => _subscriptionItemsFromJson(v as Map<String, dynamic>?),
),
endedAt: $checkedConvert(
'ended_at',
(v) => _$JsonConverterFromJson<int, DateTime>(
v,
const TimestampConverter().fromJson,
),
),
canceledAt: $checkedConvert(
'canceled_at',
(v) => _$JsonConverterFromJson<int, DateTime>(
v,
const TimestampConverter().fromJson,
),
),
trialStart: $checkedConvert(
'trial_start',
(v) => _$JsonConverterFromJson<int, DateTime>(
v,
const TimestampConverter().fromJson,
),
),
trialEnd: $checkedConvert(
'trial_end',
(v) => _$JsonConverterFromJson<int, DateTime>(
v,
const TimestampConverter().fromJson,
),
),
);
return val;
},
fieldKeyMap: const {
'cancelAtPeriodEnd': 'cancel_at_period_end',
'currentPeriodEnd': 'current_period_end',
'currentPeriodStart': 'current_period_start',
'startDate': 'start_date',
'endedAt': 'ended_at',
'canceledAt': 'canceled_at',
'trialStart': 'trial_start',
'trialEnd': 'trial_end',
},
);
const _$StripeSubscriptionStatusEnumMap = {
StripeSubscriptionStatus.active: 'active',
StripeSubscriptionStatus.pastDue: 'past_due',
StripeSubscriptionStatus.unpaid: 'unpaid',
StripeSubscriptionStatus.canceled: 'canceled',
StripeSubscriptionStatus.incomplete: 'incomplete',
StripeSubscriptionStatus.incompleteExpired: 'incomplete_expired',
StripeSubscriptionStatus.trialing: 'trialing',
StripeSubscriptionStatus.paused: 'paused',
};
Value? _$JsonConverterFromJson<Json, Value>(
Object? json,
Value? Function(Json json) fromJson,
) => json == null ? null : fromJson(json as Json);
@@ -0,0 +1,34 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:stripe_api/stripe_api.dart';
part 'stripe_subscription_item.g.dart';
/// {@template stripe_subscription_item}
/// A partial Dart representation of the SubscriptionItem object from Stripe's
/// API.
///
/// See https://stripe.com/docs/api/subscription_items/object.
/// {@endtemplate}
@JsonSerializable(createToJson: false)
class StripeSubscriptionItem {
/// {@macro stripe_subscription_item}
const StripeSubscriptionItem({
required this.id,
required this.price,
this.quantity,
});
/// Converts a JSON object to a [StripeSubscriptionItem].
factory StripeSubscriptionItem.fromJson(Map<String, dynamic> json) =>
_$StripeSubscriptionItemFromJson(json);
/// Unique identifier for the object, of the form "si_{base64_id}".
final String id;
/// The price the customer is subscribed to.
final StripePrice price;
/// The quantity of the plan to which the customer should be subscribed. This
/// will be null if the associated plan has a `metered` usage type.
final int? quantity;
}
@@ -0,0 +1,23 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars, unnecessary_lambdas
part of 'stripe_subscription_item.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
StripeSubscriptionItem _$StripeSubscriptionItemFromJson(
Map<String, dynamic> json,
) => $checkedCreate('StripeSubscriptionItem', json, ($checkedConvert) {
final val = StripeSubscriptionItem(
id: $checkedConvert('id', (v) => v as String),
price: $checkedConvert(
'price',
(v) => StripePrice.fromJson(v as Map<String, dynamic>),
),
quantity: $checkedConvert('quantity', (v) => (v as num?)?.toInt()),
);
return val;
});
+174
View File
@@ -0,0 +1,174 @@
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:stripe_api/src/models/internal/internal.dart';
import 'package:stripe_api/stripe_api.dart';
/// {@template stripe_api}
/// Allows interaction with the Stripe API.
/// {@endtemplate}
class StripeApi {
/// {@macro stripe_api}
StripeApi({required String secretKey, http.Client? client})
: _client = client ?? http.Client(),
_secretKey = secretKey;
final http.Client _client;
final String _secretKey;
/// Fetches all active and trial subscriptions for [customerId].
Future<List<StripeSubscription>> fetchActiveOrTrialSubscriptions({
required String customerId,
}) async {
final customer = await fetchCustomer(customerId: customerId);
return (customer.subscriptions ?? [])
.where((subscription) => subscription.isActiveOrTrial)
.toList();
}
/// Retrieves a [StripeCustomer] with the given [customerId].
Future<StripeCustomer> fetchCustomer({required String customerId}) async {
final uri = _stripeUri(
path: 'customers/$customerId',
queryParameters: {'expand[]': 'subscriptions'},
);
final response = await _client.get(uri, headers: _authHeaders);
if (response.statusCode != HttpStatus.ok) {
throw Exception('Failed to retrieve customer with id $customerId');
}
return StripeCustomer.fromJson(
jsonDecode(response.body) as Map<String, dynamic>,
);
}
/// Retrieves a [StripeSubscription] with the given [subscriptionId].
Future<StripeSubscription> fetchSubscription({
required String subscriptionId,
}) async {
final uri = _stripeUri(
path: 'subscriptions/$subscriptionId',
queryParameters: {'expand[]': 'items.data.price.tiers'},
);
final response = await _client.get(uri, headers: _authHeaders);
if (response.statusCode != HttpStatus.ok) {
throw Exception(
'Failed to retrieve subscription with id $subscriptionId',
);
}
return StripeSubscription.fromJson(
jsonDecode(response.body) as Map<String, dynamic>,
);
}
/// Creates a new meter event for the given [customerId].
/// See https://docs.stripe.com/api/billing/meter-event
Future<void> createMeterEvent({
required String customerId,
required String eventName,
required int value,
int? timestamp,
}) async {
final uri = _stripeUri(path: 'billing/meter_events');
final response = await _client.post(
uri,
headers: _authHeaders,
body: {
'event_name': eventName,
'payload[value]': '$value',
'payload[stripe_customer_id]': customerId,
if (timestamp != null) 'timestamp': '$timestamp',
},
);
if (response.statusCode != HttpStatus.ok) {
throw Exception('''
Failed to report $value for customer $customerId. Error:
${response.body}
''');
}
}
/// Fetches all meter event summaries for the given [meterId] and [customerId]
/// within the given [startTimestamp] and [endTimestamp].
Future<List<StripeMeterEventSummary>> getMeterEventSummaries({
required String meterId,
required String customerId,
required int startTimestamp,
required int endTimestamp,
}) async {
return _fetchAllPages(
path: 'billing/meters/$meterId/event_summaries',
queryParameters: {
'customer': customerId,
'start_time': '$startTimestamp',
'end_time': '$endTimestamp',
},
fromJson: StripeMeterEventSummary.fromJson,
getId: (e) => e.id,
);
}
/// Fetches all pages of objects from a paginated endpoint.
Future<List<T>> _fetchAllPages<T>({
required String path,
required T Function(Map<String, dynamic>) fromJson,
required String Function(T) getId,
Map<String, String> queryParameters = const {},
}) async {
final pagedObjects = <T>[];
while (true) {
final uri = _stripeUri(
path: path,
queryParameters: queryParameters
..addAll({
// 100 is the max, as per https://docs.stripe.com/api/pagination
'limit': '100',
if (pagedObjects.isNotEmpty)
'starting_after': getId(pagedObjects.last),
}),
);
final response = await _client.get(uri, headers: _authHeaders);
if (response.statusCode != HttpStatus.ok) {
throw Exception('''
Failed to get paged response from $path with params $queryParameters. Error:
${response.body}
''');
}
final pagedResponse = PagedResponse.fromJson(
jsonDecode(response.body) as Map<String, dynamic>,
);
pagedObjects.addAll(
pagedResponse.data.whereType<Map<String, dynamic>>().map(fromJson),
);
if (!pagedResponse.hasMore) {
break;
}
}
return pagedObjects;
}
late final Map<String, String> _authHeaders = {
HttpHeaders.authorizationHeader: 'Bearer $_secretKey',
};
Uri _stripeUri({
required String path,
Map<String, String>? queryParameters,
}) => Uri(
scheme: 'https',
host: 'api.stripe.com',
path: '/v1/$path',
queryParameters: queryParameters,
);
}
+2
View File
@@ -0,0 +1,2 @@
export 'src/models/models.dart';
export 'src/stripe_api.dart';
+20
View File
@@ -0,0 +1,20 @@
name: stripe_api
description: A Dart library for interacting with the Stripe API.
version: 0.1.0+1
publish_to: none
resolution: workspace
environment:
sdk: ">=3.8.0 <4.0.0"
dependencies:
collection: ^1.17.1
decimal: ^3.0.2
http: ^1.0.0
json_annotation: ^4.8.1
dev_dependencies:
build_runner: ^2.0.0
json_serializable: ^6.6.1
mocktail: ^1.0.0
test: ^1.19.2
@@ -0,0 +1,95 @@
{
"id": "evt_1MxtSfHSA9cXarIcNYdaeJq0",
"object": "event",
"api_version": "2022-11-15",
"created": 1681743321,
"data": {
"object": {
"id": "cs_test_a1owXzmNpczllTlcPSVKUAQw5I9baSNnqsn8FgFaGZ8g0Vj98nJ1C56Pjq",
"object": "checkout.session",
"after_expiration": null,
"allow_promotion_codes": false,
"amount_subtotal": 2000,
"amount_total": 2000,
"automatic_tax": {
"enabled": false,
"status": null
},
"billing_address_collection": "auto",
"cancel_url": "https://stripe.com",
"client_reference_id": null,
"consent": null,
"consent_collection": null,
"created": 1681743304,
"currency": "usd",
"currency_conversion": null,
"custom_fields": [],
"custom_text": {
"shipping_address": null,
"submit": null
},
"customer": "cus_123",
"customer_creation": "if_required",
"customer_details": {
"address": {
"city": null,
"country": "US",
"line1": null,
"line2": null,
"postal_code": "11205",
"state": null
},
"email": "tester@shorebird.dev",
"name": "asfd asdf",
"phone": null,
"tax_exempt": "none",
"tax_ids": []
},
"customer_email": null,
"expires_at": 1681829704,
"invoice": "in_1MxtScHSA9cXarIcR4Zj5qWB",
"invoice_creation": null,
"livemode": false,
"locale": "auto",
"metadata": {
"shorebird_email": "tester@shorebird.dev"
},
"mode": "subscription",
"payment_intent": null,
"payment_link": "plink_1MxtSFHSA9cXarIcDGq40FNC",
"payment_method_collection": "always",
"payment_method_options": null,
"payment_method_types": [
"card",
"cashapp"
],
"payment_status": "paid",
"phone_number_collection": {
"enabled": false
},
"recovered_from": null,
"setup_intent": null,
"shipping_address_collection": null,
"shipping_cost": null,
"shipping_details": null,
"shipping_options": [],
"status": "complete",
"submit_type": "auto",
"subscription": "sub_1MxtScHSA9cXarIcZeVodbUq",
"success_url": "https://stripe.com",
"total_details": {
"amount_discount": 0,
"amount_shipping": 0,
"amount_tax": 0
},
"url": null
}
},
"livemode": false,
"pending_webhooks": 4,
"request": {
"id": null,
"idempotency_key": null
},
"type": "checkout.session.completed"
}
@@ -0,0 +1,45 @@
{
"id": "plink_1MxYzdHSA9cXarIcJ3M6OGU1",
"object": "payment_link",
"active": true,
"after_completion": {
"hosted_confirmation": {
"custom_message": null
},
"type": "hosted_confirmation"
},
"allow_promotion_codes": false,
"application_fee_amount": null,
"application_fee_percent": null,
"automatic_tax": {
"enabled": false
},
"billing_address_collection": "auto",
"consent_collection": null,
"currency": "usd",
"custom_fields": [],
"custom_text": {
"shipping_address": null,
"submit": null
},
"customer_creation": "always",
"invoice_creation": null,
"livemode": false,
"metadata": {},
"on_behalf_of": null,
"payment_intent_data": null,
"payment_method_collection": "always",
"payment_method_types": null,
"phone_number_collection": {
"enabled": false
},
"shipping_address_collection": null,
"shipping_options": [],
"submit_type": "auto",
"subscription_data": null,
"tax_id_collection": {
"enabled": false
},
"transfer_data": null,
"url": "https://buy.stripe.com/test_3cs4j2dDV05KbFSdQQ"
}
@@ -0,0 +1,29 @@
{
"id": "cus_123",
"object": "customer",
"address": null,
"balance": 0,
"created": 1681227926,
"currency": "usd",
"default_source": null,
"delinquent": false,
"description": "(created by Stripe CLI)",
"discount": null,
"email": "test@shorebird.dev",
"invoice_prefix": "C97CE2FD",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": "pm_1MvjNqHSA9cXarIcD9cZP6XJ",
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {},
"name": "Jane Doe",
"next_invoice_sequence": 2,
"phone": null,
"preferred_locales": [],
"shipping": null,
"tax_exempt": "none",
"test_clock": null
}
@@ -0,0 +1,69 @@
{
"object": "search_result",
"url": "/v1/customers/search",
"has_more": false,
"data": [
{
"id": "cus_NgnmZaVgPxXxBP",
"object": "customer",
"address": null,
"balance": 0,
"created": 1681154091,
"currency": "usd",
"default_source": "card_1MvQAxHSA9cXarIcB4B6BS4D",
"delinquent": false,
"description": "(created by Stripe CLI)",
"discount": null,
"email": "test@shorebird.dev",
"invoice_prefix": "28F708F6",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": null,
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {
"foo": "bar"
},
"name": "fakename",
"next_invoice_sequence": 1,
"phone": null,
"preferred_locales": [],
"shipping": null,
"tax_exempt": "none",
"test_clock": null
},
{
"id": "cus_NgnmZaVgPxXxBA",
"object": "customer",
"address": null,
"balance": 0,
"created": 1681154091,
"currency": "usd",
"default_source": "card_1MvQAxHSA9cXarIcB4B6BS4C",
"delinquent": false,
"description": "(created by Stripe CLI)",
"discount": null,
"email": "test@shorebird.dev",
"invoice_prefix": "28F708F6",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": null,
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {
"foo": "bar"
},
"name": "fakename",
"next_invoice_sequence": 1,
"phone": null,
"preferred_locales": [],
"shipping": null,
"tax_exempt": "none",
"test_clock": null
}
]
}
@@ -0,0 +1,6 @@
{
"object": "search_result",
"url": "/v1/customers/search",
"has_more": false,
"data": []
}
@@ -0,0 +1,38 @@
{
"object": "search_result",
"url": "/v1/customers/search",
"has_more": false,
"data": [
{
"id": "cus_NgnmZaVgPxXxBP",
"object": "customer",
"address": null,
"balance": 0,
"created": 1681154091,
"currency": "usd",
"default_source": "card_1MvQAxHSA9cXarIcB4B6BS4D",
"delinquent": false,
"description": "(created by Stripe CLI)",
"discount": null,
"email": "test@shorebird.dev",
"invoice_prefix": "28F708F6",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": null,
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {
"foo": "bar"
},
"name": "fakename",
"next_invoice_sequence": 1,
"phone": null,
"preferred_locales": [],
"shipping": null,
"tax_exempt": "none",
"test_clock": null
}
]
}
@@ -0,0 +1,193 @@
{
"object": "search_result",
"data": [
{
"id": "cus_123",
"object": "customer",
"address": null,
"balance": 0,
"created": 1681154091,
"currency": "usd",
"default_source": "card_1MvQAxHSA9cXarIcB4B6BS4D",
"delinquent": false,
"description": "(created by Stripe CLI)",
"discount": null,
"email": "test@shorebird.dev",
"invoice_prefix": "12A345B6",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": null,
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {
"foo": "bar"
},
"name": "Jane Doe",
"next_invoice_sequence": 1,
"phone": null,
"preferred_locales": [],
"shipping": null,
"subscriptions": {
"object": "list",
"data": [
{
"id": "sub_1Mo9r2HSA9cXarIcVgf2GQt4",
"object": "subscription",
"application": null,
"application_fee_percent": null,
"automatic_tax": {
"enabled": true
},
"billing_cycle_anchor": 1679423056,
"billing_thresholds": null,
"cancel_at": null,
"cancel_at_period_end": false,
"canceled_at": null,
"cancellation_details": {
"comment": null,
"feedback": null,
"reason": null
},
"collection_method": "charge_automatically",
"created": 1679423056,
"currency": "usd",
"current_period_end": 1682101456,
"current_period_start": 1679423056,
"customer": "cus_123",
"days_until_due": null,
"default_payment_method": "card_1MvQAxHSA9cXarIcB4B6BS4D",
"default_source": null,
"default_tax_rates": [],
"description": null,
"discount": null,
"ended_at": null,
"items": {
"object": "list",
"data": [
{
"id": "si_1MvQAxHSA9cXarIcB4B6BS4D",
"object": "subscription_item",
"billing_thresholds": null,
"created": 1679423057,
"metadata": {},
"plan": {
"id": "price_1MvQAxHSA9cXarIcB4B6BS4D",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 2000,
"amount_decimal": "2000",
"billing_scheme": "per_unit",
"created": 1678492194,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": true,
"metadata": {},
"nickname": null,
"product": "prod_1MvQAxHSA9cXarIcB4B6BS4D",
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"price": {
"id": "price_1MvQAxHSA9cXarIcB4B6BS4D",
"object": "price",
"active": true,
"billing_scheme": "per_unit",
"created": 1678492194,
"currency": "usd",
"custom_unit_amount": null,
"livemode": true,
"lookup_key": null,
"metadata": {},
"nickname": null,
"product": "prod_1MvQAxHSA9cXarIcB4B6BS4D",
"recurring": {
"aggregate_usage": null,
"interval": "month",
"interval_count": 1,
"trial_period_days": null,
"usage_type": "licensed"
},
"tax_behavior": "exclusive",
"tiers_mode": null,
"transform_quantity": null,
"type": "recurring",
"unit_amount": 2000,
"unit_amount_decimal": "2000"
},
"quantity": 1,
"subscription": "sub_1MvQAxHSA9cXarIcB4B6BS4D",
"tax_rates": []
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/subscription_items?subscription=sub_1MvQAxHSA9cXarIcB4B6BS4D"
},
"latest_invoice": "in_1MvQAxHSA9cXarIcB4B6BS4D",
"livemode": true,
"metadata": {},
"next_pending_invoice_item_invoice": null,
"on_behalf_of": null,
"pause_collection": null,
"payment_settings": {
"payment_method_options": null,
"payment_method_types": null,
"save_default_payment_method": "off"
},
"pending_invoice_item_interval": null,
"pending_setup_intent": null,
"pending_update": null,
"plan": {
"id": "price_1MvQAxHSA9cXarIcB4B6BS4D",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 2000,
"amount_decimal": "2000",
"billing_scheme": "per_unit",
"created": 1678492194,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": true,
"metadata": {},
"nickname": null,
"product": "prod_1MvQAxHSA9cXarIcB4B6BS4D",
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"quantity": 1,
"schedule": null,
"start_date": 1679423056,
"status": "active",
"test_clock": null,
"transfer_data": null,
"trial_end": null,
"trial_settings": {
"end_behavior": {
"missing_payment_method": "cancel"
}
},
"trial_start": null
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/customers/cus_123/subscriptions"
},
"tax_exempt": "none",
"test_clock": null
}
],
"has_more": false,
"next_page": null,
"url": "/v1/customers/search"
}
@@ -0,0 +1,36 @@
{
"id": "cus_123",
"object": "customer",
"address": null,
"balance": 0,
"created": 1682026744,
"currency": null,
"default_source": null,
"delinquent": false,
"description": null,
"discount": null,
"email": "tester@shorebird.dev",
"invoice_prefix": "6B792ED7",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": null,
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {},
"name": "Bryan",
"next_invoice_sequence": 1,
"phone": null,
"preferred_locales": [],
"shipping": null,
"subscriptions": {
"object": "list",
"data": [],
"has_more": false,
"total_count": 0,
"url": "/v1/customers/cus_123/subscriptions"
},
"tax_exempt": "none",
"test_clock": null
}
@@ -0,0 +1,183 @@
{
"id": "cus_123",
"object": "customer",
"address": null,
"balance": 0,
"created": 1682026744,
"currency": "usd",
"default_source": "card_1Mz5EAHSA9cXarIcCcBvlE8S",
"delinquent": false,
"description": null,
"discount": null,
"email": "tester@shorebird.dev",
"invoice_prefix": "6B792ED7",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": null,
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {},
"name": "Bryan",
"next_invoice_sequence": 2,
"phone": null,
"preferred_locales": [],
"shipping": null,
"subscriptions": {
"object": "list",
"data": [
{
"id": "sub_1Mz5EJHSA9cXarIcEtgCXEFX",
"object": "subscription",
"application": null,
"application_fee_percent": null,
"automatic_tax": {
"enabled": false
},
"billing_cycle_anchor": 1682026887,
"billing_thresholds": null,
"cancel_at": null,
"cancel_at_period_end": false,
"canceled_at": null,
"cancellation_details": {
"comment": null,
"feedback": null,
"reason": null
},
"collection_method": "charge_automatically",
"created": 1682026887,
"currency": "usd",
"current_period_end": 1684618887,
"current_period_start": 1682026887,
"customer": "cus_123",
"days_until_due": null,
"default_payment_method": null,
"default_source": null,
"default_tax_rates": [],
"description": null,
"discount": null,
"ended_at": null,
"items": {
"object": "list",
"data": [
{
"id": "si_NkaPreHeOuPEvY",
"object": "subscription_item",
"billing_thresholds": null,
"created": 1682026888,
"metadata": {},
"plan": {
"id": "price_1MxZCLHSA9cXarIckOQjr3VY",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 2000,
"amount_decimal": "2000",
"billing_scheme": "per_unit",
"created": 1681665429,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {},
"nickname": null,
"product": "prod_Nj1EDNMwkkS0xc",
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"price": {
"id": "price_1MxZCLHSA9cXarIckOQjr3VY",
"object": "price",
"active": true,
"billing_scheme": "per_unit",
"created": 1681665429,
"currency": "usd",
"custom_unit_amount": null,
"livemode": false,
"lookup_key": null,
"metadata": {},
"nickname": null,
"product": "prod_Nj1EDNMwkkS0xc",
"recurring": {
"aggregate_usage": null,
"interval": "month",
"interval_count": 1,
"trial_period_days": null,
"usage_type": "licensed"
},
"tax_behavior": "exclusive",
"tiers_mode": null,
"transform_quantity": null,
"type": "recurring",
"unit_amount": 2000,
"unit_amount_decimal": "2000"
},
"quantity": 1,
"subscription": "sub_1Mz5EJHSA9cXarIcEtgCXEFX",
"tax_rates": []
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/subscription_items?subscription=sub_1Mz5EJHSA9cXarIcEtgCXEFX"
},
"latest_invoice": "in_1Mz5EJHSA9cXarIcqtI5chbK",
"livemode": false,
"metadata": {},
"next_pending_invoice_item_invoice": null,
"on_behalf_of": null,
"pause_collection": null,
"payment_settings": {
"payment_method_options": null,
"payment_method_types": null,
"save_default_payment_method": "off"
},
"pending_invoice_item_interval": null,
"pending_setup_intent": null,
"pending_update": null,
"plan": {
"id": "price_1MxZCLHSA9cXarIckOQjr3VY",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 2000,
"amount_decimal": "2000",
"billing_scheme": "per_unit",
"created": 1681665429,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {},
"nickname": null,
"product": "prod_Nj1EDNMwkkS0xc",
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"quantity": 1,
"schedule": null,
"start_date": 1682026887,
"status": "canceled",
"test_clock": null,
"transfer_data": null,
"trial_end": null,
"trial_settings": {
"end_behavior": {
"missing_payment_method": "create_invoice"
}
},
"trial_start": null
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/customers/cus_123/subscriptions"
},
"tax_exempt": "none",
"test_clock": null
}
@@ -0,0 +1,183 @@
{
"id": "cus_123",
"object": "customer",
"address": null,
"balance": 0,
"created": 1682026744,
"currency": "usd",
"default_source": "card_1Mz5EAHSA9cXarIcCcBvlE8S",
"delinquent": false,
"description": null,
"discount": null,
"email": "tester@shorebird.dev",
"invoice_prefix": "6B792ED7",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": null,
"footer": null,
"rendering_options": null
},
"livemode": false,
"metadata": {},
"name": "Bryan",
"next_invoice_sequence": 2,
"phone": null,
"preferred_locales": [],
"shipping": null,
"subscriptions": {
"object": "list",
"data": [
{
"id": "sub_1Mz5EJHSA9cXarIcEtgCXEFX",
"object": "subscription",
"application": null,
"application_fee_percent": null,
"automatic_tax": {
"enabled": false
},
"billing_cycle_anchor": 1682026887,
"billing_thresholds": null,
"cancel_at": null,
"cancel_at_period_end": false,
"canceled_at": null,
"cancellation_details": {
"comment": null,
"feedback": null,
"reason": null
},
"collection_method": "charge_automatically",
"created": 1682026887,
"currency": "usd",
"current_period_end": 1684618887,
"current_period_start": 1682026887,
"customer": "cus_123",
"days_until_due": null,
"default_payment_method": null,
"default_source": null,
"default_tax_rates": [],
"description": null,
"discount": null,
"ended_at": null,
"items": {
"object": "list",
"data": [
{
"id": "si_NkaPreHeOuPEvY",
"object": "subscription_item",
"billing_thresholds": null,
"created": 1682026888,
"metadata": {},
"plan": {
"id": "price_1MxZCLHSA9cXarIckOQjr3VY",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 2000,
"amount_decimal": "2000",
"billing_scheme": "per_unit",
"created": 1681665429,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {},
"nickname": null,
"product": "prod_123",
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"price": {
"id": "price_1MxZCLHSA9cXarIckOQjr3VY",
"object": "price",
"active": true,
"billing_scheme": "per_unit",
"created": 1681665429,
"currency": "usd",
"custom_unit_amount": null,
"livemode": false,
"lookup_key": null,
"metadata": {},
"nickname": null,
"product": "prod_123",
"recurring": {
"aggregate_usage": null,
"interval": "month",
"interval_count": 1,
"trial_period_days": null,
"usage_type": "licensed"
},
"tax_behavior": "exclusive",
"tiers_mode": null,
"transform_quantity": null,
"type": "recurring",
"unit_amount": 2000,
"unit_amount_decimal": "2000"
},
"quantity": 1,
"subscription": "sub_1Mz5EJHSA9cXarIcEtgCXEFX",
"tax_rates": []
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/subscription_items?subscription=sub_1Mz5EJHSA9cXarIcEtgCXEFX"
},
"latest_invoice": "in_1Mz5EJHSA9cXarIcqtI5chbK",
"livemode": false,
"metadata": {},
"next_pending_invoice_item_invoice": null,
"on_behalf_of": null,
"pause_collection": null,
"payment_settings": {
"payment_method_options": null,
"payment_method_types": null,
"save_default_payment_method": "off"
},
"pending_invoice_item_interval": null,
"pending_setup_intent": null,
"pending_update": null,
"plan": {
"id": "price_1MxZCLHSA9cXarIckOQjr3VY",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 2000,
"amount_decimal": "2000",
"billing_scheme": "per_unit",
"created": 1681665429,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {},
"nickname": null,
"product": "prod_123",
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"quantity": 1,
"schedule": null,
"start_date": 1682026887,
"status": "active",
"test_clock": null,
"transfer_data": null,
"trial_end": null,
"trial_settings": {
"end_behavior": {
"missing_payment_method": "create_invoice"
}
},
"trial_start": null
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/customers/cus_123/subscriptions"
},
"tax_exempt": "none",
"test_clock": null
}
@@ -0,0 +1,16 @@
{
"object": "list",
"data": [
{
"id": "mtrusg_test_6041HSA9cXarIc6U76ce6L359f4ft60m5Xl3ox5sv7bt6bJ5bx5Y459D4Xt2E17ko6M86kt7kV3bl5QJ3U87PA60g6kp3Dn3kL7Gu3HU5Xl3ox5sv7bt6bY4p52Dr7od3Hc71E6go4od4LJ6cC6UM4t17Lc3Ta6ky3fx2D33fu3LI3oA3Cy3Cy3Cy",
"object": "billing.meter_event_summary",
"aggregated_value": 610000.0,
"end_time": 1722495600,
"livemode": false,
"meter": "mtr_test_61Qs4Xo4ZBeo0c8Em41HSA9cXarIc0Ho",
"start_time": 1722492000
}
],
"has_more": false,
"url": "/v1/billing/meters/:id/event_summaries"
}
@@ -0,0 +1,16 @@
{
"object": "list",
"data": [
{
"id": "mtrusg_test_6041HSA9cXarIc6U76ce6L359f4ft60m5Xl3ox5sv7bt6bJ5bx5Y459D4Xt2E17ko6M86kt7kV3bl5QJ3U87PA60g6kp3Dn3kL7Gu3HU5Xl3ox5sv7bt6bY4p52Dr7od3Hc71E6go4od4LJ6cC6UM4t17Lc3Ta6ky3fx2D33fu3LI3oD3bk3Cy3Cy",
"object": "billing.meter_event_summary",
"aggregated_value": 610000.0,
"end_time": 1722499200,
"livemode": false,
"meter": "mtr_test_61Qs4Xo4ZBeo0c8Em41HSA9cXarIc0Ho",
"start_time": 1722495600
}
],
"has_more": true,
"url": "/v1/billing/meters/:id/event_summaries"
}
@@ -0,0 +1,60 @@
{
"id": "si_QdM8G4ozxTdW00",
"object": "subscription_item",
"billing_thresholds": null,
"created": 1723258390,
"discounts": [],
"metadata": {},
"plan": {
"id": "price_1Pm4YJHSA9cXarIcpDllPtvw",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": null,
"amount_decimal": "0.04",
"billing_scheme": "per_unit",
"created": 1723255027,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {},
"meter": "mtr_test_61QvSUDTnLya5cdwG41HSA9cXarIc144",
"nickname": null,
"product": "prod_QdLE0bb1qFBFMv",
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "metered"
},
"price": {
"id": "price_1Pm4YJHSA9cXarIcpDllPtvw",
"object": "price",
"active": true,
"billing_scheme": "per_unit",
"created": 1723255027,
"currency": "usd",
"custom_unit_amount": null,
"livemode": false,
"lookup_key": null,
"metadata": {},
"nickname": null,
"product": "prod_QdLE0bb1qFBFMv",
"recurring": {
"aggregate_usage": null,
"interval": "month",
"interval_count": 1,
"meter": "mtr_test_61QvSUDTnLya5cdwG41HSA9cXarIc144",
"trial_period_days": null,
"usage_type": "metered"
},
"tax_behavior": "exclusive",
"tiers_mode": null,
"transform_quantity": null,
"type": "recurring",
"unit_amount": null,
"unit_amount_decimal": "0.04"
},
"subscription": "sub_1Pm5QYHSA9cXarIcRDsMStDo",
"tax_rates": []
}
@@ -0,0 +1,65 @@
{
"id": "si_QdM8c5cisMLrb8",
"object": "subscription_item",
"billing_thresholds": null,
"created": 1723258390,
"discounts": [],
"metadata": {},
"plan": {
"id": "price_1Pijb9HSA9cXarIclBhJDy3G",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 2000,
"amount_decimal": "2000",
"billing_scheme": "per_unit",
"created": 1722459495,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {
"patch_install_limit": "50000"
},
"meter": null,
"nickname": null,
"product": "prod_QZtNd5YModWM7n",
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"price": {
"id": "price_1Pijb9HSA9cXarIclBhJDy3G",
"object": "price",
"active": true,
"billing_scheme": "per_unit",
"created": 1722459495,
"currency": "usd",
"custom_unit_amount": null,
"livemode": false,
"lookup_key": null,
"metadata": {
"patch_install_limit": "50000"
},
"nickname": null,
"product": "prod_QZtNd5YModWM7n",
"recurring": {
"aggregate_usage": null,
"interval": "month",
"interval_count": 1,
"meter": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"tax_behavior": "exclusive",
"tiers_mode": null,
"transform_quantity": null,
"type": "recurring",
"unit_amount": 2000,
"unit_amount_decimal": "2000"
},
"quantity": 1,
"subscription": "sub_1Pm5QYHSA9cXarIcRDsMStDo",
"tax_rates": []
}
@@ -0,0 +1,146 @@
{
"id": "sub_1MvjPuHSA9cXarIcaWYNaezR",
"object": "subscription",
"application": null,
"application_fee_percent": null,
"automatic_tax": {
"enabled": false
},
"billing_cycle_anchor": 1681228054,
"billing_thresholds": null,
"cancel_at": null,
"cancel_at_period_end": false,
"canceled_at": 1683820054,
"cancellation_details": {
"comment": null,
"feedback": null,
"reason": null
},
"collection_method": "charge_automatically",
"created": 1681228054,
"currency": "usd",
"current_period_end": 1683820054,
"current_period_start": 1681228054,
"customer": "cus_Nh7fUR7HHhO8xT",
"days_until_due": null,
"default_payment_method": null,
"default_source": null,
"default_tax_rates": [],
"description": null,
"discount": null,
"ended_at": null,
"items": {
"object": "list",
"data": [
{
"id": "si_Nh7f7SwD99EW6w",
"object": "subscription_item",
"billing_thresholds": null,
"created": 1681228055,
"metadata": {},
"plan": {
"id": "price_1MvjPuHSA9cXarIcfmWAQo72",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 1500,
"amount_decimal": "1500",
"billing_scheme": "per_unit",
"created": 1681228054,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {},
"nickname": null,
"product": "prod_Nh7fDKPeoghLht",
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"price": {
"id": "price_1MvjPuHSA9cXarIcfmWAQo72",
"object": "price",
"active": true,
"billing_scheme": "per_unit",
"created": 1681228054,
"currency": "usd",
"custom_unit_amount": null,
"livemode": false,
"lookup_key": null,
"metadata": {},
"nickname": null,
"product": "prod_Nh7fDKPeoghLht",
"recurring": {
"aggregate_usage": null,
"interval": "month",
"interval_count": 1,
"trial_period_days": null,
"usage_type": "licensed"
},
"tax_behavior": "unspecified",
"tiers_mode": null,
"transform_quantity": null,
"type": "recurring",
"unit_amount": 1500,
"unit_amount_decimal": "1500"
},
"quantity": 1,
"subscription": "sub_1MvjPuHSA9cXarIcaWYNaezR",
"tax_rates": []
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/subscription_items?subscription=sub_1MvjPuHSA9cXarIcaWYNaezR"
},
"latest_invoice": "in_1MvjPuHSA9cXarIcRh4yZJfr",
"livemode": false,
"metadata": {},
"next_pending_invoice_item_invoice": null,
"on_behalf_of": null,
"pause_collection": null,
"payment_settings": {
"payment_method_options": null,
"payment_method_types": null,
"save_default_payment_method": "off"
},
"pending_invoice_item_interval": null,
"pending_setup_intent": null,
"pending_update": null,
"plan": {
"id": "price_1MvjPuHSA9cXarIcfmWAQo72",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 1500,
"amount_decimal": "1500",
"billing_scheme": "per_unit",
"created": 1681228054,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {},
"nickname": null,
"product": "prod_Nh7fDKPeoghLht",
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"quantity": 1,
"schedule": null,
"start_date": 1681228054,
"status": "active",
"test_clock": null,
"transfer_data": null,
"trial_end": null,
"trial_settings": {
"end_behavior": {
"missing_payment_method": "create_invoice"
}
},
"trial_start": null
}
@@ -0,0 +1,161 @@
{
"id": "evt_1MvjPxHSA9cXarIcNoXsPPxL",
"object": "event",
"api_version": "2022-11-15",
"created": 1681228056,
"data": {
"object": {
"id": "sub_1MvjPuHSA9cXarIcaWYNaezR",
"object": "subscription",
"application": null,
"application_fee_percent": null,
"automatic_tax": {
"enabled": false
},
"billing_cycle_anchor": 1681228054,
"billing_thresholds": null,
"cancel_at": null,
"cancel_at_period_end": false,
"canceled_at": null,
"cancellation_details": {
"comment": null,
"feedback": null,
"reason": null
},
"collection_method": "charge_automatically",
"created": 1681228054,
"currency": "usd",
"current_period_end": 1683820054,
"current_period_start": 1681228054,
"customer": "cus_Nh7fUR7HHhO8xT",
"days_until_due": null,
"default_payment_method": null,
"default_source": null,
"default_tax_rates": [],
"description": null,
"discount": null,
"ended_at": null,
"items": {
"object": "list",
"data": [
{
"id": "si_Nh7f7SwD99EW6w",
"object": "subscription_item",
"billing_thresholds": null,
"created": 1681228055,
"metadata": {},
"plan": {
"id": "price_1MvjPuHSA9cXarIcfmWAQo72",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 1500,
"amount_decimal": "1500",
"billing_scheme": "per_unit",
"created": 1681228054,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {},
"nickname": null,
"product": "prod_Nh7fDKPeoghLht",
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"price": {
"id": "price_1MvjPuHSA9cXarIcfmWAQo72",
"object": "price",
"active": true,
"billing_scheme": "per_unit",
"created": 1681228054,
"currency": "usd",
"custom_unit_amount": null,
"livemode": false,
"lookup_key": null,
"metadata": {},
"nickname": null,
"product": "prod_Nh7fDKPeoghLht",
"recurring": {
"aggregate_usage": null,
"interval": "month",
"interval_count": 1,
"trial_period_days": null,
"usage_type": "licensed"
},
"tax_behavior": "unspecified",
"tiers_mode": null,
"transform_quantity": null,
"type": "recurring",
"unit_amount": 1500,
"unit_amount_decimal": "1500"
},
"quantity": 1,
"subscription": "sub_1MvjPuHSA9cXarIcaWYNaezR",
"tax_rates": []
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/subscription_items?subscription=sub_1MvjPuHSA9cXarIcaWYNaezR"
},
"latest_invoice": "in_1MvjPuHSA9cXarIcRh4yZJfr",
"livemode": false,
"metadata": {},
"next_pending_invoice_item_invoice": null,
"on_behalf_of": null,
"pause_collection": null,
"payment_settings": {
"payment_method_options": null,
"payment_method_types": null,
"save_default_payment_method": "off"
},
"pending_invoice_item_interval": null,
"pending_setup_intent": null,
"pending_update": null,
"plan": {
"id": "price_1MvjPuHSA9cXarIcfmWAQo72",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": 1500,
"amount_decimal": "1500",
"billing_scheme": "per_unit",
"created": 1681228054,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {},
"nickname": null,
"product": "prod_Nh7fDKPeoghLht",
"tiers_mode": null,
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"quantity": 1,
"schedule": null,
"start_date": 1681228054,
"status": "active",
"test_clock": null,
"transfer_data": null,
"trial_end": null,
"trial_settings": {
"end_behavior": {
"missing_payment_method": "create_invoice"
}
},
"trial_start": null
}
},
"livemode": false,
"pending_webhooks": 3,
"request": {
"id": "req_doI5SOWkABZxw7",
"idempotency_key": "593d6a9f-6489-4f54-8a20-87472f8287a6"
},
"type": "customer.subscription.created"
}
@@ -0,0 +1,197 @@
{
"id": "sub_1NzOsBHSA9cXarIchHmEjlmc",
"object": "subscription",
"application": null,
"application_fee_percent": null,
"automatic_tax": {
"enabled": false
},
"billing_cycle_anchor": 1696878731,
"billing_thresholds": null,
"cancel_at": null,
"cancel_at_period_end": false,
"canceled_at": null,
"cancellation_details": {
"comment": null,
"feedback": null,
"reason": null
},
"collection_method": "charge_automatically",
"created": 1696878731,
"currency": "usd",
"current_period_end": 1699557131,
"current_period_start": 1696878731,
"customer": "cus_Omyp1oQqpNqsWQ",
"days_until_due": null,
"default_payment_method": "pm_1NzOsAHSA9cXarIcblZrdytw",
"default_source": null,
"default_tax_rates": [],
"description": null,
"discount": null,
"ended_at": null,
"items": {
"object": "list",
"data": [
{
"id": "si_OmypkJfzhERoOd",
"object": "subscription_item",
"billing_thresholds": null,
"created": 1696878732,
"metadata": {},
"plan": {
"id": "price_1NzOkSHSA9cXarIcvNNb31Ou",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": null,
"amount_decimal": null,
"billing_scheme": "tiered",
"created": 1696878252,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {},
"nickname": "Teams (Volume)",
"product": "prod_Nj1EDNMwkkS0xc",
"tiers_mode": "volume",
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"price": {
"id": "price_1NzOkSHSA9cXarIcvNNb31Ou",
"object": "price",
"active": true,
"billing_scheme": "tiered",
"created": 1696878252,
"currency": "usd",
"custom_unit_amount": null,
"livemode": false,
"lookup_key": null,
"metadata": {},
"nickname": "Teams (Volume)",
"product": "prod_Nj1EDNMwkkS0xc",
"recurring": {
"aggregate_usage": null,
"interval": "month",
"interval_count": 1,
"trial_period_days": null,
"usage_type": "licensed"
},
"tax_behavior": "exclusive",
"tiers": [
{
"flat_amount": 2000,
"flat_amount_decimal": "2000",
"unit_amount": null,
"unit_amount_decimal": null,
"up_to": 50000
},
{
"flat_amount": 10000,
"flat_amount_decimal": "10000",
"unit_amount": null,
"unit_amount_decimal": null,
"up_to": 300000
},
{
"flat_amount": 30000,
"flat_amount_decimal": "30000",
"unit_amount": null,
"unit_amount_decimal": null,
"up_to": 1000000
},
{
"flat_amount": 70000,
"flat_amount_decimal": "70000",
"unit_amount": null,
"unit_amount_decimal": null,
"up_to": 2500000
},
{
"flat_amount": 125000,
"flat_amount_decimal": "125000",
"unit_amount": null,
"unit_amount_decimal": null,
"up_to": 5000000
},
{
"flat_amount": 200000,
"flat_amount_decimal": "200000",
"unit_amount": null,
"unit_amount_decimal": null,
"up_to": 10000000
},
{
"flat_amount": 500000,
"flat_amount_decimal": "500000",
"unit_amount": null,
"unit_amount_decimal": null,
"up_to": null
}
],
"tiers_mode": "volume",
"transform_quantity": null,
"type": "recurring",
"unit_amount": null,
"unit_amount_decimal": null
},
"quantity": 2000000,
"subscription": "sub_1NzOsBHSA9cXarIchHmEjlmc",
"tax_rates": []
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/subscription_items?subscription=sub_1NzOsBHSA9cXarIchHmEjlmc"
},
"latest_invoice": "in_1NzOsBHSA9cXarIcfpuFJuwh",
"livemode": false,
"metadata": {},
"next_pending_invoice_item_invoice": null,
"on_behalf_of": null,
"pause_collection": null,
"payment_settings": {
"payment_method_options": null,
"payment_method_types": null,
"save_default_payment_method": "off"
},
"pending_invoice_item_interval": null,
"pending_setup_intent": null,
"pending_update": null,
"plan": {
"id": "price_1NzOkSHSA9cXarIcvNNb31Ou",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": null,
"amount_decimal": null,
"billing_scheme": "tiered",
"created": 1696878252,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": false,
"metadata": {},
"nickname": "Teams (Volume)",
"product": "prod_Nj1EDNMwkkS0xc",
"tiers_mode": "volume",
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"quantity": 2000000,
"schedule": null,
"start_date": 1696878731,
"status": "active",
"test_clock": null,
"transfer_data": null,
"trial_end": null,
"trial_settings": {
"end_behavior": {
"missing_payment_method": "create_invoice"
}
},
"trial_start": null
}
@@ -0,0 +1,161 @@
{
"id": "sub_1OL84vHSA9cXarIcNIxi0Xho",
"object": "subscription",
"application": null,
"application_fee_percent": null,
"automatic_tax": {
"enabled": true,
"liability": {
"type": "self"
}
},
"billing_cycle_anchor": 1788053706,
"billing_cycle_anchor_config": null,
"billing_thresholds": null,
"cancel_at": null,
"cancel_at_period_end": false,
"canceled_at": null,
"cancellation_details": {
"comment": null,
"feedback": null,
"reason": null
},
"collection_method": "send_invoice",
"created": 1702057389,
"currency": "usd",
"current_period_end": 1788053706,
"current_period_start": 1725845719,
"customer": "cus_NnCoUcv8aBXCA2",
"days_until_due": 999,
"default_payment_method": null,
"default_source": null,
"default_tax_rates": [],
"description": null,
"discount": null,
"discounts": [],
"ended_at": null,
"invoice_settings": {
"account_tax_ids": null,
"issuer": {
"type": "self"
}
},
"items": {
"object": "list",
"data": [
{
"id": "si_P9QwtgfcYKifTz",
"object": "subscription_item",
"billing_thresholds": null,
"created": 1702057390,
"discounts": [],
"metadata": {},
"plan": {
"id": "price_1NzOwCHSA9cXarIciu4Vmzm0",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": null,
"amount_decimal": null,
"billing_scheme": "tiered",
"created": 1696878980,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": true,
"metadata": {},
"meter": null,
"nickname": "Team",
"product": "prod_NVGDFFgUebtGR6",
"tiers_mode": "volume",
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"price": {
"id": "price_1NzOwCHSA9cXarIciu4Vmzm0",
"object": "price",
"active": true,
"billing_scheme": "tiered",
"created": 1696878980,
"currency": "usd",
"custom_unit_amount": null,
"livemode": true,
"lookup_key": null,
"metadata": {},
"nickname": "Team",
"product": "prod_NVGDFFgUebtGR6",
"recurring": {
"aggregate_usage": null,
"interval": "month",
"interval_count": 1,
"meter": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"tax_behavior": "unspecified",
"tiers_mode": "volume",
"transform_quantity": null,
"type": "recurring",
"unit_amount": null,
"unit_amount_decimal": null
},
"quantity": 50000,
"subscription": "sub_1OL84vHSA9cXarIcNIxi0Xho",
"tax_rates": []
}
],
"has_more": false,
"total_count": 1,
"url": "/v1/subscription_items?subscription=sub_1OL84vHSA9cXarIcNIxi0Xho"
},
"latest_invoice": "in_1PwwVfHSA9cXarIcv6zzWmnb",
"livemode": true,
"metadata": {},
"next_pending_invoice_item_invoice": null,
"on_behalf_of": null,
"pause_collection": null,
"payment_settings": {
"payment_method_options": null,
"payment_method_types": null,
"save_default_payment_method": "off"
},
"pending_invoice_item_interval": null,
"pending_setup_intent": null,
"pending_update": null,
"plan": {
"id": "price_1NzOwCHSA9cXarIciu4Vmzm0",
"object": "plan",
"active": true,
"aggregate_usage": null,
"amount": null,
"amount_decimal": null,
"billing_scheme": "tiered",
"created": 1696878980,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": true,
"metadata": {},
"meter": null,
"nickname": "Team",
"product": "prod_NVGDFFgUebtGR6",
"tiers_mode": "volume",
"transform_usage": null,
"trial_period_days": null,
"usage_type": "licensed"
},
"quantity": 50000,
"schedule": null,
"start_date": 1702057389,
"status": "trialing",
"test_clock": null,
"transfer_data": null,
"trial_end": 1788053706,
"trial_settings": {
"end_behavior": {
"missing_payment_method": "create_invoice"
}
},
"trial_start": 1725845719
}
@@ -0,0 +1,95 @@
import 'dart:convert';
import 'dart:io';
const stripeJsonPath = 'test/fixtures/stripe/json';
String get checkoutSessionCompletedEventJsonString => File(
'$stripeJsonPath/checkout_session_completed_event.json',
).readAsStringSync();
Map<String, dynamic> get checkoutSessionCompletedEventJson =>
jsonDecode(checkoutSessionCompletedEventJsonString) as Map<String, dynamic>;
String get createPaymentLinkJsonString =>
File('$stripeJsonPath/create_payment_link.json').readAsStringSync();
String get customerJsonString =>
File('$stripeJsonPath/customer.json').readAsStringSync();
Map<String, dynamic> get customerJson =>
jsonDecode(customerJsonString) as Map<String, dynamic>;
String get customerWithEmptySubscriptionJsonString => File(
'$stripeJsonPath/customer_with_empty_subscriptions.json',
).readAsStringSync();
String get customerWithSubscriptionJsonString =>
File('$stripeJsonPath/customer_with_subscription.json').readAsStringSync();
String get customerWithInactiveSubscriptionJsonString => File(
'$stripeJsonPath/customer_with_inactive_subscription.json',
).readAsStringSync();
String get subscriptionJsonString =>
File('$stripeJsonPath/subscription.json').readAsStringSync();
Map<String, dynamic> get subscriptionJson =>
jsonDecode(subscriptionJsonString) as Map<String, dynamic>;
String get trialingSubscriptionString =>
File('$stripeJsonPath/trialing_subscription.json').readAsStringSync();
Map<String, dynamic> get trialingSubscriptionJson =>
jsonDecode(trialingSubscriptionString) as Map<String, dynamic>;
String get subscriptionWithPricingTiersJsonString => File(
'$stripeJsonPath/subscription_with_pricing_tiers.json',
).readAsStringSync();
Map<String, dynamic> get subscriptionWithPricingTiersJson =>
jsonDecode(subscriptionWithPricingTiersJsonString) as Map<String, dynamic>;
String get subscriptionCreatedEventJsonString =>
File('$stripeJsonPath/subscription_created_event.json').readAsStringSync();
Map<String, dynamic> get subscriptionCreatedEventJson =>
jsonDecode(subscriptionCreatedEventJsonString) as Map<String, dynamic>;
String get customerSearchOneResultWithExpandedSubscriptionsJsonString => File(
'$stripeJsonPath/customer_search_one_result_with_expanded_subscriptions.json',
).readAsStringSync();
Map<String, dynamic> get customerSearchOneResultWithExpandedSubscriptionsJson =>
jsonDecode(customerSearchOneResultWithExpandedSubscriptionsJsonString)
as Map<String, dynamic>;
String get meterEventSummariesWithNoMorePagesJsonString => File(
'$stripeJsonPath/meter_event_summaries_no_more_pages.json',
).readAsStringSync();
Map<String, dynamic> get meterEventSummariesWithNoMorePagesJson =>
jsonDecode(meterEventSummariesWithNoMorePagesJsonString)
as Map<String, dynamic>;
String get meterEventSummariesWithMorePagesJsonString => File(
'$stripeJsonPath/meter_event_summaries_with_more_pages.json',
).readAsStringSync();
Map<String, dynamic> get meterEventSummariesWithMorePagesJson =>
jsonDecode(meterEventSummariesWithMorePagesJsonString)
as Map<String, dynamic>;
String get platformAccessSubscriptionItemJsonString => File(
'$stripeJsonPath/platform_access_subscription_item.json',
).readAsStringSync();
Map<String, dynamic> get subscriptionItemJson =>
jsonDecode(platformAccessSubscriptionItemJsonString)
as Map<String, dynamic>;
String get payAsYouGoSubscriptionItemJsonString => File(
'$stripeJsonPath/pay_as_you_go_subscription_item.json',
).readAsStringSync();
Map<String, dynamic> get payAsYouGoSubscriptionItemJson =>
jsonDecode(payAsYouGoSubscriptionItemJsonString) as Map<String, dynamic>;
@@ -0,0 +1,27 @@
import 'package:stripe_api/src/converters/timestamp_converter.dart';
import 'package:test/test.dart';
void main() {
group(TimestampConverter, () {
group('toJson()', () {
test('converts a DateTime to a timestamp', () {
const converter = TimestampConverter();
final dateTime = DateTime(2023);
final timestamp = converter.toJson(dateTime);
expect(timestamp, equals(dateTime.millisecondsSinceEpoch ~/ 1000));
});
});
group('fromJson()', () {
test('converts a timestamp to a DateTime', () {
const converter = TimestampConverter();
const timestamp = 1672552800;
final dateTime = converter.fromJson(timestamp);
expect(
dateTime,
equals(DateTime.fromMillisecondsSinceEpoch(timestamp * 1000)),
);
});
});
});
}
@@ -0,0 +1,17 @@
import 'package:stripe_api/stripe_api.dart';
import 'package:test/test.dart';
import '../../fixtures/stripe/stripe_fixtures.dart';
void main() {
group(StripeCheckoutSession, () {
test('deserializes from json', () async {
final sessionEvent = StripeEvent<StripeCheckoutSession>.fromJson(
checkoutSessionCompletedEventJson,
);
final session = sessionEvent.object;
expect(session.customerId, 'cus_123');
expect(session.metadata, {'shorebird_email': 'tester@shorebird.dev'});
});
});
}
@@ -0,0 +1,42 @@
import 'package:stripe_api/stripe_api.dart';
import 'package:test/test.dart';
import '../../fixtures/stripe/stripe_fixtures.dart';
void main() {
group(StripeCustomer, () {
test('deserializes from json', () {
final customer = StripeCustomer.fromJson(customerJson);
expect(customer.id, 'cus_123');
expect(customer.name, 'Jane Doe');
expect(customer.email, 'test@shorebird.dev');
expect(customer.subscriptions, null);
expect(
customer.toString(),
equals('Jane Doe - test@shorebird.dev (id:cus_123)'),
);
});
test('deserializes from list with expanded subscriptions', () {
final data =
customerSearchOneResultWithExpandedSubscriptionsJson['data'] as List;
final customerJson = data.first as Map<String, dynamic>;
final customer = StripeCustomer.fromJson(customerJson);
expect(customer.id, 'cus_123');
expect(customer.name, 'Jane Doe');
expect(customer.email, 'test@shorebird.dev');
final subscriptions = customer.subscriptions;
expect(subscriptions!.length, equals(1));
final subscription = subscriptions.first;
expect(subscription.id, equals('sub_1Mo9r2HSA9cXarIcVgf2GQt4'));
expect(subscription.customer, equals('cus_123'));
expect(subscription.status.name, equals('active'));
});
});
}
@@ -0,0 +1,43 @@
import 'package:stripe_api/stripe_api.dart';
import 'package:test/test.dart';
import '../../fixtures/stripe/stripe_fixtures.dart';
void main() {
group(StripeEvent, () {
test('deserializes checkout session event', () {
final event = StripeEvent<StripeCheckoutSession>.fromJson(
checkoutSessionCompletedEventJson,
);
expect(event.object, isA<StripeCheckoutSession>());
expect(
event.created,
DateTime.fromMillisecondsSinceEpoch(1681743321 * 1000),
);
});
test('deserializes subscription event', () {
final event = StripeEvent<StripeSubscription>.fromJson(
subscriptionCreatedEventJson,
);
expect(event.object, isA<StripeSubscription>());
expect(
event.created,
DateTime.fromMillisecondsSinceEpoch(1681228056 * 1000),
);
});
test('throws exception on unknown event type', () {
expect(
() => StripeEvent<dynamic>.fromJson({
'id': 'evt_123',
'created': 1681228056,
'data': {
'object': {'object': 'bogus.object'},
},
}),
throwsA(isA<Exception>()),
);
});
});
}
@@ -0,0 +1,175 @@
import 'package:decimal/decimal.dart';
import 'package:stripe_api/stripe_api.dart';
import 'package:test/test.dart';
import '../../fixtures/stripe/stripe_fixtures.dart';
void main() {
group(StripePrice, () {
group('(de)serialization', () {
test('can be deserialized from json', () {
final subscription = StripeSubscription.fromJson(subscriptionJson);
final price = subscription.items.first.price;
// cspell:disable-next-line
expect(price.id, 'price_1MvjPuHSA9cXarIcfmWAQo72');
// cspell:disable-next-line
expect(price.productId, 'prod_Nh7fDKPeoghLht');
expect(price.currency, 'usd');
expect(price.billingScheme, BillingScheme.perUnit);
expect(price.unitAmount, 1500);
expect(price.unitAmountDecimal, Decimal.fromInt(1500));
expect(price.tiers, isNull);
expect(price.usageType, UsageType.licensed);
expect(price.meterId, isNull);
});
test('can be deserialized from json (pay as you go)', () {
final subscriptionItem = StripeSubscriptionItem.fromJson(
payAsYouGoSubscriptionItemJson,
);
final price = subscriptionItem.price;
// cspell:disable-next-line
expect(price.id, 'price_1Pm4YJHSA9cXarIcpDllPtvw');
// cspell:disable-next-line
expect(price.productId, 'prod_QdLE0bb1qFBFMv');
expect(price.currency, 'usd');
expect(price.billingScheme, BillingScheme.perUnit);
expect(price.unitAmount, isNull);
expect(price.unitAmountDecimal, Decimal.parse('0.04'));
expect(price.tiers, isNull);
expect(price.usageType, UsageType.metered);
expect(price.meterId, 'mtr_test_61QvSUDTnLya5cdwG41HSA9cXarIc144');
});
});
group('StripePriceTiers', () {
const fiftyThousandTier = StripePriceTier(
flatAmount: 2000,
flatAmountDecimal: '2000',
unitAmount: null,
unitAmountDecimal: null,
upTo: 50000,
);
const threeHundredThousandTier = StripePriceTier(
flatAmount: 10000,
flatAmountDecimal: '1000',
unitAmount: null,
unitAmountDecimal: null,
upTo: 300000,
);
const oneMillionTier = StripePriceTier(
flatAmount: 30000,
flatAmountDecimal: '30000',
unitAmount: null,
unitAmountDecimal: null,
upTo: 1000000,
);
const twoAndAHalfMillionTier = StripePriceTier(
flatAmount: 70000,
flatAmountDecimal: '70000',
unitAmount: null,
unitAmountDecimal: null,
upTo: 2500000,
);
const fiveMillionTier = StripePriceTier(
flatAmount: 125000,
flatAmountDecimal: '125000',
unitAmount: null,
unitAmountDecimal: null,
upTo: 5000000,
);
const tenMillionTier = StripePriceTier(
flatAmount: 200000,
flatAmountDecimal: '200000',
unitAmount: null,
unitAmountDecimal: null,
upTo: 10000000,
);
const maxTier = StripePriceTier(
flatAmount: 500000,
flatAmountDecimal: '500000',
unitAmount: null,
unitAmountDecimal: null,
upTo: null,
);
late List<StripePriceTier> tiers;
setUp(() {
tiers = [
fiftyThousandTier,
threeHundredThousandTier,
oneMillionTier,
twoAndAHalfMillionTier,
fiveMillionTier,
tenMillionTier,
maxTier,
];
});
test('tierForQuantity returns the correct tier', () {
final price = StripePrice(
id: 'test-price-id',
productId: 'test-product-id',
currency: 'usd',
billingScheme: BillingScheme.tiered,
tiers: tiers,
);
// Values equal to a tier's [upTo] field should return that tier.
expect(price.tierForQuantity(50000), fiftyThousandTier);
expect(price.tierForQuantity(300000), threeHundredThousandTier);
expect(price.tierForQuantity(1000000), oneMillionTier);
expect(price.tierForQuantity(2500000), twoAndAHalfMillionTier);
expect(price.tierForQuantity(5000000), fiveMillionTier);
expect(price.tierForQuantity(10000000), tenMillionTier);
// Values between two tiers' [upTo] fields should return the higher
// tier.
expect(price.tierForQuantity(40000), fiftyThousandTier);
expect(price.tierForQuantity(2000000), twoAndAHalfMillionTier);
expect(price.tierForQuantity(2500001), fiveMillionTier);
// Values above any tier's [upTo] field should return the last bounded
// tier.
expect(price.tierForQuantity(99999999), tenMillionTier);
});
group('when tiers are shuffled', () {
setUp(() {
tiers.shuffle();
});
test('tierForQuantity returns the correct tier', () {
final price = StripePrice(
id: 'test-price-id',
productId: 'test-product-id',
currency: 'usd',
billingScheme: BillingScheme.tiered,
tiers: tiers,
);
// Values equal to a tier's [upTo] field should return that tier.
expect(price.tierForQuantity(50000), fiftyThousandTier);
expect(price.tierForQuantity(300000), threeHundredThousandTier);
expect(price.tierForQuantity(1000000), oneMillionTier);
expect(price.tierForQuantity(2500000), twoAndAHalfMillionTier);
expect(price.tierForQuantity(5000000), fiveMillionTier);
expect(price.tierForQuantity(10000000), tenMillionTier);
// Values between two tiers' [upTo] fields should return the higher
// tier.
expect(price.tierForQuantity(40000), fiftyThousandTier);
expect(price.tierForQuantity(2000000), twoAndAHalfMillionTier);
expect(price.tierForQuantity(2500001), fiveMillionTier);
// Values above any tier's [upTo] field should return the last bounded
// tier.
expect(price.tierForQuantity(99999999), tenMillionTier);
});
});
});
});
}
@@ -0,0 +1,210 @@
// cspell:words sub_1MvjPuHSA9cXarIcaWYNaezR cus_Nh7fUR7HHhO8xT
import 'package:stripe_api/stripe_api.dart';
import 'package:test/test.dart';
import '../../fixtures/stripe/stripe_fixtures.dart';
void main() {
group(StripeSubscription, () {
test('deserializes from json', () {
final subscription = StripeSubscription.fromJson(subscriptionJson);
expect(subscription.id, 'sub_1MvjPuHSA9cXarIcaWYNaezR');
expect(subscription.cancelAtPeriodEnd, false);
expect(
subscription.canceledAt,
DateTime.fromMillisecondsSinceEpoch(1683820054 * 1000),
);
expect(
subscription.currentPeriodEnd,
DateTime.fromMillisecondsSinceEpoch(1683820054 * 1000),
);
expect(
subscription.currentPeriodStart,
DateTime.fromMillisecondsSinceEpoch(1681228054 * 1000),
);
expect(subscription.customer, 'cus_Nh7fUR7HHhO8xT');
expect(subscription.endedAt, null);
expect(
subscription.startDate,
DateTime.fromMillisecondsSinceEpoch(1681228054 * 1000),
);
expect(subscription.status, StripeSubscriptionStatus.active);
expect(subscription.trialStart, isNull);
expect(subscription.trialEnd, isNull);
});
test('deserializes from json with missing items data', () {
final updatedSubscriptionJson = subscriptionJson;
(updatedSubscriptionJson['items'] as Map<String, dynamic>).remove('data');
final subscription = StripeSubscription.fromJson(updatedSubscriptionJson);
expect(subscription.items, isEmpty);
});
group('trial subscription', () {
test('deserializes from json', () {
final subscription = StripeSubscription.fromJson(
trialingSubscriptionJson,
);
// cspell:disable-next-line
expect(subscription.id, 'sub_1OL84vHSA9cXarIcNIxi0Xho');
expect(subscription.cancelAtPeriodEnd, false);
expect(subscription.currentPeriodEnd, isNotNull);
expect(subscription.currentPeriodStart, isNotNull);
// cspell:disable-next-line
expect(subscription.customer, 'cus_NnCoUcv8aBXCA2');
expect(subscription.endedAt, null);
expect(subscription.startDate, isNotNull);
expect(subscription.status, StripeSubscriptionStatus.trialing);
expect(
subscription.trialStart,
DateTime.fromMillisecondsSinceEpoch(1725845719000),
);
expect(
subscription.trialEnd,
DateTime.fromMillisecondsSinceEpoch(1788053706000),
);
expect(subscription.isActiveOrTrial, isTrue);
});
});
group('isActiveOrTrial', () {
test('returns true if status is active', () {
final subscription = StripeSubscription(
id: 'sub_123',
cancelAtPeriodEnd: false,
currentPeriodEnd: DateTime.now(),
currentPeriodStart: DateTime.now(),
customer: 'cus_123',
startDate: DateTime.now(),
status: StripeSubscriptionStatus.active,
items: [],
);
expect(subscription.isActiveOrTrial, isTrue);
});
test('returns true if subscription is trialing', () {
final subscription = StripeSubscription.fromJson(
trialingSubscriptionJson,
);
expect(subscription.isActiveOrTrial, isTrue);
});
test('returns false if status is not active', () {
final subscription = StripeSubscription(
id: 'sub_123',
cancelAtPeriodEnd: false,
currentPeriodEnd: DateTime.now(),
currentPeriodStart: DateTime.now(),
customer: 'cus_123',
startDate: DateTime.now(),
status: StripeSubscriptionStatus.canceled,
items: [],
);
expect(subscription.isActiveOrTrial, isFalse);
});
});
group('totalCost', () {
test('returns 0 if subscription contains no items', () {
final subscription = StripeSubscription(
id: 'sub_123',
cancelAtPeriodEnd: false,
currentPeriodEnd: DateTime.now(),
currentPeriodStart: DateTime.now(),
customer: 'cus_123',
startDate: DateTime.now(),
status: StripeSubscriptionStatus.active,
items: [],
);
expect(subscription.totalCost, 0);
});
test('returns sum of item costs', () {
final subscription = StripeSubscription(
id: 'sub_123',
cancelAtPeriodEnd: false,
currentPeriodEnd: DateTime.now(),
currentPeriodStart: DateTime.now(),
customer: 'cus_123',
startDate: DateTime.now(),
status: StripeSubscriptionStatus.active,
items: [
const StripeSubscriptionItem(
id: 'item_1',
price: StripePrice(
id: 'price_1',
currency: 'usd',
productId: 'prod_123',
unitAmount: 1000,
billingScheme: BillingScheme.perUnit,
),
quantity: 1000,
),
const StripeSubscriptionItem(
id: 'item_2',
price: StripePrice(
id: 'price_2',
currency: 'usd',
productId: 'prod_123',
unitAmount: 2000,
billingScheme: BillingScheme.perUnit,
),
quantity: 1000,
),
const StripeSubscriptionItem(
id: 'item_3',
price: StripePrice(
id: 'price_3',
currency: 'usd',
productId: 'prod_123',
unitAmount: 3000,
billingScheme: BillingScheme.perUnit,
),
quantity: 1000,
),
],
);
expect(subscription.totalCost, 6000);
});
});
group('hasMeteredBilling', () {
late StripeSubscription subscription;
group("when a subscription item's price has a metered usage type", () {
setUp(() {
final subscriptionItem = StripeSubscriptionItem.fromJson(
payAsYouGoSubscriptionItemJson,
);
subscription = StripeSubscription(
id: 'sub_123',
cancelAtPeriodEnd: false,
currentPeriodEnd: DateTime.now(),
currentPeriodStart: DateTime.now(),
customer: 'cus_123',
startDate: DateTime.now(),
status: StripeSubscriptionStatus.active,
items: [subscriptionItem],
);
});
test('returns true', () {
expect(subscription.hasMeteredBilling, isTrue);
});
});
group("when no subscription item's price has a metered usage type", () {
setUp(() {
subscription = StripeSubscription.fromJson(subscriptionJson);
});
test('returns false', () {
expect(subscription.hasMeteredBilling, isFalse);
});
});
});
});
}
@@ -0,0 +1,364 @@
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';
import 'package:stripe_api/stripe_api.dart';
import 'package:test/test.dart';
import '../fixtures/stripe/stripe_fixtures.dart';
class _MockHttpClient extends Mock implements http.Client {}
void main() {
group(StripeApi, () {
const expectedAuthHeaders = {
HttpHeaders.authorizationHeader: 'Bearer secret',
};
late http.Client httpClient;
late StripeApi stripeApi;
setUpAll(() {
registerFallbackValue(Uri.parse('https://www.google.com/'));
});
setUp(() {
httpClient = _MockHttpClient();
stripeApi = StripeApi(client: httpClient, secretKey: 'secret');
});
test('can be instantiated without an explicit httpClient', () {
expect(() => StripeApi(secretKey: 'secret'), returnsNormally);
});
group('fetchActiveSubscriptions', () {
final customerUri = Uri.parse(
'https://api.stripe.com/v1/customers/cus_123?expand%5B%5D=subscriptions',
);
test('returns an empty list if the customer object is missing a '
'subscriptions list', () async {
when(
() => httpClient.get(customerUri, headers: any(named: 'headers')),
).thenAnswer(
(_) async => http.Response(customerJsonString, HttpStatus.ok),
);
final subscriptions = await stripeApi.fetchActiveOrTrialSubscriptions(
customerId: 'cus_123',
);
expect(subscriptions, isEmpty);
});
test(
'returns an empty list if the customer has no subscriptions',
() async {
when(
() => httpClient.get(customerUri, headers: any(named: 'headers')),
).thenAnswer(
(_) async => http.Response(
customerWithEmptySubscriptionJsonString,
HttpStatus.ok,
),
);
final subscriptions = await stripeApi.fetchActiveOrTrialSubscriptions(
customerId: 'cus_123',
);
expect(subscriptions, isEmpty);
},
);
test(
"returns an empty list if customer's subscriptions are inactive",
() async {
when(
() => httpClient.get(customerUri, headers: any(named: 'headers')),
).thenAnswer(
(_) async => http.Response(
customerWithInactiveSubscriptionJsonString,
HttpStatus.ok,
),
);
final subscriptions = await stripeApi.fetchActiveOrTrialSubscriptions(
customerId: 'cus_123',
);
expect(subscriptions, isEmpty);
},
);
test('returns subscriptions if the customer has subscriptions', () async {
when(
() => httpClient.get(customerUri, headers: any(named: 'headers')),
).thenAnswer(
(_) async =>
http.Response(customerWithSubscriptionJsonString, HttpStatus.ok),
);
final subscriptions = await stripeApi.fetchActiveOrTrialSubscriptions(
customerId: 'cus_123',
);
expect(subscriptions, isNotEmpty);
});
});
group('fetchCustomer', () {
final uri = Uri.parse(
'https://api.stripe.com/v1/customers/cus_123?expand%5B%5D=subscriptions',
);
test('throws exception if Stripe returns a non-200 response', () {
when(
() => httpClient.get(uri, headers: any(named: 'headers')),
).thenAnswer(
(_) async => http.Response('Not found', HttpStatus.notFound),
);
expect(
() => stripeApi.fetchCustomer(customerId: 'cus_123'),
throwsException,
);
verify(
() => httpClient.get(uri, headers: expectedAuthHeaders),
).called(1);
});
test('returns a customer on successful request', () async {
when(
() => httpClient.get(uri, headers: any(named: 'headers')),
).thenAnswer(
(_) async => http.Response(customerJsonString, HttpStatus.ok),
);
final customer = await stripeApi.fetchCustomer(customerId: 'cus_123');
expect(customer, isNotNull);
expect(customer.id, 'cus_123');
verify(
() => httpClient.get(uri, headers: expectedAuthHeaders),
).called(1);
});
});
group('fetchSubscription', () {
const subscriptionId = 'sub_123';
final uri = Uri.parse(
'https://api.stripe.com/v1/subscriptions/sub_123',
).replace(queryParameters: {'expand[]': 'items.data.price.tiers'});
test('throws exception if Stripe returns a non-200 response', () async {
when(
() => httpClient.get(uri, headers: any(named: 'headers')),
).thenAnswer(
(_) async => http.Response('Not found', HttpStatus.notFound),
);
expect(
() => stripeApi.fetchSubscription(subscriptionId: subscriptionId),
throwsException,
);
verify(
() => httpClient.get(uri, headers: expectedAuthHeaders),
).called(1);
});
test('returns a subscription on successful request', () async {
when(
() => httpClient.get(uri, headers: any(named: 'headers')),
).thenAnswer(
(_) async => http.Response(subscriptionJsonString, HttpStatus.ok),
);
final subscription = await stripeApi.fetchSubscription(
subscriptionId: subscriptionId,
);
expect(subscription, isNotNull);
// cspell:disable-next-line
expect(subscription.id, 'sub_1MvjPuHSA9cXarIcaWYNaezR');
expect(subscription.items.first.price.tiers, isNull);
});
test(
'returns a subscription with pricing tiers on successful request',
() async {
when(
() => httpClient.get(uri, headers: any(named: 'headers')),
).thenAnswer(
(_) async => http.Response(
subscriptionWithPricingTiersJsonString,
HttpStatus.ok,
),
);
final subscription = await stripeApi.fetchSubscription(
subscriptionId: subscriptionId,
);
expect(subscription, isNotNull);
// cspell:disable-next-line
expect(subscription.id, 'sub_1NzOsBHSA9cXarIchHmEjlmc');
final tiers = subscription.items.first.price.tiers;
expect(tiers, hasLength(7));
expect(tiers?.first.flatAmount, 2000);
expect(tiers?.first.upTo, 50000);
expect(tiers?.last.upTo, isNull);
},
);
});
group('createMeterEvent', () {
final uri = Uri.parse('https://api.stripe.com/v1/billing/meter_events');
setUp(() {
when(
() => httpClient.post(
any(),
headers: any(named: 'headers'),
body: any(named: 'body'),
),
).thenAnswer((_) async => http.Response('', HttpStatus.ok));
});
test('sends the correct request', () async {
await stripeApi.createMeterEvent(
customerId: 'cus_123',
eventName: 'test_event',
value: 100,
timestamp: 1234,
);
verify(
() => httpClient.post(
uri,
headers: expectedAuthHeaders,
body: {
'event_name': 'test_event',
'timestamp': '1234',
'payload[value]': '100',
'payload[stripe_customer_id]': 'cus_123',
},
),
).called(1);
});
group('when response has non-success status code', () {
setUp(() {
when(
() => httpClient.post(
any(),
headers: any(named: 'headers'),
body: any(named: 'body'),
),
).thenAnswer(
(_) async => http.Response('Not found', HttpStatus.notFound),
);
});
test('throws exception', () async {
await expectLater(
() => stripeApi.createMeterEvent(
customerId: 'cus_123',
eventName: 'my-event',
value: 100,
),
throwsException,
);
});
});
});
group('getMeterEventSummaries', () {
const customerId = 'cus_123';
const meterId = 'mtr_test_61Qs4Xo4ZBeo0c8Em41HSA9cXarIc0Ho';
const startTimestamp = 0;
const endTimestamp = 60;
setUp(() {
when(
() => httpClient.get(
Uri.parse(
'https://api.stripe.com/v1/billing/meters/mtr_test_61Qs4Xo4ZBeo0c8Em41HSA9cXarIc0Ho/event_summaries?customer=cus_123&start_time=0&end_time=60&limit=100',
),
headers: any(named: 'headers'),
),
).thenAnswer(
(_) async => http.Response(
meterEventSummariesWithMorePagesJsonString,
HttpStatus.ok,
),
);
when(
() => httpClient.get(
Uri.parse(
'https://api.stripe.com/v1/billing/meters/mtr_test_61Qs4Xo4ZBeo0c8Em41HSA9cXarIc0Ho/event_summaries?customer=cus_123&start_time=0&end_time=60&limit=100&starting_after=mtrusg_test_6041HSA9cXarIc6U76ce6L359f4ft60m5Xl3ox5sv7bt6bJ5bx5Y459D4Xt2E17ko6M86kt7kV3bl5QJ3U87PA60g6kp3Dn3kL7Gu3HU5Xl3ox5sv7bt6bY4p52Dr7od3Hc71E6go4od4LJ6cC6UM4t17Lc3Ta6ky3fx2D33fu3LI3oD3bk3Cy3Cy',
),
headers: any(named: 'headers'),
),
).thenAnswer(
(_) async => http.Response(
meterEventSummariesWithNoMorePagesJsonString,
HttpStatus.ok,
),
);
});
test('returns all pages of meter summaries', () async {
final meterEventSummary1 = StripeMeterEventSummary.fromJson(
// Ignoring dynamic call for testing purposes.
// ignore: avoid_dynamic_calls
meterEventSummariesWithMorePagesJson['data'][0]
as Map<String, dynamic>,
);
final meterEventSummary2 = StripeMeterEventSummary.fromJson(
// Ignoring dynamic call for testing purposes.
// ignore: avoid_dynamic_calls
meterEventSummariesWithNoMorePagesJson['data'][0]
as Map<String, dynamic>,
);
final result = await stripeApi.getMeterEventSummaries(
meterId: meterId,
customerId: customerId,
startTimestamp: startTimestamp,
endTimestamp: endTimestamp,
);
expect(result, hasLength(2));
expect(result[0].toJson(), equals(meterEventSummary1.toJson()));
expect(result[1].toJson(), equals(meterEventSummary2.toJson()));
});
group('when response has non-success status code', () {
setUp(() {
when(
() => httpClient.get(any(), headers: any(named: 'headers')),
).thenAnswer(
(_) async => http.Response('Not found', HttpStatus.notFound),
);
});
test('throws exception', () async {
await expectLater(
() => stripeApi.getMeterEventSummaries(
meterId: meterId,
customerId: customerId,
startTimestamp: startTimestamp,
endTimestamp: endTimestamp,
),
throwsException,
);
});
});
});
});
}
+1
View File
@@ -11,6 +11,7 @@ workspace:
- packages/shorebird_cli
- packages/shorebird_code_push_client
- packages/shorebird_code_push_protocol
- packages/stripe_api
dev_dependencies:
very_good_analysis: ^9.0.0