From 1cd940260d91d9d85959b375dd1c176a1161b7b4 Mon Sep 17 00:00:00 2001 From: solid-maksymtielnyi Date: Mon, 14 Oct 2024 10:30:33 +0300 Subject: [PATCH] Improve expanding responses --- lib/messages.dart | 2 + lib/messages.g.dart | 43 ++++-- lib/src/expanded.dart | 2 - lib/src/expanded/invoice_expanded.dart | 36 ----- lib/src/expanded/subscription_expanded.dart | 67 -------- lib/src/messages/converters.dart | 59 +++++++ lib/src/messages/discount.dart | 10 ++ lib/src/messages/enums.dart | 8 +- .../invoice_expandable_field.dart | 8 - .../subscription_expandable_field.dart | 7 - .../customer_expandable_object.dart | 4 + .../discount_expandable_object.dart | 4 + .../expandable_objects/expandable_object.dart | 60 ++++++++ .../invoice_expandable_object.dart | 20 +++ .../payment_intent_expandable_object.dart | 4 + .../subscription_expandable_object.dart | 16 ++ lib/src/messages/invoice.dart | 9 +- lib/src/resources/invoice.dart | 40 +---- lib/src/resources/payment_intent.dart | 12 ++ lib/src/resources/subscription.dart | 65 ++------ lib/src/utils/expandable.dart | 27 ++++ lib/src/utils/expandable_field.dart | 7 - .../customer_expandable_field.dart | 15 -- ...fault_payment_method_expandable_field.dart | 20 --- .../default_source_expandable_field.dart | 19 --- .../discounts_expandable_field.dart | 16 -- ...est_invoice_expanded_expandable_field.dart | 25 --- .../payment_intent_expandable_field.dart | 20 --- lib/src/utils/expandable_list_field.dart | 24 --- lib/src/utils/expandable_object_field.dart | 22 --- lib/src/utils/map_extension.dart | 13 ++ lib/stripe.dart | 1 - pubspec.yaml | 2 +- .../expandable_object_test.dart | 37 +++++ test/utils/expandable_test.dart | 145 ++++++++++++++++++ test/utils/expandable_test.g.dart | 76 +++++++++ 36 files changed, 549 insertions(+), 396 deletions(-) delete mode 100644 lib/src/expanded.dart delete mode 100644 lib/src/expanded/invoice_expanded.dart delete mode 100644 lib/src/expanded/subscription_expanded.dart delete mode 100644 lib/src/messages/enums/expandable_fields/invoice_expandable_field.dart delete mode 100644 lib/src/messages/enums/expandable_fields/subscription_expandable_field.dart create mode 100644 lib/src/messages/expandable_objects/customer_expandable_object.dart create mode 100644 lib/src/messages/expandable_objects/discount_expandable_object.dart create mode 100644 lib/src/messages/expandable_objects/expandable_object.dart create mode 100644 lib/src/messages/expandable_objects/invoice_expandable_object.dart create mode 100644 lib/src/messages/expandable_objects/payment_intent_expandable_object.dart create mode 100644 lib/src/messages/expandable_objects/subscription_expandable_object.dart create mode 100644 lib/src/utils/expandable.dart delete mode 100644 lib/src/utils/expandable_field.dart delete mode 100644 lib/src/utils/expandable_fields/customer_expandable_field.dart delete mode 100644 lib/src/utils/expandable_fields/default_payment_method_expandable_field.dart delete mode 100644 lib/src/utils/expandable_fields/default_source_expandable_field.dart delete mode 100644 lib/src/utils/expandable_fields/discounts_expandable_field.dart delete mode 100644 lib/src/utils/expandable_fields/latest_invoice_expanded_expandable_field.dart delete mode 100644 lib/src/utils/expandable_fields/payment_intent_expandable_field.dart delete mode 100644 lib/src/utils/expandable_list_field.dart delete mode 100644 lib/src/utils/expandable_object_field.dart create mode 100644 lib/src/utils/map_extension.dart create mode 100644 test/messages/expandable_objects/expandable_object_test.dart create mode 100644 test/utils/expandable_test.dart create mode 100644 test/utils/expandable_test.g.dart diff --git a/lib/messages.dart b/lib/messages.dart index a8eb3a1..ea1c238 100644 --- a/lib/messages.dart +++ b/lib/messages.dart @@ -1,6 +1,8 @@ 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/resources/payment_intent.dart'; +import 'package:stripe/src/utils/expandable.dart'; export 'package:stripe/src/messages/enums.dart'; diff --git a/lib/messages.g.dart b/lib/messages.g.dart index 832c635..9e93133 100644 --- a/lib/messages.g.dart +++ b/lib/messages.g.dart @@ -775,9 +775,15 @@ Invoice _$InvoiceFromJson(Map json) => Invoice( hostedInvoiceUrl: json['hosted_invoice_url'] as String?, status: json['status'] as String?, subscription: json['subscription'] as String?, - paymentIntent: json['payment_intent'] as String?, + paymentIntent: _$JsonConverterFromJson>( + json['payment_intent'], + const ExpandablePaymentIntentJsonConverter().fromJson), accountCountry: json['account_country'] as String?, accountName: json['account_name'] as String?, + discounts: + _$JsonConverterFromJson, ExpandableList>( + json['discounts'], + const ExpandableDiscountListJsonConverter().fromJson), ); Map _$InvoiceToJson(Invoice instance) { @@ -803,12 +809,33 @@ Map _$InvoiceToJson(Invoice instance) { val['subtotal_excluding_tax'] = instance.subtotalExcludingTax; val['total_discount_amounts'] = instance.totalDiscountAmounts.map((e) => e.toJson()).toList(); - writeNotNull('payment_intent', instance.paymentIntent); + writeNotNull( + 'payment_intent', + _$JsonConverterToJson>( + instance.paymentIntent, + const ExpandablePaymentIntentJsonConverter().toJson)); writeNotNull('account_country', instance.accountCountry); writeNotNull('account_name', instance.accountName); + writeNotNull( + 'discounts', + _$JsonConverterToJson, ExpandableList>( + instance.discounts, + const ExpandableDiscountListJsonConverter().toJson)); return val; } +Value? _$JsonConverterFromJson( + Object? json, + Value? Function(Json json) fromJson, +) => + json == null ? null : fromJson(json as Json); + +Json? _$JsonConverterToJson( + Value? value, + Json? Function(Value value) toJson, +) => + value == null ? null : toJson(value); + TotalDiscountAmount _$TotalDiscountAmountFromJson(Map json) => TotalDiscountAmount( amount: (json['amount'] as num).toInt(), @@ -853,18 +880,6 @@ const _$PauseCollectionBehaviorEnumMap = { PauseCollectionBehavior.void_: 'void', }; -Value? _$JsonConverterFromJson( - Object? json, - Value? Function(Json json) fromJson, -) => - json == null ? null : fromJson(json as Json); - -Json? _$JsonConverterToJson( - Value? value, - Json? Function(Value value) toJson, -) => - value == null ? null : toJson(value); - PaymentIntent _$PaymentIntentFromJson(Map json) => PaymentIntent( object: $enumDecode(_$_PaymentIntentObjectEnumMap, json['object']), diff --git a/lib/src/expanded.dart b/lib/src/expanded.dart deleted file mode 100644 index 54ee9af..0000000 --- a/lib/src/expanded.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'expanded/invoice_expanded.dart'; -export 'expanded/subscription_expanded.dart'; diff --git a/lib/src/expanded/invoice_expanded.dart b/lib/src/expanded/invoice_expanded.dart deleted file mode 100644 index 98f0d15..0000000 --- a/lib/src/expanded/invoice_expanded.dart +++ /dev/null @@ -1,36 +0,0 @@ -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'; - -class InvoiceExpanded { - final Invoice invoice; - final PaymentIntent? paymentIntent; - 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 deleted file mode 100644 index 07e49cc..0000000 --- a/lib/src/expanded/subscription_expanded.dart +++ /dev/null @@ -1,67 +0,0 @@ -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/default_payment_method_expandable_field.dart'; -import 'package:stripe/src/utils/expandable_fields/default_source_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'; - -class SubscriptionExpanded { - final Subscription subscription; - final List? discounts; - final InvoiceExpanded? latestInvoice; - final Customer? customer; - final PaymentMethod? defaultPaymentMethod; - final Source? defaultSource; - - SubscriptionExpanded({ - required this.subscription, - this.discounts, - this.latestInvoice, - this.customer, - this.defaultPaymentMethod, - this.defaultSource, - }); - - 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); - } - - PaymentMethod? defaultPaymentMethod; - if (expand.contains(SubscriptionExpandableField.defaultPaymentMethod)) { - defaultPaymentMethod = - const DefaultPaymentMethodExpandableField().extract(json); - } - - Source? defaultSource; - if (expand.contains(SubscriptionExpandableField.defaultSource)) { - defaultSource = const DefaultSourceExpandableField().extract(json); - } - - return SubscriptionExpanded( - subscription: Subscription.fromJson(json), - discounts: discounts, - latestInvoice: latestInvoice, - customer: customer, - defaultPaymentMethod: defaultPaymentMethod, - defaultSource: defaultSource, - ); - } -} diff --git a/lib/src/messages/converters.dart b/lib/src/messages/converters.dart index d84eb05..d889695 100644 --- a/lib/src/messages/converters.dart +++ b/lib/src/messages/converters.dart @@ -1,4 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; +import 'package:stripe/src/utils/expandable.dart'; /// Converts a [DateTime] to a timestamp int and vice versa. class TimestampConverter implements JsonConverter { @@ -11,3 +12,61 @@ class TimestampConverter implements JsonConverter { @override int toJson(DateTime type) => type.millisecondsSinceEpoch ~/ 1000; } + +class ExpandableJsonConverter + implements JsonConverter, Object> { + final T Function(Map json) expandedFromJson; + + const ExpandableJsonConverter(this.expandedFromJson); + + @override + Expandable fromJson(Object json) { + if (json is String) { + return Expandable(id: json); + } + + if (json is Map) { + return Expandable( + id: json['id'] as String, + expanded: expandedFromJson(json), + ); + } + + throw ArgumentError('expected an String or a Map'); + } + + @override + Object toJson(Expandable object) { + return object.id; + } +} + +class ExpandableListJsonConverter + implements JsonConverter, List> { + final T Function(Map json) expandedFromJson; + + const ExpandableListJsonConverter(this.expandedFromJson); + + @override + ExpandableList fromJson(List json) { + if (json.isEmpty) { + return ExpandableList(ids: [], expanded: []); + } + + if (json.first is String) { + return ExpandableList(ids: json.cast()); + } + + // Assuming that json is List>. + final jsonList = json.cast>(); + final idsList = jsonList.map((element) => element['id'] as String).toList(); + final parsedList = jsonList.map(expandedFromJson).toList(); + + return ExpandableList(ids: idsList, expanded: parsedList); + } + + @override + List toJson(ExpandableList object) { + return object.ids; + } +} diff --git a/lib/src/messages/discount.dart b/lib/src/messages/discount.dart index cb99e67..3ce581a 100644 --- a/lib/src/messages/discount.dart +++ b/lib/src/messages/discount.dart @@ -71,3 +71,13 @@ class Discount extends Message { @override Map toJson() => _$DiscountToJson(this); } + +class ExpandableDiscountJsonConverter + extends ExpandableJsonConverter { + const ExpandableDiscountJsonConverter() : super(Discount.fromJson); +} + +class ExpandableDiscountListJsonConverter + extends ExpandableListJsonConverter { + const ExpandableDiscountListJsonConverter() : super(Discount.fromJson); +} diff --git a/lib/src/messages/enums.dart b/lib/src/messages/enums.dart index bff5233..563b511 100644 --- a/lib/src/messages/enums.dart +++ b/lib/src/messages/enums.dart @@ -1,8 +1,6 @@ -export 'enums/expandable_fields/invoice_expandable_field.dart'; -export 'enums/expandable_fields/subscription_expandable_field.dart'; +export 'enums/pause_collection_behavior.dart'; export 'enums/payment_behavior.dart'; export 'enums/proration_behavior.dart'; -export 'enums/stripe_api_error_type.dart'; -export 'enums/pause_collection_behavior.dart'; -export 'enums/source_type.dart'; export 'enums/source_status.dart'; +export 'enums/source_type.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 071723f..0000000 --- a/lib/src/messages/enums/expandable_fields/subscription_expandable_field.dart +++ /dev/null @@ -1,7 +0,0 @@ -enum SubscriptionExpandableField { - discounts, - latestInvoice, - customer, - defaultPaymentMethod, - defaultSource, -} diff --git a/lib/src/messages/expandable_objects/customer_expandable_object.dart b/lib/src/messages/expandable_objects/customer_expandable_object.dart new file mode 100644 index 0000000..a5060b4 --- /dev/null +++ b/lib/src/messages/expandable_objects/customer_expandable_object.dart @@ -0,0 +1,4 @@ +import 'package:stripe/messages.dart'; +import 'package:stripe/src/messages/expandable_objects/expandable_object.dart'; + +typedef CustomerExpandableObject = SimpleExpandableObject; diff --git a/lib/src/messages/expandable_objects/discount_expandable_object.dart b/lib/src/messages/expandable_objects/discount_expandable_object.dart new file mode 100644 index 0000000..ed6dbe8 --- /dev/null +++ b/lib/src/messages/expandable_objects/discount_expandable_object.dart @@ -0,0 +1,4 @@ +import 'package:stripe/messages.dart'; +import 'package:stripe/src/messages/expandable_objects/expandable_object.dart'; + +typedef DiscountExpandableObject = SimpleExpandableObject; diff --git a/lib/src/messages/expandable_objects/expandable_object.dart b/lib/src/messages/expandable_objects/expandable_object.dart new file mode 100644 index 0000000..80afcc8 --- /dev/null +++ b/lib/src/messages/expandable_objects/expandable_object.dart @@ -0,0 +1,60 @@ +import 'package:stripe/src/utils/map_extension.dart'; + +/// A class that helps generating queries for expanding API responses. +/// [expandableFields] must be implemented. +/// +/// Examples of queries that can be generated by [expandQuery] method: +/// ``` +/// ["discounts"] +/// ["latest_invoice.payment_intent", "latest_invoice.payment_intent"] +/// ``` +/// +/// /// Expanding Stripe API responses: https://docs.stripe.com/expand +abstract class ExpandableObject { + Map get expandableFields; + + Map get expand => + expandableFields.whereValueNotNull(); + + const ExpandableObject(); + + List? expandQuery() { + final query = _generateExpandQuery().toList(); + + if (query.isEmpty) return null; + + return query.toList(); + } + + Iterable _generateExpandQuery() sync* { + for (final fieldEntry in expand.entries) { + final fieldName = fieldEntry.key; + final fieldExpandObject = fieldEntry.value; + yield* _generateExpandQueryForField(fieldName, fieldExpandObject); + } + } + + Iterable _generateExpandQueryForField( + String name, + ExpandableObject object, + ) sync* { + final innerExpandFields = object.expandQuery(); + + if (innerExpandFields == null || innerExpandFields.isEmpty) { + yield name; + } else { + for (final field in innerExpandFields) { + yield [name, field].join('.'); + } + } + } +} + +/// An implementation of ExpandableObject for objects that don't have any +/// nested expandable fields. +class SimpleExpandableObject extends ExpandableObject { + @override + Map get expandableFields => const {}; + + const SimpleExpandableObject(); +} diff --git a/lib/src/messages/expandable_objects/invoice_expandable_object.dart b/lib/src/messages/expandable_objects/invoice_expandable_object.dart new file mode 100644 index 0000000..3ad63e9 --- /dev/null +++ b/lib/src/messages/expandable_objects/invoice_expandable_object.dart @@ -0,0 +1,20 @@ +import 'package:stripe/messages.dart'; +import 'package:stripe/src/messages/expandable_objects/discount_expandable_object.dart'; +import 'package:stripe/src/messages/expandable_objects/expandable_object.dart'; +import 'package:stripe/src/messages/expandable_objects/payment_intent_expandable_object.dart'; + +class InvoiceExpandableObject extends ExpandableObject { + final PaymentIntentExpandableObject? paymentIntent; + final DiscountExpandableObject? discounts; + + @override + Map get expandableFields => { + 'payment_intent': paymentIntent, + 'discounts': discounts, + }; + + const InvoiceExpandableObject({ + this.paymentIntent, + this.discounts, + }); +} diff --git a/lib/src/messages/expandable_objects/payment_intent_expandable_object.dart b/lib/src/messages/expandable_objects/payment_intent_expandable_object.dart new file mode 100644 index 0000000..69459b9 --- /dev/null +++ b/lib/src/messages/expandable_objects/payment_intent_expandable_object.dart @@ -0,0 +1,4 @@ +import 'package:stripe/messages.dart'; +import 'package:stripe/src/messages/expandable_objects/expandable_object.dart'; + +typedef PaymentIntentExpandableObject = SimpleExpandableObject; diff --git a/lib/src/messages/expandable_objects/subscription_expandable_object.dart b/lib/src/messages/expandable_objects/subscription_expandable_object.dart new file mode 100644 index 0000000..b385657 --- /dev/null +++ b/lib/src/messages/expandable_objects/subscription_expandable_object.dart @@ -0,0 +1,16 @@ +import 'package:stripe/messages.dart'; +import 'package:stripe/src/messages/expandable_objects/expandable_object.dart'; +import 'package:stripe/src/messages/expandable_objects/invoice_expandable_object.dart'; + +class SubscriptionExpandableObject extends ExpandableObject { + final InvoiceExpandableObject? latestInvoice; + + @override + Map get expandableFields => { + 'latest_invoice': latestInvoice, + }; + + const SubscriptionExpandableObject({ + this.latestInvoice, + }); +} diff --git a/lib/src/messages/invoice.dart b/lib/src/messages/invoice.dart index 9282a91..02562a0 100644 --- a/lib/src/messages/invoice.dart +++ b/lib/src/messages/invoice.dart @@ -53,7 +53,8 @@ class Invoice extends Message { /// The PaymentIntent associated with this invoice. The PaymentIntent is /// generated when the invoice is finalized, and can then be used to pay the /// invoice. Note that voiding an invoice will cancel the PaymentIntent. - final String? paymentIntent; + @ExpandablePaymentIntentJsonConverter() + final Expandable? paymentIntent; /// The country of the business associated with this invoice, most often the /// business creating the invoice. @@ -63,6 +64,11 @@ class Invoice extends Message { /// the business creating the invoice. final String? accountName; + /// The discounts applied to the invoice. Line item discounts are applied + /// before invoice discounts. Use expand[]=discounts to expand each discount. + @ExpandableDiscountListJsonConverter() + final ExpandableList? discounts; + Invoice({ required this.id, required this.currency, @@ -79,6 +85,7 @@ class Invoice extends Message { this.paymentIntent, this.accountCountry, this.accountName, + this.discounts, }); factory Invoice.fromJson(Map json) => diff --git a/lib/src/resources/invoice.dart b/lib/src/resources/invoice.dart index aa5206d..811cbbe 100644 --- a/lib/src/resources/invoice.dart +++ b/lib/src/resources/invoice.dart @@ -1,10 +1,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/messages/expandable_objects/invoice_expandable_object.dart'; import '../client.dart'; import '_resource.dart'; @@ -14,45 +11,18 @@ class InvoiceResource extends Resource { InvoiceResource(Client client) : super(client); - Future createPreview(CreatePreviewInvoiceRequest request) async { - final response = - await post('$_resourceName/create_preview', data: request.toJson()); - - return Invoice.fromJson(response); - } - - Future createPreviewExpanded( + Future createPreview( CreatePreviewInvoiceRequest request, { - required Set expand, + InvoiceExpandableObject? expand, }) async { - final expandableFields = _expandableFields(expand); final response = await post( '$_resourceName/create_preview', data: { ...request.toJson(), - 'expand': expandableFields.map((e) => e.field).toList(), + if (expand != null) 'expand': expand.expandQuery(), }, ); - 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 Invoice.fromJson(response); } } diff --git a/lib/src/resources/payment_intent.dart b/lib/src/resources/payment_intent.dart index 8209b97..3efb25c 100644 --- a/lib/src/resources/payment_intent.dart +++ b/lib/src/resources/payment_intent.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:logging/logging.dart'; import 'package:stripe/messages.dart'; +import 'package:stripe/src/messages/converters.dart'; import '../client.dart'; import '_resource.dart'; @@ -78,3 +79,14 @@ class PaymentIntentResource extends Resource { return PaymentIntent.fromJson(response); } } + +class ExpandablePaymentIntentJsonConverter + extends ExpandableJsonConverter { + const ExpandablePaymentIntentJsonConverter() : super(PaymentIntent.fromJson); +} + +class ExpandablePaymentIntentListJsonConverter + extends ExpandableListJsonConverter { + const ExpandablePaymentIntentListJsonConverter() + : super(PaymentIntent.fromJson); +} diff --git a/lib/src/resources/subscription.dart b/lib/src/resources/subscription.dart index 97101c9..065c488 100644 --- a/lib/src/resources/subscription.dart +++ b/lib/src/resources/subscription.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:stripe/messages.dart'; -import 'package:stripe/src/expanded.dart'; +import 'package:stripe/src/messages/expandable_objects/subscription_expandable_object.dart'; import '../client.dart'; import '_resource.dart'; @@ -12,76 +12,39 @@ class SubscriptionResource extends Resource { SubscriptionResource(Client client) : super(client); /// https://docs.stripe.com/api/subscriptions/create - Future create(CreateSubscriptionRequest request) async { + Future create(CreateSubscriptionRequest request) async { final response = await post(_resourceName, data: request.toJson()); - return SubscriptionExpanded.fromJson(response, { - SubscriptionExpandableField.discounts, - SubscriptionExpandableField.latestInvoice, - }); - } - - Future retrieve(String id) async { - final response = await get('$_resourceName/$id'); return Subscription.fromJson(response); } - Future retrieveExpanded( + Future retrieve( String id, { - required Set expand, + SubscriptionExpandableObject? expand, }) async { final response = await get( '$_resourceName/$id', queryParameters: { - 'expand': _expandParamComponents(expand), + if (expand != null) 'expand': expand.expandQuery(), }, ); - - return SubscriptionExpanded.fromJson(response, expand); - } - - List _expandParamComponents(Set fields) { - return fields.map((field) { - switch (field) { - case SubscriptionExpandableField.discounts: - return 'discounts'; - case SubscriptionExpandableField.latestInvoice: - return 'latest_invoice.payment_intent'; - case SubscriptionExpandableField.customer: - return 'customer'; - case SubscriptionExpandableField.defaultPaymentMethod: - return 'default_payment_method'; - case SubscriptionExpandableField.defaultSource: - return 'default_source'; - } - }).toList(); - } - - Future> list( - [ListSubscriptionsRequest? request]) async { - final map = await get(_resourceName, queryParameters: request?.toJson()); - return DataList.fromJson( - map, (value) => Subscription.fromJson(value as Map)); + return Subscription.fromJson(response); } - Future> listExpanded({ - required Set expand, + Future> list([ ListSubscriptionsRequest? request, - }) async { - final response = await get( + SubscriptionExpandableObject? expand, + ]) async { + final map = await get( _resourceName, queryParameters: { ...?request?.toJson(), - 'expand': _expandParamComponents(expand).map((e) => 'data.$e').toList(), + if (expand != null) 'expand': expand.expandQuery(), }, ); - - return DataList.fromJson( - response, - (value) => SubscriptionExpanded.fromJson( - value as Map, - expand, - ), + return DataList.fromJson( + map, + (value) => Subscription.fromJson(value as Map), ); } diff --git a/lib/src/utils/expandable.dart b/lib/src/utils/expandable.dart new file mode 100644 index 0000000..6dc53a8 --- /dev/null +++ b/lib/src/utils/expandable.dart @@ -0,0 +1,27 @@ +/// A container that always contains [id] and contains [expanded] only if the +/// object was queried with the `expand` option. +/// +/// Expanding Stripe API responses: https://docs.stripe.com/expand +class Expandable { + final String id; + final T? expanded; + + const Expandable({ + required this.id, + this.expanded, + }); +} + +/// A container that always contains [ids] and contains [expanded] only if the +/// list was queried with the `expand` option. +/// +/// Expanding Stripe API responses: https://docs.stripe.com/expand +class ExpandableList { + final List ids; + final List? expanded; + + const ExpandableList({ + required this.ids, + this.expanded, + }); +} diff --git a/lib/src/utils/expandable_field.dart b/lib/src/utils/expandable_field.dart deleted file mode 100644 index 680d144..0000000 --- a/lib/src/utils/expandable_field.dart +++ /dev/null @@ -1,7 +0,0 @@ -abstract class ExpandableField { - String get field; - - 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 deleted file mode 100644 index d6d70af..0000000 --- a/lib/src/utils/expandable_fields/customer_expandable_field.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:stripe/messages.dart'; -import 'package:stripe/src/utils/expandable_object_field.dart'; - -class CustomerExpandableField extends ExpandableObjectField { - @override - String get field => 'customer'; - - const CustomerExpandableField(); - - @override - Customer parse(Map object) => Customer.fromJson(object); - - @override - String replacement(Customer parsedValue) => parsedValue.id; -} diff --git a/lib/src/utils/expandable_fields/default_payment_method_expandable_field.dart b/lib/src/utils/expandable_fields/default_payment_method_expandable_field.dart deleted file mode 100644 index 1f7b7df..0000000 --- a/lib/src/utils/expandable_fields/default_payment_method_expandable_field.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:stripe/messages.dart'; -import 'package:stripe/src/utils/expandable_object_field.dart'; - -class DefaultPaymentMethodExpandableField - extends ExpandableObjectField { - @override - String get field => 'default_payment_method'; - - const DefaultPaymentMethodExpandableField(); - - @override - PaymentMethod parse(Map object) { - return PaymentMethod.fromJson(object); - } - - @override - String replacement(PaymentMethod parsedValue) { - return parsedValue.id; - } -} diff --git a/lib/src/utils/expandable_fields/default_source_expandable_field.dart b/lib/src/utils/expandable_fields/default_source_expandable_field.dart deleted file mode 100644 index 9d50a97..0000000 --- a/lib/src/utils/expandable_fields/default_source_expandable_field.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:stripe/messages.dart'; -import 'package:stripe/src/utils/expandable_object_field.dart'; - -class DefaultSourceExpandableField extends ExpandableObjectField { - @override - String get field => 'default_source'; - - const DefaultSourceExpandableField(); - - @override - Source parse(Map object) { - return Source.fromJson(object); - } - - @override - String replacement(Source parsedValue) { - return parsedValue.id; - } -} diff --git a/lib/src/utils/expandable_fields/discounts_expandable_field.dart b/lib/src/utils/expandable_fields/discounts_expandable_field.dart deleted file mode 100644 index 2f8addb..0000000 --- a/lib/src/utils/expandable_fields/discounts_expandable_field.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:stripe/messages.dart'; -import 'package:stripe/src/utils/expandable_list_field.dart'; - -class DiscountsExpandableField extends ExpandableListField { - @override - String get field => 'discounts'; - - const DiscountsExpandableField(); - - @override - String elementReplacement(Discount element) => element.id; - - @override - Discount parseElement(Map element) => - Discount.fromJson(element); -} 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/payment_intent_expandable_field.dart b/lib/src/utils/expandable_fields/payment_intent_expandable_field.dart deleted file mode 100644 index 5f306c2..0000000 --- a/lib/src/utils/expandable_fields/payment_intent_expandable_field.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:stripe/messages.dart'; -import 'package:stripe/src/utils/expandable_object_field.dart'; - -class PaymentIntentExpandableField - extends ExpandableObjectField { - @override - String get field => 'payment_intent'; - - const PaymentIntentExpandableField(); - - @override - PaymentIntent parse(Map object) { - return PaymentIntent.fromJson(object); - } - - @override - String replacement(PaymentIntent parsedValue) { - return parsedValue.id; - } -} diff --git a/lib/src/utils/expandable_list_field.dart b/lib/src/utils/expandable_list_field.dart deleted file mode 100644 index 149ec29..0000000 --- a/lib/src/utils/expandable_list_field.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:stripe/src/utils/expandable_field.dart'; - -abstract class ExpandableListField extends ExpandableField?> { - const ExpandableListField(); - - T parseElement(Map element); - - String elementReplacement(T element); - - @override - List? extract(Map json) { - final expandedFieldJsonList = - (json[field] as List?)?.cast>(); - final expandedFieldParsed = - expandedFieldJsonList?.map(parseElement).toList(); - final expandedFieldReplacement = - expandedFieldParsed?.map(elementReplacement).toList(); - // Replace the initial field value to allow parsing the JSON using the - // regular non-expanded model. - json[field] = expandedFieldReplacement; - - return expandedFieldParsed; - } -} diff --git a/lib/src/utils/expandable_object_field.dart b/lib/src/utils/expandable_object_field.dart deleted file mode 100644 index 323aa7e..0000000 --- a/lib/src/utils/expandable_object_field.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:stripe/src/utils/expandable_field.dart'; - -abstract class ExpandableObjectField extends ExpandableField { - const ExpandableObjectField(); - - T parse(Map object); - - String replacement(T parsedValue); - - @override - T? extract(Map json) { - final expandedFieldJson = json[field] as Map?; - if (expandedFieldJson == null) return null; - - final expandedFieldParsed = parse(expandedFieldJson); - // Replace the initial field value to allow parsing the JSON using the - // regular non-expanded model. - json[field] = replacement(expandedFieldParsed); - - return expandedFieldParsed; - } -} diff --git a/lib/src/utils/map_extension.dart b/lib/src/utils/map_extension.dart new file mode 100644 index 0000000..debe2ca --- /dev/null +++ b/lib/src/utils/map_extension.dart @@ -0,0 +1,13 @@ +extension MapExtension on Map { + Map whereValueNotNull() { + final Map result = {}; + + for (final key in keys) { + final value = this[key]; + + if (value != null) result[key] = value; + } + + return result; + } +} diff --git a/lib/stripe.dart b/lib/stripe.dart index 9d4e620..5d2b824 100644 --- a/lib/stripe.dart +++ b/lib/stripe.dart @@ -21,7 +21,6 @@ import 'src/resources/subscription_item.dart'; export 'messages.dart'; export 'src/client.dart'; export 'src/exceptions.dart'; -export 'src/expanded.dart'; export 'src/webhook.dart'; /// [Stripe] is the Class that provides the Interface for external calls via the diff --git a/pubspec.yaml b/pubspec.yaml index e02a81e..634d4bd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ description: A simple Stripe API wrapper, that is meant to be used on the server homepage: https://github.com/enyo/stripe-dart documentation: https://github.com/enyo/stripe-dart/blob/main/README.md environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.15.0 <3.0.0" dependencies: crypto: ^3.0.0 http: ^0.13.1 diff --git a/test/messages/expandable_objects/expandable_object_test.dart b/test/messages/expandable_objects/expandable_object_test.dart new file mode 100644 index 0000000..fc9f6de --- /dev/null +++ b/test/messages/expandable_objects/expandable_object_test.dart @@ -0,0 +1,37 @@ +import 'package:stripe/src/messages/expandable_objects/discount_expandable_object.dart'; +import 'package:stripe/src/messages/expandable_objects/invoice_expandable_object.dart'; +import 'package:stripe/src/messages/expandable_objects/payment_intent_expandable_object.dart'; +import 'package:stripe/src/messages/expandable_objects/subscription_expandable_object.dart'; +import 'package:test/test.dart'; + +void main() { + group('ExpandableObject.expandQuery', () { + test('returns null if there are no expanded fields', () { + final object = SubscriptionExpandableObject(); + expect(object.expandQuery(), isNull); + }); + + test('returns 1 level of nested expanded fields', () { + final object = SubscriptionExpandableObject( + latestInvoice: InvoiceExpandableObject(), + ); + expect(object.expandQuery(), ['latest_invoice']); + }); + + test('returns multiple levels of nested expanded fields', () { + final object = SubscriptionExpandableObject( + latestInvoice: InvoiceExpandableObject( + paymentIntent: PaymentIntentExpandableObject(), + discounts: DiscountExpandableObject(), + ), + ); + expect( + object.expandQuery(), + [ + 'latest_invoice.payment_intent', + 'latest_invoice.discounts', + ], + ); + }); + }); +} diff --git a/test/utils/expandable_test.dart b/test/utils/expandable_test.dart new file mode 100644 index 0000000..d206131 --- /dev/null +++ b/test/utils/expandable_test.dart @@ -0,0 +1,145 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:stripe/src/messages/converters.dart'; +import 'package:stripe/src/utils/expandable.dart'; +import 'package:test/test.dart'; + +part 'expandable_test.g.dart'; + +@JsonSerializable() +class _ObjectWithExpandableFields { + @_ExpandableNestedObjectJsonConverter() + final Expandable<_NestedObject> nestedObject; + + @_ExpandableNestedObjectJsonConverter() + final Expandable<_NestedObject>? nestedObjectNullable; + + @_ExpandableNestedObjectListJsonConverter() + final ExpandableList<_NestedObject> nestedObjectsList; + + @_ExpandableNestedObjectListJsonConverter() + final ExpandableList<_NestedObject>? nestedObjectsListNullable; + + _ObjectWithExpandableFields({ + required this.nestedObject, + required this.nestedObjectsList, + this.nestedObjectNullable, + this.nestedObjectsListNullable, + }); + + factory _ObjectWithExpandableFields.fromJson(Map json) => + _$ObjectWithExpandableFieldsFromJson(json); + + Map toJson() => _$ObjectWithExpandableFieldsToJson(this); +} + +@JsonSerializable() +class _NestedObject { + final String id; + final String data; + + _NestedObject(this.id, this.data); + + factory _NestedObject.fromJson(Map json) => + _$NestedObjectFromJson(json); + + Map toJson() => _$NestedObjectToJson(this); +} + +class _ExpandableNestedObjectJsonConverter + extends ExpandableJsonConverter<_NestedObject> { + const _ExpandableNestedObjectJsonConverter() : super(_NestedObject.fromJson); +} + +class _ExpandableNestedObjectListJsonConverter + extends ExpandableListJsonConverter<_NestedObject> { + const _ExpandableNestedObjectListJsonConverter() + : super(_NestedObject.fromJson); +} + +void main() { + group('Expandable objects:', () { + test( + 'Object with non-expanded fields is converted toJson and fromJson correctly', + () { + final sourceJson = { + 'nested_object': '1', + 'nested_object_nullable': '1', + 'nested_objects_list': ['2', '3'], + 'nested_objects_list_nullable': ['2', '3'], + }; + final parsed = _ObjectWithExpandableFields.fromJson(sourceJson); + final convertedJson = parsed.toJson(); + expect(parsed.nestedObject.id, '1'); + expect(parsed.nestedObject.expanded, isNull); + expect(parsed.nestedObjectsList.ids, ['2', '3']); + expect(parsed.nestedObjectsList.expanded, isNull); + expect(convertedJson.toString(), equals(sourceJson.toString())); + }, + ); + + test( + 'Object with empty expandable lists is converted toJson and fromJson correctly', + () { + final sourceJson = { + 'nested_object': '1', + 'nested_object_nullable': '1', + 'nested_objects_list': [], + 'nested_objects_list_nullable': [], + }; + final parsed = _ObjectWithExpandableFields.fromJson(sourceJson); + final convertedJson = parsed.toJson(); + expect(parsed.nestedObjectsList.ids, []); + expect(parsed.nestedObjectsList.expanded, []); + expect(convertedJson.toString(), equals(sourceJson.toString())); + }, + ); + + test( + 'Object with fields equal to null is converted toJson and fromJson correctly', + () { + final sourceJson = { + 'nested_object': '1', + 'nested_objects_list': ['2', '3'], + }; + final parsed = _ObjectWithExpandableFields.fromJson(sourceJson); + final convertedJson = parsed.toJson(); + expect(parsed.nestedObjectNullable?.id, isNull); + expect(parsed.nestedObjectNullable?.expanded, isNull); + expect(parsed.nestedObjectsListNullable?.ids, isNull); + expect(parsed.nestedObjectsListNullable?.expanded, isNull); + expect(convertedJson.toString(), equals(sourceJson.toString())); + }, + ); + + test( + 'Object with expanded fields is converted toJson and fromJson correctly', + () { + final sourceJson = { + 'nested_object': {'id': '1', 'data': 'data1'}, + 'nested_objects_list': [ + { + 'id': '2', + 'data': 'data2', + }, + { + 'id': '3', + 'data': 'data3', + }, + ], + }; + final parsed = _ObjectWithExpandableFields.fromJson(sourceJson); + final convertedJson = parsed.toJson(); + final expectedJson = { + 'nested_object': '1', + 'nested_objects_list': ['2', '3'], + }; + expect(parsed.nestedObject.id, '1'); + expect(parsed.nestedObject.expanded, isNotNull); + expect(parsed.nestedObjectsList.ids, ['2', '3']); + expect(parsed.nestedObjectsList.expanded, isNotNull); + expect(parsed.nestedObjectsList.expanded, hasLength(2)); + expect(convertedJson.toString(), equals(expectedJson.toString())); + }, + ); + }); +} diff --git a/test/utils/expandable_test.g.dart b/test/utils/expandable_test.g.dart new file mode 100644 index 0000000..4250e20 --- /dev/null +++ b/test/utils/expandable_test.g.dart @@ -0,0 +1,76 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'expandable_test.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_ObjectWithExpandableFields _$ObjectWithExpandableFieldsFromJson( + Map json) => + _ObjectWithExpandableFields( + nestedObject: const _ExpandableNestedObjectJsonConverter() + .fromJson(json['nested_object'] as Object), + nestedObjectsList: const _ExpandableNestedObjectListJsonConverter() + .fromJson(json['nested_objects_list'] as List), + nestedObjectNullable: + _$JsonConverterFromJson>( + json['nested_object_nullable'], + const _ExpandableNestedObjectJsonConverter().fromJson), + nestedObjectsListNullable: + _$JsonConverterFromJson, ExpandableList<_NestedObject>>( + json['nested_objects_list_nullable'], + const _ExpandableNestedObjectListJsonConverter().fromJson), + ); + +Map _$ObjectWithExpandableFieldsToJson( + _ObjectWithExpandableFields instance) { + final val = { + 'nested_object': const _ExpandableNestedObjectJsonConverter() + .toJson(instance.nestedObject), + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull( + 'nested_object_nullable', + _$JsonConverterToJson>( + instance.nestedObjectNullable, + const _ExpandableNestedObjectJsonConverter().toJson)); + val['nested_objects_list'] = const _ExpandableNestedObjectListJsonConverter() + .toJson(instance.nestedObjectsList); + writeNotNull( + 'nested_objects_list_nullable', + _$JsonConverterToJson, ExpandableList<_NestedObject>>( + instance.nestedObjectsListNullable, + const _ExpandableNestedObjectListJsonConverter().toJson)); + return val; +} + +Value? _$JsonConverterFromJson( + Object? json, + Value? Function(Json json) fromJson, +) => + json == null ? null : fromJson(json as Json); + +Json? _$JsonConverterToJson( + Value? value, + Json? Function(Value value) toJson, +) => + value == null ? null : toJson(value); + +_NestedObject _$NestedObjectFromJson(Map json) => + _NestedObject( + json['id'] as String, + json['data'] as String, + ); + +Map _$NestedObjectToJson(_NestedObject instance) => + { + 'id': instance.id, + 'data': instance.data, + };