feat: move stripe_api into our public repo (#3260)
Co-authored-by: Felix Angelov <felix@shorebird.dev>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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 customer’s full name or business name.
|
||||
final String? name;
|
||||
|
||||
/// The email address associated with this customer.
|
||||
final String? email;
|
||||
|
||||
/// The customer’s 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;
|
||||
});
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export 'src/models/models.dart';
|
||||
export 'src/stripe_api.dart';
|
||||
@@ -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
|
||||
+95
@@ -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
|
||||
}
|
||||
+69
@@ -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": []
|
||||
}
|
||||
+38
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
+193
@@ -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"
|
||||
}
|
||||
+36
@@ -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
|
||||
}
|
||||
+183
@@ -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
|
||||
}
|
||||
+183
@@ -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
|
||||
}
|
||||
+16
@@ -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"
|
||||
}
|
||||
+16
@@ -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"
|
||||
}
|
||||
+60
@@ -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": []
|
||||
}
|
||||
+65
@@ -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
|
||||
}
|
||||
+161
@@ -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"
|
||||
}
|
||||
+197
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user