diff --git a/lib/messages.dart b/lib/messages.dart index f4a69fb..b6f3552 100644 --- a/lib/messages.dart +++ b/lib/messages.dart @@ -1,7 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stripe/src/messages/converters.dart'; import 'package:stripe/src/messages/enums.dart'; -import 'package:stripe/src/messages/enums/pause_collection_behavior.dart'; export 'package:stripe/src/messages/enums.dart'; @@ -19,8 +18,8 @@ part 'src/messages/data_list.dart'; part 'src/messages/discount.dart'; part 'src/messages/event.dart'; part 'src/messages/invoice.dart'; -part 'src/messages/payment_intent.dart'; part 'src/messages/pause_collection.dart'; +part 'src/messages/payment_intent.dart'; part 'src/messages/payment_method.dart'; part 'src/messages/portal_session.dart'; part 'src/messages/price.dart'; diff --git a/lib/src/expanded/customer_expanded.dart b/lib/src/expanded/customer_expanded.dart new file mode 100644 index 0000000..272e2a9 --- /dev/null +++ b/lib/src/expanded/customer_expanded.dart @@ -0,0 +1,9 @@ +import 'package:stripe/messages.dart'; + +class CustomerExpanded { + final Customer customer; + + CustomerExpanded({ + required this.customer, + }); +} diff --git a/lib/src/expanded/discount_expanded.dart b/lib/src/expanded/discount_expanded.dart new file mode 100644 index 0000000..0bcd83e --- /dev/null +++ b/lib/src/expanded/discount_expanded.dart @@ -0,0 +1,9 @@ +import 'package:stripe/messages.dart'; + +class DiscountExpanded { + final Discount discount; + + DiscountExpanded({ + required this.discount, + }); +} diff --git a/lib/src/expanded/invoice_expanded.dart b/lib/src/expanded/invoice_expanded.dart index 98f0d15..9da7f76 100644 --- a/lib/src/expanded/invoice_expanded.dart +++ b/lib/src/expanded/invoice_expanded.dart @@ -1,36 +1,14 @@ import 'package:stripe/messages.dart'; -import 'package:stripe/src/utils/expandable_fields/discounts_expandable_field.dart'; -import 'package:stripe/src/utils/expandable_fields/payment_intent_expandable_field.dart'; +import 'package:stripe/src/expanded/discount_expanded.dart'; class InvoiceExpanded { final Invoice invoice; final PaymentIntent? paymentIntent; - final List? discounts; + final List? discounts; InvoiceExpanded({ required this.invoice, this.paymentIntent, this.discounts, }); - - factory InvoiceExpanded.fromJson( - Map json, - Set expand, - ) { - PaymentIntent? paymentIntent; - if (expand.contains(InvoiceExpandableField.paymentIntent)) { - paymentIntent = PaymentIntentExpandableField().extract(json); - } - - List? discounts; - if (expand.contains(InvoiceExpandableField.discounts)) { - discounts = DiscountsExpandableField().extract(json); - } - - return InvoiceExpanded( - invoice: Invoice.fromJson(json), - paymentIntent: paymentIntent, - discounts: discounts, - ); - } } diff --git a/lib/src/expanded/subscription_expanded.dart b/lib/src/expanded/subscription_expanded.dart index 87d7dfa..30f7b86 100644 --- a/lib/src/expanded/subscription_expanded.dart +++ b/lib/src/expanded/subscription_expanded.dart @@ -1,14 +1,13 @@ import 'package:stripe/messages.dart'; import 'package:stripe/src/expanded.dart'; -import 'package:stripe/src/utils/expandable_fields/customer_expandable_field.dart'; -import 'package:stripe/src/utils/expandable_fields/discounts_expandable_field.dart'; -import 'package:stripe/src/utils/expandable_fields/latest_invoice_expanded_expandable_field.dart'; +import 'package:stripe/src/expanded/customer_expanded.dart'; +import 'package:stripe/src/expanded/discount_expanded.dart'; class SubscriptionExpanded { final Subscription subscription; - final List? discounts; + final List? discounts; final InvoiceExpanded? latestInvoice; - final Customer? customer; + final CustomerExpanded? customer; SubscriptionExpanded({ required this.subscription, @@ -16,33 +15,4 @@ class SubscriptionExpanded { this.latestInvoice, this.customer, }); - - factory SubscriptionExpanded.fromJson( - Map json, - Set expand, - ) { - List? discounts; - if (expand.contains(SubscriptionExpandableField.discounts)) { - discounts = const DiscountsExpandableField().extract(json); - } - - InvoiceExpanded? latestInvoice; - if (expand.contains(SubscriptionExpandableField.latestInvoice)) { - latestInvoice = const LatestInvoiceExpandedExpandableField( - expand: {InvoiceExpandableField.paymentIntent}, - ).extract(json); - } - - Customer? customer; - if (expand.contains(SubscriptionExpandableField.customer)) { - customer = const CustomerExpandableField().extract(json); - } - - return SubscriptionExpanded( - subscription: Subscription.fromJson(json), - discounts: discounts, - latestInvoice: latestInvoice, - customer: customer, - ); - } } diff --git a/lib/src/messages/enums.dart b/lib/src/messages/enums.dart index 680b4dc..75b538d 100644 --- a/lib/src/messages/enums.dart +++ b/lib/src/messages/enums.dart @@ -1,5 +1,3 @@ -export 'enums/expandable_fields/invoice_expandable_field.dart'; -export 'enums/expandable_fields/subscription_expandable_field.dart'; export 'enums/payment_behavior.dart'; export 'enums/proration_behavior.dart'; export 'enums/stripe_api_error_type.dart'; diff --git a/lib/src/messages/enums/expandable_fields/invoice_expandable_field.dart b/lib/src/messages/enums/expandable_fields/invoice_expandable_field.dart deleted file mode 100644 index d970151..0000000 --- a/lib/src/messages/enums/expandable_fields/invoice_expandable_field.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -enum InvoiceExpandableField { - @JsonValue('payment_intent') - paymentIntent, - @JsonValue('discounts') - discounts, -} diff --git a/lib/src/messages/enums/expandable_fields/subscription_expandable_field.dart b/lib/src/messages/enums/expandable_fields/subscription_expandable_field.dart deleted file mode 100644 index c148873..0000000 --- a/lib/src/messages/enums/expandable_fields/subscription_expandable_field.dart +++ /dev/null @@ -1,5 +0,0 @@ -enum SubscriptionExpandableField { - discounts, - latestInvoice, - customer, -} diff --git a/lib/src/resources/invoice.dart b/lib/src/resources/invoice.dart index aa5206d..2172328 100644 --- a/lib/src/resources/invoice.dart +++ b/lib/src/resources/invoice.dart @@ -2,9 +2,7 @@ import 'dart:async'; import 'package:stripe/messages.dart'; import 'package:stripe/src/expanded.dart'; -import 'package:stripe/src/utils/expandable_field.dart'; -import 'package:stripe/src/utils/expandable_fields/discounts_expandable_field.dart'; -import 'package:stripe/src/utils/expandable_fields/payment_intent_expandable_field.dart'; +import 'package:stripe/src/utils/expandable_fields/invoice_expandable_field.dart'; import '../client.dart'; import '_resource.dart'; @@ -23,36 +21,16 @@ class InvoiceResource extends Resource { Future createPreviewExpanded( CreatePreviewInvoiceRequest request, { - required Set expand, + required InvoiceExpandableField expand, }) async { - final expandableFields = _expandableFields(expand); final response = await post( '$_resourceName/create_preview', data: { ...request.toJson(), - 'expand': expandableFields.map((e) => e.field).toList(), + 'expand': expand.innerNestedFields.toList(), }, ); - return InvoiceExpanded.fromJson(response, expand); - } - - Iterable _expandableFields( - Set fields, - ) { - return fields.map( - (field) => _expandableField(field), - ); - } - - ExpandableField _expandableField( - InvoiceExpandableField field, - ) { - switch (field) { - case InvoiceExpandableField.paymentIntent: - return PaymentIntentExpandableField(); - case InvoiceExpandableField.discounts: - return DiscountsExpandableField(); - } + return expand.parse(response); } } diff --git a/lib/src/resources/subscription.dart b/lib/src/resources/subscription.dart index 6b6c3e7..9185cfd 100644 --- a/lib/src/resources/subscription.dart +++ b/lib/src/resources/subscription.dart @@ -2,10 +2,9 @@ import 'dart:async'; import 'package:stripe/messages.dart'; import 'package:stripe/src/expanded.dart'; -import 'package:stripe/src/utils/expandable_field.dart'; -import 'package:stripe/src/utils/expandable_fields/customer_expandable_field.dart'; import 'package:stripe/src/utils/expandable_fields/discounts_expandable_field.dart'; -import 'package:stripe/src/utils/expandable_fields/latest_invoice_expanded_expandable_field.dart'; +import 'package:stripe/src/utils/expandable_fields/latest_invoice_expandable_field.dart'; +import 'package:stripe/src/utils/expandable_fields/subscription_expandable_field.dart'; import '../client.dart'; import '_resource.dart'; @@ -19,10 +18,10 @@ class SubscriptionResource extends Resource { Future create(CreateSubscriptionRequest request) async { final response = await post(_resourceName, data: request.toJson()); - return SubscriptionExpanded.fromJson(response, { - SubscriptionExpandableField.discounts, - SubscriptionExpandableField.latestInvoice, - }); + return SubscriptionExpandableField( + discountsExpansion: DiscountsExpandableField(), + latestInvoiceExpansion: LatestInvoiceExpandableField(), + ).extract(response)!; } Future retrieve(String id) async { @@ -32,40 +31,17 @@ class SubscriptionResource extends Resource { Future retrieveExpanded( String id, { - required Set expand, + required SubscriptionExpandableField expand, }) async { - final expandableFields = _expandableFields(expand); final response = await get( '$_resourceName/$id', queryParameters: { - 'expand': expandableFields.map((e) => e.field).toList(), + for (int i = 0; i < expand.innerNestedFields.length; i++) + 'expand[$i]': expand.innerNestedFields.elementAt(i), }, ); - return SubscriptionExpanded.fromJson(response, expand); - } - - Iterable _expandableFields( - Set fields, - ) { - return fields.map( - (field) => _expandableField(field), - ); - } - - ExpandableField _expandableField( - SubscriptionExpandableField field, - ) { - switch (field) { - case SubscriptionExpandableField.discounts: - return DiscountsExpandableField(); - case SubscriptionExpandableField.latestInvoice: - return LatestInvoiceExpandedExpandableField( - expand: {InvoiceExpandableField.paymentIntent}, - ); - case SubscriptionExpandableField.customer: - return CustomerExpandableField(); - } + return expand.parse(response); } Future> list( @@ -76,25 +52,20 @@ class SubscriptionResource extends Resource { } Future> listExpanded({ - required Set expand, + required SubscriptionExpandableField expand, ListSubscriptionsRequest? request, }) async { - final expandableFields = _expandableFields(expand); - final response = await get( _resourceName, queryParameters: { ...?request?.toJson(), - 'expand': expandableFields.map((e) => 'data.${e.field}').toList(), + 'expand': expand.nestedFields.toList(), }, ); return DataList.fromJson( response, - (value) => SubscriptionExpanded.fromJson( - value as Map, - expand, - ), + (value) => expand.extract(value as Map)!, ); } diff --git a/lib/src/utils/expandable_field.dart b/lib/src/utils/expandable_field.dart index 680d144..bf4ddda 100644 --- a/lib/src/utils/expandable_field.dart +++ b/lib/src/utils/expandable_field.dart @@ -1,6 +1,14 @@ abstract class ExpandableField { String get field; + Iterable get innerNestedFields => []; + + Iterable get nestedFields { + if (innerNestedFields.isEmpty) return [field]; + + return innerNestedFields.map((nestedField) => '$field.$nestedField'); + } + const ExpandableField(); T extract(Map json); diff --git a/lib/src/utils/expandable_fields/customer_expandable_field.dart b/lib/src/utils/expandable_fields/customer_expandable_field.dart index d6d70af..f4f164f 100644 --- a/lib/src/utils/expandable_fields/customer_expandable_field.dart +++ b/lib/src/utils/expandable_fields/customer_expandable_field.dart @@ -1,15 +1,17 @@ import 'package:stripe/messages.dart'; +import 'package:stripe/src/expanded/customer_expanded.dart'; import 'package:stripe/src/utils/expandable_object_field.dart'; -class CustomerExpandableField extends ExpandableObjectField { +class CustomerExpandableField extends ExpandableObjectField { @override String get field => 'customer'; const CustomerExpandableField(); @override - Customer parse(Map object) => Customer.fromJson(object); + CustomerExpanded parse(Map object) => + CustomerExpanded(customer: Customer.fromJson(object)); @override - String replacement(Customer parsedValue) => parsedValue.id; + String replacement(CustomerExpanded parsedValue) => parsedValue.customer.id; } diff --git a/lib/src/utils/expandable_fields/discounts_expandable_field.dart b/lib/src/utils/expandable_fields/discounts_expandable_field.dart index 2f8addb..0c9ddfc 100644 --- a/lib/src/utils/expandable_fields/discounts_expandable_field.dart +++ b/lib/src/utils/expandable_fields/discounts_expandable_field.dart @@ -1,16 +1,19 @@ import 'package:stripe/messages.dart'; +import 'package:stripe/src/expanded/discount_expanded.dart'; import 'package:stripe/src/utils/expandable_list_field.dart'; -class DiscountsExpandableField extends ExpandableListField { +class DiscountsExpandableField extends ExpandableListField { @override String get field => 'discounts'; const DiscountsExpandableField(); @override - String elementReplacement(Discount element) => element.id; + String elementReplacement(DiscountExpanded element) => element.discount.id; @override - Discount parseElement(Map element) => - Discount.fromJson(element); + DiscountExpanded parseElement(Map element) => + DiscountExpanded( + discount: Discount.fromJson(element), + ); } diff --git a/lib/src/utils/expandable_fields/invoice_expandable_field.dart b/lib/src/utils/expandable_fields/invoice_expandable_field.dart new file mode 100644 index 0000000..7fa35a9 --- /dev/null +++ b/lib/src/utils/expandable_fields/invoice_expandable_field.dart @@ -0,0 +1,49 @@ +import 'package:stripe/messages.dart'; +import 'package:stripe/src/expanded.dart'; +import 'package:stripe/src/expanded/discount_expanded.dart'; +import 'package:stripe/src/utils/expandable_fields/discounts_expandable_field.dart'; +import 'package:stripe/src/utils/expandable_fields/payment_intent_expandable_field.dart'; +import 'package:stripe/src/utils/expandable_object_field.dart'; + +class InvoiceExpandableField extends ExpandableObjectField { + final PaymentIntentExpandableField? paymentIntentExpansion; + final DiscountsExpandableField? discountsExpansion; + + @override + String get field => 'invoice'; + + @override + Iterable get innerNestedFields => [ + ...?paymentIntentExpansion?.nestedFields, + ...?discountsExpansion?.nestedFields, + ]; + + const InvoiceExpandableField({ + this.paymentIntentExpansion, + this.discountsExpansion, + }); + + @override + InvoiceExpanded parse(Map object) { + PaymentIntent? paymentIntent; + if (paymentIntentExpansion != null) { + paymentIntent = paymentIntentExpansion!.extract(object); + } + + List? discounts; + if (discountsExpansion != null) { + discounts = discountsExpansion!.extract(object); + } + + return InvoiceExpanded( + invoice: Invoice.fromJson(object), + paymentIntent: paymentIntent, + discounts: discounts, + ); + } + + @override + String replacement(InvoiceExpanded parsedValue) { + return parsedValue.invoice.id; + } +} diff --git a/lib/src/utils/expandable_fields/latest_invoice_expandable_field.dart b/lib/src/utils/expandable_fields/latest_invoice_expandable_field.dart new file mode 100644 index 0000000..2214625 --- /dev/null +++ b/lib/src/utils/expandable_fields/latest_invoice_expandable_field.dart @@ -0,0 +1,16 @@ +import 'package:stripe/src/utils/expandable_fields/discounts_expandable_field.dart'; +import 'package:stripe/src/utils/expandable_fields/invoice_expandable_field.dart'; +import 'package:stripe/src/utils/expandable_fields/payment_intent_expandable_field.dart'; + +class LatestInvoiceExpandableField extends InvoiceExpandableField { + @override + String get field => 'latest_invoice'; + + const LatestInvoiceExpandableField({ + PaymentIntentExpandableField? paymentIntentExpansion, + DiscountsExpandableField? discountsExpansion, + }) : super( + paymentIntentExpansion: paymentIntentExpansion, + discountsExpansion: discountsExpansion, + ); +} diff --git a/lib/src/utils/expandable_fields/latest_invoice_expanded_expandable_field.dart b/lib/src/utils/expandable_fields/latest_invoice_expanded_expandable_field.dart deleted file mode 100644 index 2bfd161..0000000 --- a/lib/src/utils/expandable_fields/latest_invoice_expanded_expandable_field.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:stripe/src/expanded.dart'; -import 'package:stripe/src/messages/enums/expandable_fields/invoice_expandable_field.dart'; -import 'package:stripe/src/utils/expandable_object_field.dart'; - -class LatestInvoiceExpandedExpandableField - extends ExpandableObjectField { - final Set expand; - - @override - String get field => 'latest_invoice'; - - const LatestInvoiceExpandedExpandableField({ - required this.expand, - }); - - @override - InvoiceExpanded parse(Map object) { - return InvoiceExpanded.fromJson(object, expand); - } - - @override - String replacement(InvoiceExpanded parsedValue) { - return parsedValue.invoice.id; - } -} diff --git a/lib/src/utils/expandable_fields/subscription_expandable_field.dart b/lib/src/utils/expandable_fields/subscription_expandable_field.dart new file mode 100644 index 0000000..c3b0abd --- /dev/null +++ b/lib/src/utils/expandable_fields/subscription_expandable_field.dart @@ -0,0 +1,61 @@ +import 'package:stripe/messages.dart'; +import 'package:stripe/src/expanded/customer_expanded.dart'; +import 'package:stripe/src/expanded/discount_expanded.dart'; +import 'package:stripe/src/expanded/invoice_expanded.dart'; +import 'package:stripe/src/expanded/subscription_expanded.dart'; +import 'package:stripe/src/utils/expandable_fields/customer_expandable_field.dart'; +import 'package:stripe/src/utils/expandable_fields/discounts_expandable_field.dart'; +import 'package:stripe/src/utils/expandable_fields/latest_invoice_expandable_field.dart'; +import 'package:stripe/src/utils/expandable_object_field.dart'; + +class SubscriptionExpandableField + extends ExpandableObjectField { + final DiscountsExpandableField? discountsExpansion; + final LatestInvoiceExpandableField? latestInvoiceExpansion; + final CustomerExpandableField? customerExpansion; + + const SubscriptionExpandableField({ + this.discountsExpansion, + this.latestInvoiceExpansion, + this.customerExpansion, + }); + + @override + String get field => 'subscription'; + + @override + Iterable get innerNestedFields => [ + ...?customerExpansion?.nestedFields, + ...?latestInvoiceExpansion?.nestedFields, + ...?discountsExpansion?.nestedFields, + ]; + + @override + SubscriptionExpanded parse(Map object) { + List? discounts; + if (discountsExpansion != null) { + discounts = discountsExpansion!.extract(object); + } + + InvoiceExpanded? latestInvoice; + if (latestInvoiceExpansion != null) { + latestInvoice = latestInvoiceExpansion!.extract(object); + } + + CustomerExpanded? customer; + if (customerExpansion != null) { + customer = customerExpansion!.extract(object); + } + + return SubscriptionExpanded( + subscription: Subscription.fromJson(object), + discounts: discounts, + latestInvoice: latestInvoice, + customer: customer, + ); + } + + @override + String replacement(SubscriptionExpanded parsedValue) => + parsedValue.subscription.id; +} diff --git a/test/resources/expanded/subscription_expandable_field_test.dart b/test/resources/expanded/subscription_expandable_field_test.dart new file mode 100644 index 0000000..7f304dd --- /dev/null +++ b/test/resources/expanded/subscription_expandable_field_test.dart @@ -0,0 +1,39 @@ +import 'package:stripe/src/utils/expandable_fields/customer_expandable_field.dart'; +import 'package:stripe/src/utils/expandable_fields/discounts_expandable_field.dart'; +import 'package:stripe/src/utils/expandable_fields/latest_invoice_expandable_field.dart'; +import 'package:stripe/src/utils/expandable_fields/payment_intent_expandable_field.dart'; +import 'package:stripe/src/utils/expandable_fields/subscription_expandable_field.dart'; +import 'package:test/test.dart'; + +void main() { + group('SubscriptionExpandableField', () { + const expansionSpec = SubscriptionExpandableField( + customerExpansion: CustomerExpandableField(), + discountsExpansion: DiscountsExpandableField(), + latestInvoiceExpansion: LatestInvoiceExpandableField( + discountsExpansion: DiscountsExpandableField(), + paymentIntentExpansion: PaymentIntentExpandableField(), + ), + ); + test('Provides own expansion parameters correctly', () async { + expect( + expansionSpec.nestedFields.toSet(), + containsAll({ + 'subscription.latest_invoice.payment_intent', + 'subscription.latest_invoice.discounts', + 'subscription.customer', + 'subscription.discounts', + })); + }); + test('Provides nested expansion parameters correctly', () async { + expect( + expansionSpec.innerNestedFields.toSet(), + containsAll({ + 'latest_invoice.payment_intent', + 'latest_invoice.discounts', + 'customer', + 'discounts', + })); + }); + }); +} diff --git a/test/resources/subscription/subscription_expanded_test.dart b/test/resources/subscription/subscription_expanded_test.dart new file mode 100644 index 0000000..1ed4f85 --- /dev/null +++ b/test/resources/subscription/subscription_expanded_test.dart @@ -0,0 +1,662 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:stripe/src/client.dart'; +import 'package:stripe/src/expanded/discount_expanded.dart'; +import 'package:stripe/src/resources/subscription.dart'; +import 'package:stripe/src/utils/expandable_fields/customer_expandable_field.dart'; +import 'package:stripe/src/utils/expandable_fields/discounts_expandable_field.dart'; +import 'package:stripe/src/utils/expandable_fields/latest_invoice_expandable_field.dart'; +import 'package:stripe/src/utils/expandable_fields/payment_intent_expandable_field.dart'; +import 'package:stripe/src/utils/expandable_fields/subscription_expandable_field.dart'; +import 'package:test/test.dart'; + +void main() { + late DioClient client; + late SubscriptionResource subscriptionResource; + setUp(() { + // We set the baseUrl to something unreachable, because we define + // interceptors in the tests. + client = DioClient(apiKey: 'sk_foobar', baseUrl: 'http://void/'); + subscriptionResource = SubscriptionResource(client); + }); + group('SubscriptionExpanded', () { + test('properly decodes all values', () async { + const subscriptionId = 'sub_testSubscriptionId'; + final expansionSpec = SubscriptionExpandableField( + customerExpansion: CustomerExpandableField(), + discountsExpansion: DiscountsExpandableField(), + latestInvoiceExpansion: LatestInvoiceExpandableField( + discountsExpansion: DiscountsExpandableField(), + paymentIntentExpansion: PaymentIntentExpandableField(), + ), + ); + + client.dio.interceptors.add(InterceptorsWrapper( + onRequest: (options, handler) { + expect(options.method, 'GET'); + expect(options.data, null); + expect( + options.queryParameters.values.toSet(), + containsAll({ + 'latest_invoice.payment_intent', + 'latest_invoice.discounts', + 'customer', + 'discounts', + })); + handler.resolve( + Response( + requestOptions: options, + data: jsonDecode(retrieveExpandedSubscriptionResponse), + statusCode: HttpStatus.ok, + ), + ); + }, + )); + + final subscriptionExpanded = await subscriptionResource.retrieveExpanded( + subscriptionId, + expand: expansionSpec, + ); + + final subscription = subscriptionExpanded.subscription; + expect(subscription.id, subscriptionId); + expect(subscription.items.data.length, 1); + + final subscriptionItem = subscription.items.data.first; + expect(subscriptionItem.id, 'si_testSubscriptionItemId'); + expect(subscriptionItem.price.id, 'price_testPlanPriceId'); + expect(subscriptionItem.price.active, isTrue); + + expect( + subscriptionExpanded.discounts, + isA>() + .having((l) => l.isEmpty, 'isEmpty', isTrue), + ); + + final customer = subscriptionExpanded.customer?.customer; + if (customer == null) fail('Customer is null'); + expect(customer.id, 'cus_testCustomerId'); + expect(customer.email, 'testmail@test.com'); + + final latestInvoice = subscriptionExpanded.latestInvoice?.invoice; + if (latestInvoice == null) fail('latestInvoice is null'); + expect(latestInvoice.id, 'in_testInvoiceId'); + expect(latestInvoice.accountName, 'A very profitable company'); + + final paymentIntent = subscriptionExpanded.latestInvoice?.paymentIntent; + if (paymentIntent == null) fail('latestInvoice.paymentIntent is null'); + expect(paymentIntent.id, 'pi_testPaymentIntentId'); + expect(paymentIntent.amount, 29900); + + final invoiceDiscounts = subscriptionExpanded.latestInvoice?.discounts; + if (invoiceDiscounts == null) fail('latestInvoice.discounts is null'); + expect(invoiceDiscounts, isEmpty); + }); + }); +} + +const retrieveExpandedSubscriptionResponse = r''' +{ + "id": "sub_testSubscriptionId", + "object": "subscription", + "application": null, + "application_fee_percent": null, + "automatic_tax": { + "enabled": false, + "liability": null + }, + "billing_cycle_anchor": 1727074800, + "billing_cycle_anchor_config": { + "day_of_month": 23, + "hour": 7, + "minute": 0, + "month": null, + "second": 0 + }, + "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": 1724414109, + "currency": "usd", + "current_period_end": 1727074800, + "current_period_start": 1724414109, + "customer": { + "id": "cus_testCustomerId", + "object": "customer", + "address": null, + "balance": 0, + "created": 1721299529, + "currency": "usd", + "default_currency": "usd", + "default_source": null, + "delinquent": false, + "description": null, + "discount": null, + "email": "testmail@test.com", + "invoice_prefix": "EADE9237", + "invoice_settings": { + "custom_fields": null, + "default_payment_method": "pm_testPaymentMethodId", + "footer": null, + "rendering_options": null + }, + "livemode": false, + "metadata": {}, + "name": "John Doe", + "next_invoice_sequence": 59, + "phone": null, + "preferred_locales": [], + "shipping": null, + "tax_exempt": "none", + "test_clock": null + }, + "days_until_due": null, + "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_testSubscriptionItemId", + "object": "subscription_item", + "billing_thresholds": null, + "created": 1724414109, + "discounts": [], + "metadata": {}, + "plan": { + "id": "price_testPlanPriceId", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": 29900, + "amount_decimal": "29900", + "billing_scheme": "per_unit", + "created": 1720570375, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "meter": null, + "nickname": null, + "product": "prod_testProductId", + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "price": { + "id": "price_testPlanPriceId", + "object": "price", + "active": true, + "billing_scheme": "per_unit", + "created": 1720570375, + "currency": "usd", + "custom_unit_amount": null, + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "product": "prod_testProductId", + "recurring": { + "aggregate_usage": null, + "interval": "month", + "interval_count": 1, + "meter": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "recurring", + "unit_amount": 29900, + "unit_amount_decimal": "29900" + }, + "quantity": 1, + "subscription": "sub_testSubscriptionId", + "tax_rates": [] + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/subscription_items?subscription=sub_testSubscriptionId" + }, + "latest_invoice": { + "id": "in_testInvoiceId", + "object": "invoice", + "account_country": "US", + "account_name": "A very profitable company", + "account_tax_ids": null, + "amount_due": 29900, + "amount_paid": 29900, + "amount_remaining": 0, + "amount_shipping": 0, + "application": null, + "application_fee_amount": null, + "attempt_count": 1, + "attempted": true, + "auto_advance": false, + "automatic_tax": { + "enabled": false, + "liability": null, + "status": null + }, + "automatically_finalizes_at": null, + "billing_reason": "subscription_create", + "charge": "ch_testChargeId", + "collection_method": "charge_automatically", + "created": 1724414109, + "currency": "usd", + "custom_fields": null, + "customer": "cus_testCustomerId", + "customer_address": null, + "customer_email": "testmail@test.com", + "customer_name": "John Doe", + "customer_phone": null, + "customer_shipping": null, + "customer_tax_exempt": "none", + "customer_tax_ids": [], + "default_payment_method": null, + "default_source": null, + "default_tax_rates": [], + "description": null, + "discount": null, + "discounts": [], + "due_date": null, + "effective_at": 1724414109, + "ending_balance": 0, + "footer": null, + "from_invoice": null, + "hosted_invoice_url": "https://void", + "invoice_pdf": "https://void", + "issuer": { + "type": "self" + }, + "last_finalization_error": null, + "latest_revision": null, + "lines": { + "object": "list", + "data": [ + { + "id": "il_testLineItemId", + "object": "line_item", + "amount": 29900, + "amount_excluding_tax": 29900, + "currency": "usd", + "description": "Time on a subscription plan from 23 Aug 2024 until 23 Sep 2024", + "discount_amounts": [], + "discountable": false, + "discounts": [], + "invoice": "in_testInvoiceId", + "invoice_item": "ii_testInvoiceItemId", + "livemode": false, + "metadata": {}, + "period": { + "end": 1727074800, + "start": 1724396400 + }, + "plan": { + "id": "price_testPlanPriceId", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": 29900, + "amount_decimal": "29900", + "billing_scheme": "per_unit", + "created": 1720570375, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "meter": null, + "nickname": null, + "product": "prod_testProductId", + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "price": { + "id": "price_testPlanPriceId", + "object": "price", + "active": true, + "billing_scheme": "per_unit", + "created": 1720570375, + "currency": "usd", + "custom_unit_amount": null, + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "product": "prod_testProductId", + "recurring": { + "aggregate_usage": null, + "interval": "month", + "interval_count": 1, + "meter": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "tax_behavior": "unspecified", + "tiers_mode": null, + "transform_quantity": null, + "type": "recurring", + "unit_amount": 29900, + "unit_amount_decimal": "29900" + }, + "proration": true, + "proration_details": { + "credited_items": null + }, + "quantity": 1, + "subscription": "sub_testSubscriptionId", + "subscription_item": "si_testSubscriptionItemId", + "tax_amounts": [], + "tax_rates": [], + "type": "invoiceitem", + "unit_amount_excluding_tax": "29900" + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/invoices/in_testInvoiceId/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "number": "XXXXXX-XXXX", + "on_behalf_of": null, + "paid": true, + "paid_out_of_band": false, + "payment_intent": { + "id": "pi_testPaymentIntentId", + "object": "payment_intent", + "amount": 29900, + "amount_capturable": 0, + "amount_details": { + "tip": {} + }, + "amount_received": 29900, + "application": null, + "application_fee_amount": null, + "automatic_payment_methods": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "charges": { + "object": "list", + "data": [ + { + "id": "ch_testChargeId", + "object": "charge", + "amount": 29900, + "amount_captured": 29900, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": "txn_testTransactionId", + "billing_details": { + "address": { + "city": null, + "country": "GE", + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "calculated_statement_descriptor": "TEST.COM", + "captured": true, + "created": 1724414114, + "currency": "usd", + "customer": "cus_testCustomerId", + "description": "Subscription creation", + "destination": null, + "dispute": null, + "disputed": false, + "failure_balance_transaction": null, + "failure_code": null, + "failure_message": null, + "fraud_details": {}, + "invoice": "in_testInvoiceId", + "livemode": false, + "metadata": {}, + "on_behalf_of": null, + "order": null, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "normal", + "risk_score": 51, + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": true, + "payment_intent": "pi_testPaymentIntentId", + "payment_method": "pm_testPaymentMethodId", + "payment_method_details": { + "card": { + "amount_authorized": 29900, + "authorization_code": null, + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": null + }, + "country": "US", + "exp_month": 4, + "exp_year": 2026, + "extended_authorization": { + "status": "disabled" + }, + "fingerprint": "testFingerprintId", + "funding": "credit", + "incremental_authorization": { + "status": "unavailable" + }, + "installments": null, + "last4": "4242", + "mandate": null, + "multicapture": { + "status": "unavailable" + }, + "network": "visa", + "network_token": { + "used": false + }, + "overcapture": { + "maximum_amount_capturable": 29900, + "status": "unavailable" + }, + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": "testmail@test.com", + "receipt_number": null, + "receipt_url": "https://void", + "refunded": false, + "refunds": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/charges/ch_testChargeId/refunds" + }, + "review": null, + "shipping": null, + "source": null, + "source_transfer": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/charges?payment_intent=pi_testPaymentIntentId" + }, + "client_secret": "pi_testPaymentIntentId_secret_testSecret", + "confirmation_method": "automatic", + "created": 1724414109, + "currency": "usd", + "customer": "cus_testCustomerId", + "description": "Subscription creation", + "invoice": "in_testInvoiceId", + "last_payment_error": null, + "latest_charge": "ch_testChargeId", + "livemode": false, + "metadata": {}, + "next_action": null, + "on_behalf_of": null, + "payment_method": "pm_testPaymentMethodId", + "payment_method_configuration_details": null, + "payment_method_options": { + "card": { + "installments": null, + "mandate_options": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card" + ], + "processing": null, + "receipt_email": "testmail@test.com", + "review": null, + "setup_future_usage": "off_session", + "shipping": null, + "source": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + }, + "payment_settings": { + "default_mandate": null, + "payment_method_options": null, + "payment_method_types": [ + "card" + ] + }, + "period_end": 1724414109, + "period_start": 1724414109, + "post_payment_credit_notes_amount": 0, + "pre_payment_credit_notes_amount": 0, + "quote": null, + "receipt_number": null, + "rendering": null, + "rendering_options": null, + "shipping_cost": null, + "shipping_details": null, + "starting_balance": 0, + "statement_descriptor": null, + "status": "paid", + "status_transitions": { + "finalized_at": 1724414109, + "marked_uncollectible_at": null, + "paid_at": 1724414115, + "voided_at": null + }, + "subscription": "sub_testSubscriptionId", + "subscription_details": { + "metadata": { + "contract_id": "157" + } + }, + "subtotal": 29900, + "subtotal_excluding_tax": 29900, + "tax": null, + "test_clock": null, + "total": 29900, + "total_discount_amounts": [], + "total_excluding_tax": 29900, + "total_tax_amounts": [], + "transfer_data": null, + "webhooks_delivered_at": 1724414111 + }, + "livemode": false, + "metadata": { + "contract_id": "157" + }, + "next_pending_invoice_item_invoice": null, + "on_behalf_of": null, + "pause_collection": null, + "payment_settings": { + "payment_method_options": null, + "payment_method_types": [ + "card" + ], + "save_default_payment_method": "off" + }, + "pending_invoice_item_interval": null, + "pending_setup_intent": null, + "pending_update": null, + "plan": { + "id": "price_testPlanPriceId", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": 29900, + "amount_decimal": "29900", + "billing_scheme": "per_unit", + "created": 1720570375, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "meter": null, + "nickname": null, + "product": "prod_testProductId", + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "quantity": 1, + "schedule": null, + "start_date": 1724396400, + "status": "active", + "test_clock": null, + "transfer_data": null, + "trial_end": null, + "trial_settings": { + "end_behavior": { + "missing_payment_method": "create_invoice" + } + }, + "trial_start": null +}'''; diff --git a/test/resources/subscription_test.dart b/test/resources/subscription/subscription_test.dart similarity index 100% rename from test/resources/subscription_test.dart rename to test/resources/subscription/subscription_test.dart