From b52e3154af4c3300a09742f9dafb9320640d15a4 Mon Sep 17 00:00:00 2001 From: Victor Fernandez Saborit Date: Thu, 11 Jan 2024 10:19:13 +0100 Subject: [PATCH] Initial commit --- .gitattributes | 13 + .github/CODEOWNERS | 1 + .github/ISSUE_TEMPLATE/bug_report.yml | 71 + .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/feature_request.yml | 32 + .github/dependabot.yml | 13 + .github/workflows/ci.yml | 40 + .gitignore | 8 + .php-cs-fixer.php | 42 + CHANGELOG.md | 38 + CONTRIBUTING.md | 5 + LICENSE | 201 ++ Makefile | 31 + README.md | 164 + SECURITY.md | 14 + composer.json | 63 + dockerfile | 15 + examples/catalog_management.php | 177 ++ examples/event_polling.php | 47 + examples/logging.php | 28 + phpstan.neon.dist | 7 + phpunit.xml | 21 + rector.php | 29 + src/Client.php | 208 ++ src/Entities/Address.php | 53 + src/Entities/Adjustment.php | 63 + .../Adjustment/AdjustmentCustomerBalance.php | 27 + src/Entities/Adjustment/AdjustmentItem.php | 22 + .../Adjustment/AdjustmentItemTotals.php | 28 + .../Adjustment/AdjustmentProration.php | 21 + .../Adjustment/AdjustmentTimePeriod.php | 21 + src/Entities/Adjustment/AdjustmentType.php | 20 + src/Entities/Business.php | 50 + src/Entities/Business/BusinessesContacts.php | 21 + .../Collections/AddressCollection.php | 30 + .../AdjustmentsAdjustmentCollection.php | 30 + .../Collections/BusinessCollection.php | 30 + src/Entities/Collections/Collection.php | 61 + .../Collections/CreditBalanceCollection.php | 30 + .../Collections/CustomerCollection.php | 30 + .../CustomerIncludesCollection.php | 30 + .../Collections/DiscountCollection.php | 30 + src/Entities/Collections/EventCollection.php | 30 + .../Collections/EventTypeCollection.php | 30 + .../Collections/NotificationCollection.php | 30 + .../Collections/NotificationLogCollection.php | 30 + .../NotificationSettingCollection.php | 30 + src/Entities/Collections/Paginator.php | 43 + src/Entities/Collections/PriceCollection.php | 30 + .../PriceWithIncludesCollection.php | 30 + .../Collections/ProductCollection.php | 30 + .../ProductWithIncludesCollection.php | 30 + src/Entities/Collections/ReportCollection.php | 30 + .../Collections/SubscriptionCollection.php | 30 + .../SubscriptionPreviewCollection.php | 30 + .../SubscriptionWithIncludesCollection.php | 30 + .../SubscriptionsTransactionCollection.php | 30 + .../Collections/TransactionCollection.php | 30 + .../TransactionPreviewCollection.php | 30 + .../TransactionWithIncludesCollection.php | 30 + .../TransactionsDataCollection.php | 30 + src/Entities/CreditBalance.php | 34 + src/Entities/Customer.php | 46 + src/Entities/CustomerIncludes.php | 46 + src/Entities/DateTime.php | 36 + src/Entities/Discount.php | 61 + src/Entities/Discount/DiscountStatus.php | 13 + src/Entities/Discount/DiscountType.php | 19 + src/Entities/Entity.php | 20 + src/Entities/Event.php | 44 + src/Entities/Event/EventTypeName.php | 54 + src/Entities/EventType.php | 35 + src/Entities/Notification.php | 46 + .../Notification/NotificationDiscount.php | 61 + .../Notification/NotificationOrigin.php | 11 + .../Notification/NotificationPayout.php | 29 + .../Notification/NotificationPayoutStatus.php | 11 + .../Notification/NotificationStatus.php | 13 + .../Notification/NotificationSubscription.php | 80 + src/Entities/NotificationLog.php | 28 + src/Entities/NotificationSetting.php | 46 + .../NotificationSettingType.php | 11 + src/Entities/Price.php | 66 + src/Entities/PricePreview.php | 51 + src/Entities/PriceWithIncludes.php | 68 + .../PricingPreview/PricePreviewDetails.php | 32 + .../PricingPreview/PricePreviewDiscounts.php | 34 + .../PricingPreview/PricePreviewItem.php | 31 + .../PricingPreview/PricePreviewLineItem.php | 52 + .../PricePreviewTotalsFormatted.php | 35 + .../PricePreviewUnitTotalsFormatted.php | 35 + src/Entities/Product.php | 48 + src/Entities/ProductWithIncludes.php | 51 + src/Entities/Report.php | 48 + src/Entities/Report/ReportFilters.php | 31 + src/Entities/Report/ReportName.php | 21 + src/Entities/Report/ReportOperator.php | 18 + src/Entities/Report/ReportStatus.php | 20 + src/Entities/Report/ReportType.php | 20 + src/Entities/ReportCSV.php | 27 + src/Entities/Shared/Action.php | 22 + src/Entities/Shared/AddressPreview.php | 29 + src/Entities/Shared/AdjustmentItemTotals.php | 22 + .../Shared/AvailablePaymentMethods.php | 23 + src/Entities/Shared/BillingDetails.php | 33 + src/Entities/Shared/BillingDetailsUpdate.php | 23 + src/Entities/Shared/Card.php | 37 + src/Entities/Shared/CatalogType.php | 18 + src/Entities/Shared/ChargebackFee.php | 26 + src/Entities/Shared/Checkout.php | 25 + src/Entities/Shared/CollectionMode.php | 18 + src/Entities/Shared/Contacts.php | 26 + src/Entities/Shared/CountryCode.php | 245 ++ src/Entities/Shared/CurrencyCode.php | 46 + .../Shared/CurrencyCodeAdjustments.php | 19 + src/Entities/Shared/CurrencyCodePayouts.php | 29 + src/Entities/Shared/CustomData.php | 25 + src/Entities/Shared/Data.php | 20 + src/Entities/Shared/ErrorCode.php | 33 + src/Entities/Shared/Interval.php | 20 + src/Entities/Shared/Meta.php | 20 + src/Entities/Shared/MetaPaginated.php | 21 + src/Entities/Shared/MethodDetails.php | 29 + src/Entities/Shared/Money.php | 31 + src/Entities/Shared/Original.php | 26 + src/Entities/Shared/Pagination.php | 23 + .../Shared/PayoutTotalsAdjustment.php | 39 + src/Entities/Shared/PriceQuantity.php | 26 + src/Entities/Shared/Status.php | 18 + src/Entities/Shared/StatusAdjustment.php | 20 + src/Entities/Shared/StatusPaymentAttempt.php | 26 + src/Entities/Shared/StatusTransaction.php | 23 + src/Entities/Shared/TaxCategory.php | 25 + src/Entities/Shared/TaxMode.php | 19 + src/Entities/Shared/TaxRatesUsed.php | 29 + src/Entities/Shared/TimePeriod.php | 29 + src/Entities/Shared/TotalAdjustments.php | 37 + src/Entities/Shared/Totals.php | 33 + .../Shared/TransactionDetailsPreview.php | 35 + .../Shared/TransactionLineItemPreview.php | 39 + src/Entities/Shared/TransactionOrigin.php | 22 + .../Shared/TransactionPaymentAttempt.php | 43 + .../Shared/TransactionPayoutTotals.php | 45 + .../TransactionPayoutTotalsAdjusted.php | 39 + src/Entities/Shared/TransactionTotals.php | 45 + .../Shared/TransactionTotalsAdjusted.php | 39 + src/Entities/Shared/Type.php | 28 + src/Entities/Shared/UnitPriceOverride.php | 29 + src/Entities/Shared/UnitTotals.php | 33 + src/Entities/Subscription.php | 90 + .../Subscription/SubscriptionAdjustment.php | 42 + .../SubscriptionAdjustmentItem.php | 27 + .../SubscriptionAdjustmentPreview.php | 20 + .../Subscription/SubscriptionCharge.php | 28 + .../Subscription/SubscriptionCredit.php | 28 + .../Subscription/SubscriptionDetails.php | 47 + .../Subscription/SubscriptionDiscount.php | 33 + .../SubscriptionEffectiveFrom.php | 18 + .../Subscription/SubscriptionItem.php | 49 + .../Subscription/SubscriptionItemStatus.php | 19 + .../Subscription/SubscriptionItems.php | 21 + .../SubscriptionItemsWithPrice.php | 21 + .../SubscriptionManagementUrls.php | 26 + .../SubscriptionNextTransaction.php | 36 + .../SubscriptionNonCatalogPrice.php | 36 + ...SubscriptionNonCatalogPriceWithProduct.php | 36 + .../SubscriptionNonCatalogProduct.php | 29 + .../SubscriptionOnPaymentFailure.php | 18 + ...iptionPreviewSubscriptionUpdateSummary.php | 31 + .../Subscription/SubscriptionPrice.php | 43 + .../Subscription/SubscriptionProration.php | 26 + .../SubscriptionProrationBillingMode.php | 21 + .../Subscription/SubscriptionResult.php | 29 + .../Subscription/SubscriptionResultAction.php | 18 + .../SubscriptionScheduledChange.php | 33 + .../SubscriptionScheduledChangeAction.php | 19 + .../Subscription/SubscriptionStatus.php | 22 + .../Subscription/SubscriptionTimePeriod.php | 31 + .../SubscriptionTransactionItem.php | 25 + .../SubscriptionTransactionLineItem.php | 45 + .../Subscription/SubscriptionUpdateItem.php | 21 + src/Entities/SubscriptionPreview.php | 101 + src/Entities/SubscriptionTransaction.php | 97 + src/Entities/SubscriptionWithIncludes.php | 98 + src/Entities/Transaction.php | 85 + .../Transaction/TransactionAdjustment.php | 63 + .../Transaction/TransactionAdjustmentItem.php | 28 + .../TransactionAdjustmentsTotals.php | 41 + .../Transaction/TransactionBreakdown.php | 27 + .../Transaction/TransactionCardType.php | 26 + .../Transaction/TransactionCreateItem.php | 21 + .../TransactionCreateItemWithPrice.php | 21 + .../Transaction/TransactionDetails.php | 47 + src/Entities/Transaction/TransactionItem.php | 35 + ...nsactionItemPreviewWithNonCatalogPrice.php | 31 + .../TransactionItemPreviewWithPrice.php | 35 + .../TransactionItemPreviewWithPriceId.php | 31 + .../Transaction/TransactionLineItem.php | 45 + .../TransactionNonCatalogPrice.php | 39 + .../TransactionNonCatalogPriceWithProduct.php | 39 + .../TransactionNonCatalogProduct.php | 27 + .../Transaction/TransactionProration.php | 29 + .../Transaction/TransactionTimePeriod.php | 31 + .../TransactionUpdateTransactionItem.php | 21 + src/Entities/TransactionData.php | 25 + src/Entities/TransactionPreview.php | 57 + src/Entities/TransactionWithIncludes.php | 100 + src/Environment.php | 19 + src/Exceptions/ApiError.php | 39 + src/Exceptions/ApiError/AddressApiError.php | 11 + .../ApiError/AdjustmentApiError.php | 11 + src/Exceptions/ApiError/BusinessApiError.php | 11 + src/Exceptions/ApiError/CustomerApiError.php | 11 + src/Exceptions/ApiError/DiscountApiError.php | 11 + src/Exceptions/ApiError/PriceApiError.php | 11 + src/Exceptions/ApiError/ProductApiError.php | 11 + .../ApiError/SubscriptionApiError.php | 11 + .../ApiError/TransactionApiError.php | 11 + src/Exceptions/FieldError.php | 12 + src/Exceptions/SdkException.php | 9 + .../InvalidArgumentException.php | 20 + .../SdkExceptions/MalformedResponse.php | 15 + src/FiltersUndefined.php | 16 + src/HasParameters.php | 16 + src/Logger/Formatter.php | 43 + src/Notifications/Events/AddressCreated.php | 21 + src/Notifications/Events/AddressUpdated.php | 21 + .../Events/AdjustmentCreated.php | 21 + .../Events/AdjustmentUpdated.php | 21 + src/Notifications/Events/BusinessCreated.php | 21 + src/Notifications/Events/BusinessUpdated.php | 21 + src/Notifications/Events/CustomerCreated.php | 21 + src/Notifications/Events/CustomerUpdated.php | 21 + src/Notifications/Events/DiscountCreated.php | 21 + src/Notifications/Events/DiscountImported.php | 21 + src/Notifications/Events/DiscountUpdated.php | 21 + src/Notifications/Events/PayoutCreated.php | 21 + src/Notifications/Events/PayoutPaid.php | 21 + src/Notifications/Events/PriceCreated.php | 21 + src/Notifications/Events/PriceUpdated.php | 21 + src/Notifications/Events/ProductCreated.php | 21 + src/Notifications/Events/ProductUpdated.php | 21 + .../Events/SubscriptionActivated.php | 21 + .../Events/SubscriptionCanceled.php | 21 + .../Events/SubscriptionCreated.php | 21 + .../Events/SubscriptionImported.php | 21 + .../Events/SubscriptionPastDue.php | 21 + .../Events/SubscriptionPaused.php | 21 + .../Events/SubscriptionResumed.php | 21 + .../Events/SubscriptionTrialing.php | 21 + .../Events/SubscriptionUpdated.php | 21 + .../Events/TransactionBilled.php | 21 + .../Events/TransactionCanceled.php | 21 + .../Events/TransactionCompleted.php | 21 + .../Events/TransactionCreated.php | 21 + src/Notifications/Events/TransactionPaid.php | 21 + .../Events/TransactionPastDue.php | 21 + .../Events/TransactionPaymentFailed.php | 21 + src/Notifications/Events/TransactionReady.php | 21 + .../Events/TransactionUpdated.php | 21 + src/Notifications/PaddleSignature.php | 69 + src/Notifications/Secret.php | 14 + src/Notifications/Verifier.php | 36 + src/Options.php | 14 + src/Resources/Addresses/AddressesClient.php | 99 + .../Addresses/Operations/CreateOperation.php | 41 + .../Addresses/Operations/ListOperation.php | 42 + .../Addresses/Operations/UpdateOperation.php | 44 + .../Adjustments/AdjustmentsClient.php | 60 + .../Operations/CreateOperation.php | 42 + .../Adjustments/Operations/ListOperation.php | 70 + src/Resources/Businesses/BusinessesClient.php | 99 + .../Businesses/Operations/CreateOperation.php | 38 + .../Businesses/Operations/ListOperation.php | 48 + .../Businesses/Operations/UpdateOperation.php | 41 + src/Resources/Customers/CustomersClient.php | 114 + .../Customers/Operations/CreateOperation.php | 32 + .../Customers/Operations/ListOperation.php | 48 + .../Customers/Operations/UpdateOperation.php | 35 + src/Resources/Discounts/DiscountsClient.php | 99 + .../Discounts/Operations/CreateOperation.php | 50 + .../Discounts/Operations/ListOperation.php | 53 + .../Discounts/Operations/UpdateOperation.php | 53 + src/Resources/EventTypes/EventTypesClient.php | 39 + src/Resources/Events/EventsClient.php | 44 + .../Events/Operations/ListOperation.php | 20 + .../NotificationLogsClient.php | 37 + .../Operations/ListOperation.php | 20 + .../NotificationSettingsClient.php | 92 + .../Operations/CreateOperation.php | 40 + .../Operations/UpdateOperation.php | 39 + .../Notifications/NotificationsClient.php | 73 + .../Operations/ListOperation.php | 45 + .../Prices/Operations/CreateOperation.php | 55 + .../Prices/Operations/List/Includes.php | 10 + .../Prices/Operations/ListOperation.php | 71 + .../Prices/Operations/UpdateOperation.php | 56 + src/Resources/Prices/PricesClient.php | 111 + .../Operations/PreviewPricesOperation.php | 45 + .../PricingPreviews/PricingPreviewsClient.php | 40 + .../Products/Operations/CreateOperation.php | 38 + .../Products/Operations/List/Includes.php | 10 + .../Products/Operations/ListOperation.php | 70 + .../Products/Operations/UpdateOperation.php | 41 + src/Resources/Products/ProductsClient.php | 111 + .../Reports/Operations/CreateOperation.php | 37 + .../Reports/Operations/ListOperation.php | 39 + src/Resources/Reports/ReportsClient.php | 87 + .../Shared/Operations/List/Comparator.php | 13 + .../Shared/Operations/List/DateComparison.php | 26 + .../Shared/Operations/List/OrderBy.php | 27 + .../Shared/Operations/List/Pager.php | 24 + .../Operations/CancelOperation.php | 22 + .../CreateOneTimeChargeOperation.php | 36 + .../Subscriptions/Operations/Get/Includes.php | 11 + .../Operations/ListOperation.php | 76 + .../Operations/PauseOperation.php | 25 + .../PreviewOneTimeChargeOperation.php | 36 + .../Operations/PreviewUpdateOperation.php | 61 + .../Operations/ResumeOperation.php | 25 + .../Update/SubscriptionDiscount.php | 23 + .../Operations/UpdateOperation.php | 61 + .../Subscriptions/SubscriptionsClient.php | 165 + .../Operations/CreateOperation.php | 59 + .../Transactions/Operations/List/Includes.php | 15 + .../Transactions/Operations/ListOperation.php | 83 + .../Operations/PreviewOperation.php | 51 + .../Operations/UpdateOperation.php | 58 + .../Transactions/TransactionsClient.php | 139 + src/ResponseParser.php | 86 + src/Undefined.php | 9 + .../Addresses/AddressesClientTest.php | 231 ++ .../_fixtures/request/create_basic.json | 3 + .../_fixtures/request/create_full.json | 12 + .../_fixtures/request/update_full.json | 13 + .../_fixtures/request/update_partial.json | 4 + .../_fixtures/request/update_single.json | 3 + .../_fixtures/response/full_entity.json | 19 + .../_fixtures/response/list_default.json | 41 + .../_fixtures/response/minimal_entity.json | 19 + .../Adjustments/AdjustmentsClientTest.php | 211 ++ .../_fixtures/request/create_basic.json | 12 + .../_fixtures/request/create_full.json | 17 + .../_fixtures/response/full_entity.json | 59 + .../_fixtures/response/list_default.json | 269 ++ .../_fixtures/response/minimal_entity.json | 47 + .../Businesses/BusinessesClientTest.php | 245 ++ .../_fixtures/request/create_basic.json | 3 + .../_fixtures/request/create_full.json | 14 + .../_fixtures/request/update_full.json | 23 + .../_fixtures/request/update_partial.json | 17 + .../_fixtures/request/update_single.json | 3 + .../_fixtures/response/full_entity.json | 23 + .../_fixtures/response/list_default.json | 31 + .../_fixtures/response/minimal_entity.json | 16 + .../Customers/CustomersClientTest.php | 240 ++ .../_fixtures/request/create_basic.json | 3 + .../_fixtures/request/create_full.json | 8 + .../_fixtures/request/update_full.json | 9 + .../_fixtures/request/update_partial.json | 4 + .../_fixtures/request/update_single.json | 3 + .../_fixtures/response/full_entity.json | 18 + .../response/list_credit_balances.json | 16 + .../_fixtures/response/list_default.json | 48 + .../_fixtures/response/minimal_entity.json | 16 + .../Discounts/DiscountsClientTest.php | 246 ++ .../_fixtures/request/create_basic.json | 8 + .../_fixtures/request/create_full.json | 16 + .../_fixtures/request/update_full.json | 17 + .../_fixtures/request/update_partial.json | 4 + .../_fixtures/request/update_single.json | 3 + .../_fixtures/response/full_entity.json | 26 + .../_fixtures/response/list_default.json | 92 + .../_fixtures/response/minimal_entity.json | 27 + .../EventTypes/EventTypesClientTest.php | 47 + .../_fixtures/response/list_default.json | 255 ++ .../Resources/Events/EventsClientTest.php | 80 + .../_fixtures/response/list_default.json | 2777 +++++++++++++++++ .../NotificationLogsClientTest.php | 84 + .../_fixtures/response/list_default.json | 83 + .../NotificationSettingsClientTest.php | 238 ++ .../_fixtures/request/create_basic.json | 14 + .../_fixtures/request/create_full.json | 23 + .../_fixtures/request/update_full.json | 23 + .../_fixtures/request/update_partial.json | 4 + .../_fixtures/request/update_single.json | 3 + .../_fixtures/response/full_entity.json | 107 + .../_fixtures/response/list_default.json | 211 ++ .../_fixtures/response/minimal_entity.json | 53 + .../Notifications/NotificationsClientTest.php | 174 ++ .../_fixtures/response/full_entity.json | 47 + .../_fixtures/response/list_default.json | 458 +++ .../_fixtures/response/replay.json | 8 + .../Resources/Prices/PricesClientTest.php | 283 ++ .../_fixtures/request/create_basic.json | 8 + .../Prices/_fixtures/request/create_full.json | 37 + .../Prices/_fixtures/request/update_full.json | 35 + .../_fixtures/request/update_partial.json | 7 + .../_fixtures/request/update_single.json | 3 + .../_fixtures/response/full_entity.json | 44 + .../response/full_entity_with_includes.json | 65 + .../_fixtures/response/list_default.json | 534 ++++ .../_fixtures/response/minimal_entity.json | 28 + .../PricingPreviewsClientTest.php | 102 + .../request/preview_prices_full.json | 22 + .../request/preview_prices_minimal.json | 8 + .../request/preview_prices_multiple.json | 15 + .../_fixtures/response/full_entity.json | 195 ++ .../Resources/Products/ProductsClientTest.php | 258 ++ .../_fixtures/request/create_basic.json | 4 + .../_fixtures/request/create_full.json | 13 + .../_fixtures/request/update_full.json | 13 + .../_fixtures/request/update_partial.json | 4 + .../_fixtures/request/update_single.json | 3 + .../_fixtures/response/full_entity.json | 21 + .../response/full_entity_with_includes.json | 46 + .../_fixtures/response/list_default.json | 86 + .../_fixtures/response/minimal_entity.json | 15 + .../Resources/Reports/ReportsClientTest.php | 167 + .../_fixtures/request/create_basic.json | 3 + .../_fixtures/request/create_full.json | 10 + .../_fixtures/response/full_entity.json | 21 + .../_fixtures/response/list_default.json | 36 + .../_fixtures/response/report_csv_entity.json | 8 + .../Subscriptions/SubscriptionsClientTest.php | 617 ++++ .../_fixtures/request/cancel_none.json | 3 + .../_fixtures/request/cancel_single.json | 3 + .../request/create_one_time_charge_full.json | 18 + .../create_one_time_charge_minimal.json | 9 + .../_fixtures/request/pause_full.json | 4 + .../_fixtures/request/pause_none.json | 4 + .../_fixtures/request/pause_single.json | 4 + .../request/preview_one_time_charge_full.json | 17 + .../preview_one_time_charge_minimal.json | 9 + .../request/preview_update_full.json | 22 + .../request/preview_update_partial.json | 4 + .../request/preview_update_single.json | 3 + .../_fixtures/request/resume_none.json | 3 + .../request/resume_single_as_date.json | 3 + .../request/resume_single_as_enum.json | 3 + .../_fixtures/request/update_full.json | 22 + .../_fixtures/request/update_partial.json | 4 + .../_fixtures/request/update_single.json | 3 + .../_fixtures/response/full_entity.json | 72 + .../response/full_entity_with_includes.json | 222 ++ ...ment_method_change_transaction_entity.json | 170 + .../_fixtures/response/list_default.json | 452 +++ .../response/preview_charge_full_entity.json | 402 +++ .../response/preview_update_full_entity.json | 402 +++ .../Transactions/TransactionsClientTest.php | 495 +++ .../_fixtures/request/create_basic.json | 8 + .../_fixtures/request/create_manual.json | 24 + .../create_with_non_catalog_price.json | 28 + .../_fixtures/request/preview_basic.json | 9 + .../preview_with_non_catalog_price.json | 29 + .../_fixtures/request/update_partial.json | 6 + .../_fixtures/request/update_single.json | 3 + .../_fixtures/response/full_entity.json | 165 + .../response/full_entity_with_includes.json | 219 ++ .../response/get_invoice_pdf_default.json | 8 + .../_fixtures/response/list_default.json | 1236 ++++++++ .../response/list_paginated_page_one.json | 1222 ++++++++ .../response/list_paginated_page_two.json | 1222 ++++++++ .../_fixtures/response/minimal_entity.json | 122 + .../_fixtures/response/preview_entity.json | 161 + tests/Unit/Notifications/VerifierTest.php | 37 + .../Assertions/AssertsDeepMatchesData.php | 51 + tests/Utils/ReadsFixtures.php | 37 + 468 files changed, 28830 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .php-cs-fixer.php create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 composer.json create mode 100644 dockerfile create mode 100644 examples/catalog_management.php create mode 100644 examples/event_polling.php create mode 100644 examples/logging.php create mode 100644 phpstan.neon.dist create mode 100644 phpunit.xml create mode 100644 rector.php create mode 100644 src/Client.php create mode 100644 src/Entities/Address.php create mode 100644 src/Entities/Adjustment.php create mode 100644 src/Entities/Adjustment/AdjustmentCustomerBalance.php create mode 100644 src/Entities/Adjustment/AdjustmentItem.php create mode 100644 src/Entities/Adjustment/AdjustmentItemTotals.php create mode 100644 src/Entities/Adjustment/AdjustmentProration.php create mode 100644 src/Entities/Adjustment/AdjustmentTimePeriod.php create mode 100644 src/Entities/Adjustment/AdjustmentType.php create mode 100644 src/Entities/Business.php create mode 100644 src/Entities/Business/BusinessesContacts.php create mode 100644 src/Entities/Collections/AddressCollection.php create mode 100644 src/Entities/Collections/AdjustmentsAdjustmentCollection.php create mode 100644 src/Entities/Collections/BusinessCollection.php create mode 100644 src/Entities/Collections/Collection.php create mode 100644 src/Entities/Collections/CreditBalanceCollection.php create mode 100644 src/Entities/Collections/CustomerCollection.php create mode 100644 src/Entities/Collections/CustomerIncludesCollection.php create mode 100644 src/Entities/Collections/DiscountCollection.php create mode 100644 src/Entities/Collections/EventCollection.php create mode 100644 src/Entities/Collections/EventTypeCollection.php create mode 100644 src/Entities/Collections/NotificationCollection.php create mode 100644 src/Entities/Collections/NotificationLogCollection.php create mode 100644 src/Entities/Collections/NotificationSettingCollection.php create mode 100644 src/Entities/Collections/Paginator.php create mode 100644 src/Entities/Collections/PriceCollection.php create mode 100644 src/Entities/Collections/PriceWithIncludesCollection.php create mode 100644 src/Entities/Collections/ProductCollection.php create mode 100644 src/Entities/Collections/ProductWithIncludesCollection.php create mode 100644 src/Entities/Collections/ReportCollection.php create mode 100644 src/Entities/Collections/SubscriptionCollection.php create mode 100644 src/Entities/Collections/SubscriptionPreviewCollection.php create mode 100644 src/Entities/Collections/SubscriptionWithIncludesCollection.php create mode 100644 src/Entities/Collections/SubscriptionsTransactionCollection.php create mode 100644 src/Entities/Collections/TransactionCollection.php create mode 100644 src/Entities/Collections/TransactionPreviewCollection.php create mode 100644 src/Entities/Collections/TransactionWithIncludesCollection.php create mode 100644 src/Entities/Collections/TransactionsDataCollection.php create mode 100644 src/Entities/CreditBalance.php create mode 100644 src/Entities/Customer.php create mode 100644 src/Entities/CustomerIncludes.php create mode 100644 src/Entities/DateTime.php create mode 100644 src/Entities/Discount.php create mode 100644 src/Entities/Discount/DiscountStatus.php create mode 100644 src/Entities/Discount/DiscountType.php create mode 100644 src/Entities/Entity.php create mode 100644 src/Entities/Event.php create mode 100644 src/Entities/Event/EventTypeName.php create mode 100644 src/Entities/EventType.php create mode 100644 src/Entities/Notification.php create mode 100644 src/Entities/Notification/NotificationDiscount.php create mode 100644 src/Entities/Notification/NotificationOrigin.php create mode 100644 src/Entities/Notification/NotificationPayout.php create mode 100644 src/Entities/Notification/NotificationPayoutStatus.php create mode 100644 src/Entities/Notification/NotificationStatus.php create mode 100644 src/Entities/Notification/NotificationSubscription.php create mode 100644 src/Entities/NotificationLog.php create mode 100644 src/Entities/NotificationSetting.php create mode 100644 src/Entities/NotificationSetting/NotificationSettingType.php create mode 100644 src/Entities/Price.php create mode 100644 src/Entities/PricePreview.php create mode 100644 src/Entities/PriceWithIncludes.php create mode 100644 src/Entities/PricingPreview/PricePreviewDetails.php create mode 100644 src/Entities/PricingPreview/PricePreviewDiscounts.php create mode 100644 src/Entities/PricingPreview/PricePreviewItem.php create mode 100644 src/Entities/PricingPreview/PricePreviewLineItem.php create mode 100644 src/Entities/PricingPreview/PricePreviewTotalsFormatted.php create mode 100644 src/Entities/PricingPreview/PricePreviewUnitTotalsFormatted.php create mode 100644 src/Entities/Product.php create mode 100644 src/Entities/ProductWithIncludes.php create mode 100644 src/Entities/Report.php create mode 100644 src/Entities/Report/ReportFilters.php create mode 100644 src/Entities/Report/ReportName.php create mode 100644 src/Entities/Report/ReportOperator.php create mode 100644 src/Entities/Report/ReportStatus.php create mode 100644 src/Entities/Report/ReportType.php create mode 100644 src/Entities/ReportCSV.php create mode 100644 src/Entities/Shared/Action.php create mode 100644 src/Entities/Shared/AddressPreview.php create mode 100644 src/Entities/Shared/AdjustmentItemTotals.php create mode 100644 src/Entities/Shared/AvailablePaymentMethods.php create mode 100644 src/Entities/Shared/BillingDetails.php create mode 100644 src/Entities/Shared/BillingDetailsUpdate.php create mode 100644 src/Entities/Shared/Card.php create mode 100644 src/Entities/Shared/CatalogType.php create mode 100644 src/Entities/Shared/ChargebackFee.php create mode 100644 src/Entities/Shared/Checkout.php create mode 100644 src/Entities/Shared/CollectionMode.php create mode 100644 src/Entities/Shared/Contacts.php create mode 100644 src/Entities/Shared/CountryCode.php create mode 100644 src/Entities/Shared/CurrencyCode.php create mode 100644 src/Entities/Shared/CurrencyCodeAdjustments.php create mode 100644 src/Entities/Shared/CurrencyCodePayouts.php create mode 100644 src/Entities/Shared/CustomData.php create mode 100644 src/Entities/Shared/Data.php create mode 100644 src/Entities/Shared/ErrorCode.php create mode 100644 src/Entities/Shared/Interval.php create mode 100644 src/Entities/Shared/Meta.php create mode 100644 src/Entities/Shared/MetaPaginated.php create mode 100644 src/Entities/Shared/MethodDetails.php create mode 100644 src/Entities/Shared/Money.php create mode 100644 src/Entities/Shared/Original.php create mode 100644 src/Entities/Shared/Pagination.php create mode 100644 src/Entities/Shared/PayoutTotalsAdjustment.php create mode 100644 src/Entities/Shared/PriceQuantity.php create mode 100644 src/Entities/Shared/Status.php create mode 100644 src/Entities/Shared/StatusAdjustment.php create mode 100644 src/Entities/Shared/StatusPaymentAttempt.php create mode 100644 src/Entities/Shared/StatusTransaction.php create mode 100644 src/Entities/Shared/TaxCategory.php create mode 100644 src/Entities/Shared/TaxMode.php create mode 100644 src/Entities/Shared/TaxRatesUsed.php create mode 100644 src/Entities/Shared/TimePeriod.php create mode 100644 src/Entities/Shared/TotalAdjustments.php create mode 100644 src/Entities/Shared/Totals.php create mode 100644 src/Entities/Shared/TransactionDetailsPreview.php create mode 100644 src/Entities/Shared/TransactionLineItemPreview.php create mode 100644 src/Entities/Shared/TransactionOrigin.php create mode 100644 src/Entities/Shared/TransactionPaymentAttempt.php create mode 100644 src/Entities/Shared/TransactionPayoutTotals.php create mode 100644 src/Entities/Shared/TransactionPayoutTotalsAdjusted.php create mode 100644 src/Entities/Shared/TransactionTotals.php create mode 100644 src/Entities/Shared/TransactionTotalsAdjusted.php create mode 100644 src/Entities/Shared/Type.php create mode 100644 src/Entities/Shared/UnitPriceOverride.php create mode 100644 src/Entities/Shared/UnitTotals.php create mode 100644 src/Entities/Subscription.php create mode 100644 src/Entities/Subscription/SubscriptionAdjustment.php create mode 100644 src/Entities/Subscription/SubscriptionAdjustmentItem.php create mode 100644 src/Entities/Subscription/SubscriptionAdjustmentPreview.php create mode 100644 src/Entities/Subscription/SubscriptionCharge.php create mode 100644 src/Entities/Subscription/SubscriptionCredit.php create mode 100644 src/Entities/Subscription/SubscriptionDetails.php create mode 100644 src/Entities/Subscription/SubscriptionDiscount.php create mode 100644 src/Entities/Subscription/SubscriptionEffectiveFrom.php create mode 100644 src/Entities/Subscription/SubscriptionItem.php create mode 100644 src/Entities/Subscription/SubscriptionItemStatus.php create mode 100644 src/Entities/Subscription/SubscriptionItems.php create mode 100644 src/Entities/Subscription/SubscriptionItemsWithPrice.php create mode 100644 src/Entities/Subscription/SubscriptionManagementUrls.php create mode 100644 src/Entities/Subscription/SubscriptionNextTransaction.php create mode 100644 src/Entities/Subscription/SubscriptionNonCatalogPrice.php create mode 100644 src/Entities/Subscription/SubscriptionNonCatalogPriceWithProduct.php create mode 100644 src/Entities/Subscription/SubscriptionNonCatalogProduct.php create mode 100644 src/Entities/Subscription/SubscriptionOnPaymentFailure.php create mode 100644 src/Entities/Subscription/SubscriptionPreviewSubscriptionUpdateSummary.php create mode 100644 src/Entities/Subscription/SubscriptionPrice.php create mode 100644 src/Entities/Subscription/SubscriptionProration.php create mode 100644 src/Entities/Subscription/SubscriptionProrationBillingMode.php create mode 100644 src/Entities/Subscription/SubscriptionResult.php create mode 100644 src/Entities/Subscription/SubscriptionResultAction.php create mode 100644 src/Entities/Subscription/SubscriptionScheduledChange.php create mode 100644 src/Entities/Subscription/SubscriptionScheduledChangeAction.php create mode 100644 src/Entities/Subscription/SubscriptionStatus.php create mode 100644 src/Entities/Subscription/SubscriptionTimePeriod.php create mode 100644 src/Entities/Subscription/SubscriptionTransactionItem.php create mode 100644 src/Entities/Subscription/SubscriptionTransactionLineItem.php create mode 100644 src/Entities/Subscription/SubscriptionUpdateItem.php create mode 100644 src/Entities/SubscriptionPreview.php create mode 100644 src/Entities/SubscriptionTransaction.php create mode 100644 src/Entities/SubscriptionWithIncludes.php create mode 100644 src/Entities/Transaction.php create mode 100644 src/Entities/Transaction/TransactionAdjustment.php create mode 100644 src/Entities/Transaction/TransactionAdjustmentItem.php create mode 100644 src/Entities/Transaction/TransactionAdjustmentsTotals.php create mode 100644 src/Entities/Transaction/TransactionBreakdown.php create mode 100644 src/Entities/Transaction/TransactionCardType.php create mode 100644 src/Entities/Transaction/TransactionCreateItem.php create mode 100644 src/Entities/Transaction/TransactionCreateItemWithPrice.php create mode 100644 src/Entities/Transaction/TransactionDetails.php create mode 100644 src/Entities/Transaction/TransactionItem.php create mode 100644 src/Entities/Transaction/TransactionItemPreviewWithNonCatalogPrice.php create mode 100644 src/Entities/Transaction/TransactionItemPreviewWithPrice.php create mode 100644 src/Entities/Transaction/TransactionItemPreviewWithPriceId.php create mode 100644 src/Entities/Transaction/TransactionLineItem.php create mode 100644 src/Entities/Transaction/TransactionNonCatalogPrice.php create mode 100644 src/Entities/Transaction/TransactionNonCatalogPriceWithProduct.php create mode 100644 src/Entities/Transaction/TransactionNonCatalogProduct.php create mode 100644 src/Entities/Transaction/TransactionProration.php create mode 100644 src/Entities/Transaction/TransactionTimePeriod.php create mode 100644 src/Entities/Transaction/TransactionUpdateTransactionItem.php create mode 100644 src/Entities/TransactionData.php create mode 100644 src/Entities/TransactionPreview.php create mode 100644 src/Entities/TransactionWithIncludes.php create mode 100644 src/Environment.php create mode 100644 src/Exceptions/ApiError.php create mode 100644 src/Exceptions/ApiError/AddressApiError.php create mode 100644 src/Exceptions/ApiError/AdjustmentApiError.php create mode 100644 src/Exceptions/ApiError/BusinessApiError.php create mode 100644 src/Exceptions/ApiError/CustomerApiError.php create mode 100644 src/Exceptions/ApiError/DiscountApiError.php create mode 100644 src/Exceptions/ApiError/PriceApiError.php create mode 100644 src/Exceptions/ApiError/ProductApiError.php create mode 100644 src/Exceptions/ApiError/SubscriptionApiError.php create mode 100644 src/Exceptions/ApiError/TransactionApiError.php create mode 100644 src/Exceptions/FieldError.php create mode 100644 src/Exceptions/SdkException.php create mode 100644 src/Exceptions/SdkExceptions/InvalidArgumentException.php create mode 100644 src/Exceptions/SdkExceptions/MalformedResponse.php create mode 100644 src/FiltersUndefined.php create mode 100644 src/HasParameters.php create mode 100644 src/Logger/Formatter.php create mode 100644 src/Notifications/Events/AddressCreated.php create mode 100644 src/Notifications/Events/AddressUpdated.php create mode 100644 src/Notifications/Events/AdjustmentCreated.php create mode 100644 src/Notifications/Events/AdjustmentUpdated.php create mode 100644 src/Notifications/Events/BusinessCreated.php create mode 100644 src/Notifications/Events/BusinessUpdated.php create mode 100644 src/Notifications/Events/CustomerCreated.php create mode 100644 src/Notifications/Events/CustomerUpdated.php create mode 100644 src/Notifications/Events/DiscountCreated.php create mode 100644 src/Notifications/Events/DiscountImported.php create mode 100644 src/Notifications/Events/DiscountUpdated.php create mode 100644 src/Notifications/Events/PayoutCreated.php create mode 100644 src/Notifications/Events/PayoutPaid.php create mode 100644 src/Notifications/Events/PriceCreated.php create mode 100644 src/Notifications/Events/PriceUpdated.php create mode 100644 src/Notifications/Events/ProductCreated.php create mode 100644 src/Notifications/Events/ProductUpdated.php create mode 100644 src/Notifications/Events/SubscriptionActivated.php create mode 100644 src/Notifications/Events/SubscriptionCanceled.php create mode 100644 src/Notifications/Events/SubscriptionCreated.php create mode 100644 src/Notifications/Events/SubscriptionImported.php create mode 100644 src/Notifications/Events/SubscriptionPastDue.php create mode 100644 src/Notifications/Events/SubscriptionPaused.php create mode 100644 src/Notifications/Events/SubscriptionResumed.php create mode 100644 src/Notifications/Events/SubscriptionTrialing.php create mode 100644 src/Notifications/Events/SubscriptionUpdated.php create mode 100644 src/Notifications/Events/TransactionBilled.php create mode 100644 src/Notifications/Events/TransactionCanceled.php create mode 100644 src/Notifications/Events/TransactionCompleted.php create mode 100644 src/Notifications/Events/TransactionCreated.php create mode 100644 src/Notifications/Events/TransactionPaid.php create mode 100644 src/Notifications/Events/TransactionPastDue.php create mode 100644 src/Notifications/Events/TransactionPaymentFailed.php create mode 100644 src/Notifications/Events/TransactionReady.php create mode 100644 src/Notifications/Events/TransactionUpdated.php create mode 100644 src/Notifications/PaddleSignature.php create mode 100644 src/Notifications/Secret.php create mode 100644 src/Notifications/Verifier.php create mode 100644 src/Options.php create mode 100644 src/Resources/Addresses/AddressesClient.php create mode 100644 src/Resources/Addresses/Operations/CreateOperation.php create mode 100644 src/Resources/Addresses/Operations/ListOperation.php create mode 100644 src/Resources/Addresses/Operations/UpdateOperation.php create mode 100644 src/Resources/Adjustments/AdjustmentsClient.php create mode 100644 src/Resources/Adjustments/Operations/CreateOperation.php create mode 100644 src/Resources/Adjustments/Operations/ListOperation.php create mode 100644 src/Resources/Businesses/BusinessesClient.php create mode 100644 src/Resources/Businesses/Operations/CreateOperation.php create mode 100644 src/Resources/Businesses/Operations/ListOperation.php create mode 100644 src/Resources/Businesses/Operations/UpdateOperation.php create mode 100644 src/Resources/Customers/CustomersClient.php create mode 100644 src/Resources/Customers/Operations/CreateOperation.php create mode 100644 src/Resources/Customers/Operations/ListOperation.php create mode 100644 src/Resources/Customers/Operations/UpdateOperation.php create mode 100644 src/Resources/Discounts/DiscountsClient.php create mode 100644 src/Resources/Discounts/Operations/CreateOperation.php create mode 100644 src/Resources/Discounts/Operations/ListOperation.php create mode 100644 src/Resources/Discounts/Operations/UpdateOperation.php create mode 100644 src/Resources/EventTypes/EventTypesClient.php create mode 100644 src/Resources/Events/EventsClient.php create mode 100644 src/Resources/Events/Operations/ListOperation.php create mode 100644 src/Resources/NotificationLogs/NotificationLogsClient.php create mode 100644 src/Resources/NotificationLogs/Operations/ListOperation.php create mode 100644 src/Resources/NotificationSettings/NotificationSettingsClient.php create mode 100644 src/Resources/NotificationSettings/Operations/CreateOperation.php create mode 100644 src/Resources/NotificationSettings/Operations/UpdateOperation.php create mode 100644 src/Resources/Notifications/NotificationsClient.php create mode 100644 src/Resources/Notifications/Operations/ListOperation.php create mode 100644 src/Resources/Prices/Operations/CreateOperation.php create mode 100644 src/Resources/Prices/Operations/List/Includes.php create mode 100644 src/Resources/Prices/Operations/ListOperation.php create mode 100644 src/Resources/Prices/Operations/UpdateOperation.php create mode 100644 src/Resources/Prices/PricesClient.php create mode 100644 src/Resources/PricingPreviews/Operations/PreviewPricesOperation.php create mode 100644 src/Resources/PricingPreviews/PricingPreviewsClient.php create mode 100644 src/Resources/Products/Operations/CreateOperation.php create mode 100644 src/Resources/Products/Operations/List/Includes.php create mode 100644 src/Resources/Products/Operations/ListOperation.php create mode 100644 src/Resources/Products/Operations/UpdateOperation.php create mode 100644 src/Resources/Products/ProductsClient.php create mode 100644 src/Resources/Reports/Operations/CreateOperation.php create mode 100644 src/Resources/Reports/Operations/ListOperation.php create mode 100644 src/Resources/Reports/ReportsClient.php create mode 100644 src/Resources/Shared/Operations/List/Comparator.php create mode 100644 src/Resources/Shared/Operations/List/DateComparison.php create mode 100644 src/Resources/Shared/Operations/List/OrderBy.php create mode 100644 src/Resources/Shared/Operations/List/Pager.php create mode 100644 src/Resources/Subscriptions/Operations/CancelOperation.php create mode 100644 src/Resources/Subscriptions/Operations/CreateOneTimeChargeOperation.php create mode 100644 src/Resources/Subscriptions/Operations/Get/Includes.php create mode 100644 src/Resources/Subscriptions/Operations/ListOperation.php create mode 100644 src/Resources/Subscriptions/Operations/PauseOperation.php create mode 100644 src/Resources/Subscriptions/Operations/PreviewOneTimeChargeOperation.php create mode 100644 src/Resources/Subscriptions/Operations/PreviewUpdateOperation.php create mode 100644 src/Resources/Subscriptions/Operations/ResumeOperation.php create mode 100644 src/Resources/Subscriptions/Operations/Update/SubscriptionDiscount.php create mode 100644 src/Resources/Subscriptions/Operations/UpdateOperation.php create mode 100644 src/Resources/Subscriptions/SubscriptionsClient.php create mode 100644 src/Resources/Transactions/Operations/CreateOperation.php create mode 100644 src/Resources/Transactions/Operations/List/Includes.php create mode 100644 src/Resources/Transactions/Operations/ListOperation.php create mode 100644 src/Resources/Transactions/Operations/PreviewOperation.php create mode 100644 src/Resources/Transactions/Operations/UpdateOperation.php create mode 100644 src/Resources/Transactions/TransactionsClient.php create mode 100644 src/ResponseParser.php create mode 100644 src/Undefined.php create mode 100644 tests/Functional/Resources/Addresses/AddressesClientTest.php create mode 100644 tests/Functional/Resources/Addresses/_fixtures/request/create_basic.json create mode 100644 tests/Functional/Resources/Addresses/_fixtures/request/create_full.json create mode 100644 tests/Functional/Resources/Addresses/_fixtures/request/update_full.json create mode 100644 tests/Functional/Resources/Addresses/_fixtures/request/update_partial.json create mode 100644 tests/Functional/Resources/Addresses/_fixtures/request/update_single.json create mode 100644 tests/Functional/Resources/Addresses/_fixtures/response/full_entity.json create mode 100644 tests/Functional/Resources/Addresses/_fixtures/response/list_default.json create mode 100644 tests/Functional/Resources/Addresses/_fixtures/response/minimal_entity.json create mode 100644 tests/Functional/Resources/Adjustments/AdjustmentsClientTest.php create mode 100644 tests/Functional/Resources/Adjustments/_fixtures/request/create_basic.json create mode 100644 tests/Functional/Resources/Adjustments/_fixtures/request/create_full.json create mode 100644 tests/Functional/Resources/Adjustments/_fixtures/response/full_entity.json create mode 100644 tests/Functional/Resources/Adjustments/_fixtures/response/list_default.json create mode 100644 tests/Functional/Resources/Adjustments/_fixtures/response/minimal_entity.json create mode 100644 tests/Functional/Resources/Businesses/BusinessesClientTest.php create mode 100644 tests/Functional/Resources/Businesses/_fixtures/request/create_basic.json create mode 100644 tests/Functional/Resources/Businesses/_fixtures/request/create_full.json create mode 100644 tests/Functional/Resources/Businesses/_fixtures/request/update_full.json create mode 100644 tests/Functional/Resources/Businesses/_fixtures/request/update_partial.json create mode 100644 tests/Functional/Resources/Businesses/_fixtures/request/update_single.json create mode 100644 tests/Functional/Resources/Businesses/_fixtures/response/full_entity.json create mode 100644 tests/Functional/Resources/Businesses/_fixtures/response/list_default.json create mode 100644 tests/Functional/Resources/Businesses/_fixtures/response/minimal_entity.json create mode 100644 tests/Functional/Resources/Customers/CustomersClientTest.php create mode 100644 tests/Functional/Resources/Customers/_fixtures/request/create_basic.json create mode 100644 tests/Functional/Resources/Customers/_fixtures/request/create_full.json create mode 100644 tests/Functional/Resources/Customers/_fixtures/request/update_full.json create mode 100644 tests/Functional/Resources/Customers/_fixtures/request/update_partial.json create mode 100644 tests/Functional/Resources/Customers/_fixtures/request/update_single.json create mode 100644 tests/Functional/Resources/Customers/_fixtures/response/full_entity.json create mode 100644 tests/Functional/Resources/Customers/_fixtures/response/list_credit_balances.json create mode 100644 tests/Functional/Resources/Customers/_fixtures/response/list_default.json create mode 100644 tests/Functional/Resources/Customers/_fixtures/response/minimal_entity.json create mode 100644 tests/Functional/Resources/Discounts/DiscountsClientTest.php create mode 100644 tests/Functional/Resources/Discounts/_fixtures/request/create_basic.json create mode 100644 tests/Functional/Resources/Discounts/_fixtures/request/create_full.json create mode 100644 tests/Functional/Resources/Discounts/_fixtures/request/update_full.json create mode 100644 tests/Functional/Resources/Discounts/_fixtures/request/update_partial.json create mode 100644 tests/Functional/Resources/Discounts/_fixtures/request/update_single.json create mode 100644 tests/Functional/Resources/Discounts/_fixtures/response/full_entity.json create mode 100644 tests/Functional/Resources/Discounts/_fixtures/response/list_default.json create mode 100644 tests/Functional/Resources/Discounts/_fixtures/response/minimal_entity.json create mode 100644 tests/Functional/Resources/EventTypes/EventTypesClientTest.php create mode 100644 tests/Functional/Resources/EventTypes/_fixtures/response/list_default.json create mode 100644 tests/Functional/Resources/Events/EventsClientTest.php create mode 100644 tests/Functional/Resources/Events/_fixtures/response/list_default.json create mode 100644 tests/Functional/Resources/NotificationLogs/NotificationLogsClientTest.php create mode 100644 tests/Functional/Resources/NotificationLogs/_fixtures/response/list_default.json create mode 100644 tests/Functional/Resources/NotificationSettings/NotificationSettingsClientTest.php create mode 100644 tests/Functional/Resources/NotificationSettings/_fixtures/request/create_basic.json create mode 100644 tests/Functional/Resources/NotificationSettings/_fixtures/request/create_full.json create mode 100644 tests/Functional/Resources/NotificationSettings/_fixtures/request/update_full.json create mode 100644 tests/Functional/Resources/NotificationSettings/_fixtures/request/update_partial.json create mode 100644 tests/Functional/Resources/NotificationSettings/_fixtures/request/update_single.json create mode 100644 tests/Functional/Resources/NotificationSettings/_fixtures/response/full_entity.json create mode 100644 tests/Functional/Resources/NotificationSettings/_fixtures/response/list_default.json create mode 100644 tests/Functional/Resources/NotificationSettings/_fixtures/response/minimal_entity.json create mode 100644 tests/Functional/Resources/Notifications/NotificationsClientTest.php create mode 100644 tests/Functional/Resources/Notifications/_fixtures/response/full_entity.json create mode 100644 tests/Functional/Resources/Notifications/_fixtures/response/list_default.json create mode 100644 tests/Functional/Resources/Notifications/_fixtures/response/replay.json create mode 100644 tests/Functional/Resources/Prices/PricesClientTest.php create mode 100644 tests/Functional/Resources/Prices/_fixtures/request/create_basic.json create mode 100644 tests/Functional/Resources/Prices/_fixtures/request/create_full.json create mode 100644 tests/Functional/Resources/Prices/_fixtures/request/update_full.json create mode 100644 tests/Functional/Resources/Prices/_fixtures/request/update_partial.json create mode 100644 tests/Functional/Resources/Prices/_fixtures/request/update_single.json create mode 100644 tests/Functional/Resources/Prices/_fixtures/response/full_entity.json create mode 100644 tests/Functional/Resources/Prices/_fixtures/response/full_entity_with_includes.json create mode 100644 tests/Functional/Resources/Prices/_fixtures/response/list_default.json create mode 100644 tests/Functional/Resources/Prices/_fixtures/response/minimal_entity.json create mode 100644 tests/Functional/Resources/PricingPreviews/PricingPreviewsClientTest.php create mode 100644 tests/Functional/Resources/PricingPreviews/_fixtures/request/preview_prices_full.json create mode 100644 tests/Functional/Resources/PricingPreviews/_fixtures/request/preview_prices_minimal.json create mode 100644 tests/Functional/Resources/PricingPreviews/_fixtures/request/preview_prices_multiple.json create mode 100644 tests/Functional/Resources/PricingPreviews/_fixtures/response/full_entity.json create mode 100644 tests/Functional/Resources/Products/ProductsClientTest.php create mode 100644 tests/Functional/Resources/Products/_fixtures/request/create_basic.json create mode 100644 tests/Functional/Resources/Products/_fixtures/request/create_full.json create mode 100644 tests/Functional/Resources/Products/_fixtures/request/update_full.json create mode 100644 tests/Functional/Resources/Products/_fixtures/request/update_partial.json create mode 100644 tests/Functional/Resources/Products/_fixtures/request/update_single.json create mode 100644 tests/Functional/Resources/Products/_fixtures/response/full_entity.json create mode 100644 tests/Functional/Resources/Products/_fixtures/response/full_entity_with_includes.json create mode 100644 tests/Functional/Resources/Products/_fixtures/response/list_default.json create mode 100644 tests/Functional/Resources/Products/_fixtures/response/minimal_entity.json create mode 100644 tests/Functional/Resources/Reports/ReportsClientTest.php create mode 100644 tests/Functional/Resources/Reports/_fixtures/request/create_basic.json create mode 100644 tests/Functional/Resources/Reports/_fixtures/request/create_full.json create mode 100644 tests/Functional/Resources/Reports/_fixtures/response/full_entity.json create mode 100644 tests/Functional/Resources/Reports/_fixtures/response/list_default.json create mode 100644 tests/Functional/Resources/Reports/_fixtures/response/report_csv_entity.json create mode 100644 tests/Functional/Resources/Subscriptions/SubscriptionsClientTest.php create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/request/cancel_none.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/request/cancel_single.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/request/create_one_time_charge_full.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/request/create_one_time_charge_minimal.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/request/pause_full.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/request/pause_none.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/request/pause_single.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/request/preview_one_time_charge_full.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/request/preview_one_time_charge_minimal.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/request/preview_update_full.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/request/preview_update_partial.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/request/preview_update_single.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/request/resume_none.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/request/resume_single_as_date.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/request/resume_single_as_enum.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/request/update_full.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/request/update_partial.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/request/update_single.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/response/full_entity.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/response/full_entity_with_includes.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/response/get_payment_method_change_transaction_entity.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/response/list_default.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/response/preview_charge_full_entity.json create mode 100644 tests/Functional/Resources/Subscriptions/_fixtures/response/preview_update_full_entity.json create mode 100644 tests/Functional/Resources/Transactions/TransactionsClientTest.php create mode 100644 tests/Functional/Resources/Transactions/_fixtures/request/create_basic.json create mode 100644 tests/Functional/Resources/Transactions/_fixtures/request/create_manual.json create mode 100644 tests/Functional/Resources/Transactions/_fixtures/request/create_with_non_catalog_price.json create mode 100644 tests/Functional/Resources/Transactions/_fixtures/request/preview_basic.json create mode 100644 tests/Functional/Resources/Transactions/_fixtures/request/preview_with_non_catalog_price.json create mode 100644 tests/Functional/Resources/Transactions/_fixtures/request/update_partial.json create mode 100644 tests/Functional/Resources/Transactions/_fixtures/request/update_single.json create mode 100644 tests/Functional/Resources/Transactions/_fixtures/response/full_entity.json create mode 100644 tests/Functional/Resources/Transactions/_fixtures/response/full_entity_with_includes.json create mode 100644 tests/Functional/Resources/Transactions/_fixtures/response/get_invoice_pdf_default.json create mode 100644 tests/Functional/Resources/Transactions/_fixtures/response/list_default.json create mode 100644 tests/Functional/Resources/Transactions/_fixtures/response/list_paginated_page_one.json create mode 100644 tests/Functional/Resources/Transactions/_fixtures/response/list_paginated_page_two.json create mode 100644 tests/Functional/Resources/Transactions/_fixtures/response/minimal_entity.json create mode 100644 tests/Functional/Resources/Transactions/_fixtures/response/preview_entity.json create mode 100644 tests/Unit/Notifications/VerifierTest.php create mode 100644 tests/Utils/Assertions/AssertsDeepMatchesData.php create mode 100644 tests/Utils/ReadsFixtures.php diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4a44b6c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +# A list of files and folders those will be excluded from archives and the Composer package. +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github export-ignore +/.php-cs-fixer.php export-ignore +/.vscode export-ignore +/examples export-ignore +/Makefile export-ignore +/phpdoc.dist.xml export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml export-ignore +/tests export-ignore +/dockerfile export-ignore \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..1d2ee7f --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @PaddleHQ/dx diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..7d27772 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,71 @@ +name: Bug report +description: Create a bug report to help us improve +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: textarea + id: description + attributes: + label: Describe the bug + description: Brief description of the bug and its impact. + placeholder: A clear and concise description of what is happening. + validations: + required: true + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps to reproduce + description: Explain the steps to recreate the issue. + placeholder: | + 1. Create entity ... + 2. Get entity ... + 3. See error ... + validations: + required: true + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + - type: textarea + id: code-snippets + attributes: + label: Code snippets + description: If applicable, add code snippets to help explain your problem. + render: PHP + validations: + required: false + - type: input + id: language-version + attributes: + label: PHP version + placeholder: PHP 8.1 + validations: + required: true + - type: input + id: sdk-version + attributes: + label: SDK version + placeholder: paddle-php-sdk 1.20.1 + validations: + required: true + - type: input + id: api-version + attributes: + label: API version + description: See [Versioning](https://developer.paddle.com/api-reference/about/versioning) in the API Reference to find which version you're using + placeholder: "Paddle Version 1" + validations: + required: true + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Add any other context about the problem here. + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..f09fee3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Paddle support + url: https://www.paddle.com/help + about: | + Please only open issues here that you believe represent actual bugs or feature requests for the Paddle PHP SDK. + + For help with the Paddle API or building your integration, contact our support team at sellers@paddle.com. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..41a5c15 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,32 @@ +name: Feature request +description: Suggest a new feature or idea for this SDK +labels: ["feature-request"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this feature request! + - type: textarea + id: description + attributes: + label: Please describe your feature request. + description: A clear and concise description of what the problem is. + placeholder: A clear and concise description of what the problem is. + - type: textarea + id: solution + attributes: + label: Describe your solution. + description: A clear and concise description of what you want to happen. + placeholder: A clear and concise description of what you want to happen. + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you have considered. + description: A clear and concise description of any other options or features you have considered. + placeholder: A clear and concise description of any other options or features you have considered. + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context about the feature request. + placeholder: Add any other context about the feature request. \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d862a3d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: "composer" + directory: / + schedule: + interval: "weekly" + day: "wednesday" + time: "08:00" + open-pull-requests-limit: 2 + reviewers: + - "@PaddleHQ/dx" + labels: + - "release:patch" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d61bfab --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + types: + - opened + - synchronize + schedule: + - cron: '0 10 * * *' + +jobs: + build: + name: "Package stability ${{ matrix.stability }}" + runs-on: ubuntu-latest + + strategy: + matrix: + stability: [ 'stable', 'lowest' ] + + steps: + - name: Checkout Repo + uses: actions/checkout@v2 + + - name: Build Container + run: make build + + - name: Install Dependencies + run: COMPOSER_STABILITY=${{ matrix.stability }} make install + + - name: Run Linter + run: make lint + + - name: Run Static Analysis + run: make stan + + - name: Run Tests + run: make test \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7db2727 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/vendor +/.phpunit.result.cache +/.phpunit.cache +/.php-cs-fixer.cache +/build +/composer.lock +.DS_Store +phpstan.neon \ No newline at end of file diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..4958fe4 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,42 @@ +exclude('vendor') + ->in([getcwd()]); + +$config = new Config(); + +return $config->setRules([ + '@PSR12' => true, + '@Symfony' => true, + 'trailing_comma_in_multiline' => [ + 'after_heredoc' => true, + 'elements' => ['arguments', 'arrays', 'match', 'parameters'], + ], + 'array_syntax' => ['syntax' => 'short'], + 'yoda_style' => [ + 'equal' => false, + 'identical' => false, + 'less_and_greater' => false, + ], + 'declare_strict_types' => true, + 'visibility_required' => [ + 'elements' => ['property', 'method'], + ], + 'no_superfluous_phpdoc_tags' => true, + 'php_unit_method_casing' => [ + 'case' => 'snake_case', + ], + 'not_operator_with_successor_space' => true, + 'concat_space' => [ + 'spacing' => 'one', + ], + 'self_accessor' => true, +]) + ->setRiskyAllowed(true) + ->setFinder($finder); diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0265d86 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,38 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +> **Important:** While in early access we may introduce breaking changes. Where we can, we'll tag breaking changes in this changelog and communicate ahead of time. + +Check our main [developer changelog](https://developer.paddle.com/?utm_source=dx&utm_medium=paddle-php-sdk) for information about changes to the Paddle Billing platform, the Paddle API, and other developer tools. + +## dev - 2024-01-10 + +### Added + +- Added `available_payment_methods` to [transaction preview and pricing preview](https://developer.paddle.com/changelog/2023/available-payment-methods?utm_source=dx&utm_medium=paddle-node-sdk) +- Added non-catalog items to [subscriptions](https://developer.paddle.com/changelog/2023/bill-custom-items-one-time-subscription-charge?utm_source=dx&utm_medium=paddle-node-sdk) +- Added non catalog items to [transactions](https://developer.paddle.com/changelog/2023/add-custom-items-transaction?utm_source=dx&utm_medium=paddle-node-sdk) +- Added `on_payment_failure` to [subscriptions](https://developer.paddle.com/changelog/2023/payment-failure-behavior-update-subscription?utm_source=dx&utm_medium=paddle-node-sdk) + +### Fixed + +- Correctly handle optional `ends_at` for discount under Subscriptions entity. +- Correctly handle optional `resume_at` for scheduled change under Subscriptions entity. + +## dev - 2024-01-05 + +### Added + +- Added `reports->list()` to [list reports](https://developer.paddle.com/api-reference/reports/list-reports?utm_source=dx&utm_medium=paddle-php-sdk) +- Added `reports->create()` to [create a new report](https://developer.paddle.com/api-reference/reports/create-report?utm_source=dx&utm_medium=paddle-php-sdk) +- Added `reports->get()` to [get a report](https://developer.paddle.com/api-reference/reports/get-report?utm_source=dx&utm_medium=paddle-php-sdk) +- Added `reports->getReportCsv()` to [get a CSV file for a report](https://developer.paddle.com/api-reference/reports/get-report-csv?utm_source=dx&utm_medium=paddle-php-sdk) + +## dev - 2023-12-14 + +### Added + +- Initial early access release. Added support for the most frequently used Paddle Billing entities and API operations. Check the [README](README.md) for more information. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8975696 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,5 @@ +## Contributing + +If you've spotted a problem with this package or have a new feature request please open an issue. + +For help with the Paddle API or building your integration, contact our support team at [sellers@paddle.com](mailto:sellers@paddle.com). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0999755 --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +command = @docker run $(3) -t --rm --entrypoint $(1) -v `pwd`:/app paddle-php-sdk $(2) + +COMPOSER_STABILITY?=stable + +.PHONY: build +build: + @docker build -t paddle-php-sdk . + +.PHONY: install +install: + $(call command, /usr/local/bin/composer, update --prefer-${COMPOSER_STABILITY}) + +.PHONY: test +test: + $(call command, /app/vendor/bin/phpunit, --no-coverage) + +.PHONY: stan +stan: + $(call command, /app/vendor/bin/phpstan, analyse src) + +.PHONY: rector +rector: + $(call command, /app/vendor/bin/rector, process src) + +.PHONY: format +format: + $(call command, /app/vendor/bin/php-cs-fixer, fix) + +.PHONY: lint +lint: + $(call command, /app/vendor/bin/php-cs-fixer, fix --dry-run) diff --git a/README.md b/README.md new file mode 100644 index 0000000..249d10a --- /dev/null +++ b/README.md @@ -0,0 +1,164 @@ +# Paddle PHP SDK + +[![Build Status](https://github.com/PaddleHQ/paddle-php-sdk/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/PaddleHQ/paddle-php-sdk/actions/?query=branch%3Amain) +[![Latest Stable Version](https://poser.pugx.org/paddlehq/paddle-php-sdk/v/stable.svg)](https://packagist.org/packages/paddlehq/paddle-php-sdk) +[![PHP Version Require](http://poser.pugx.org/paddlehq/paddle-php-sdk/require/php)](https://packagist.org/packages/paddlehq/paddle-php-sdk) +[![Total Downloads](https://poser.pugx.org/paddlehq/paddle-php-sdk/downloads.svg)](https://packagist.org/packages/paddlehq/paddle-php-sdk) +[![License](https://poser.pugx.org/paddlehq/paddle-php-sdk/license.svg)](https://packagist.org/packages/paddlehq/paddle-php-sdk) + +[Paddle Billing](https://www.paddle.com/billing?utm_source=dx&utm_medium=paddle-php-sdk) is a complete digital product sales and subscription management platform, designed for modern software businesses. It helps you increase your revenue, retain customers, and scale your operations. + +This is a [PHP](https://www.php.net/) SDK that you can use to integrate Paddle Billing with applications written in PHP. + +For working with Paddle in your frontend, use [Paddle.js](https://developer.paddle.com/paddlejs/overview?utm_source=dx&utm_medium=paddle-php-sdk). You can open checkouts, securely collect payment information, build pricing pages, and integrate with Paddle Retain. + +> **Important:** This package works with Paddle Billing. It does not support Paddle Classic. To work with Paddle Classic, see: [Paddle Classic API reference](https://developer.paddle.com/classic/api-reference/1384a288aca7a-api-reference?utm_source=dx&utm_medium=paddle-php-sdk) + +## Requirements + +PHP 8.1 and later. + +## Composer + +You can install the bindings via [Composer](http://getcomposer.org/). Run the following command: + +```bash +composer require paddlehq/paddle-php-sdk +``` + +To use the bindings, use Composer's [autoload](https://getcomposer.org/doc/01-basic-usage.md#autoloading): + +```php +require_once 'vendor/autoload.php'; +``` + +## Before you begin + +If you've used this SDK, we'd love to hear how you found it! Email us at [team-dx@paddle.com](mailto:team-dx@paddle.com) with any thoughts. + +While in early access, we may introduce breaking changes. Where we can, we'll tag breaking changes and communicate ahead of time. + +## Usage + +To authenticate, you'll need an API key. You can create and manage API keys in **Paddle > Developer tools > Authentication**. + +Pass your API key while initializing a new Paddle client. + +``` php +use Paddle\SDK\Client; + +$paddle = new Client('API_KEY'); +``` + +You can also pass an environment to work with the sandbox: + +``` php +use Paddle\SDK\Client; +use Paddle\SDK\Environment; +use Paddle\SDK\Options; + +$paddle = new Client( + apiKey: 'API_KEY', + options: new Options(Environment::SANDBOX), +); +``` + +Keep in mind that API keys are separate for your sandbox and live accounts, so you'll need to generate keys for each environment. + +## Examples + +### List entities + +You can list supported entities with the `list` function in the resource. It returns an iterator to help when working with multiple pages. + +``` php +use Paddle\SDK\Client; + +$paddle = new Client('API_KEY'); + +$products = $paddle->products->list(); + +// List returns an iterable, so pagination is handled automatically. +foreach ($products as $product) { + echo $product->id; +} +``` + +### Create an entity + +You can create a supported entity with the `create` function in the resource. It accepts the resource's corresponding `CreateOperation`. The created entity is returned. + +``` php +use Paddle\SDK\Client; +use Paddle\SDK\Entities\Shared\TaxCategory; +use Paddle\SDK\Resources\Products\Operations\CreateOperation; + +$paddle = new Client('API_KEY'); + +$product = $paddle->products->create( + new CreateOperation( + name: 'ChatApp Education', + taxCategory: TaxCategory::Standard, + ), +); +``` + +### Update an entity + +You can update a supported entity with the `update` function in the resource. It accepts the `id` of the entity to update and the corresponding `UpdateOperation`. The updated entity is returned. + +``` php +use Paddle\SDK\Client; +use Paddle\SDK\Resources\Products\Operations\UpdateOperation; + +$paddle = new Client('API_KEY'); + +$operation = new UpdateOperation( + name: 'ChatApp Professional' +); + +$product = $paddle->products->update('id', $operation); +``` + +Where operations require more than one `id`, the `update` function accepts multiple arguments. For example, to update an address for a customer, pass the `customerId` and the `addressId`: + +``` php +$address = $paddle->products->update( + 'customer_id', + 'address_id', + $operation, +); +``` + +### Get an entity + +You can get an entity with the `get` function in the resource. It accepts the `id` of the entity to get. The entity is returned. + +``` php +use Paddle\SDK\Client; + +$paddle = new Client('API_KEY'); + +$product = $paddle->products->get('id'); +``` + +## Resources + +### Webhook signature verification + +The SDK includes a helper class to verify webhook signatures sent by Notifications from Paddle. + +``` php +use Paddle\SDK\Notifications\Secret; +use Paddle\SDK\Notifications\Verifier; + +(new Verifier())->verify( + $request, + new Secret('WEBHOOK_SECRET_KEY') +); +``` + +## Learn more + +- [Paddle API reference](https://developer.paddle.com/api-reference/overview?utm_source=dx&utm_medium=paddle-php-sdk) +- [Sign up for Paddle Billing](https://login.paddle.com/signup?utm_source=dx&utm_medium=paddle-php-sdk) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..171810f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +- [Security Policy](#security-policy) + - [Reporting a Vulnerability](#reporting-a-vulnerability) + +# Security policy + +## Reporting a vulnerability + +Please see the [Paddle Vulnerability Disclosure Policy](https://www.paddle.com/vulnerability-disclosure-policy) and +report any vulnerabilities using https://vdp.paddle.com/p/Report-a-Vulnerability. + +> [!WARNING] +> Do not create issues for potential security vulnerabilities. Issues are public and can be seen by potentially malicious actors. + +Thanks for helping to make the Paddle platform safe for everyone. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7d81e0a --- /dev/null +++ b/composer.json @@ -0,0 +1,63 @@ +{ + "name": "paddlehq/paddle-php-sdk", + "description": "Paddle's PHP SDK for Paddle Billing.", + "keywords": ["paddle", "sdk", "php"], + "homepage": "https://developer.paddle.com/api-reference/overview", + "license": "Apache-2.0", + "authors": [ + { + "name": "Paddle and contributors", + "homepage": "https://github.com/PaddleHQ/paddle-php-sdk/contributors" + } + ], + "type": "library", + "require": { + "php": "^8.1", + "ext-json": "*", + "ext-mbstring": "*", + "php-http/async-client-implementation": "^1.0", + "php-http/client-common": "^1.5 || ^2.0", + "php-http/discovery": "^1.15", + "php-http/httplug": "^1.1 || ^2.0", + "php-http/logger-plugin": "^1.3", + "php-http/message": "^1.5", + "php-http/message-factory": "^1.1", + "psr/http-factory": "^1.0", + "psr/http-factory-implementation": "^1.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/uid": "^5.4 || ^6.3", + "symfony/property-access": "^5.4 || ^6.3", + "symfony/serializer": "^5.4 || ^6.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.35", + "guzzlehttp/psr7": "^2.6", + "monolog/monolog": "^3.5", + "php-http/curl-client": "^2.3", + "php-http/mock-client": "^1.6", + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^9.5 || ^10.0", + "rector/rector": "dev-main", + "symfony/var-dumper": "^6.3" + }, + "autoload": { + "psr-4": { + "Paddle\\SDK\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Paddle\\SDK\\Tests\\": "tests" + } + }, + "config": { + "lock": false, + "sort-packages": true, + "allow-plugins": { + "php-http/discovery": false + } + }, + "minimum-stability": "dev", + "prefer-stable": true, + "optimize-autoloader": true +} diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..68103c5 --- /dev/null +++ b/dockerfile @@ -0,0 +1,15 @@ +FROM php:8.1-cli-alpine + +ENV XDEBUG_MODE=off + +RUN apk add --no-cache --update bash \ + && apk add --no-cache --update linux-headers \ + && apk add --no-cache --update $PHPIZE_DEPS \ + && pecl install xdebug \ + && docker-php-ext-enable xdebug + +COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer + +COPY . /app + +WORKDIR /app diff --git a/examples/catalog_management.php b/examples/catalog_management.php new file mode 100644 index 0000000..01c2126 --- /dev/null +++ b/examples/catalog_management.php @@ -0,0 +1,177 @@ +products->create(new Products\Operations\CreateOperation( + name: 'Kitten Service', + taxCategory: TaxCategory::Standard, + description: 'Simply an awesome product', + imageUrl: 'http://placekitten.com/200/300', + customData: new CustomData(['foo' => 'bar']), + )); +} catch (ProductApiError|ApiError|MalformedResponse $e) { + var_dump($e); + exit; +} + +echo sprintf("Created product '%s': %s \n", $product->id, $product->description); + +// ┌─── +// │ Update Product │ +// └────────────────┘ +$update = new Products\Operations\UpdateOperation( + name: 'Bear Service', + imageUrl: 'https://placebear.com/200/300', + customData: new CustomData(['beep' => 'boop']), +); + +try { + $product = $paddle->products->update($product->id, $update); +} catch (ProductApiError|ApiError|MalformedResponse $e) { + var_dump($e); + exit; +} + +echo sprintf("Updated product '%s': %s \n", $product->id, $product->description); + +// ┌─── +// │ Create Price │ +// └──────────────┘ +try { + $price = $paddle->prices->create(new Prices\Operations\CreateOperation( + description: 'Bear Hug', + productId: $product->id, + unitPrice: new Money('1000', CurrencyCode::GBP), + unitPriceOverrides: [ + new UnitPriceOverride( + [CountryCode::CA, CountryCode::US], + new Money('5000', CurrencyCode::USD), + ), + ], + trialPeriod: new TimePeriod(Interval::Week, 1), + billingCycle: new TimePeriod(Interval::Year, 1), + quantity: new PriceQuantity(1, 1), + customData: new CustomData(['foo' => 'bar']), + )); +} catch (PriceApiError|ApiError|MalformedResponse $e) { + var_dump($e); + exit; +} + +echo sprintf("Created price '%s': %s \n", $price->id, $price->description); + +// ┌─── +// │ Update Price │ +// └──────────────┘ +$update = new Prices\Operations\UpdateOperation( + description: 'One-off Bear Hug', + unitPrice: new Money('500', CurrencyCode::GBP), + customData: new CustomData(['beep' => 'boop']), +); + +try { + $test = json_encode($update, JSON_THROW_ON_ERROR); +} catch (JsonException $e) { + $test = 1; +} + +try { + $price = $paddle->prices->update($price->id, $update); +} catch (PriceApiError|ApiError|MalformedResponse $e) { + var_dump($e); + exit; +} + +echo sprintf("Updated price '%s': %s \n", $price->id, $price->description); + +// ┌─── +// │ Get Product with Prices │ +// └─────────────────────────┘ +try { + $product = $paddle->products->get($product->id, [ProductIncludes::Prices]); +} catch (ApiError|MalformedResponse $e) { + var_dump($e); + exit; +} + +echo sprintf( + "Read product '%s' with prices %s \n", + $product->id, + implode(', ', array_map(fn (PriceWithIncludes $price) => $price->id, iterator_to_array($product->prices))), +); + +// ┌─── +// │ Get Price with Product │ +// └────────────────────────┘ +try { + $price = $paddle->prices->get($price->id, [PriceIncludes::Product]); +} catch (ApiError|MalformedResponse $e) { + var_dump($e); + exit; +} + +echo sprintf("Read price '%s' with product %s \n", $price->id, $price->product?->id ?? '????'); + +// ┌─── +// │ Get Products │ +// └──────────────┘ +try { + $products = $paddle->products->list(new Products\Operations\ListOperation( + includes: [ProductIncludes::Prices], + statuses: [Status::Active], + )); +} catch (ApiError|MalformedResponse $e) { + var_dump($e); + exit; +} + +// ┌─── +// │ Iterate Products and Prices │ +// └─────────────────────────────┘ +foreach ($products as $product) { + echo $product->name; + echo "\n"; + echo str_repeat('-', strlen($product->name)) . "\n"; + foreach ($product->prices ?? [] as $price) { + echo sprintf("%s - %s\n", $price->unitPrice->amount, $price->description); + } + echo "\n"; +} diff --git a/examples/event_polling.php b/examples/event_polling.php new file mode 100644 index 0000000..225f290 --- /dev/null +++ b/examples/event_polling.php @@ -0,0 +1,47 @@ +events->list(new ListOperation(new Pager(after: $lastProcessedEventId))); +} catch (ApiError|MalformedResponse $e) { + // Handle an error, terminate the poll + var_dump($e->getMessage()); + exit; +} + +foreach ($events as $event) { + // Will read until the most recent event + $lastProcessedEventId = $event->eventId; + + echo sprintf( + "event: %s\t\t Type: %s\t\t Occurred At: %s\n", + $event->eventId, + str_pad($event->eventType->value, 28), + $event->occurredAt->format(Paddle\SDK\Entities\DateTime::PADDLE_RFC3339), + ); +} + +// Here you're up-to-date, you'd keep a record of where you got to... diff --git a/examples/logging.php b/examples/logging.php new file mode 100644 index 0000000..b1ebc92 --- /dev/null +++ b/examples/logging.php @@ -0,0 +1,28 @@ +products->list(); diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..b7f8783 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,7 @@ +parameters: + level: 5 + + reportUnmatchedIgnoredErrors: false + treatPhpDocTypesAsCertain: false + ignoreErrors: + - '#Method .+ should return .+ but returns Paddle\\SDK\\Entities\\Entity.#' \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..e767f57 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,21 @@ + + + + + ./tests/Unit + + + ./tests/Functional + + + + + ./src + + + \ No newline at end of file diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..625082e --- /dev/null +++ b/rector.php @@ -0,0 +1,29 @@ +sets([ + LevelSetList::UP_TO_PHP_81, + SetList::CODE_QUALITY, + SetList::DEAD_CODE, + SetList::TYPE_DECLARATION, + ]); + + $rectorConfig->paths([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]); + + $rectorConfig->skip([ + '*.json', + '*/Fixture/*', + ]); + + $rectorConfig->importNames(); + $rectorConfig->importShortClasses(false); +}; diff --git a/src/Client.php b/src/Client.php new file mode 100644 index 0000000..a20e09b --- /dev/null +++ b/src/Client.php @@ -0,0 +1,208 @@ +options = $options ?: new Options(); + $this->logger = $logger ?: new NullLogger(); + + $this->requestFactory = $requestFactory ?: Psr17FactoryDiscovery::findRequestFactory(); + $this->streamFactory = $streamFactory ?: Psr17FactoryDiscovery::findStreamFactory(); + $this->uriFactory = $uriFactory ?: Psr17FactoryDiscovery::findUriFactory(); + + $this->httpClient = $this->buildClient( + $httpClient ?: HttpAsyncClientDiscovery::find(), + ); + + $this->products = new ProductsClient($this); + $this->prices = new PricesClient($this); + $this->transactions = new TransactionsClient($this); + $this->adjustments = new AdjustmentsClient($this); + $this->customers = new CustomersClient($this); + $this->addresses = new AddressesClient($this); + $this->businesses = new BusinessesClient($this); + $this->discounts = new DiscountsClient($this); + $this->subscriptions = new SubscriptionsClient($this); + $this->eventTypes = new EventTypesClient($this); + $this->events = new EventsClient($this); + $this->pricingPreviews = new PricingPreviewsClient($this); + $this->notificationSettings = new NotificationSettingsClient($this); + $this->notifications = new NotificationsClient($this); + $this->notificationLogs = new NotificationLogsClient($this); + $this->reports = new ReportsClient($this); + } + + public function getRaw(string|UriInterface $uri, array|HasParameters $parameters = []): ResponseInterface + { + if ($parameters) { + $parameters = $parameters instanceof HasParameters ? $parameters->getParameters() : $parameters; + $query = \http_build_query($parameters); + + if ($uri instanceof UriInterface) { + $uri = $uri->withQuery($query); + } else { + $uri .= '?' . $query; + } + } + + return $this->requestRaw('GET', $uri); + } + + public function patchRaw(string|UriInterface $uri, array|\JsonSerializable $payload): ResponseInterface + { + return $this->requestRaw('PATCH', $uri, $payload); + } + + public function postRaw(string|UriInterface $uri, array|\JsonSerializable $payload = [], array|HasParameters $parameters = []): ResponseInterface + { + if ($parameters) { + $parameters = $parameters instanceof HasParameters ? $parameters->getParameters() : $parameters; + $query = \http_build_query($parameters); + + if ($uri instanceof UriInterface) { + $uri = $uri->withQuery($query); + } else { + $uri .= '?' . $query; + } + } + + return $this->requestRaw('POST', $uri, $payload); + } + + public function deleteRaw(string|UriInterface $uri): ResponseInterface + { + return $this->requestRaw('DELETE', $uri); + } + + private function requestRaw(string $method, string|UriInterface $uri, array|\JsonSerializable $payload = null): ResponseInterface + { + if (\is_string($uri)) { + $components = \parse_url($this->options->environment->baseUrl()); + + $uri = $this->uriFactory->createUri($uri) + ->withScheme($components['scheme']) + ->withHost($components['host']); + } + + $request = $this->requestFactory->createRequest($method, $uri); + + $serializer = new Serializer( + [new BackedEnumNormalizer(), new JsonSerializableNormalizer(), new ObjectNormalizer(nameConverter: new CamelCaseToSnakeCaseNameConverter())], + [new JsonEncoder()], + ); + + $body = $serializer->serialize($payload, 'json'); + + $request = $request->withBody( + // Satisfies empty body requests. + $this->streamFactory->createStream($body === '[]' ? '{}' : $body), + ); + + $request = $request->withAddedHeader('X-Transaction-ID', $this->transactionId ?? (string) new Ulid()); + + return $this->httpClient->sendAsyncRequest($request)->wait(); + } + + private function buildClient(HttpAsyncClient $httpClient): PluginClient + { + return new PluginClient($httpClient, [ + new AuthenticationPlugin(new Bearer($this->apiKey)), + new ContentTypePlugin(), + new ContentLengthPlugin(), + new DecoderPlugin(['use_content_encoding' => false]), + new HeaderSetPlugin([ + 'User-Agent' => 'PaddleSDK/php ' . self::SDK_VERSION, + ]), + new RetryPlugin([ + 'retries' => $this->options->retries, + ]), + new LoggerPlugin($this->logger, new Formatter()), + new ResponseSeekableBodyPlugin(), + ]); + } +} diff --git a/src/Entities/Address.php b/src/Entities/Address.php new file mode 100644 index 0000000..7fabe84 --- /dev/null +++ b/src/Entities/Address.php @@ -0,0 +1,53 @@ + $items + */ + public function __construct( + public string $id, + public Action $action, + public string $transactionId, + public string|null $subscriptionId, + public string $customerId, + public string $reason, + public bool|null $creditAppliedToBalance, + public CurrencyCode $currencyCode, + public StatusAdjustment $status, + public array $items, + public TotalAdjustments $totals, + public PayoutTotalsAdjustment|null $payoutTotals, + public \DateTimeInterface $createdAt, + public \DateTimeInterface|null $updatedAt, + ) { + } + + public static function from(array $data): self + { + return new self( + id: $data['id'], + action: Action::from($data['action']), + transactionId: $data['transaction_id'], + subscriptionId: $data['subscription_id'] ?? null, + customerId: $data['customer_id'], + reason: $data['reason'], + creditAppliedToBalance: $data['credit_applied_to_balance'] ?? null, + currencyCode: CurrencyCode::from($data['currency_code']), + status: StatusAdjustment::from($data['status']), + items: $data['items'], + totals: TotalAdjustments::from($data['totals']), + payoutTotals: isset($data['payout_totals']) ? PayoutTotalsAdjustment::from($data['payout_totals']) : null, + createdAt: DateTime::from($data['created_at']), + updatedAt: DateTime::from($data['updated_at']), + ); + } +} diff --git a/src/Entities/Adjustment/AdjustmentCustomerBalance.php b/src/Entities/Adjustment/AdjustmentCustomerBalance.php new file mode 100644 index 0000000..c915167 --- /dev/null +++ b/src/Entities/Adjustment/AdjustmentCustomerBalance.php @@ -0,0 +1,27 @@ + $contacts + */ + public function __construct( + public string $id, + public string $name, + public string|null $companyNumber, + public string|null $taxIdentifier, + public Status $status, + public array $contacts, + public \DateTimeInterface $createdAt, + public \DateTimeInterface $updatedAt, + public CustomData|null $customData, + ) { + } + + public static function from(array $data): self + { + return new self( + id: $data['id'], + name: $data['name'], + companyNumber: $data['company_number'] ?? null, + taxIdentifier: $data['tax_identifier'] ?? null, + status: Status::from($data['status']), + contacts: array_map(fn (array $contact): Contacts => Contacts::from($contact), $data['contacts']), + createdAt: DateTime::from($data['created_at']), + updatedAt: DateTime::from($data['updated_at']), + customData: isset($data['custom_data']) ? new CustomData($data['custom_data']) : null, + ); + } +} diff --git a/src/Entities/Business/BusinessesContacts.php b/src/Entities/Business/BusinessesContacts.php new file mode 100644 index 0000000..2e5a8ef --- /dev/null +++ b/src/Entities/Business/BusinessesContacts.php @@ -0,0 +1,21 @@ + Address::from($item), $itemsData), + $paginator, + ); + } + + public function current(): Address + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/AdjustmentsAdjustmentCollection.php b/src/Entities/Collections/AdjustmentsAdjustmentCollection.php new file mode 100644 index 0000000..6c89db6 --- /dev/null +++ b/src/Entities/Collections/AdjustmentsAdjustmentCollection.php @@ -0,0 +1,30 @@ + Adjustment::from($item), $itemsData), + $paginator, + ); + } + + public function current(): Adjustment + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/BusinessCollection.php b/src/Entities/Collections/BusinessCollection.php new file mode 100644 index 0000000..749dceb --- /dev/null +++ b/src/Entities/Collections/BusinessCollection.php @@ -0,0 +1,30 @@ + Business::from($item), $itemsData), + $paginator, + ); + } + + public function current(): Business + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/Collection.php b/src/Entities/Collections/Collection.php new file mode 100644 index 0000000..a816460 --- /dev/null +++ b/src/Entities/Collections/Collection.php @@ -0,0 +1,61 @@ +items[$this->pointer]; + } + + public function next(): void + { + ++$this->pointer; + } + + public function key(): mixed + { + return $this->items[$this->pointer]?->id ?? $this->pointer; + } + + public function valid(): bool + { + $loaded = isset($this->items[$this->pointer]); + + if ($loaded) { + return true; + } + + if ($this->paginator?->hasMore()) { + $collection = $this->paginator->nextPage(); + + $this->rewind(); + $this->items = $collection->items; + $this->paginator = $collection->paginator; + + return true; + } + + return false; + } + + public function rewind(): void + { + $this->pointer = 0; + } +} diff --git a/src/Entities/Collections/CreditBalanceCollection.php b/src/Entities/Collections/CreditBalanceCollection.php new file mode 100644 index 0000000..c9f3087 --- /dev/null +++ b/src/Entities/Collections/CreditBalanceCollection.php @@ -0,0 +1,30 @@ + CreditBalance::from($item), $itemsData), + $paginator, + ); + } + + public function current(): CreditBalance + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/CustomerCollection.php b/src/Entities/Collections/CustomerCollection.php new file mode 100644 index 0000000..c909e87 --- /dev/null +++ b/src/Entities/Collections/CustomerCollection.php @@ -0,0 +1,30 @@ + Customer::from($item), $itemsData), + $paginator, + ); + } + + public function current(): Customer + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/CustomerIncludesCollection.php b/src/Entities/Collections/CustomerIncludesCollection.php new file mode 100644 index 0000000..8842afd --- /dev/null +++ b/src/Entities/Collections/CustomerIncludesCollection.php @@ -0,0 +1,30 @@ + CustomerIncludes::from($item), $itemsData), + $paginator, + ); + } + + public function current(): CustomerIncludes + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/DiscountCollection.php b/src/Entities/Collections/DiscountCollection.php new file mode 100644 index 0000000..2a1b718 --- /dev/null +++ b/src/Entities/Collections/DiscountCollection.php @@ -0,0 +1,30 @@ + Discount::from($item), $itemsData), + $paginator, + ); + } + + public function current(): Discount + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/EventCollection.php b/src/Entities/Collections/EventCollection.php new file mode 100644 index 0000000..588c5b2 --- /dev/null +++ b/src/Entities/Collections/EventCollection.php @@ -0,0 +1,30 @@ + Event::from($item), $itemsData), + $paginator, + ); + } + + public function current(): Event + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/EventTypeCollection.php b/src/Entities/Collections/EventTypeCollection.php new file mode 100644 index 0000000..c3d54ef --- /dev/null +++ b/src/Entities/Collections/EventTypeCollection.php @@ -0,0 +1,30 @@ + EventType::from($item), $itemsData), + $paginator, + ); + } + + public function current(): EventType + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/NotificationCollection.php b/src/Entities/Collections/NotificationCollection.php new file mode 100644 index 0000000..1aa8d99 --- /dev/null +++ b/src/Entities/Collections/NotificationCollection.php @@ -0,0 +1,30 @@ + Notification::from($item), $itemsData), + $paginator, + ); + } + + public function current(): Notification + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/NotificationLogCollection.php b/src/Entities/Collections/NotificationLogCollection.php new file mode 100644 index 0000000..271c1c5 --- /dev/null +++ b/src/Entities/Collections/NotificationLogCollection.php @@ -0,0 +1,30 @@ + NotificationLog::from($item), $itemsData), + $paginator, + ); + } + + public function current(): NotificationLog + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/NotificationSettingCollection.php b/src/Entities/Collections/NotificationSettingCollection.php new file mode 100644 index 0000000..1e3067c --- /dev/null +++ b/src/Entities/Collections/NotificationSettingCollection.php @@ -0,0 +1,30 @@ + NotificationSetting::from($item), $itemsData), + $paginator, + ); + } + + public function current(): NotificationSetting + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/Paginator.php b/src/Entities/Collections/Paginator.php new file mode 100644 index 0000000..1e34b07 --- /dev/null +++ b/src/Entities/Collections/Paginator.php @@ -0,0 +1,43 @@ + $mapper + */ + public function __construct( + protected Client $client, + protected Pagination $pagination, + protected string $mapper, + ) { + } + + public function hasMore(): bool + { + return $this->pagination->hasMore; + } + + /** + * @throws ApiError On a generic API error + */ + public function nextPage(): Collection + { + $response = $this->client->getRaw($this->pagination->next); + + $responseParser = new ResponseParser($response); + + return $this->mapper::from( + $responseParser->getData(), + new self($this->client, $responseParser->getPagination(), $this->mapper), + ); + } +} diff --git a/src/Entities/Collections/PriceCollection.php b/src/Entities/Collections/PriceCollection.php new file mode 100644 index 0000000..32fc87f --- /dev/null +++ b/src/Entities/Collections/PriceCollection.php @@ -0,0 +1,30 @@ + Price::from($item), $itemsData), + $paginator, + ); + } + + public function current(): Price + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/PriceWithIncludesCollection.php b/src/Entities/Collections/PriceWithIncludesCollection.php new file mode 100644 index 0000000..c44e1b4 --- /dev/null +++ b/src/Entities/Collections/PriceWithIncludesCollection.php @@ -0,0 +1,30 @@ + PriceWithIncludes::from($item), $itemsData), + $paginator, + ); + } + + public function current(): PriceWithIncludes + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/ProductCollection.php b/src/Entities/Collections/ProductCollection.php new file mode 100644 index 0000000..3a23250 --- /dev/null +++ b/src/Entities/Collections/ProductCollection.php @@ -0,0 +1,30 @@ + Product::from($item), $itemsData), + $paginator, + ); + } + + public function current(): Product + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/ProductWithIncludesCollection.php b/src/Entities/Collections/ProductWithIncludesCollection.php new file mode 100644 index 0000000..48e0b86 --- /dev/null +++ b/src/Entities/Collections/ProductWithIncludesCollection.php @@ -0,0 +1,30 @@ + ProductWithIncludes::from($item), $itemsData), + $paginator, + ); + } + + public function current(): ProductWithIncludes + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/ReportCollection.php b/src/Entities/Collections/ReportCollection.php new file mode 100644 index 0000000..d5fa608 --- /dev/null +++ b/src/Entities/Collections/ReportCollection.php @@ -0,0 +1,30 @@ + Report::from($item), $itemsData), + $paginator, + ); + } + + public function current(): Report + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/SubscriptionCollection.php b/src/Entities/Collections/SubscriptionCollection.php new file mode 100644 index 0000000..40fdc8c --- /dev/null +++ b/src/Entities/Collections/SubscriptionCollection.php @@ -0,0 +1,30 @@ + Subscription::from($item), $itemsData), + $paginator, + ); + } + + public function current(): Subscription + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/SubscriptionPreviewCollection.php b/src/Entities/Collections/SubscriptionPreviewCollection.php new file mode 100644 index 0000000..2258580 --- /dev/null +++ b/src/Entities/Collections/SubscriptionPreviewCollection.php @@ -0,0 +1,30 @@ + SubscriptionPreview::from($item), $itemsData), + $paginator, + ); + } + + public function current(): SubscriptionPreview + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/SubscriptionWithIncludesCollection.php b/src/Entities/Collections/SubscriptionWithIncludesCollection.php new file mode 100644 index 0000000..5260331 --- /dev/null +++ b/src/Entities/Collections/SubscriptionWithIncludesCollection.php @@ -0,0 +1,30 @@ + SubscriptionWithIncludes::from($item), $itemsData), + $paginator, + ); + } + + public function current(): SubscriptionWithIncludes + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/SubscriptionsTransactionCollection.php b/src/Entities/Collections/SubscriptionsTransactionCollection.php new file mode 100644 index 0000000..0a18087 --- /dev/null +++ b/src/Entities/Collections/SubscriptionsTransactionCollection.php @@ -0,0 +1,30 @@ + SubscriptionTransaction::from($item), $itemsData), + $paginator, + ); + } + + public function current(): SubscriptionTransaction + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/TransactionCollection.php b/src/Entities/Collections/TransactionCollection.php new file mode 100644 index 0000000..7f1356d --- /dev/null +++ b/src/Entities/Collections/TransactionCollection.php @@ -0,0 +1,30 @@ + Transaction::from($item), $itemsData), + $paginator, + ); + } + + public function current(): Transaction + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/TransactionPreviewCollection.php b/src/Entities/Collections/TransactionPreviewCollection.php new file mode 100644 index 0000000..1e18466 --- /dev/null +++ b/src/Entities/Collections/TransactionPreviewCollection.php @@ -0,0 +1,30 @@ + TransactionPreview::from($item), $itemsData), + $paginator, + ); + } + + public function current(): TransactionPreview + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/TransactionWithIncludesCollection.php b/src/Entities/Collections/TransactionWithIncludesCollection.php new file mode 100644 index 0000000..ffeffa2 --- /dev/null +++ b/src/Entities/Collections/TransactionWithIncludesCollection.php @@ -0,0 +1,30 @@ + TransactionWithIncludes::from($item), $itemsData), + $paginator, + ); + } + + public function current(): TransactionWithIncludes + { + return parent::current(); + } +} diff --git a/src/Entities/Collections/TransactionsDataCollection.php b/src/Entities/Collections/TransactionsDataCollection.php new file mode 100644 index 0000000..02503b5 --- /dev/null +++ b/src/Entities/Collections/TransactionsDataCollection.php @@ -0,0 +1,30 @@ + TransactionData::from($item), $itemsData), + $paginator, + ); + } + + public function current(): TransactionData + { + return parent::current(); + } +} diff --git a/src/Entities/CreditBalance.php b/src/Entities/CreditBalance.php new file mode 100644 index 0000000..5635819 --- /dev/null +++ b/src/Entities/CreditBalance.php @@ -0,0 +1,34 @@ +format(self::PADDLE_RFC3339); + + try { + return new self($date); + } catch (\Exception) { + return null; + } + } +} diff --git a/src/Entities/Discount.php b/src/Entities/Discount.php new file mode 100644 index 0000000..bb9d972 --- /dev/null +++ b/src/Entities/Discount.php @@ -0,0 +1,61 @@ + $entity */ + $entity = match ($type) { + 'discount' => NotificationDiscount::class, + 'subscription' => NotificationSubscription::class, + 'adjustment' => Adjustment::class, + default => sprintf('\Paddle\SDK\Entities\%s', ucfirst($type)), + }; + + if (! class_exists($entity) || ! in_array(Entity::class, class_implements($entity), true)) { + throw new \UnexpectedValueException("Event type '{$type}' cannot be mapped to an object"); + } + + return new self( + $data['event_id'], + EventTypeName::from($data['event_type']), + DateTime::from($data['occurred_at']), + $entity::from($data['data']), + ); + } +} diff --git a/src/Entities/Event/EventTypeName.php b/src/Entities/Event/EventTypeName.php new file mode 100644 index 0000000..ae28a0e --- /dev/null +++ b/src/Entities/Event/EventTypeName.php @@ -0,0 +1,54 @@ + $items + */ + public function __construct( + public string $id, + public SubscriptionStatus $status, + public string $customerId, + public string $addressId, + public string|null $businessId, + public CurrencyCode $currencyCode, + public \DateTimeInterface $createdAt, + public \DateTimeInterface $updatedAt, + public \DateTimeInterface|null $startedAt, + public \DateTimeInterface|null $firstBilledAt, + public \DateTimeInterface|null $nextBilledAt, + public \DateTimeInterface|null $pausedAt, + public \DateTimeInterface|null $canceledAt, + public SubscriptionDiscount|null $discount, + public CollectionMode $collectionMode, + public BillingDetails|null $billingDetails, + public SubscriptionTimePeriod $currentBillingPeriod, + public TimePeriod $billingCycle, + public SubscriptionScheduledChange|null $scheduledChange, + public array $items, + public CustomData|null $customData, + ) { + } + + public static function from(array $data): self + { + return new self( + id: $data['id'], + status: SubscriptionStatus::from($data['status']), + customerId: $data['customer_id'], + addressId: $data['address_id'], + businessId: $data['business_id'] ?? null, + currencyCode: CurrencyCode::from($data['currency_code']), + createdAt: DateTime::from($data['created_at']), + updatedAt: DateTime::from($data['updated_at']), + startedAt: isset($data['started_at']) ? DateTime::from($data['started_at']) : null, + firstBilledAt: isset($data['first_billed_at']) ? DateTime::from($data['first_billed_at']) : null, + nextBilledAt: isset($data['next_billed_at']) ? DateTime::from($data['next_billed_at']) : null, + pausedAt: isset($data['paused_at']) ? DateTime::from($data['paused_at']) : null, + canceledAt: isset($data['canceled_at']) ? DateTime::from($data['canceled_at']) : null, + discount: isset($data['discount']) ? SubscriptionDiscount::from($data['discount']) : null, + collectionMode: CollectionMode::from($data['collection_mode']), + billingDetails: isset($data['billing_details']) ? BillingDetails::from($data['billing_details']) : null, + currentBillingPeriod: isset($data['current_billing_period']) + ? SubscriptionTimePeriod::from($data['current_billing_period']) + : null, + billingCycle: TimePeriod::from($data['billing_cycle']), + scheduledChange: isset($data['scheduled_change']) + ? SubscriptionScheduledChange::from($data['scheduled_change']) + : null, + items: array_map(fn (array $item): SubscriptionItem => SubscriptionItem::from($item), $data['items']), + customData: isset($data['custom_data']) ? new CustomData($data['custom_data']) : null, + ); + } +} diff --git a/src/Entities/NotificationLog.php b/src/Entities/NotificationLog.php new file mode 100644 index 0000000..36df3cb --- /dev/null +++ b/src/Entities/NotificationLog.php @@ -0,0 +1,28 @@ + $unitPriceOverrides + */ + public function __construct( + public string $id, + public string $productId, + public string|null $name, + public string $description, + public CatalogType|null $type, + public TimePeriod|null $billingCycle, + public TimePeriod|null $trialPeriod, + public TaxMode $taxMode, + public Money $unitPrice, + public array $unitPriceOverrides, + public PriceQuantity $quantity, + public Status $status, + public CustomData|null $customData, + ) { + } + + public static function from(array $data): self + { + return new self( + id: $data['id'], + productId: $data['product_id'], + name: $data['name'] ?? null, + description: $data['description'], + type: CatalogType::tryFrom($data['type'] ?? CatalogType::Standard->value), + billingCycle: isset($data['billing_cycle']) ? TimePeriod::from($data['billing_cycle']) : null, + trialPeriod: isset($data['trial_period']) ? TimePeriod::from($data['trial_period']) : null, + taxMode: isset($data['tax_mode']) ? TaxMode::from($data['tax_mode']) : null, + unitPrice: Money::from($data['unit_price']), + unitPriceOverrides: array_map( + fn (array $override): UnitPriceOverride => UnitPriceOverride::from($override), + $data['unit_price_overrides'] ?? [], + ), + quantity: PriceQuantity::from($data['quantity']), + status: Status::from($data['status']), + customData: isset($data['custom_data']) ? new CustomData($data['custom_data']) : null, + ); + } +} diff --git a/src/Entities/PricePreview.php b/src/Entities/PricePreview.php new file mode 100644 index 0000000..4f032c6 --- /dev/null +++ b/src/Entities/PricePreview.php @@ -0,0 +1,51 @@ + $availablePaymentMethods + */ + public function __construct( + public string|null $customerId, + public string|null $addressId, + public string|null $businessId, + public CurrencyCode $currencyCode, + public string|null $discountId, + public AddressPreview|null $address, + public string|null $customerIpAddress, + public PricePreviewDetails $details, + public array $availablePaymentMethods, + ) { + } + + public static function from(array $data): self + { + return new self( + $data['customer_id'] ?? null, + $data['address_id'] ?? null, + $data['business_id'] ?? null, + CurrencyCode::from($data['currency_code']), + $data['discount_id'] ?? null, + isset($data['address']) ? AddressPreview::from($data['address']) : null, + $data['customer_ip_address'] ?? null, + PricePreviewDetails::from($data['details']), + availablePaymentMethods: array_map(fn (string $item): AvailablePaymentMethods => AvailablePaymentMethods::from($item), $data['available_payment_methods']), + ); + } +} diff --git a/src/Entities/PriceWithIncludes.php b/src/Entities/PriceWithIncludes.php new file mode 100644 index 0000000..639b889 --- /dev/null +++ b/src/Entities/PriceWithIncludes.php @@ -0,0 +1,68 @@ + $unitPriceOverrides + */ + public function __construct( + public string $id, + public string $productId, + public string|null $name, + public string $description, + public CatalogType|null $type, + public TimePeriod|null $billingCycle, + public TimePeriod|null $trialPeriod, + public TaxMode|null $taxMode, + public Money $unitPrice, + public array $unitPriceOverrides, + public PriceQuantity $quantity, + public Status $status, + public CustomData|null $customData, + public ProductWithIncludes|null $product, + ) { + } + + public static function from(array $data): self + { + return new self( + id: $data['id'], + productId: $data['product_id'], + name: $data['name'] ?? null, + description: $data['description'], + type: CatalogType::tryFrom($data['type'] ?? ''), + billingCycle: isset($data['billing_cycle']) ? TimePeriod::from($data['billing_cycle']) : null, + trialPeriod: isset($data['trial_period']) ? TimePeriod::from($data['trial_period']) : null, + taxMode: isset($data['tax_mode']) ? TaxMode::from($data['tax_mode']) : null, + unitPrice: Money::from($data['unit_price']), + unitPriceOverrides: array_map( + fn (array $override): UnitPriceOverride => UnitPriceOverride::from($override), + $data['unit_price_overrides'] ?? [], + ), + quantity: PriceQuantity::from($data['quantity']), + status: Status::from($data['status']), + customData: isset($data['custom_data']) ? new CustomData($data['custom_data']) : null, + product: isset($data['product']) ? ProductWithIncludes::from($data['product']) : null, + ); + } +} diff --git a/src/Entities/PricingPreview/PricePreviewDetails.php b/src/Entities/PricingPreview/PricePreviewDetails.php new file mode 100644 index 0000000..08c8a23 --- /dev/null +++ b/src/Entities/PricingPreview/PricePreviewDetails.php @@ -0,0 +1,32 @@ + $lineItems + */ + public function __construct( + public array $lineItems, + ) { + } + + public static function from(array $data): self + { + return new self( + array_map(fn ($item): PricePreviewLineItem => PricePreviewLineItem::from($item), $data['line_items']), + ); + } +} diff --git a/src/Entities/PricingPreview/PricePreviewDiscounts.php b/src/Entities/PricingPreview/PricePreviewDiscounts.php new file mode 100644 index 0000000..88ba5dd --- /dev/null +++ b/src/Entities/PricingPreview/PricePreviewDiscounts.php @@ -0,0 +1,34 @@ + PricePreviewDiscounts::from($item), $data['discounts']), + ); + } +} diff --git a/src/Entities/PricingPreview/PricePreviewTotalsFormatted.php b/src/Entities/PricingPreview/PricePreviewTotalsFormatted.php new file mode 100644 index 0000000..6c12dc4 --- /dev/null +++ b/src/Entities/PricingPreview/PricePreviewTotalsFormatted.php @@ -0,0 +1,35 @@ + $filters + */ + public function __construct( + public string $id, + public ReportStatus $status, + public int|null $rows, + public ReportType $type, + public array $filters, + public \DateTimeInterface|null $expiresAt, + public \DateTimeInterface $createdAt, + public \DateTimeInterface $updatedAt, + ) { + } + + public static function from(array $data): self + { + return new self( + id: $data['id'], + status: ReportStatus::from($data['status']), + rows: $data['rows'] ?? null, + type: ReportType::from($data['type']), + filters: array_map(fn (array $filter): ReportFilters => ReportFilters::from($filter), $data['filters'] ?? []), + expiresAt: isset($data['expires_at']) ? DateTime::from($data['expires_at']) : null, + createdAt: DateTime::from($data['created_at']), + updatedAt: DateTime::from($data['updated_at']), + ); + } +} diff --git a/src/Entities/Report/ReportFilters.php b/src/Entities/Report/ReportFilters.php new file mode 100644 index 0000000..a9b5f21 --- /dev/null +++ b/src/Entities/Report/ReportFilters.php @@ -0,0 +1,31 @@ +data; + } +} diff --git a/src/Entities/Shared/Data.php b/src/Entities/Shared/Data.php new file mode 100644 index 0000000..2b49f13 --- /dev/null +++ b/src/Entities/Shared/Data.php @@ -0,0 +1,20 @@ + $taxRatesUsed + * @param array $lineItems + */ + public function __construct( + public array $taxRatesUsed, + public TransactionTotals $totals, + public array $lineItems, + ) { + } + + public static function from(array $data): self + { + return new self( + taxRatesUsed: array_map(fn (array $rate): TaxRatesUsed => TaxRatesUsed::from($rate), $data['tax_rates_used']), + totals: TransactionTotals::from($data['totals']), + lineItems: array_map(fn (array $item): TransactionLineItemPreview => TransactionLineItemPreview::from($item), $data['line_items']), + ); + } +} diff --git a/src/Entities/Shared/TransactionLineItemPreview.php b/src/Entities/Shared/TransactionLineItemPreview.php new file mode 100644 index 0000000..ba8adfc --- /dev/null +++ b/src/Entities/Shared/TransactionLineItemPreview.php @@ -0,0 +1,39 @@ + $countryCodes + */ + public function __construct( + public array $countryCodes, + public Money $unitPrice, + ) { + } + + public static function from(array $data): self + { + return new self($data['country_codes'], Money::from($data['unit_price'])); + } +} diff --git a/src/Entities/Shared/UnitTotals.php b/src/Entities/Shared/UnitTotals.php new file mode 100644 index 0000000..e2f9c66 --- /dev/null +++ b/src/Entities/Shared/UnitTotals.php @@ -0,0 +1,33 @@ + $items + */ + public function __construct( + public string $id, + public SubscriptionStatus $status, + public string $customerId, + public string $addressId, + public string|null $businessId, + public CurrencyCode $currencyCode, + public \DateTimeInterface $createdAt, + public \DateTimeInterface $updatedAt, + public \DateTimeInterface|null $startedAt, + public \DateTimeInterface|null $firstBilledAt, + public \DateTimeInterface|null $nextBilledAt, + public \DateTimeInterface|null $pausedAt, + public \DateTimeInterface|null $canceledAt, + public SubscriptionDiscount|null $discount, + public CollectionMode $collectionMode, + public BillingDetails|null $billingDetails, + public SubscriptionTimePeriod $currentBillingPeriod, + public TimePeriod $billingCycle, + public SubscriptionScheduledChange|null $scheduledChange, + public SubscriptionManagementUrls $managementUrls, + public array $items, + public CustomData|null $customData, + ) { + } + + public static function from(array $data): self + { + return new self( + id: $data['id'], + status: SubscriptionStatus::from($data['status']), + customerId: $data['customer_id'], + addressId: $data['address_id'], + businessId: $data['business_id'] ?? null, + currencyCode: CurrencyCode::from($data['currency_code']), + createdAt: DateTime::from($data['created_at']), + updatedAt: DateTime::from($data['updated_at']), + startedAt: isset($data['started_at']) ? DateTime::from($data['started_at']) : null, + firstBilledAt: isset($data['first_billed_at']) ? DateTime::from($data['first_billed_at']) : null, + nextBilledAt: isset($data['next_billed_at']) ? DateTime::from($data['next_billed_at']) : null, + pausedAt: isset($data['paused_at']) ? DateTime::from($data['paused_at']) : null, + canceledAt: isset($data['canceled_at']) ? DateTime::from($data['canceled_at']) : null, + discount: isset($data['discount']) ? SubscriptionDiscount::from($data['discount']) : null, + collectionMode: CollectionMode::from($data['collection_mode']), + billingDetails: isset($data['billing_details']) ? BillingDetails::from($data['billing_details']) : null, + currentBillingPeriod: isset($data['current_billing_period']) + ? SubscriptionTimePeriod::from($data['current_billing_period']) + : null, + billingCycle: TimePeriod::from($data['billing_cycle']), + scheduledChange: isset($data['scheduled_change']) + ? SubscriptionScheduledChange::from($data['scheduled_change']) + : null, + managementUrls: isset($data['management_urls']) + ? SubscriptionManagementUrls::from($data['management_urls']) + : null, + items: array_map(fn (array $item): SubscriptionItem => SubscriptionItem::from($item), $data['items']), + customData: isset($data['custom_data']) ? new CustomData($data['custom_data']) : null, + ); + } +} diff --git a/src/Entities/Subscription/SubscriptionAdjustment.php b/src/Entities/Subscription/SubscriptionAdjustment.php new file mode 100644 index 0000000..b4dad18 --- /dev/null +++ b/src/Entities/Subscription/SubscriptionAdjustment.php @@ -0,0 +1,42 @@ + $items + */ + public function __construct( + public string $id, + public Action $action, + public string $transactionId, + public string|null $subscriptionId, + public string $customerId, + public string $reason, + public bool $creditAppliedToBalance, + public CurrencyCode $currencyCode, + public StatusAdjustment $status, + public array $items, + public TotalAdjustments $totals, + public PayoutTotalsAdjustment $payoutTotals, + public \DateTimeInterface $createdAt, + public \DateTimeInterface $updatedAt, + ) { + } +} diff --git a/src/Entities/Subscription/SubscriptionAdjustmentItem.php b/src/Entities/Subscription/SubscriptionAdjustmentItem.php new file mode 100644 index 0000000..0a66ab0 --- /dev/null +++ b/src/Entities/Subscription/SubscriptionAdjustmentItem.php @@ -0,0 +1,27 @@ + $items + */ + public function __construct( + public string $transactionId, + public array $items, + public TotalAdjustments $totals, + ) { + } +} diff --git a/src/Entities/Subscription/SubscriptionCharge.php b/src/Entities/Subscription/SubscriptionCharge.php new file mode 100644 index 0000000..63e9217 --- /dev/null +++ b/src/Entities/Subscription/SubscriptionCharge.php @@ -0,0 +1,28 @@ + $taxRatesUsed + * @param array $lineItems + */ + public function __construct( + public array $taxRatesUsed, + public TransactionTotals $totals, + public TransactionTotalsAdjusted $adjustedTotals, + public TransactionPayoutTotals $payoutTotals, + public TransactionPayoutTotalsAdjusted $adjustedPayoutTotals, + public array $lineItems, + ) { + } + + public static function from(array $data): self + { + return new self( + array_map(fn (array $taxRate): TaxRatesUsed => TaxRatesUsed::from($taxRate), $data['tax_rate_used'] ?? []), + TransactionTotals::from($data['transaction_totals']), + TransactionTotalsAdjusted::from($data['transaction_totals_adjusted']), + TransactionPayoutTotals::from($data['transaction_payout_totals']), + TransactionPayoutTotalsAdjusted::from($data['transaction_payout_totals_adjusted']), + array_map(fn (array $item): SubscriptionTransactionLineItem => SubscriptionTransactionLineItem::from($item), $data['line_items'] ?? []), + ); + } +} diff --git a/src/Entities/Subscription/SubscriptionDiscount.php b/src/Entities/Subscription/SubscriptionDiscount.php new file mode 100644 index 0000000..72c9c5a --- /dev/null +++ b/src/Entities/Subscription/SubscriptionDiscount.php @@ -0,0 +1,33 @@ + $adjustments + */ + public function __construct( + public SubscriptionTimePeriod $billingPeriod, + public TransactionDetailsPreview $details, + public array $adjustments, + ) { + } + + public static function from(array $data): self + { + return new self( + billingPeriod: SubscriptionTimePeriod::from($data['billing_period']), + details: TransactionDetailsPreview::from($data['details']), + adjustments: [], + ); + } +} diff --git a/src/Entities/Subscription/SubscriptionNonCatalogPrice.php b/src/Entities/Subscription/SubscriptionNonCatalogPrice.php new file mode 100644 index 0000000..3e3089a --- /dev/null +++ b/src/Entities/Subscription/SubscriptionNonCatalogPrice.php @@ -0,0 +1,36 @@ + $unitPriceOverrides + */ + public function __construct( + public string $description, + public string|null $name, + public string $productId, + public TaxMode $taxMode, + public Money $unitPrice, + public array $unitPriceOverrides, + public PriceQuantity $quantity, + public CustomData|null $customData, + ) { + } +} diff --git a/src/Entities/Subscription/SubscriptionNonCatalogPriceWithProduct.php b/src/Entities/Subscription/SubscriptionNonCatalogPriceWithProduct.php new file mode 100644 index 0000000..a76e2a2 --- /dev/null +++ b/src/Entities/Subscription/SubscriptionNonCatalogPriceWithProduct.php @@ -0,0 +1,36 @@ + $unitPriceOverrides + */ + public function __construct( + public string $description, + public string|null $name, + public SubscriptionNonCatalogProduct $product, + public TaxMode $taxMode, + public Money $unitPrice, + public array $unitPriceOverrides, + public PriceQuantity $quantity, + public CustomData|null $customData, + ) { + } +} diff --git a/src/Entities/Subscription/SubscriptionNonCatalogProduct.php b/src/Entities/Subscription/SubscriptionNonCatalogProduct.php new file mode 100644 index 0000000..72b2c18 --- /dev/null +++ b/src/Entities/Subscription/SubscriptionNonCatalogProduct.php @@ -0,0 +1,29 @@ + $items + */ + public function __construct( + public SubscriptionStatus $status, + public string $customerId, + public string $addressId, + public string|null $businessId, + public CurrencyCode $currencyCode, + public \DateTimeInterface $createdAt, + public \DateTimeInterface $updatedAt, + public \DateTimeInterface|null $startedAt, + public \DateTimeInterface|null $firstBilledAt, + public \DateTimeInterface|null $nextBilledAt, + public \DateTimeInterface|null $pausedAt, + public \DateTimeInterface|null $canceledAt, + public SubscriptionDiscount|null $discount, + public CollectionMode $collectionMode, + public BillingDetails|null $billingDetails, + public SubscriptionTimePeriod|null $currentBillingPeriod, + public TimePeriod $billingCycle, + public SubscriptionScheduledChange|null $scheduledChange, + public SubscriptionManagementUrls $managementUrls, + public array $items, + public CustomData|null $customData, + public SubscriptionNextTransaction $immediateTransaction, + public SubscriptionNextTransaction $nextTransaction, + public TransactionDetailsPreview $recurringTransactionDetails, + public SubscriptionPreviewSubscriptionUpdateSummary|null $updateSummary, + ) { + } + + public static function from(array $data): self + { + return new self( + status: SubscriptionStatus::from($data['status']), + customerId: $data['customer_id'], + addressId: $data['address_id'], + businessId: $data['business_id'] ?? null, + currencyCode: CurrencyCode::from($data['currency_code']), + createdAt: DateTime::from($data['created_at']), + updatedAt: DateTime::from($data['updated_at']), + startedAt: isset($data['started_at']) ? DateTime::from($data['started_at']) : null, + firstBilledAt: isset($data['first_billed_at']) ? DateTime::from($data['first_billed_at']) : null, + nextBilledAt: isset($data['next_billed_at']) ? DateTime::from($data['next_billed_at']) : null, + pausedAt: isset($data['paused_at']) ? DateTime::from($data['paused_at']) : null, + canceledAt: isset($data['canceled_at']) ? DateTime::from($data['canceled_at']) : null, + discount: isset($data['discount']) ? SubscriptionDiscount::from($data['discount']) : null, + collectionMode: CollectionMode::from($data['collection_mode']), + billingDetails: isset($data['billing_details']) ? BillingDetails::from($data['billing_details']) : null, + currentBillingPeriod: isset($data['current_billing_period']) + ? SubscriptionTimePeriod::from($data['current_billing_period']) + : null, + billingCycle: TimePeriod::from($data['billing_cycle']), + scheduledChange: isset($data['scheduled_change']) + ? SubscriptionScheduledChange::from($data['scheduled_change']) + : null, + managementUrls: isset($data['management_urls']) + ? SubscriptionManagementUrls::from($data['management_urls']) + : null, + items: array_map(fn (array $item): SubscriptionItem => SubscriptionItem::from($item), $data['items']), + customData: isset($data['custom_data']) ? new CustomData($data['custom_data']) : null, + immediateTransaction: isset($data['immediate_transaction']) ? SubscriptionNextTransaction::from($data['immediate_transaction']) : null, + nextTransaction: isset($data['next_transaction']) ? SubscriptionNextTransaction::from($data['next_transaction']) : null, + recurringTransactionDetails: isset($data['recurring_transaction_details']) + ? TransactionDetailsPreview::from($data['recurring_transaction_details']) + : null, + updateSummary: isset($data['update_summary']) ? SubscriptionPreviewSubscriptionUpdateSummary::from($data['update_summary']) : null, + ); + } +} diff --git a/src/Entities/SubscriptionTransaction.php b/src/Entities/SubscriptionTransaction.php new file mode 100644 index 0000000..a0f4120 --- /dev/null +++ b/src/Entities/SubscriptionTransaction.php @@ -0,0 +1,97 @@ + $items + * @param array $payments + * @param array $adjustments + */ + public function __construct( + public string $id, + public StatusTransaction $status, + public string|null $customerId, + public string|null $addressId, + public string|null $businessId, + public CustomData|null $customData, + public CurrencyCode $currencyCode, + public TransactionOrigin $origin, + public string|null $subscriptionId, + public string|null $invoiceId, + public string|null $invoiceNumber, + public CollectionMode $collectionMode, + public string|null $discountId, + public BillingDetails|null $billingDetails, + public SubscriptionTimePeriod $billingPeriod, + public array $items, + public SubscriptionDetails $details, + public array $payments, + public Checkout $checkout, + public \DateTimeInterface $createdAt, + public \DateTimeInterface $updatedAt, + public \DateTimeInterface|null $billedAt, + public Customer $customer, + public Address $address, + public Business $business, + public Discount $discount, + public array $adjustments, + ) { + } + + public static function from(array $data): self + { + return new self( + id: $data['id'], + status: StatusTransaction::from($data['status']), + customerId: $data['customer_id'] ?? null, + addressId: $data['address_id'] ?? null, + businessId: $data['business_id'] ?? null, + customData: new CustomData($data['custom_data'] ?? []), + currencyCode: CurrencyCode::from($data['currency_code']), + origin: TransactionOrigin::from($data['origin']), + subscriptionId: $data['subscription_id'] ?? null, + invoiceId: $data['invoice_id'] ?? null, + invoiceNumber: $data['invoice_number'] ?? null, + collectionMode: CollectionMode::from($data['collection_mode']), + discountId: $data['discount_id'] ?? null, + billingDetails: BillingDetails::from($data['billing_details']), + billingPeriod: SubscriptionTimePeriod::from($data['billing_period']), + items: $data['items'], + details: SubscriptionDetails::from($data['details']), + payments: $data['payments'], + checkout: Checkout::from($data['checkout']), + createdAt: DateTime::from($data['created_at']), + updatedAt: DateTime::from($data['updated_at']), + billedAt: isset($data['billed_at']) ? DateTime::from($data['billed_at']) : null, + customer: Customer::from($data['customer']), + address: Address::from($data['address']), + business: Business::from($data['business']), + discount: Discount::from($data['discount']), + adjustments: $data['adjustments'], + ); + } +} diff --git a/src/Entities/SubscriptionWithIncludes.php b/src/Entities/SubscriptionWithIncludes.php new file mode 100644 index 0000000..51bfe61 --- /dev/null +++ b/src/Entities/SubscriptionWithIncludes.php @@ -0,0 +1,98 @@ + $items + */ + public function __construct( + public string $id, + public SubscriptionStatus $status, + public string $customerId, + public string $addressId, + public string|null $businessId, + public CurrencyCode $currencyCode, + public \DateTimeInterface $createdAt, + public \DateTimeInterface $updatedAt, + public \DateTimeInterface|null $startedAt, + public \DateTimeInterface|null $firstBilledAt, + public \DateTimeInterface|null $nextBilledAt, + public \DateTimeInterface|null $pausedAt, + public \DateTimeInterface|null $canceledAt, + public SubscriptionDiscount|null $discount, + public CollectionMode $collectionMode, + public BillingDetails|null $billingDetails, + public SubscriptionTimePeriod|null $currentBillingPeriod, + public TimePeriod $billingCycle, + public SubscriptionScheduledChange|null $scheduledChange, + public SubscriptionManagementUrls|null $managementUrls, + public array $items, + public CustomData|null $customData, + public SubscriptionNextTransaction|null $nextTransaction, + public TransactionDetailsPreview|null $recurringTransactionDetails, + ) { + } + + public static function from(array $data): self + { + return new self( + id: $data['id'], + status: SubscriptionStatus::from($data['status']), + customerId: $data['customer_id'], + addressId: $data['address_id'], + businessId: $data['business_id'] ?? null, + currencyCode: CurrencyCode::from($data['currency_code']), + createdAt: DateTime::from($data['created_at']), + updatedAt: DateTime::from($data['updated_at']), + startedAt: isset($data['started_at']) ? DateTime::from($data['started_at']) : null, + firstBilledAt: isset($data['first_billed_at']) ? DateTime::from($data['first_billed_at']) : null, + nextBilledAt: isset($data['next_billed_at']) ? DateTime::from($data['next_billed_at']) : null, + pausedAt: isset($data['paused_at']) ? DateTime::from($data['paused_at']) : null, + canceledAt: isset($data['canceled_at']) ? DateTime::from($data['canceled_at']) : null, + discount: isset($data['discount']) ? SubscriptionDiscount::from($data['discount']) : null, + collectionMode: CollectionMode::from($data['collection_mode']), + billingDetails: isset($data['billing_details']) ? BillingDetails::from($data['billing_details']) : null, + currentBillingPeriod: isset($data['current_billing_period']) + ? SubscriptionTimePeriod::from($data['current_billing_period']) + : null, + billingCycle: TimePeriod::from($data['billing_cycle']), + scheduledChange: isset($data['scheduled_change']) + ? SubscriptionScheduledChange::from($data['scheduled_change']) + : null, + managementUrls: isset($data['management_urls']) + ? SubscriptionManagementUrls::from($data['management_urls']) + : null, + items: array_map(fn (array $item): SubscriptionItem => SubscriptionItem::from($item), $data['items']), + customData: isset($data['custom_data']) ? new CustomData($data['custom_data']) : null, + nextTransaction: isset($data['next_transaction']) ? SubscriptionNextTransaction::from($data['next_transaction']) : null, + recurringTransactionDetails: isset($data['recurring_transaction_details']) + ? TransactionDetailsPreview::from($data['recurring_transaction_details']) + : null, + ); + } +} diff --git a/src/Entities/Transaction.php b/src/Entities/Transaction.php new file mode 100644 index 0000000..a6fa7f9 --- /dev/null +++ b/src/Entities/Transaction.php @@ -0,0 +1,85 @@ + $items + * @param array $payments + */ + public function __construct( + public string $id, + public StatusTransaction $status, + public string|null $customerId, + public string|null $addressId, + public string|null $businessId, + public CustomData|null $customData, + public CurrencyCode $currencyCode, + public TransactionOrigin $origin, + public string|null $subscriptionId, + public string|null $invoiceId, + public string|null $invoiceNumber, + public CollectionMode $collectionMode, + public string|null $discountId, + public BillingDetails|null $billingDetails, + public TransactionTimePeriod|null $billingPeriod, + public array $items, + public TransactionDetails $details, + public array $payments, + public Checkout $checkout, + public \DateTimeInterface $createdAt, + public \DateTimeInterface $updatedAt, + public \DateTimeInterface|null $billedAt, + ) { + } + + public static function from(array $data): self + { + return new self( + id: $data['id'], + status: StatusTransaction::from($data['status']), + customerId: $data['customer_id'] ?? null, + addressId: $data['address_id'] ?? null, + businessId: $data['business_id'] ?? null, + customData: isset($data['custom_data']) ? new CustomData($data['custom_data']) : null, + currencyCode: CurrencyCode::from($data['currency_code']), + origin: TransactionOrigin::from($data['origin']), + subscriptionId: $data['subscription_id'] ?? null, + invoiceId: $data['invoice_id'] ?? null, + invoiceNumber: $data['invoice_number'] ?? null, + collectionMode: CollectionMode::from($data['collection_mode']), + discountId: $data['discount_id'] ?? null, + billingDetails: isset($data['billing_details']) ? BillingDetails::from($data['billing_details']) : null, + billingPeriod: isset($data['billing_period']) ? TransactionTimePeriod::from($data['billing_period']) : null, + items: array_map(fn (array $item): TransactionItem => TransactionItem::from($item), $data['items'] ?? []), + details: TransactionDetails::from($data['details']), + payments: array_map(fn (array $payment): TransactionPaymentAttempt => TransactionPaymentAttempt::from($payment), $data['payments'] ?? []), + checkout: isset($data['checkout']) ? new Checkout($data['checkout']['url'] ?? null) : null, + createdAt: DateTime::from($data['created_at']), + updatedAt: DateTime::from($data['updated_at']), + billedAt: isset($data['billed_at']) ? DateTime::from($data['billed_at']) : null, + ); + } +} diff --git a/src/Entities/Transaction/TransactionAdjustment.php b/src/Entities/Transaction/TransactionAdjustment.php new file mode 100644 index 0000000..ab696eb --- /dev/null +++ b/src/Entities/Transaction/TransactionAdjustment.php @@ -0,0 +1,63 @@ + $items + */ + public function __construct( + public string $id, + public Action $action, + public string $transactionId, + public string|null $subscriptionId, + public string $customerId, + public string $reason, + public bool $creditAppliedToBalance, + public CurrencyCode $currencyCode, + public StatusAdjustment $status, + public array $items, + public TotalAdjustments $totals, + public PayoutTotalsAdjustment $payoutTotals, + public \DateTimeInterface $createdAt, + public \DateTimeInterface $updatedAt, + ) { + } + + public static function from(array $data): self + { + return new self( + id: $data['id'], + action: Action::from($data['action']), + transactionId: $data['transaction_id'], + subscriptionId: $data['subscription_id'] ?? null, + customerId: $data['customer_id'], + reason: $data['reason'], + creditAppliedToBalance: $data['credit_applied_to_balance'] ?? null, + currencyCode: CurrencyCode::from($data['currency_code']), + status: StatusAdjustment::from($data['status']), + items: $data['items'], + totals: TotalAdjustments::from($data['totals']), + payoutTotals: isset($data['payout_totals']) ? PayoutTotalsAdjustment::from($data['payout_totals']) : null, + createdAt: DateTime::from($data['created_at']), + updatedAt: DateTime::from($data['updated_at']), + ); + } +} diff --git a/src/Entities/Transaction/TransactionAdjustmentItem.php b/src/Entities/Transaction/TransactionAdjustmentItem.php new file mode 100644 index 0000000..50fad43 --- /dev/null +++ b/src/Entities/Transaction/TransactionAdjustmentItem.php @@ -0,0 +1,28 @@ + $taxRatesUsed + * @param array $lineItems + */ + public function __construct( + public array $taxRatesUsed, + public TransactionTotals $totals, + public TransactionTotalsAdjusted|null $adjustedTotals, + public TransactionPayoutTotals|null $payoutTotals, + public TransactionPayoutTotalsAdjusted|null $adjustedPayoutTotals, + public array $lineItems, + ) { + } + + public static function from(array $data): self + { + return new self( + array_map(fn (array $taxRateUsed): TaxRatesUsed => TaxRatesUsed::from($taxRateUsed), $data['tax_rates_used']), + TransactionTotals::from($data['totals']), + isset($data['adjusted_totals']) ? TransactionTotalsAdjusted::from($data['adjusted_totals']) : null, + isset($data['payout_totals']) ? TransactionPayoutTotals::from($data['payout_totals']) : null, + isset($data['adjusted_payout_totals']) ? TransactionPayoutTotalsAdjusted::from($data['adjusted_payout_totals']) : null, + array_map(fn (array $lineItem): TransactionLineItem => TransactionLineItem::from($lineItem), $data['line_items']), + ); + } +} diff --git a/src/Entities/Transaction/TransactionItem.php b/src/Entities/Transaction/TransactionItem.php new file mode 100644 index 0000000..655e299 --- /dev/null +++ b/src/Entities/Transaction/TransactionItem.php @@ -0,0 +1,35 @@ + $unitPriceOverrides + */ + public function __construct( + public string $description, + public string|null $name, + public TimePeriod|null $billingCycle, + public TimePeriod|null $trialPeriod, + public TaxMode $taxMode, + public Money $unitPrice, + public array $unitPriceOverrides, + public PriceQuantity $quantity, + public CustomData|null $customData, + public string $productId, + ) { + } +} diff --git a/src/Entities/Transaction/TransactionNonCatalogPriceWithProduct.php b/src/Entities/Transaction/TransactionNonCatalogPriceWithProduct.php new file mode 100644 index 0000000..d020966 --- /dev/null +++ b/src/Entities/Transaction/TransactionNonCatalogPriceWithProduct.php @@ -0,0 +1,39 @@ + $unitPriceOverrides + */ + public function __construct( + public string $description, + public string|null $name, + public TimePeriod|null $billingCycle, + public TimePeriod|null $trialPeriod, + public TaxMode $taxMode, + public Money $unitPrice, + public array $unitPriceOverrides, + public PriceQuantity $quantity, + public CustomData|null $customData, + public TransactionNonCatalogProduct $product, + ) { + } +} diff --git a/src/Entities/Transaction/TransactionNonCatalogProduct.php b/src/Entities/Transaction/TransactionNonCatalogProduct.php new file mode 100644 index 0000000..7a48476 --- /dev/null +++ b/src/Entities/Transaction/TransactionNonCatalogProduct.php @@ -0,0 +1,27 @@ + $items + * @param array $availablePaymentMethods + */ + public function __construct( + public string|null $customerId, + public string|null $addressId, + public string|null $businessId, + public CurrencyCode $currencyCode, + public string|null $discountId, + public string|null $customerIpAddress, + public AddressPreview|null $address, + public bool $ignoreTrials, + public array $items, + public TransactionDetailsPreview $details, + public array $availablePaymentMethods, + ) { + } + + public static function from(array $data): self + { + return new self( + customerId: $data['customer_id'] ?? null, + addressId: $data['address_id'] ?? null, + businessId: $data['business_id'] ?? null, + currencyCode: CurrencyCode::from($data['currency_code']), + discountId: $data['discount_id'] ?? null, + customerIpAddress: $data['customer_ip_address'] ?? null, + address: isset($data['address']) ? AddressPreview::from($data['address']) : null, + ignoreTrials: $data['ignore_trials'], + items: array_map(fn (array $item): TransactionItemPreviewWithPrice => TransactionItemPreviewWithPrice::from($item), $data['items']), + details: TransactionDetailsPreview::from($data['details']), + availablePaymentMethods: array_map(fn (string $item): AvailablePaymentMethods => AvailablePaymentMethods::from($item), $data['available_payment_methods']), + ); + } +} diff --git a/src/Entities/TransactionWithIncludes.php b/src/Entities/TransactionWithIncludes.php new file mode 100644 index 0000000..9cbb074 --- /dev/null +++ b/src/Entities/TransactionWithIncludes.php @@ -0,0 +1,100 @@ + $items + * @param array $payments + * @param array $adjustments + */ + public function __construct( + public string $id, + public StatusTransaction $status, + public string|null $customerId, + public string|null $addressId, + public string|null $businessId, + public CustomData|null $customData, + public CurrencyCode $currencyCode, + public TransactionOrigin $origin, + public string|null $subscriptionId, + public string|null $invoiceId, + public string|null $invoiceNumber, + public CollectionMode $collectionMode, + public string|null $discountId, + public BillingDetails|null $billingDetails, + public TransactionTimePeriod|null $billingPeriod, + public array $items, + public TransactionDetails $details, + public array $payments, + public Checkout|null $checkout, + public \DateTimeInterface $createdAt, + public \DateTimeInterface $updatedAt, + public \DateTimeInterface|null $billedAt, + public Address|null $address, + public array $adjustments, + public TransactionAdjustmentsTotals|null $adjustmentsTotals, + public Business|null $business, + public Customer|null $customer, + public Discount|null $discount, + ) { + } + + public static function from(array $data): self + { + return new self( + id: $data['id'], + status: StatusTransaction::from($data['status']), + customerId: $data['customer_id'] ?? null, + addressId: $data['address_id'] ?? null, + businessId: $data['business_id'] ?? null, + customData: isset($data['custom_data']) ? new CustomData($data['custom_data']) : null, + currencyCode: CurrencyCode::from($data['currency_code']), + origin: TransactionOrigin::from($data['origin']), + subscriptionId: $data['subscription_id'] ?? null, + invoiceId: $data['invoice_id'] ?? null, + invoiceNumber: $data['invoice_number'] ?? null, + collectionMode: CollectionMode::from($data['collection_mode']), + discountId: $data['discount_id'] ?? null, + billingDetails: isset($data['billing_details']) ? BillingDetails::from($data['billing_details']) : null, + billingPeriod: isset($data['billing_period']) ? TransactionTimePeriod::from($data['billing_period']) : null, + items: array_map(fn (array $item): TransactionItem => TransactionItem::from($item), $data['items'] ?? []), + details: TransactionDetails::from($data['details']), + payments: array_map(fn (array $payment): TransactionPaymentAttempt => TransactionPaymentAttempt::from($payment), $data['payments'] ?? []), + checkout: isset($data['checkout']) ? new Checkout($data['checkout']['url'] ?? null) : null, + createdAt: DateTime::from($data['created_at']), + updatedAt: DateTime::from($data['updated_at']), + billedAt: isset($data['billed_at']) ? DateTime::from($data['billed_at']) : null, + address: isset($data['address']) ? Address::from($data['address']) : null, + adjustments: array_map(fn (array $adjustment): TransactionAdjustment => TransactionAdjustment::from($adjustment), $data['adjustments'] ?? []), + adjustmentsTotals: isset($data['adjustments_totals']) ? TransactionAdjustmentsTotals::from($data['adjustments_totals']) : null, + business: isset($data['business']) ? Business::from($data['business']) : null, + customer: isset($data['customer']) ? Customer::from($data['customer']) : null, + discount: isset($data['discount']) ? Discount::from($data['discount']) : null, + ); + } +} diff --git a/src/Environment.php b/src/Environment.php new file mode 100644 index 0000000..e44ef76 --- /dev/null +++ b/src/Environment.php @@ -0,0 +1,19 @@ + 'https://api.paddle.com', + self::SANDBOX => 'https://sandbox-api.paddle.com', + }; + } +} diff --git a/src/Exceptions/ApiError.php b/src/Exceptions/ApiError.php new file mode 100644 index 0000000..dd4fae5 --- /dev/null +++ b/src/Exceptions/ApiError.php @@ -0,0 +1,39 @@ + */ + public array $fieldErrors; + + final public function __construct( + public string $type, + public string $errorCode, + public string $detail, + public string $docsUrl, + FieldError ...$fieldErrors, + ) { + $this->fieldErrors = $fieldErrors; + + parent::__construct($this->detail); + } + + public static function fromErrorData(array $error): static + { + return new static( + $error['type'], + $error['code'], + $error['detail'], + $error['documentation_url'], + ...array_map( + fn (array $fieldError): FieldError => new FieldError($fieldError['field'], $fieldError['message']), + $error['errors'] ?? [], + ), + ); + } +} diff --git a/src/Exceptions/ApiError/AddressApiError.php b/src/Exceptions/ApiError/AddressApiError.php new file mode 100644 index 0000000..2e5e04a --- /dev/null +++ b/src/Exceptions/ApiError/AddressApiError.php @@ -0,0 +1,11 @@ +getMessage(), $e->getCode(), $e->getPrevious()); + } +} diff --git a/src/FiltersUndefined.php b/src/FiltersUndefined.php new file mode 100644 index 0000000..6f968b6 --- /dev/null +++ b/src/FiltersUndefined.php @@ -0,0 +1,16 @@ + $values + */ + public function filterUndefined(array $values): array + { + return array_filter($values, fn ($value): bool => ! is_a($value, Undefined::class)); + } +} diff --git a/src/HasParameters.php b/src/HasParameters.php new file mode 100644 index 0000000..a8d2d06 --- /dev/null +++ b/src/HasParameters.php @@ -0,0 +1,16 @@ + bar which would result in foo=bar in an API call. + * + * @return array + */ + public function getParameters(): array; +} diff --git a/src/Logger/Formatter.php b/src/Logger/Formatter.php new file mode 100644 index 0000000..6091cb4 --- /dev/null +++ b/src/Logger/Formatter.php @@ -0,0 +1,43 @@ +getMethod(), + $request->getUri()->__toString(), + $request->getProtocolVersion(), + $request->getHeaderLine('X-Transaction-ID') ?: '-', + ); + } + + public function formatResponse(ResponseInterface $response): string + { + return sprintf( + '%s %s %s -', + $response->getStatusCode(), + $response->getReasonPhrase(), + $response->getProtocolVersion(), + ); + } + + public function formatResponseForRequest(ResponseInterface $response, RequestInterface $request): string + { + return sprintf( + '%s %s %s %s', + $response->getStatusCode(), + $response->getReasonPhrase(), + $response->getProtocolVersion(), + $request->getHeaderLine('X-Transaction-ID') ?: '-', + ); + } +} diff --git a/src/Notifications/Events/AddressCreated.php b/src/Notifications/Events/AddressCreated.php new file mode 100644 index 0000000..53b1bd1 --- /dev/null +++ b/src/Notifications/Events/AddressCreated.php @@ -0,0 +1,21 @@ +hashes as $hashAlgorithm => $possibleHashes) { + $hash = match ($hashAlgorithm) { + self::HASH_ALGORITHM_1 => self::calculateHMAC("{$this->timestamp}:{$data}", $secret->key), + default => throw new \LogicException('Unknown hash algorithm ' . var_export($hashAlgorithm, true)), + }; + + foreach ($possibleHashes as $possibleHash) { + if (\hash_equals($hash, $possibleHash)) { + return true; + } + } + } + } + + return false; + } + + public static function calculateHMAC(string $data, string $key): string + { + return \hash_hmac('sha256', $data, $key); + } + + public static function parse(string $header): self + { + $components = [ + self::TIMESTAMP => 0, + 'hashes' => [], + ]; + + foreach (\explode(';', $header) as $part) { + if (\str_contains($part, '=')) { + [$key, $value] = \explode('=', $part, 2); + + match ($key) { + self::TIMESTAMP => $components[self::TIMESTAMP] = (int) $value, + self::HASH_ALGORITHM_1 => $components['hashes'][self::HASH_ALGORITHM_1][] = $value, + default => throw new \LogicException('Unknown key ' . var_export($key, true)), + }; + } + } + + return new self( + $components[self::TIMESTAMP], + $components['hashes'], + ); + } +} diff --git a/src/Notifications/Secret.php b/src/Notifications/Secret.php new file mode 100644 index 0000000..1193372 --- /dev/null +++ b/src/Notifications/Secret.php @@ -0,0 +1,14 @@ +getHeader(PaddleSignature::HEADER); + if ($signatureData === []) { + return false; + } + + $signature = PaddleSignature::parse($signatureData[0]); + + if ($this->maximumVariance > 0 && \time() > $signature->timestamp + $this->maximumVariance) { + return false; + } + + $request->getBody()->rewind(); + + return $signature->verify( + (string) $request->getBody(), + ...$secrets, + ); + } +} diff --git a/src/Options.php b/src/Options.php new file mode 100644 index 0000000..3cf9c48 --- /dev/null +++ b/src/Options.php @@ -0,0 +1,14 @@ +client->getRaw("/customers/{$customerId}/addresses", $listOperation), + ); + + return AddressCollection::from( + $parser->getData(), + new Paginator($this->client, $parser->getPagination(), AddressCollection::class), + ); + } + + /** + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function get(string $customerId, string $id): Address + { + $parser = new ResponseParser( + $this->client->getRaw("/customers/{$customerId}/addresses/{$id}"), + ); + + return Address::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\AddressApiError On an address specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function create(string $customerId, CreateOperation $createOperation): Address + { + $parser = new ResponseParser( + $this->client->postRaw("/customers/{$customerId}/addresses", $createOperation), + ); + + return Address::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\AddressApiError On an address specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function update(string $customerId, string $id, UpdateOperation $operation): Address + { + $parser = new ResponseParser( + $this->client->patchRaw("/customers/{$customerId}/addresses/{$id}", $operation), + ); + + return Address::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\AddressApiError On an address specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function archive(string $customerId, string $id): Address + { + return $this->update($customerId, $id, new UpdateOperation(status: Status::Archived)); + } +} diff --git a/src/Resources/Addresses/Operations/CreateOperation.php b/src/Resources/Addresses/Operations/CreateOperation.php new file mode 100644 index 0000000..fa961ad --- /dev/null +++ b/src/Resources/Addresses/Operations/CreateOperation.php @@ -0,0 +1,41 @@ +filterUndefined([ + 'country_code' => $this->countryCode, + 'description' => $this->description, + 'first_line' => $this->firstLine, + 'second_line' => $this->secondLine, + 'city' => $this->city, + 'postal_code' => $this->postalCode, + 'region' => $this->region, + 'custom_data' => $this->customData, + ]); + } +} diff --git a/src/Resources/Addresses/Operations/ListOperation.php b/src/Resources/Addresses/Operations/ListOperation.php new file mode 100644 index 0000000..4375bff --- /dev/null +++ b/src/Resources/Addresses/Operations/ListOperation.php @@ -0,0 +1,42 @@ +ids, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('ids', 'string', implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->statuses, fn ($value): bool => ! $value instanceof Status)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('statuses', Status::class, implode(', ', $invalid)); + } + } + + public function getParameters(): array + { + $enumStringify = fn ($enum) => $enum->value; + + return array_merge( + $this->pager?->getParameters() ?? [], + array_filter([ + 'id' => implode(',', $this->ids), + 'status' => implode(',', array_map($enumStringify, $this->statuses)), + 'search' => $this->search, + ]), + ); + } +} diff --git a/src/Resources/Addresses/Operations/UpdateOperation.php b/src/Resources/Addresses/Operations/UpdateOperation.php new file mode 100644 index 0000000..cdbb06b --- /dev/null +++ b/src/Resources/Addresses/Operations/UpdateOperation.php @@ -0,0 +1,44 @@ +filterUndefined([ + 'country_code' => $this->countryCode, + 'description' => $this->description, + 'first_line' => $this->firstLine, + 'second_line' => $this->secondLine, + 'city' => $this->city, + 'postal_code' => $this->postalCode, + 'region' => $this->region, + 'custom_data' => $this->customData, + 'status' => $this->status, + ]); + } +} diff --git a/src/Resources/Adjustments/AdjustmentsClient.php b/src/Resources/Adjustments/AdjustmentsClient.php new file mode 100644 index 0000000..f2440c5 --- /dev/null +++ b/src/Resources/Adjustments/AdjustmentsClient.php @@ -0,0 +1,60 @@ +client->getRaw('/adjustments', $listOperation), + ); + + return AdjustmentsAdjustmentCollection::from( + $parser->getData(), + new Paginator($this->client, $parser->getPagination(), AdjustmentsAdjustmentCollection::class), + ); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\AdjustmentApiError On an adjustment specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function create(CreateOperation $createOperation): Adjustment + { + $parser = new ResponseParser( + $this->client->postRaw('/adjustments', $createOperation), + ); + + return Adjustment::from($parser->getData()); + } +} diff --git a/src/Resources/Adjustments/Operations/CreateOperation.php b/src/Resources/Adjustments/Operations/CreateOperation.php new file mode 100644 index 0000000..a2196b4 --- /dev/null +++ b/src/Resources/Adjustments/Operations/CreateOperation.php @@ -0,0 +1,42 @@ + $items + */ + public function __construct( + public readonly Action $action, + public readonly array $items, + public readonly string $reason, + public readonly string $transactionId, + ) { + } + + public function jsonSerialize(): array + { + $items = []; + + foreach ($this->items as $item) { + $items[] = [ + 'item_id' => $item->itemId, + 'type' => $item->type->value, + 'amount' => $item->amount, + ]; + } + + return [ + 'action' => $this->action, + 'items' => $items, + 'reason' => $this->reason, + 'transaction_id' => $this->transactionId, + ]; + } +} diff --git a/src/Resources/Adjustments/Operations/ListOperation.php b/src/Resources/Adjustments/Operations/ListOperation.php new file mode 100644 index 0000000..d309304 --- /dev/null +++ b/src/Resources/Adjustments/Operations/ListOperation.php @@ -0,0 +1,70 @@ + $ids + * @param array $statuses + * @param array $customerIds + * @param array $transactionIds + * @param array $subscriptionIds + * + * @throws InvalidArgumentException On invalid array arguments + */ + public function __construct( + private readonly ?Pager $pager = null, + private readonly array $ids = [], + private readonly array $statuses = [], + private readonly array $customerIds = [], + private readonly array $transactionIds = [], + private readonly array $subscriptionIds = [], + private readonly ?Action $action = null, + ) { + if ($invalid = array_filter($this->ids, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('ids', 'string', implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->statuses, fn ($value): bool => ! $value instanceof StatusAdjustment)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('statuses', StatusAdjustment::class, implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->customerIds, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('customerIds', 'string', implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->transactionIds, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('transactionIds', 'string', implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->subscriptionIds, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('subscriptionIds', 'string', implode(', ', $invalid)); + } + } + + public function getParameters(): array + { + $enumStringify = fn ($enum) => $enum->value; + + return array_merge( + $this->pager?->getParameters() ?? [], + array_filter([ + 'id' => implode(',', $this->ids), + 'status' => implode(',', array_map($enumStringify, $this->statuses)), + 'customer_id' => implode(',', $this->customerIds), + 'transaction_id' => implode(',', $this->transactionIds), + 'subscription_id' => implode(',', $this->subscriptionIds), + 'action' => $this->action?->value, + ]), + ); + } +} diff --git a/src/Resources/Businesses/BusinessesClient.php b/src/Resources/Businesses/BusinessesClient.php new file mode 100644 index 0000000..22f4999 --- /dev/null +++ b/src/Resources/Businesses/BusinessesClient.php @@ -0,0 +1,99 @@ +client->getRaw("/customers/{$customerId}/businesses", $listOperation), + ); + + return BusinessCollection::from( + $parser->getData(), + new Paginator($this->client, $parser->getPagination(), BusinessCollection::class), + ); + } + + /** + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function get(string $customerId, string $id): Business + { + $parser = new ResponseParser( + $this->client->getRaw("/customers/{$customerId}/businesses/{$id}"), + ); + + return Business::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\BusinessApiError On an business specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function create(string $customerId, CreateOperation $createOperation): Business + { + $parser = new ResponseParser( + $this->client->postRaw("/customers/{$customerId}/businesses", $createOperation), + ); + + return Business::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\BusinessApiError On an business specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function update(string $customerId, string $id, UpdateOperation $operation): Business + { + $parser = new ResponseParser( + $this->client->patchRaw("/customers/{$customerId}/businesses/{$id}", $operation), + ); + + return Business::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\BusinessApiError On an business specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function archive(string $customerId, string $id): Business + { + return $this->update($customerId, $id, new UpdateOperation(status: Status::Archived)); + } +} diff --git a/src/Resources/Businesses/Operations/CreateOperation.php b/src/Resources/Businesses/Operations/CreateOperation.php new file mode 100644 index 0000000..aecabd8 --- /dev/null +++ b/src/Resources/Businesses/Operations/CreateOperation.php @@ -0,0 +1,38 @@ + $contacts + */ + public function __construct( + public readonly string $name, + public readonly string|null|Undefined $companyNumber = new Undefined(), + public readonly string|null|Undefined $taxIdentifier = new Undefined(), + public readonly array|Undefined $contacts = new Undefined(), + public readonly CustomData|null|Undefined $customData = new Undefined(), + ) { + } + + public function jsonSerialize(): array + { + return $this->filterUndefined([ + 'name' => $this->name, + 'company_number' => $this->companyNumber, + 'tax_identifier' => $this->taxIdentifier, + 'contacts' => $this->contacts, + 'custom_data' => $this->customData, + ]); + } +} diff --git a/src/Resources/Businesses/Operations/ListOperation.php b/src/Resources/Businesses/Operations/ListOperation.php new file mode 100644 index 0000000..b6bfc36 --- /dev/null +++ b/src/Resources/Businesses/Operations/ListOperation.php @@ -0,0 +1,48 @@ + $ids + * @param array $statuses + * + * @throws InvalidArgumentException On invalid array contents + */ + public function __construct( + private readonly ?Pager $pager = null, + private readonly array $ids = [], + private readonly array $statuses = [], + private readonly ?string $search = null, + ) { + if ($invalid = array_filter($this->ids, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('ids', 'string', implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->statuses, fn ($value): bool => ! $value instanceof Status)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('statuses', Status::class, implode(', ', $invalid)); + } + } + + public function getParameters(): array + { + $enumStringify = fn ($enum) => $enum->value; + + return array_merge( + $this->pager?->getParameters() ?? [], + array_filter([ + 'id' => implode(',', $this->ids), + 'status' => implode(',', array_map($enumStringify, $this->statuses)), + 'search' => $this->search, + ]), + ); + } +} diff --git a/src/Resources/Businesses/Operations/UpdateOperation.php b/src/Resources/Businesses/Operations/UpdateOperation.php new file mode 100644 index 0000000..46b95c9 --- /dev/null +++ b/src/Resources/Businesses/Operations/UpdateOperation.php @@ -0,0 +1,41 @@ + $contacts + */ + public function __construct( + public readonly string|Undefined $name = new Undefined(), + public readonly string|null|Undefined $companyNumber = new Undefined(), + public readonly string|null|Undefined $taxIdentifier = new Undefined(), + public readonly array|Undefined $contacts = new Undefined(), + public readonly CustomData|null|Undefined $customData = new Undefined(), + public readonly Status|Undefined $status = new Undefined(), + ) { + } + + public function jsonSerialize(): array + { + return $this->filterUndefined([ + 'name' => $this->name, + 'company_number' => $this->companyNumber, + 'tax_identifier' => $this->taxIdentifier, + 'contacts' => $this->contacts, + 'custom_data' => $this->customData, + 'status' => $this->status, + ]); + } +} diff --git a/src/Resources/Customers/CustomersClient.php b/src/Resources/Customers/CustomersClient.php new file mode 100644 index 0000000..3b7cabe --- /dev/null +++ b/src/Resources/Customers/CustomersClient.php @@ -0,0 +1,114 @@ +client->getRaw('/customers', $listOperation), + ); + + return CustomerCollection::from( + $parser->getData(), + new Paginator($this->client, $parser->getPagination(), CustomerCollection::class), + ); + } + + /** + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function get(string $id): Customer + { + $parser = new ResponseParser( + $this->client->getRaw("/customers/{$id}"), + ); + + return Customer::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\CustomerApiError On a customer specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function create(CreateOperation $createOperation): Customer + { + $parser = new ResponseParser( + $this->client->postRaw('/customers', $createOperation), + ); + + return Customer::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\CustomerApiError On a customer specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function update(string $id, UpdateOperation $operation): Customer + { + $parser = new ResponseParser( + $this->client->patchRaw("/customers/{$id}", $operation), + ); + + return Customer::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\CustomerApiError On a customer specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function archive(string $id): Customer + { + return $this->update($id, new UpdateOperation(status: Status::Archived)); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\CustomerApiError On a customer specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function creditBalances(string $id): CreditBalanceCollection + { + $parser = new ResponseParser( + $this->client->getRaw("/customers/{$id}/credit-balances"), + ); + + return CreditBalanceCollection::from($parser->getData()); + } +} diff --git a/src/Resources/Customers/Operations/CreateOperation.php b/src/Resources/Customers/Operations/CreateOperation.php new file mode 100644 index 0000000..47a4412 --- /dev/null +++ b/src/Resources/Customers/Operations/CreateOperation.php @@ -0,0 +1,32 @@ +filterUndefined([ + 'email' => $this->email, + 'name' => $this->name, + 'custom_data' => $this->customData, + 'locale' => $this->locale, + ]); + } +} diff --git a/src/Resources/Customers/Operations/ListOperation.php b/src/Resources/Customers/Operations/ListOperation.php new file mode 100644 index 0000000..a905291 --- /dev/null +++ b/src/Resources/Customers/Operations/ListOperation.php @@ -0,0 +1,48 @@ + $ids + * @param array $statuses + * + * @throws InvalidArgumentException On invalid array contents + */ + public function __construct( + private readonly ?Pager $pager = null, + private readonly array $ids = [], + private readonly array $statuses = [], + private readonly ?string $search = null, + ) { + if ($invalid = array_filter($this->ids, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('ids', 'string', implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->statuses, fn ($value): bool => ! $value instanceof Status)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('statuses', Status::class, implode(', ', $invalid)); + } + } + + public function getParameters(): array + { + $enumStringify = fn ($enum) => $enum->value; + + return array_merge( + $this->pager?->getParameters() ?? [], + array_filter([ + 'id' => implode(',', $this->ids), + 'status' => implode(',', array_map($enumStringify, $this->statuses)), + 'search' => $this->search, + ]), + ); + } +} diff --git a/src/Resources/Customers/Operations/UpdateOperation.php b/src/Resources/Customers/Operations/UpdateOperation.php new file mode 100644 index 0000000..4143913 --- /dev/null +++ b/src/Resources/Customers/Operations/UpdateOperation.php @@ -0,0 +1,35 @@ +filterUndefined([ + 'email' => $this->email, + 'name' => $this->name, + 'custom_data' => $this->customData, + 'locale' => $this->locale, + 'status' => $this->status, + ]); + } +} diff --git a/src/Resources/Discounts/DiscountsClient.php b/src/Resources/Discounts/DiscountsClient.php new file mode 100644 index 0000000..cf8ac4a --- /dev/null +++ b/src/Resources/Discounts/DiscountsClient.php @@ -0,0 +1,99 @@ +client->getRaw('/discounts', $listOperation), + ); + + return DiscountCollection::from( + $parser->getData(), + new Paginator($this->client, $parser->getPagination(), DiscountCollection::class), + ); + } + + /** + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function get(string $id): Discount + { + $parser = new ResponseParser( + $this->client->getRaw("/discounts/{$id}"), + ); + + return Discount::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\DiscountApiError On a discount specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function create(CreateOperation $createOperation): Discount + { + $parser = new ResponseParser( + $this->client->postRaw('/discounts', $createOperation), + ); + + return Discount::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\DiscountApiError On a discount specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function update(string $id, UpdateOperation $operation): Discount + { + $parser = new ResponseParser( + $this->client->patchRaw("/discounts/{$id}", $operation), + ); + + return Discount::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\DiscountApiError On a discount specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function archive(string $id): Discount + { + return $this->update($id, new UpdateOperation(status: DiscountStatus::Archived)); + } +} diff --git a/src/Resources/Discounts/Operations/CreateOperation.php b/src/Resources/Discounts/Operations/CreateOperation.php new file mode 100644 index 0000000..80a1e94 --- /dev/null +++ b/src/Resources/Discounts/Operations/CreateOperation.php @@ -0,0 +1,50 @@ +|null $restrictTo + */ + public function __construct( + public readonly string $amount, + public readonly string $description, + public readonly DiscountType $type, + public readonly bool $enabledForCheckout, + public readonly bool $recur, + public readonly CurrencyCode $currencyCode, + public readonly string|null|Undefined $code = new Undefined(), + public readonly int|null|Undefined $maximumRecurringIntervals = new Undefined(), + public readonly int|null|Undefined $usageLimit = new Undefined(), + public readonly array|null|Undefined $restrictTo = new Undefined(), + public readonly string|null|Undefined $expiresAt = new Undefined(), + ) { + } + + public function jsonSerialize(): array + { + return $this->filterUndefined([ + 'amount' => $this->amount, + 'description' => $this->description, + 'type' => $this->type, + 'enabled_for_checkout' => $this->enabledForCheckout, + 'code' => $this->code, + 'currency_code' => $this->currencyCode, + 'recur' => $this->recur, + 'maximum_recurring_intervals' => $this->maximumRecurringIntervals, + 'usage_limit' => $this->usageLimit, + 'restrict_to' => $this->restrictTo, + 'expires_at' => $this->expiresAt, + ]); + } +} diff --git a/src/Resources/Discounts/Operations/ListOperation.php b/src/Resources/Discounts/Operations/ListOperation.php new file mode 100644 index 0000000..0156e67 --- /dev/null +++ b/src/Resources/Discounts/Operations/ListOperation.php @@ -0,0 +1,53 @@ + $ids + * @param array $statuses + * @param array $codes + * + * @throws InvalidArgumentException On invalid array contents + */ + public function __construct( + private readonly ?Pager $pager = null, + private readonly array $ids = [], + private readonly array $statuses = [], + private readonly array $codes = [], + ) { + if ($invalid = array_filter($this->ids, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('ids', 'string', implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->statuses, fn ($value): bool => ! $value instanceof Status)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('statuses', Status::class, implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->codes, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('codes', 'string', implode(', ', $invalid)); + } + } + + public function getParameters(): array + { + $enumStringify = fn ($enum) => $enum->value; + + return array_merge( + $this->pager?->getParameters() ?? [], + array_filter([ + 'id' => implode(',', $this->ids), + 'status' => implode(',', array_map($enumStringify, $this->statuses)), + 'code' => implode(',', $this->codes), + ]), + ); + } +} diff --git a/src/Resources/Discounts/Operations/UpdateOperation.php b/src/Resources/Discounts/Operations/UpdateOperation.php new file mode 100644 index 0000000..c219316 --- /dev/null +++ b/src/Resources/Discounts/Operations/UpdateOperation.php @@ -0,0 +1,53 @@ +|null $restrictTo + */ + public function __construct( + public readonly string|Undefined $amount = new Undefined(), + public readonly string|Undefined $description = new Undefined(), + public readonly DiscountType|Undefined $type = new Undefined(), + public readonly bool|Undefined $enabledForCheckout = new Undefined(), + public readonly bool|Undefined $recur = new Undefined(), + public readonly CurrencyCode|Undefined $currencyCode = new Undefined(), + public readonly string|null|Undefined $code = new Undefined(), + public readonly int|null|Undefined $maximumRecurringIntervals = new Undefined(), + public readonly int|null|Undefined $usageLimit = new Undefined(), + public readonly array|null|Undefined $restrictTo = new Undefined(), + public readonly string|null|Undefined $expiresAt = new Undefined(), + public readonly DiscountStatus|Undefined $status = new Undefined(), + ) { + } + + public function jsonSerialize(): array + { + return $this->filterUndefined([ + 'amount' => $this->amount, + 'description' => $this->description, + 'type' => $this->type, + 'enabled_for_checkout' => $this->enabledForCheckout, + 'code' => $this->code, + 'currency_code' => $this->currencyCode, + 'recur' => $this->recur, + 'maximum_recurring_intervals' => $this->maximumRecurringIntervals, + 'usage_limit' => $this->usageLimit, + 'restrict_to' => $this->restrictTo, + 'expires_at' => $this->expiresAt, + 'status' => $this->status, + ]); + } +} diff --git a/src/Resources/EventTypes/EventTypesClient.php b/src/Resources/EventTypes/EventTypesClient.php new file mode 100644 index 0000000..cbdd03b --- /dev/null +++ b/src/Resources/EventTypes/EventTypesClient.php @@ -0,0 +1,39 @@ +client->getRaw('/event-types'), + ); + + return EventTypeCollection::from($parser->getData()); + } +} diff --git a/src/Resources/Events/EventsClient.php b/src/Resources/Events/EventsClient.php new file mode 100644 index 0000000..f235576 --- /dev/null +++ b/src/Resources/Events/EventsClient.php @@ -0,0 +1,44 @@ +client->getRaw('/events', $listOperation), + ); + + return EventCollection::from( + $parser->getData(), + new Paginator($this->client, $parser->getPagination(), EventCollection::class), + ); + } +} diff --git a/src/Resources/Events/Operations/ListOperation.php b/src/Resources/Events/Operations/ListOperation.php new file mode 100644 index 0000000..a8023f4 --- /dev/null +++ b/src/Resources/Events/Operations/ListOperation.php @@ -0,0 +1,20 @@ +pager?->getParameters() ?? []; + } +} diff --git a/src/Resources/NotificationLogs/NotificationLogsClient.php b/src/Resources/NotificationLogs/NotificationLogsClient.php new file mode 100644 index 0000000..dd9251f --- /dev/null +++ b/src/Resources/NotificationLogs/NotificationLogsClient.php @@ -0,0 +1,37 @@ +client->getRaw("/notifications/{$notificationId}/logs", $listOperation), + ); + + return NotificationLogCollection::from( + $parser->getData(), + new Paginator($this->client, $parser->getPagination(), NotificationLogCollection::class), + ); + } +} diff --git a/src/Resources/NotificationLogs/Operations/ListOperation.php b/src/Resources/NotificationLogs/Operations/ListOperation.php new file mode 100644 index 0000000..817948a --- /dev/null +++ b/src/Resources/NotificationLogs/Operations/ListOperation.php @@ -0,0 +1,20 @@ +pager?->getParameters() ?? []; + } +} diff --git a/src/Resources/NotificationSettings/NotificationSettingsClient.php b/src/Resources/NotificationSettings/NotificationSettingsClient.php new file mode 100644 index 0000000..86d466c --- /dev/null +++ b/src/Resources/NotificationSettings/NotificationSettingsClient.php @@ -0,0 +1,92 @@ +client->getRaw('notification-settings'), + ); + + return NotificationSettingCollection::from( + $parser->getData(), + ); + } + + /** + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function get(string $id): NotificationSetting + { + $parser = new ResponseParser( + $this->client->getRaw("notification-settings/{$id}"), + ); + + return NotificationSetting::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function create(CreateOperation $createOperation): NotificationSetting + { + $parser = new ResponseParser( + $this->client->postRaw('notification-settings', $createOperation), + ); + + return NotificationSetting::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function update(string $id, UpdateOperation $operation): NotificationSetting + { + $parser = new ResponseParser( + $this->client->patchRaw("notification-settings/{$id}", $operation), + ); + + return NotificationSetting::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function delete(string $id): void + { + new ResponseParser($this->client->deleteRaw("notification-settings/{$id}")); + } +} diff --git a/src/Resources/NotificationSettings/Operations/CreateOperation.php b/src/Resources/NotificationSettings/Operations/CreateOperation.php new file mode 100644 index 0000000..79298c7 --- /dev/null +++ b/src/Resources/NotificationSettings/Operations/CreateOperation.php @@ -0,0 +1,40 @@ +filterUndefined([ + 'description' => $this->description, + 'destination' => $this->destination, + 'subscribed_events' => $this->subscribedEvents, + 'type' => $this->type, + 'include_sensitive_fields' => $this->includeSensitiveFields, + 'api_version' => $this->apiVersion, + ]); + } +} diff --git a/src/Resources/NotificationSettings/Operations/UpdateOperation.php b/src/Resources/NotificationSettings/Operations/UpdateOperation.php new file mode 100644 index 0000000..3fdd55a --- /dev/null +++ b/src/Resources/NotificationSettings/Operations/UpdateOperation.php @@ -0,0 +1,39 @@ +filterUndefined([ + 'description' => $this->description, + 'destination' => $this->destination, + 'active' => $this->active, + 'api_version' => $this->apiVersion, + 'include_sensitive_fields' => $this->includeSensitiveFields, + 'subscribed_events' => $this->subscribedEvents, + ]); + } +} diff --git a/src/Resources/Notifications/NotificationsClient.php b/src/Resources/Notifications/NotificationsClient.php new file mode 100644 index 0000000..8ce2181 --- /dev/null +++ b/src/Resources/Notifications/NotificationsClient.php @@ -0,0 +1,73 @@ +client->getRaw('/notifications', $listOperation), + ); + + return NotificationCollection::from( + $parser->getData(), + new Paginator($this->client, $parser->getPagination(), NotificationCollection::class), + ); + } + + /** + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function get(string $id): Notification + { + $parser = new ResponseParser( + $this->client->getRaw("/notifications/{$id}"), + ); + + return Notification::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function replay(string $id): string + { + $parser = new ResponseParser( + $this->client->postRaw("/notifications/{$id}"), + ); + + $data = $parser->getData(); + + return $data['notification_id'] ?? ''; + } +} diff --git a/src/Resources/Notifications/Operations/ListOperation.php b/src/Resources/Notifications/Operations/ListOperation.php new file mode 100644 index 0000000..f478bde --- /dev/null +++ b/src/Resources/Notifications/Operations/ListOperation.php @@ -0,0 +1,45 @@ + $notificationSettingId + * @param array $status + */ + public function __construct( + private readonly ?Pager $pager = null, + private readonly array $notificationSettingId = [], + private readonly string|null $search = null, + private readonly array $status = [], + private readonly string|null $filter = null, + private readonly \DateTimeInterface|null $to = null, + private readonly \DateTimeInterface|null $from = null, + ) { + } + + public function getParameters(): array + { + $enumStringify = fn ($enum) => $enum->value; + + return array_merge( + $this->pager?->getParameters() ?? [], + array_filter([ + 'notification_setting_id' => implode(',', $this->notificationSettingId), + 'search' => $this->search, + 'status' => implode(',', array_map($enumStringify, $this->status)), + 'filter' => $this->filter, + 'to' => isset($this->to) ? DateTime::from($this->to)?->format() : null, + 'from' => isset($this->from) ? DateTime::from($this->from)?->format() : null, + ]), + ); + } +} diff --git a/src/Resources/Prices/Operations/CreateOperation.php b/src/Resources/Prices/Operations/CreateOperation.php new file mode 100644 index 0000000..51be095 --- /dev/null +++ b/src/Resources/Prices/Operations/CreateOperation.php @@ -0,0 +1,55 @@ +filterUndefined([ + 'description' => $this->description, + 'product_id' => $this->productId, + 'unit_price' => $this->unitPrice, + 'name' => $this->name, + 'type' => $this->type, + 'unit_price_overrides' => $this->unitPriceOverrides, + 'trial_period' => $this->trialPeriod, + 'billing_cycle' => $this->billingCycle, + 'custom_data' => $this->customData, + 'tax_mode' => $this->taxMode, + 'quantity' => $this->quantity, + ]); + } +} diff --git a/src/Resources/Prices/Operations/List/Includes.php b/src/Resources/Prices/Operations/List/Includes.php new file mode 100644 index 0000000..09666e6 --- /dev/null +++ b/src/Resources/Prices/Operations/List/Includes.php @@ -0,0 +1,10 @@ + $includes + * @param array $ids + * @param array $types + * @param array $productIds + * @param array $statuses + * + * @throws InvalidArgumentException If includes, ids, statuses or taxCategories contain the incorrect type + */ + public function __construct( + private readonly ?Pager $pager = null, + private readonly array $includes = [], + private readonly array $ids = [], + private readonly array $types = [], + private readonly array $productIds = [], + private readonly array $statuses = [], + private readonly ?bool $recurring = null, + ) { + if ($invalid = array_filter($this->includes, fn ($value): bool => ! $value instanceof Includes)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('includes', Includes::class, implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->ids, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('ids', 'string', implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->types, fn ($value): bool => ! $value instanceof CatalogType)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('types', CatalogType::class, implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->productIds, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('productIds', 'string', implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->statuses, fn ($value): bool => ! $value instanceof Status)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('statuses', Status::class, implode(', ', $invalid)); + } + } + + public function getParameters(): array + { + $enumStringify = fn ($enum) => $enum->value; + + return array_merge( + $this->pager?->getParameters() ?? [], + array_filter([ + 'include' => implode(',', array_map($enumStringify, $this->includes)), + 'id' => implode(',', $this->ids), + 'type' => implode(',', array_map($enumStringify, $this->types)), + 'product_id' => implode(',', $this->productIds), + 'status' => implode(',', array_map($enumStringify, $this->statuses)), + 'recurring' => isset($this->recurring) ? ($this->recurring ? 'true' : 'false') : null, + ]), + ); + } +} diff --git a/src/Resources/Prices/Operations/UpdateOperation.php b/src/Resources/Prices/Operations/UpdateOperation.php new file mode 100644 index 0000000..3ba94a6 --- /dev/null +++ b/src/Resources/Prices/Operations/UpdateOperation.php @@ -0,0 +1,56 @@ +filterUndefined([ + 'description' => $this->description, + 'name' => $this->name, + 'type' => $this->type, + 'billing_cycle' => $this->billingCycle, + 'trial_period' => $this->trialPeriod, + 'tax_mode' => $this->taxMode, + 'unit_price' => $this->unitPrice, + 'unit_price_overrides' => $this->unitPriceOverrides, + 'quantity' => $this->quantity, + 'status' => $this->status, + 'custom_data' => $this->customData, + ]); + } +} diff --git a/src/Resources/Prices/PricesClient.php b/src/Resources/Prices/PricesClient.php new file mode 100644 index 0000000..e77b952 --- /dev/null +++ b/src/Resources/Prices/PricesClient.php @@ -0,0 +1,111 @@ +client->getRaw('/prices', $listOperation), + ); + + return PriceWithIncludesCollection::from( + $parser->getData(), + new Paginator($this->client, $parser->getPagination(), PriceWithIncludesCollection::class), + ); + } + + /** + * @param array $includes + * + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function get(string $id, array $includes = []): PriceWithIncludes + { + if ($invalid = array_filter($includes, fn ($value): bool => ! $value instanceof Includes)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('includes', Includes::class, implode(', ', $invalid)); + } + + $params = $includes === [] + ? [] + : ['include' => implode(',', array_map(fn ($enum) => $enum->value, $includes))]; + + $parser = new ResponseParser( + $this->client->getRaw("/prices/{$id}", $params), + ); + + return PriceWithIncludes::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\PriceApiError On a price specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function create(CreateOperation $createOperation): PriceWithIncludes + { + $parser = new ResponseParser( + $this->client->postRaw('/prices', $createOperation), + ); + + return PriceWithIncludes::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\PriceApiError On a price specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function update(string $id, UpdateOperation $operation): PriceWithIncludes + { + $parser = new ResponseParser( + $this->client->patchRaw("/prices/{$id}", $operation), + ); + + return PriceWithIncludes::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\PriceApiError On a price specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function archive(string $id): PriceWithIncludes + { + return $this->update($id, new UpdateOperation(status: Status::Archived)); + } +} diff --git a/src/Resources/PricingPreviews/Operations/PreviewPricesOperation.php b/src/Resources/PricingPreviews/Operations/PreviewPricesOperation.php new file mode 100644 index 0000000..a33762a --- /dev/null +++ b/src/Resources/PricingPreviews/Operations/PreviewPricesOperation.php @@ -0,0 +1,45 @@ +filterUndefined([ + 'items' => $this->items, + 'customer_id' => $this->customerId, + 'address_id' => $this->addressId, + 'business_id' => $this->businessId, + 'currency_code' => $this->currencyCode, + 'discount_id' => $this->discountId, + 'address' => $this->address, + 'customer_ip_address' => $this->customerIpAddress, + ]); + } +} diff --git a/src/Resources/PricingPreviews/PricingPreviewsClient.php b/src/Resources/PricingPreviews/PricingPreviewsClient.php new file mode 100644 index 0000000..0a75bba --- /dev/null +++ b/src/Resources/PricingPreviews/PricingPreviewsClient.php @@ -0,0 +1,40 @@ +client->postRaw('/pricing-preview', $operation), + ); + + return PricePreview::from($parser->getData()); + } +} diff --git a/src/Resources/Products/Operations/CreateOperation.php b/src/Resources/Products/Operations/CreateOperation.php new file mode 100644 index 0000000..2aa138c --- /dev/null +++ b/src/Resources/Products/Operations/CreateOperation.php @@ -0,0 +1,38 @@ +filterUndefined([ + 'name' => $this->name, + 'tax_category' => $this->taxCategory, + 'type' => $this->type, + 'description' => $this->description, + 'image_url' => $this->imageUrl, + 'custom_data' => $this->customData, + ]); + } +} diff --git a/src/Resources/Products/Operations/List/Includes.php b/src/Resources/Products/Operations/List/Includes.php new file mode 100644 index 0000000..d86137a --- /dev/null +++ b/src/Resources/Products/Operations/List/Includes.php @@ -0,0 +1,10 @@ +includes, fn ($value): bool => ! $value instanceof Includes)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('includes', Includes::class, implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->ids, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('ids', 'string', implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->types, fn ($value): bool => ! $value instanceof CatalogType)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('types', CatalogType::class, implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->statuses, fn ($value): bool => ! $value instanceof Status)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('statuses', Status::class, implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->taxCategories, fn ($value): bool => ! $value instanceof TaxCategory)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('taxCategories', Status::class, implode(', ', $invalid)); + } + } + + public function getParameters(): array + { + $enumStringify = fn ($enum) => $enum->value; + + return array_merge( + $this->pager?->getParameters() ?? [], + array_filter([ + 'include' => implode(',', array_map($enumStringify, $this->includes)), + 'id' => implode(',', $this->ids), + 'type' => implode(',', array_map($enumStringify, $this->types)), + 'status' => implode(',', array_map($enumStringify, $this->statuses)), + 'tax_category' => implode(',', array_map($enumStringify, $this->taxCategories)), + ]), + ); + } +} diff --git a/src/Resources/Products/Operations/UpdateOperation.php b/src/Resources/Products/Operations/UpdateOperation.php new file mode 100644 index 0000000..3b3ae94 --- /dev/null +++ b/src/Resources/Products/Operations/UpdateOperation.php @@ -0,0 +1,41 @@ +filterUndefined([ + 'name' => $this->name, + 'description' => $this->description, + 'type' => $this->type, + 'tax_category' => $this->taxCategory, + 'image_url' => $this->imageUrl, + 'custom_data' => $this->customData, + 'status' => $this->status, + ]); + } +} diff --git a/src/Resources/Products/ProductsClient.php b/src/Resources/Products/ProductsClient.php new file mode 100644 index 0000000..b118a90 --- /dev/null +++ b/src/Resources/Products/ProductsClient.php @@ -0,0 +1,111 @@ +client->getRaw('/products', $listOperation), + ); + + return ProductWithIncludesCollection::from( + $parser->getData(), + new Paginator($this->client, $parser->getPagination(), ProductWithIncludesCollection::class), + ); + } + + /** + * @param array $includes + * + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function get(string $id, array $includes = []): ProductWithIncludes + { + if ($invalid = array_filter($includes, fn ($value): bool => ! $value instanceof Includes)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('includes', Includes::class, implode(', ', $invalid)); + } + + $params = $includes === [] + ? [] + : ['include' => implode(',', array_map(fn ($enum) => $enum->value, $includes))]; + + $parser = new ResponseParser( + $this->client->getRaw("/products/{$id}", $params), + ); + + return ProductWithIncludes::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\ProductApiError On a product specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function create(CreateOperation $createOperation): ProductWithIncludes + { + $parser = new ResponseParser( + $this->client->postRaw('/products', $createOperation), + ); + + return ProductWithIncludes::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\ProductApiError On a product specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function update(string $id, UpdateOperation $operation): ProductWithIncludes + { + $parser = new ResponseParser( + $this->client->patchRaw("/products/{$id}", $operation), + ); + + return ProductWithIncludes::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\ProductApiError On a product specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function archive(string $id): ProductWithIncludes + { + return $this->update($id, new UpdateOperation(status: Status::Archived)); + } +} diff --git a/src/Resources/Reports/Operations/CreateOperation.php b/src/Resources/Reports/Operations/CreateOperation.php new file mode 100644 index 0000000..f0ce343 --- /dev/null +++ b/src/Resources/Reports/Operations/CreateOperation.php @@ -0,0 +1,37 @@ + $filters + * + * @throws InvalidArgumentException + */ + public function __construct( + public readonly ReportType $type, + public readonly array $filters = [], + ) { + if ($invalid = array_filter($this->filters, fn ($value): bool => ! $value instanceof ReportFilters)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('filters', ReportFilters::class, implode(', ', $invalid)); + } + } + + public function jsonSerialize(): array + { + return $this->filterUndefined(array_filter([ + 'type' => $this->type, + 'filters' => $this->filters, + ])); + } +} diff --git a/src/Resources/Reports/Operations/ListOperation.php b/src/Resources/Reports/Operations/ListOperation.php new file mode 100644 index 0000000..656afa0 --- /dev/null +++ b/src/Resources/Reports/Operations/ListOperation.php @@ -0,0 +1,39 @@ + $statuses + * + * @throws InvalidArgumentException + */ + public function __construct( + private readonly ?Pager $pager = null, + private readonly array $statuses = [], + ) { + if ($invalid = array_filter($this->statuses, fn ($value): bool => ! $value instanceof ReportStatus)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('statuses', ReportStatus::class, implode(', ', $invalid)); + } + } + + public function getParameters(): array + { + $enumStringify = fn ($enum) => $enum->value; + + return array_merge( + $this->pager?->getParameters() ?? [], + array_filter([ + 'status' => implode(',', array_map($enumStringify, $this->statuses)), + ]), + ); + } +} diff --git a/src/Resources/Reports/ReportsClient.php b/src/Resources/Reports/ReportsClient.php new file mode 100644 index 0000000..41b0448 --- /dev/null +++ b/src/Resources/Reports/ReportsClient.php @@ -0,0 +1,87 @@ +client->getRaw('/reports', $listOperation), + ); + + return ReportCollection::from( + $parser->getData(), + new Paginator($this->client, $parser->getPagination(), ReportCollection::class), + ); + } + + /** + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function get(string $id): Report + { + $parser = new ResponseParser( + $this->client->getRaw("/reports/{$id}"), + ); + + return Report::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function getReportCsv(string $id): ReportCSV + { + $parser = new ResponseParser( + $this->client->getRaw("/reports/{$id}/download-url"), + ); + + return ReportCSV::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\AddressApiError On an address specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function create(CreateOperation $createOperation): Report + { + $parser = new ResponseParser( + $this->client->postRaw('/reports', $createOperation), + ); + + return Report::from($parser->getData()); + } +} diff --git a/src/Resources/Shared/Operations/List/Comparator.php b/src/Resources/Shared/Operations/List/Comparator.php new file mode 100644 index 0000000..9e11d7b --- /dev/null +++ b/src/Resources/Shared/Operations/List/Comparator.php @@ -0,0 +1,13 @@ +comparator) ? sprintf('[%s]', $this->comparator->value) : ''; + } + + public function formatted(): string + { + return DateTime::from($this->date)?->format() ?? ''; + } +} diff --git a/src/Resources/Shared/Operations/List/OrderBy.php b/src/Resources/Shared/Operations/List/OrderBy.php new file mode 100644 index 0000000..4fc971e --- /dev/null +++ b/src/Resources/Shared/Operations/List/OrderBy.php @@ -0,0 +1,27 @@ +field, $this->direction); + } +} diff --git a/src/Resources/Shared/Operations/List/Pager.php b/src/Resources/Shared/Operations/List/Pager.php new file mode 100644 index 0000000..dc373be --- /dev/null +++ b/src/Resources/Shared/Operations/List/Pager.php @@ -0,0 +1,24 @@ +orderBy ??= OrderBy::idAscending(); + } + + public function getParameters(): array + { + return array_filter([ + 'after' => $this->after, + 'order_by' => (string) $this->orderBy, + 'per_page' => $this->perPage, + ]); + } +} diff --git a/src/Resources/Subscriptions/Operations/CancelOperation.php b/src/Resources/Subscriptions/Operations/CancelOperation.php new file mode 100644 index 0000000..33301ed --- /dev/null +++ b/src/Resources/Subscriptions/Operations/CancelOperation.php @@ -0,0 +1,22 @@ + $this->effectiveFrom, + ]; + } +} diff --git a/src/Resources/Subscriptions/Operations/CreateOneTimeChargeOperation.php b/src/Resources/Subscriptions/Operations/CreateOneTimeChargeOperation.php new file mode 100644 index 0000000..3792b7e --- /dev/null +++ b/src/Resources/Subscriptions/Operations/CreateOneTimeChargeOperation.php @@ -0,0 +1,36 @@ + $items + */ + public function __construct( + public readonly SubscriptionEffectiveFrom $effectiveFrom, + public readonly array $items, + public readonly SubscriptionOnPaymentFailure|Undefined $onPaymentFailure = new Undefined(), + ) { + } + + public function jsonSerialize(): array + { + return $this->filterUndefined([ + 'effective_from' => $this->effectiveFrom, + 'items' => $this->items, + 'on_payment_failure' => $this->onPaymentFailure, + ]); + } +} diff --git a/src/Resources/Subscriptions/Operations/Get/Includes.php b/src/Resources/Subscriptions/Operations/Get/Includes.php new file mode 100644 index 0000000..dbdaac6 --- /dev/null +++ b/src/Resources/Subscriptions/Operations/Get/Includes.php @@ -0,0 +1,11 @@ + $addressIds + * @param array $customerIds + * @param array $ids + * @param array $priceIds + * @param array $scheduledChangeActions + * @param array $statuses + */ + public function __construct( + private readonly ?Pager $pager = null, + private readonly array $addressIds = [], + private readonly ?CollectionMode $collectionMode = null, + private readonly array $customerIds = [], + private readonly array $ids = [], + private readonly array $priceIds = [], + private readonly array $scheduledChangeActions = [], + private readonly array $statuses = [], + ) { + if ($invalid = array_filter($this->addressIds, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('addressIds', 'string', implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->customerIds, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('customerIds', 'string', implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->ids, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('ids', 'string', implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->priceIds, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('priceIds', 'string', implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->scheduledChangeActions, fn ($value): bool => ! $value instanceof SubscriptionScheduledChangeAction)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('scheduledChangeActions', SubscriptionScheduledChangeAction::class, implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->statuses, fn ($value): bool => ! $value instanceof SubscriptionStatus)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('statuses', SubscriptionStatus::class, implode(', ', $invalid)); + } + } + + public function getParameters(): array + { + $enumStringify = fn ($enum) => $enum->value; + + return array_merge( + $this->pager?->getParameters() ?? [], + array_filter([ + 'address_id' => implode(',', $this->addressIds), + 'collection_mode' => $this->collectionMode?->value, + 'customer_id' => implode(',', $this->customerIds), + 'id' => implode(',', $this->ids), + 'price_id' => implode(',', $this->priceIds), + 'scheduled_change_action' => implode(',', array_map($enumStringify, $this->scheduledChangeActions)), + 'status' => implode(',', array_map($enumStringify, $this->statuses)), + ]), + ); + } +} diff --git a/src/Resources/Subscriptions/Operations/PauseOperation.php b/src/Resources/Subscriptions/Operations/PauseOperation.php new file mode 100644 index 0000000..ba2cef5 --- /dev/null +++ b/src/Resources/Subscriptions/Operations/PauseOperation.php @@ -0,0 +1,25 @@ + $this->effectiveFrom, + 'resume_at' => isset($this->resumeAt) ? DateTime::from($this->resumeAt)?->format() : null, + ]; + } +} diff --git a/src/Resources/Subscriptions/Operations/PreviewOneTimeChargeOperation.php b/src/Resources/Subscriptions/Operations/PreviewOneTimeChargeOperation.php new file mode 100644 index 0000000..b5545de --- /dev/null +++ b/src/Resources/Subscriptions/Operations/PreviewOneTimeChargeOperation.php @@ -0,0 +1,36 @@ + $items + */ + public function __construct( + public readonly SubscriptionEffectiveFrom $effectiveFrom, + public readonly array $items, + public readonly SubscriptionOnPaymentFailure|Undefined $onPaymentFailure = new Undefined(), + ) { + } + + public function jsonSerialize(): array + { + return $this->filterUndefined([ + 'effective_from' => $this->effectiveFrom, + 'items' => $this->items, + 'on_payment_failure' => $this->onPaymentFailure, + ]); + } +} diff --git a/src/Resources/Subscriptions/Operations/PreviewUpdateOperation.php b/src/Resources/Subscriptions/Operations/PreviewUpdateOperation.php new file mode 100644 index 0000000..a08a685 --- /dev/null +++ b/src/Resources/Subscriptions/Operations/PreviewUpdateOperation.php @@ -0,0 +1,61 @@ + $items + */ + public function __construct( + public readonly string|Undefined $customerId = new Undefined(), + public readonly string|Undefined $addressId = new Undefined(), + public readonly string|null|Undefined $businessId = new Undefined(), + public readonly CurrencyCode|Undefined $currencyCode = new Undefined(), + public readonly \DateTimeInterface|Undefined $nextBilledAt = new Undefined(), + public readonly SubscriptionDiscount|null|Undefined $discount = new Undefined(), + public readonly CollectionMode|Undefined $collectionMode = new Undefined(), + public readonly BillingDetails|null|Undefined $billingDetails = new Undefined(), + public readonly null|Undefined $scheduledChange = new Undefined(), + public readonly array|Undefined $items = new Undefined(), + public readonly CustomData|null|Undefined $customData = new Undefined(), + public readonly SubscriptionProrationBillingMode|Undefined $prorationBillingMode = new Undefined(), + public readonly SubscriptionOnPaymentFailure|Undefined $onPaymentFailure = new Undefined(), + ) { + } + + public function jsonSerialize(): array + { + return $this->filterUndefined([ + 'customer_id' => $this->customerId, + 'address_id' => $this->addressId, + 'business_id' => $this->businessId, + 'currency_code' => $this->currencyCode, + 'next_billed_at' => is_a($this->nextBilledAt, \DateTimeInterface::class) ? DateTime::from($this->nextBilledAt)?->format() : $this->nextBilledAt, + 'discount' => $this->discount, + 'collection_mode' => $this->collectionMode, + 'billing_details' => $this->billingDetails, + 'scheduled_change' => $this->scheduledChange, + 'items' => $this->items, + 'custom_data' => $this->customData, + 'proration_billing_mode' => $this->prorationBillingMode, + 'on_payment_failure' => $this->onPaymentFailure, + ]); + } +} diff --git a/src/Resources/Subscriptions/Operations/ResumeOperation.php b/src/Resources/Subscriptions/Operations/ResumeOperation.php new file mode 100644 index 0000000..c31062a --- /dev/null +++ b/src/Resources/Subscriptions/Operations/ResumeOperation.php @@ -0,0 +1,25 @@ + $this->effectiveFrom instanceof \DateTimeInterface + ? DateTime::from($this->effectiveFrom)?->format() + : $this->effectiveFrom, + ]; + } +} diff --git a/src/Resources/Subscriptions/Operations/Update/SubscriptionDiscount.php b/src/Resources/Subscriptions/Operations/Update/SubscriptionDiscount.php new file mode 100644 index 0000000..04b6640 --- /dev/null +++ b/src/Resources/Subscriptions/Operations/Update/SubscriptionDiscount.php @@ -0,0 +1,23 @@ + $items + */ + public function __construct( + public readonly string|Undefined $customerId = new Undefined(), + public readonly string|Undefined $addressId = new Undefined(), + public readonly string|null|Undefined $businessId = new Undefined(), + public readonly CurrencyCode|Undefined $currencyCode = new Undefined(), + public readonly \DateTimeInterface|Undefined $nextBilledAt = new Undefined(), + public readonly SubscriptionDiscount|null|Undefined $discount = new Undefined(), + public readonly CollectionMode|Undefined $collectionMode = new Undefined(), + public readonly BillingDetails|null|Undefined $billingDetails = new Undefined(), + public readonly null|Undefined $scheduledChange = new Undefined(), + public readonly array|Undefined $items = new Undefined(), + public readonly CustomData|null|Undefined $customData = new Undefined(), + public readonly SubscriptionProrationBillingMode|Undefined $prorationBillingMode = new Undefined(), + public readonly SubscriptionOnPaymentFailure|Undefined $onPaymentFailure = new Undefined(), + ) { + } + + public function jsonSerialize(): array + { + return $this->filterUndefined([ + 'customer_id' => $this->customerId, + 'address_id' => $this->addressId, + 'business_id' => $this->businessId, + 'currency_code' => $this->currencyCode, + 'next_billed_at' => is_a($this->nextBilledAt, \DateTimeInterface::class) ? DateTime::from($this->nextBilledAt)?->format() : $this->nextBilledAt, + 'discount' => $this->discount, + 'collection_mode' => $this->collectionMode, + 'billing_details' => $this->billingDetails, + 'scheduled_change' => $this->scheduledChange, + 'items' => $this->items, + 'custom_data' => $this->customData, + 'proration_billing_mode' => $this->prorationBillingMode, + 'on_payment_failure' => $this->onPaymentFailure, + ]); + } +} diff --git a/src/Resources/Subscriptions/SubscriptionsClient.php b/src/Resources/Subscriptions/SubscriptionsClient.php new file mode 100644 index 0000000..8753fc0 --- /dev/null +++ b/src/Resources/Subscriptions/SubscriptionsClient.php @@ -0,0 +1,165 @@ +client->getRaw('/subscriptions', $listOperation), + ); + + return SubscriptionWithIncludesCollection::from( + $parser->getData(), + new Paginator($this->client, $parser->getPagination(), SubscriptionWithIncludesCollection::class), + ); + } + + /** + * @param Includes[] $includes + * + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function get(string $id, array $includes = []): SubscriptionWithIncludes + { + if ($invalid = array_filter($includes, fn ($value): bool => ! $value instanceof Includes)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('includes', Includes::class, implode(', ', $invalid)); + } + + $params = $includes === [] + ? [] + : ['include' => implode(',', array_map(fn ($enum) => $enum->value, $includes))]; + + $parser = new ResponseParser( + $this->client->getRaw("/subscriptions/{$id}", $params), + ); + + return SubscriptionWithIncludes::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\SubscriptionApiError On a subscription specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function update(string $id, UpdateOperation $operation): SubscriptionWithIncludes + { + $parser = new ResponseParser( + $this->client->patchRaw("/subscriptions/{$id}", $operation), + ); + + return SubscriptionWithIncludes::from($parser->getData()); + } + + public function pause(string $id, PauseOperation $operation): SubscriptionWithIncludes + { + $parser = new ResponseParser( + $this->client->postRaw("/subscriptions/{$id}/pause", $operation), + ); + + return SubscriptionWithIncludes::from($parser->getData()); + } + + public function resume(string $id, ResumeOperation $operation): SubscriptionWithIncludes + { + $parser = new ResponseParser( + $this->client->postRaw("/subscriptions/{$id}/resume", $operation), + ); + + return SubscriptionWithIncludes::from($parser->getData()); + } + + public function cancel(string $id, CancelOperation $operation): SubscriptionWithIncludes + { + $parser = new ResponseParser( + $this->client->postRaw("/subscriptions/{$id}/cancel", $operation), + ); + + return SubscriptionWithIncludes::from($parser->getData()); + } + + public function getPaymentMethodChangeTransaction(string $id): TransactionWithIncludes + { + $parser = new ResponseParser( + $this->client->getRaw("/subscriptions/{$id}/update-payment-method-transaction"), + ); + + return TransactionWithIncludes::from($parser->getData()); + } + + public function activate(string $id): SubscriptionWithIncludes + { + $parser = new ResponseParser( + $this->client->postRaw("/subscriptions/{$id}/activate"), + ); + + return SubscriptionWithIncludes::from($parser->getData()); + } + + public function createOneTimeCharge(string $id, CreateOneTimeChargeOperation $operation): SubscriptionWithIncludes + { + $parser = new ResponseParser( + $this->client->postRaw("/subscriptions/{$id}/charge", $operation), + ); + + return SubscriptionWithIncludes::from($parser->getData()); + } + + public function previewUpdate(string $id, PreviewUpdateOperation $operation): SubscriptionPreview + { + $parser = new ResponseParser( + $this->client->patchRaw("/subscriptions/{$id}/preview", $operation), + ); + + return SubscriptionPreview::from($parser->getData()); + } + + public function previewOneTimeCharge(string $id, PreviewOneTimeChargeOperation $operation): SubscriptionPreview + { + $parser = new ResponseParser( + $this->client->postRaw("/subscriptions/{$id}/charge/preview", $operation), + ); + + return SubscriptionPreview::from($parser->getData()); + } +} diff --git a/src/Resources/Transactions/Operations/CreateOperation.php b/src/Resources/Transactions/Operations/CreateOperation.php new file mode 100644 index 0000000..0a81a1f --- /dev/null +++ b/src/Resources/Transactions/Operations/CreateOperation.php @@ -0,0 +1,59 @@ + $items + */ + public function __construct( + public readonly array $items, + public readonly StatusTransaction|Undefined $status = new Undefined(), + public readonly string|null|Undefined $customerId = new Undefined(), + public readonly string|null|Undefined $addressId = new Undefined(), + public readonly string|null|Undefined $businessId = new Undefined(), + public readonly CustomData|null|Undefined $customData = new Undefined(), + public readonly CurrencyCode|Undefined $currencyCode = new Undefined(), + public readonly CollectionMode|Undefined $collectionMode = new Undefined(), + public readonly string|null|Undefined $discountId = new Undefined(), + public readonly BillingDetails|null|Undefined $billingDetails = new Undefined(), + public readonly TransactionTimePeriod|null|Undefined $billingPeriod = new Undefined(), + public readonly Checkout|null|Undefined $checkout = new Undefined(), + ) { + } + + public function jsonSerialize(): array + { + return $this->filterUndefined([ + 'items' => $this->items, + 'status' => $this->status, + 'customer_id' => $this->customerId, + 'address_id' => $this->addressId, + 'business_id' => $this->businessId, + 'custom_data' => $this->customData, + 'currency_code' => $this->currencyCode, + 'collection_mode' => $this->collectionMode, + 'discount_id' => $this->discountId, + 'billing_details' => $this->billingDetails, + 'billing_period' => $this->billingPeriod, + 'checkout' => $this->checkout, + ]); + } +} diff --git a/src/Resources/Transactions/Operations/List/Includes.php b/src/Resources/Transactions/Operations/List/Includes.php new file mode 100644 index 0000000..4181d71 --- /dev/null +++ b/src/Resources/Transactions/Operations/List/Includes.php @@ -0,0 +1,15 @@ + $customerIds + * @param array $ids + * @param array $includes + * @param array $invoiceNumbers + * @param array $statuses + * @param array $subscriptionIds + */ + public function __construct( + private readonly ?Pager $pager = null, + private readonly ?DateComparison $billedAt = null, + private readonly ?CollectionMode $collectionMode = null, + private readonly ?DateComparison $createdAt = null, + private readonly array $customerIds = [], + private readonly array $ids = [], + private readonly array $includes = [], + private readonly array $invoiceNumbers = [], + private readonly array $statuses = [], + private readonly array $subscriptionIds = [], + private readonly ?DateComparison $updatedAt = null, + ) { + if ($invalid = array_filter($this->customerIds, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('customerIds', 'string', implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->ids, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('ids', 'string', implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->includes, fn ($value): bool => ! $value instanceof Includes)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('includes', Includes::class, implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->invoiceNumbers, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('invoiceNumbers', 'string', implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->statuses, fn ($value): bool => ! $value instanceof StatusTransaction)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('statuses', StatusTransaction::class, implode(', ', $invalid)); + } + + if ($invalid = array_filter($this->subscriptionIds, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('subscriptionIds', 'string', implode(', ', $invalid)); + } + } + + public function getParameters(): array + { + $enumStringify = fn ($enum) => $enum->value; + + return array_merge( + $this->pager?->getParameters() ?? [], + array_filter([ + 'billed_at' . $this->billedAt?->comparator() => $this->billedAt?->formatted(), + 'collection_mode' => $this->collectionMode?->value, + 'created_at' . $this->createdAt?->comparator() => $this->createdAt?->formatted(), + 'customer_id' => implode(',', $this->customerIds), + 'id' => implode(',', $this->ids), + 'include' => implode(',', array_map($enumStringify, $this->includes)), + 'invoice_number' => implode(',', $this->invoiceNumbers), + 'status' => implode(',', array_map($enumStringify, $this->statuses)), + 'subscription_id' => implode(',', $this->subscriptionIds), + 'updated_at' . $this->updatedAt?->comparator() => $this->updatedAt?->formatted(), + ]), + ); + } +} diff --git a/src/Resources/Transactions/Operations/PreviewOperation.php b/src/Resources/Transactions/Operations/PreviewOperation.php new file mode 100644 index 0000000..92d8096 --- /dev/null +++ b/src/Resources/Transactions/Operations/PreviewOperation.php @@ -0,0 +1,51 @@ + $items + */ + public function __construct( + public readonly array $items, + public readonly string|null|Undefined $customerId = new Undefined(), + public readonly string|null|Undefined $addressId = new Undefined(), + public readonly string|null|Undefined $businessId = new Undefined(), + public readonly CurrencyCode|Undefined $currencyCode = new Undefined(), + public readonly CollectionMode|Undefined $collectionMode = new Undefined(), + public readonly string|null|Undefined $discountId = new Undefined(), + public readonly string|null|Undefined $customerIpAddress = new Undefined(), + public readonly AddressPreview|null|Undefined $address = new Undefined(), + public readonly bool|Undefined $ignoreTrials = new Undefined(), + ) { + } + + public function jsonSerialize(): array + { + return $this->filterUndefined([ + 'items' => $this->items, + 'customer_id' => $this->customerId, + 'address_id' => $this->addressId, + 'business_id' => $this->businessId, + 'currency_code' => $this->currencyCode, + 'collection_mode' => $this->collectionMode, + 'discount_id' => $this->discountId, + 'customer_ip_address' => $this->customerIpAddress, + 'address' => $this->address, + 'ignore_trials' => $this->ignoreTrials, + ]); + } +} diff --git a/src/Resources/Transactions/Operations/UpdateOperation.php b/src/Resources/Transactions/Operations/UpdateOperation.php new file mode 100644 index 0000000..ca4ed19 --- /dev/null +++ b/src/Resources/Transactions/Operations/UpdateOperation.php @@ -0,0 +1,58 @@ + $items + */ + public function __construct( + public readonly StatusTransaction|Undefined $status = new Undefined(), + public readonly string|null|Undefined $customerId = new Undefined(), + public readonly string|null|Undefined $addressId = new Undefined(), + public readonly string|null|Undefined $businessId = new Undefined(), + public readonly CustomData|null|Undefined $customData = new Undefined(), + public readonly CurrencyCode|Undefined $currencyCode = new Undefined(), + public readonly CollectionMode|Undefined $collectionMode = new Undefined(), + public readonly string|null|Undefined $discountId = new Undefined(), + public readonly BillingDetails|null|Undefined $billingDetails = new Undefined(), + public readonly TransactionTimePeriod|null|Undefined $billingPeriod = new Undefined(), + public readonly array|Undefined $items = new Undefined(), + public readonly Checkout|null|Undefined $checkout = new Undefined(), + ) { + } + + public function jsonSerialize(): array + { + return $this->filterUndefined([ + 'status' => $this->status, + 'customer_id' => $this->customerId, + 'address_id' => $this->addressId, + 'business_id' => $this->businessId, + 'custom_data' => $this->customData, + 'currency_code' => $this->currencyCode, + 'collection_mode' => $this->collectionMode, + 'discount_id' => $this->discountId, + 'billing_details' => $this->billingDetails, + 'billing_period' => $this->billingPeriod, + 'items' => $this->items, + 'checkout' => $this->checkout, + ]); + } +} diff --git a/src/Resources/Transactions/TransactionsClient.php b/src/Resources/Transactions/TransactionsClient.php new file mode 100644 index 0000000..a5216ce --- /dev/null +++ b/src/Resources/Transactions/TransactionsClient.php @@ -0,0 +1,139 @@ +client->getRaw('/transactions', $listOperation), + ); + + return TransactionWithIncludesCollection::from( + $parser->getData(), + new Paginator($this->client, $parser->getPagination(), TransactionWithIncludesCollection::class), + ); + } + + /** + * @param Includes[] $includes + * + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function get(string $id, array $includes = []): TransactionWithIncludes + { + if ($invalid = array_filter($includes, fn ($value): bool => ! $value instanceof Includes)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('includes', Includes::class, implode(', ', $invalid)); + } + + $params = $includes === [] + ? [] + : ['include' => implode(',', array_map(fn ($enum) => $enum->value, $includes))]; + + $parser = new ResponseParser( + $this->client->getRaw("/transactions/{$id}", $params), + ); + + return TransactionWithIncludes::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\TransactionApiError On a transaction specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function create(CreateOperation $createOperation, array $includes = []): TransactionWithIncludes + { + if ($invalid = array_filter($includes, fn ($value): bool => ! $value instanceof Includes)) { + throw InvalidArgumentException::arrayContainsInvalidTypes('includes', Includes::class, implode(', ', $invalid)); + } + + $params = $includes === [] + ? [] + : ['include' => implode(',', array_map(fn ($enum) => $enum->value, $includes))]; + + $parser = new ResponseParser( + $this->client->postRaw('/transactions', $createOperation, $params), + ); + + return TransactionWithIncludes::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\TransactionApiError On a transaction specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function update(string $id, UpdateOperation $operation): TransactionWithIncludes + { + $parser = new ResponseParser( + $this->client->patchRaw("/transactions/{$id}", $operation), + ); + + return TransactionWithIncludes::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\TransactionApiError On a transaction specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function preview(PreviewOperation $operation): TransactionPreview + { + $parser = new ResponseParser( + $this->client->postRaw('/transactions/preview', $operation), + ); + + return TransactionPreview::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\TransactionApiError On a transaction specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function getInvoicePDF(string $id): TransactionData + { + $parser = new ResponseParser( + $this->client->getRaw("/transactions/{$id}/preview"), + ); + + return TransactionData::from($parser->getData()); + } +} diff --git a/src/ResponseParser.php b/src/ResponseParser.php new file mode 100644 index 0000000..0c0a4d1 --- /dev/null +++ b/src/ResponseParser.php @@ -0,0 +1,86 @@ +body = json_decode( + json: (string) $response->getBody(), + associative: true, + flags: JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION, + ); + } catch (\JsonException) { + $this->body = null; + } + + $this->parseErrors(); + } + + public function getData(): array + { + return $this->body['data'] ?? []; + } + + public function getPagination(): Pagination + { + return new Pagination( + perPage: $this->body['meta']['pagination']['per_page'], + next: $this->body['meta']['pagination']['next'], + hasMore: $this->body['meta']['pagination']['has_more'], + estimatedTotal: $this->body['meta']['pagination']['estimated_total'], + ); + } + + /** + * @throws ApiError When an API returns an error + * + * @see https://developer.paddle.com/api-reference/about/errors + * @see https://developer.paddle.com/errors/overview + */ + private function parseErrors(): self + { + if (! isset($this->body['error'])) { + return $this; + } + + /** @var class-string $exceptionClass */ + $exceptionClass = $this->findExceptionClassFromCode($this->body['error']['code'] ?? 'shared_error'); + + throw $exceptionClass::fromErrorData($this->body['error']); + } + + /** + * @return class-string + */ + private function findExceptionClassFromCode(string $code): string + { + $parts = explode('_', $code); + $resource = ucfirst($parts[0] ?? ''); + + if ($resource === '') { + return ApiError::class; + } + + $className = "Paddle\\SDK\\Exceptions\\ApiError\\{$resource}ApiError"; + + if (! class_exists($className)) { + return ApiError::class; + } + + return $className; + } +} diff --git a/src/Undefined.php b/src/Undefined.php new file mode 100644 index 0000000..24e58db --- /dev/null +++ b/src/Undefined.php @@ -0,0 +1,9 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** + * @test + * + * @dataProvider createOperationsProvider + */ + public function it_uses_expected_payload_on_create( + CreateOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->addresses->create('ctm_01h844p3h41s12zs5mn4axja51', $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/customers/ctm_01h844p3h41s12zs5mn4axja51/addresses', + urldecode((string) $request->getUri()), + ); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function createOperationsProvider(): \Generator + { + yield 'Basic Create' => [ + new CreateOperation(CountryCode::AG), + new Response(200, body: self::readRawJsonFixture('response/minimal_entity')), + self::readRawJsonFixture('request/create_basic'), + ]; + + yield 'Create with Data' => [ + new CreateOperation( + countryCode: CountryCode::US, + description: 'Head Office', + firstLine: '4050 Jefferson Plaza, 41st Floor', + secondLine: null, + city: 'New York', + postalCode: '10021', + region: 'NY', + customData: new CustomData(['shippable' => true]), + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/create_full'), + ]; + } + + /** + * @test + * + * @dataProvider updateOperationsProvider + */ + public function it_uses_expected_payload_on_update( + UpdateOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->addresses->update('ctm_01h844p3h41s12zs5mn4axja51', 'add_01h848pep46enq8y372x7maj0p', $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('PATCH', $request->getMethod()); + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/customers/ctm_01h844p3h41s12zs5mn4axja51/addresses/add_01h848pep46enq8y372x7maj0p', + urldecode((string) $request->getUri()), + ); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function updateOperationsProvider(): \Generator + { + yield 'Update Single' => [ + new UpdateOperation( + description: 'Head Office', + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_single'), + ]; + + yield 'Update Partial' => [ + new UpdateOperation(description: 'Head Office', city: 'New York'), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_partial'), + ]; + + yield 'Update All' => [ + new UpdateOperation( + description: 'Head Office', + firstLine: '4050 Jefferson Plaza, 41st Floor', + secondLine: null, + city: 'New York', + postalCode: '10021', + region: 'NY', + countryCode: CountryCode::US, + customData: new CustomData(['shippable' => true]), + status: Status::Active, + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_full'), + ]; + } + + /** + * @test + * + * @dataProvider listOperationsProvider + */ + public function list_hits_expected_uri( + ListOperation $operation, + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->addresses->list('ctm_01h8441jn5pcwrfhwh78jqt8hk', $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function listOperationsProvider(): \Generator + { + yield 'Default' => [ + new ListOperation(), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/customers/ctm_01h8441jn5pcwrfhwh78jqt8hk/addresses', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Default Paged' => [ + new ListOperation(new Pager()), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/customers/ctm_01h8441jn5pcwrfhwh78jqt8hk/addresses?order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Default Paged with After' => [ + new ListOperation(new Pager(after: 'add_01h848pep46enq8y372x7maj0p')), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/customers/ctm_01h8441jn5pcwrfhwh78jqt8hk/addresses?after=add_01h848pep46enq8y372x7maj0p&order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'NotificationStatus Filtered' => [ + new ListOperation(statuses: [Status::Archived]), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/customers/ctm_01h8441jn5pcwrfhwh78jqt8hk/addresses?status=archived', Environment::SANDBOX->baseUrl()), + ]; + + yield 'ID Filtered' => [ + new ListOperation(ids: ['add_01h848pep46enq8y372x7maj0p']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/customers/ctm_01h8441jn5pcwrfhwh78jqt8hk/addresses?id=add_01h848pep46enq8y372x7maj0p', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Multiple ID Filtered' => [ + new ListOperation(ids: ['add_01h8494f4w5rwfp8b12yqh8fp1', 'add_01h848pep46enq8y372x7maj0p']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/customers/ctm_01h8441jn5pcwrfhwh78jqt8hk/addresses?id=add_01h8494f4w5rwfp8b12yqh8fp1,add_01h848pep46enq8y372x7maj0p', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Search Filtered' => [ + new ListOperation(search: 'Office'), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/customers/ctm_01h8441jn5pcwrfhwh78jqt8hk/addresses?search=Office', Environment::SANDBOX->baseUrl()), + ]; + } + + /** @test */ + public function get_hits_expected_uri(): void + { + $response = new Response(200, body: self::readRawJsonFixture('response/full_entity')); + $this->mockClient->addResponse($response); + $this->client->addresses->get('ctm_01h844p3h41s12zs5mn4axja51', 'add_01h848pep46enq8y372x7maj0p'); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals( + sprintf('%s/customers/ctm_01h844p3h41s12zs5mn4axja51/addresses/add_01h848pep46enq8y372x7maj0p', Environment::SANDBOX->baseUrl()), + urldecode((string) $request->getUri()), + ); + } +} diff --git a/tests/Functional/Resources/Addresses/_fixtures/request/create_basic.json b/tests/Functional/Resources/Addresses/_fixtures/request/create_basic.json new file mode 100644 index 0000000..d12166d --- /dev/null +++ b/tests/Functional/Resources/Addresses/_fixtures/request/create_basic.json @@ -0,0 +1,3 @@ +{ + "country_code": "AG" +} diff --git a/tests/Functional/Resources/Addresses/_fixtures/request/create_full.json b/tests/Functional/Resources/Addresses/_fixtures/request/create_full.json new file mode 100644 index 0000000..b4c3b11 --- /dev/null +++ b/tests/Functional/Resources/Addresses/_fixtures/request/create_full.json @@ -0,0 +1,12 @@ +{ + "description": "Head Office", + "first_line": "4050 Jefferson Plaza, 41st Floor", + "second_line": null, + "city": "New York", + "postal_code": "10021", + "region": "NY", + "country_code": "US", + "custom_data": { + "shippable": true + } +} diff --git a/tests/Functional/Resources/Addresses/_fixtures/request/update_full.json b/tests/Functional/Resources/Addresses/_fixtures/request/update_full.json new file mode 100644 index 0000000..cc6f6ab --- /dev/null +++ b/tests/Functional/Resources/Addresses/_fixtures/request/update_full.json @@ -0,0 +1,13 @@ +{ + "description": "Head Office", + "first_line": "4050 Jefferson Plaza, 41st Floor", + "second_line": null, + "city": "New York", + "postal_code": "10021", + "region": "NY", + "country_code": "US", + "custom_data": { + "shippable": true + }, + "status": "active" +} diff --git a/tests/Functional/Resources/Addresses/_fixtures/request/update_partial.json b/tests/Functional/Resources/Addresses/_fixtures/request/update_partial.json new file mode 100644 index 0000000..62708b8 --- /dev/null +++ b/tests/Functional/Resources/Addresses/_fixtures/request/update_partial.json @@ -0,0 +1,4 @@ +{ + "description": "Head Office", + "city": "New York" +} diff --git a/tests/Functional/Resources/Addresses/_fixtures/request/update_single.json b/tests/Functional/Resources/Addresses/_fixtures/request/update_single.json new file mode 100644 index 0000000..95b743d --- /dev/null +++ b/tests/Functional/Resources/Addresses/_fixtures/request/update_single.json @@ -0,0 +1,3 @@ +{ + "description": "Head Office" +} diff --git a/tests/Functional/Resources/Addresses/_fixtures/response/full_entity.json b/tests/Functional/Resources/Addresses/_fixtures/response/full_entity.json new file mode 100644 index 0000000..c302249 --- /dev/null +++ b/tests/Functional/Resources/Addresses/_fixtures/response/full_entity.json @@ -0,0 +1,19 @@ +{ + "data": { + "id": "add_01h848pep46enq8y372x7maj0p", + "status": "active", + "description": "Head Office", + "first_line": "4050 Jefferson Plaza, 41st Floor", + "second_line": null, + "city": "New York", + "postal_code": "10021", + "region": "NY", + "country_code": "US", + "custom_data": null, + "created_at": "2023-08-18T12:07:36.9Z", + "updated_at": "2023-08-18T12:07:36.9Z" + }, + "meta": { + "request_id": "f00bb3ca-399d-4686-889c-50b028f4c999" + } +} diff --git a/tests/Functional/Resources/Addresses/_fixtures/response/list_default.json b/tests/Functional/Resources/Addresses/_fixtures/response/list_default.json new file mode 100644 index 0000000..4ba1273 --- /dev/null +++ b/tests/Functional/Resources/Addresses/_fixtures/response/list_default.json @@ -0,0 +1,41 @@ +{ + "data": [ + { + "id": "add_01h848pep46enq8y372x7maj0p", + "status": "active", + "description": "Head Office", + "first_line": "4050 Jefferson Plaza, 41st Floor", + "second_line": null, + "city": "New York", + "postal_code": "10021", + "region": "NY", + "country_code": "US", + "custom_data": null, + "created_at": "2023-08-18T12:07:36.9Z", + "updated_at": "2023-08-18T12:07:36.9Z" + }, + { + "id": "add_01h8494f4w5rwfp8b12yqh8fp1", + "status": "active", + "description": "London Office", + "first_line": "81 Richmond Road", + "second_line": null, + "city": "Shoreditch", + "postal_code": "E1 1EK", + "region": "London", + "country_code": "GB", + "custom_data": null, + "created_at": "2023-08-18T12:15:16.124Z", + "updated_at": "2023-08-18T12:15:16.124Z" + } + ], + "meta": { + "request_id": "26a794f3-c26d-4c06-aac0-81d8e0b7ffbd", + "pagination": { + "per_page": 50, + "next": "https://api.paddle.com/customers/ctm_01h8441jn5pcwrfhwh78jqt8hk/addresses?after=add_01h8494f4w5rwfp8b12yqh8fp1", + "has_more": false, + "estimated_total": 2 + } + } +} diff --git a/tests/Functional/Resources/Addresses/_fixtures/response/minimal_entity.json b/tests/Functional/Resources/Addresses/_fixtures/response/minimal_entity.json new file mode 100644 index 0000000..0ed3b08 --- /dev/null +++ b/tests/Functional/Resources/Addresses/_fixtures/response/minimal_entity.json @@ -0,0 +1,19 @@ +{ + "data": { + "id": "add_01he8brnckeq87t52hajkgfrg5", + "status": "active", + "description": null, + "first_line": null, + "second_line": null, + "city": null, + "postal_code": null, + "region": null, + "country_code": "AG", + "custom_data": null, + "created_at": "2023-11-02T15:52:23.699Z", + "updated_at": "2023-11-02T15:52:23.699Z" + }, + "meta": { + "request_id": "e36223a7-c047-457e-b937-95fdce44766d" + } +} diff --git a/tests/Functional/Resources/Adjustments/AdjustmentsClientTest.php b/tests/Functional/Resources/Adjustments/AdjustmentsClientTest.php new file mode 100644 index 0000000..97556e7 --- /dev/null +++ b/tests/Functional/Resources/Adjustments/AdjustmentsClientTest.php @@ -0,0 +1,211 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** + * @test + * + * @dataProvider createOperationsProvider + */ + public function it_uses_expected_payload_on_create( + CreateOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->adjustments->create($operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/adjustments', + urldecode((string) $request->getUri()), + ); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function createOperationsProvider(): \Generator + { + yield 'Basic Create' => [ + new CreateOperation( + Action::Refund, + [new AdjustmentItem( + 'txnitm_01h8bxryv3065dyh6103p3yg28', + AdjustmentType::Partial, + '100', + )], + 'error', + 'txn_01h8bxpvx398a7zbawb77y0kp5', + ), + new Response(200, body: self::readRawJsonFixture('response/minimal_entity')), + self::readRawJsonFixture('request/create_basic'), + ]; + + yield 'Create with Data' => [ + new CreateOperation( + Action::Refund, + [ + new AdjustmentItem( + 'txnitm_01h8bxryv3065dyh6103p3yg28', + AdjustmentType::Partial, + '100', + ), + new AdjustmentItem( + 'txnitm_01h8bxryv3065dyh6103p3yg29', + AdjustmentType::Full, + '1949', + ), + ], + 'error', + 'txn_01h8bxpvx398a7zbawb77y0kp5', + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/create_full'), + ]; + } + + /** + * @test + * + * @dataProvider listOperationsProvider + */ + public function list_hits_expected_uri( + ListOperation $operation, + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->adjustments->list($operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function listOperationsProvider(): \Generator + { + yield 'Default' => [ + new ListOperation(), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/adjustments', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Default Paged' => [ + new ListOperation(new Pager()), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/adjustments?order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Default Paged with After' => [ + new ListOperation(new Pager(after: 'adj_01h8c65c2ggq5nxswnnwv78e75')), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/adjustments?after=adj_01h8c65c2ggq5nxswnnwv78e75&order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'NotificationStatus Filtered' => [ + new ListOperation(statuses: [StatusAdjustment::PendingApproval]), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/adjustments?status=pending_approval', Environment::SANDBOX->baseUrl()), + ]; + + yield 'ID Filtered' => [ + new ListOperation(ids: ['adj_01h8c65c2ggq5nxswnnwv78e75']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/adjustments?id=adj_01h8c65c2ggq5nxswnnwv78e75', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Multiple ID Filtered' => [ + new ListOperation(ids: ['add_01h8494f4w5rwfp8b12yqh8fp1', 'adj_01h8c65c2ggq5nxswnnwv78e75']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/adjustments?id=add_01h8494f4w5rwfp8b12yqh8fp1,adj_01h8c65c2ggq5nxswnnwv78e75', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Customer ID Filtered' => [ + new ListOperation(customerIds: ['ctm_01h8441jn5pcwrfhwh78jqt8hk']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/adjustments?customer_id=ctm_01h8441jn5pcwrfhwh78jqt8hk', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Multiple Customer ID Filtered' => [ + new ListOperation(customerIds: ['ctm_01h8441jn5pcwrfhwh78jqt8hk', 'ctm_01h7hswb86rtps5ggbq7ybydcw']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/adjustments?customer_id=ctm_01h8441jn5pcwrfhwh78jqt8hk,ctm_01h7hswb86rtps5ggbq7ybydcw', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Transaction ID Filtered' => [ + new ListOperation(transactionIds: ['txn_01h8bxpvx398a7zbawb77y0kp5']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/adjustments?transaction_id=txn_01h8bxpvx398a7zbawb77y0kp5', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Multiple Transaction ID Filtered' => [ + new ListOperation(transactionIds: ['ctm_01h8441jn5pcwrfhwh78jqt8hk', 'txn_01h8bx69629a16wwm9z8rjmak3']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/adjustments?transaction_id=ctm_01h8441jn5pcwrfhwh78jqt8hk,txn_01h8bx69629a16wwm9z8rjmak3', Environment::SANDBOX->baseUrl()), + ]; + + yield 'NotificationSubscription ID Filtered' => [ + new ListOperation(subscriptionIds: ['sub_01h8bxswamxysj44zt5n48njwh']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/adjustments?subscription_id=sub_01h8bxswamxysj44zt5n48njwh', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Multiple NotificationSubscription ID Filtered' => [ + new ListOperation(subscriptionIds: ['sub_01h8bxswamxysj44zt5n48njwh', 'sub_01h8bx8fmywym11t6swgzba704']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/adjustments?subscription_id=sub_01h8bxswamxysj44zt5n48njwh,sub_01h8bx8fmywym11t6swgzba704', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Action Filtered' => [ + new ListOperation(action: Action::Refund), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/adjustments?action=refund', Environment::SANDBOX->baseUrl()), + ]; + } +} diff --git a/tests/Functional/Resources/Adjustments/_fixtures/request/create_basic.json b/tests/Functional/Resources/Adjustments/_fixtures/request/create_basic.json new file mode 100644 index 0000000..80cfe42 --- /dev/null +++ b/tests/Functional/Resources/Adjustments/_fixtures/request/create_basic.json @@ -0,0 +1,12 @@ +{ + "action": "refund", + "items": [ + { + "item_id": "txnitm_01h8bxryv3065dyh6103p3yg28", + "type": "partial", + "amount": "100" + } + ], + "reason": "error", + "transaction_id": "txn_01h8bxpvx398a7zbawb77y0kp5" +} diff --git a/tests/Functional/Resources/Adjustments/_fixtures/request/create_full.json b/tests/Functional/Resources/Adjustments/_fixtures/request/create_full.json new file mode 100644 index 0000000..daff4fd --- /dev/null +++ b/tests/Functional/Resources/Adjustments/_fixtures/request/create_full.json @@ -0,0 +1,17 @@ +{ + "action": "refund", + "items": [ + { + "item_id": "txnitm_01h8bxryv3065dyh6103p3yg28", + "type": "partial", + "amount": "100" + }, + { + "item_id": "txnitm_01h8bxryv3065dyh6103p3yg29", + "type": "full", + "amount": "1949" + } + ], + "reason": "error", + "transaction_id": "txn_01h8bxpvx398a7zbawb77y0kp5" +} diff --git a/tests/Functional/Resources/Adjustments/_fixtures/response/full_entity.json b/tests/Functional/Resources/Adjustments/_fixtures/response/full_entity.json new file mode 100644 index 0000000..61ca79e --- /dev/null +++ b/tests/Functional/Resources/Adjustments/_fixtures/response/full_entity.json @@ -0,0 +1,59 @@ +{ + "data": { + "id": "adj_01h8c65c2ggq5nxswnnwv78e75", + "action": "refund", + "transaction_id": "txn_01h8bxpvx398a7zbawb77y0kp5", + "subscription_id": "sub_01h8bxswamxysj44zt5n48njwh", + "customer_id": "ctm_01h8441jn5pcwrfhwh78jqt8hk", + "reason": "error", + "currency_code": "USD", + "status": "pending_approval", + "items": [ + { + "id": "adjitm_01h8c65c2ggq5nxswnnz4kxkkr", + "item_id": "txnitm_01h8bxryv3065dyh6103p3yg28", + "type": "partial", + "amount": "100", + "proration": null, + "totals": { + "subtotal": "92", + "tax": "8", + "total": "100" + } + }, + { + "id": "adjitm_01h8c65c2ggq5nxswnnz4klrrs", + "item_id": "txnitm_01h8bxryv3065dyh6103p3yg29", + "type": "full", + "amount": "1949", + "proration": null, + "totals": { + "subtotal": "1794", + "tax": "155", + "total": "1949" + } + } + ], + "totals": { + "subtotal": "1886", + "tax": "8", + "total": "100", + "fee": "5", + "earnings": "87", + "currency_code": "USD" + }, + "payout_totals": { + "subtotal": "92", + "tax": "163", + "total": "2049", + "fee": "20", + "earnings": "2029", + "currency_code": "USD" + }, + "created_at": "2023-08-21T13:57:15.489634Z", + "updated_at": "0001-01-01T00:00:00Z" + }, + "meta": { + "request_id": "4ec9ff32-c88a-4eef-93b6-d8b99b5fba4b" + } +} diff --git a/tests/Functional/Resources/Adjustments/_fixtures/response/list_default.json b/tests/Functional/Resources/Adjustments/_fixtures/response/list_default.json new file mode 100644 index 0000000..6333b42 --- /dev/null +++ b/tests/Functional/Resources/Adjustments/_fixtures/response/list_default.json @@ -0,0 +1,269 @@ +{ + "data": [ + { + "id": "adj_01h8c65c2ggq5nxswnnwv78e75", + "action": "refund", + "transaction_id": "txn_01h8bxpvx398a7zbawb77y0kp5", + "subscription_id": "sub_01h8bxswamxysj44zt5n48njwh", + "customer_id": "ctm_01h8441jn5pcwrfhwh78jqt8hk", + "reason": "error", + "currency_code": "USD", + "status": "pending_approval", + "items": [ + { + "id": "adjitm_01h8c65c2ggq5nxswnnz4kxkkr", + "item_id": "txnitm_01h8bxryv3065dyh6103p3yg28", + "type": "partial", + "amount": "100", + "proration": null, + "totals": { + "subtotal": "92", + "tax": "8", + "total": "100" + } + } + ], + "totals": { + "subtotal": "92", + "tax": "8", + "total": "100", + "fee": "5", + "earnings": "87", + "currency_code": "USD" + }, + "payout_totals": { + "subtotal": "92", + "tax": "8", + "total": "100", + "fee": "5", + "earnings": "87", + "currency_code": "USD" + }, + "created_at": "2023-08-21T13:57:15.489634Z", + "updated_at": "2023-08-21T13:57:15.489634Z" + }, + { + "id": "adj_01h8bxezh16gm6t8rx21dx271b", + "action": "credit", + "credit_applied_to_balance": true, + "transaction_id": "txn_01h8bx69629a16wwm9z8rjmak3", + "subscription_id": "sub_01h8bx8fmywym11t6swgzba704", + "customer_id": "ctm_01h8441jn5pcwrfhwh78jqt8hk", + "reason": "subscription_recurring", + "currency_code": "USD", + "status": "pending_approval", + "items": [ + { + "id": "adjitm_01h8bxezh16gm6t8rx21s3bvfq", + "item_id": "txnitm_01h8bx69hvh6c94cngs0fzxm31", + "type": "proration", + "amount": "163297", + "proration": { + "rate": "0.99991", + "billing_period": { + "starts_at": "2023-08-21T11:25:12.474Z", + "ends_at": "2023-09-21T11:21:38.547106Z" + } + }, + "totals": { + "subtotal": "149986", + "tax": "13311", + "total": "163297" + } + } + ], + "totals": { + "subtotal": "149986", + "tax": "13311", + "total": "163297", + "fee": "8215", + "earnings": "141771", + "currency_code": "USD" + }, + "payout_totals": { + "subtotal": "149986", + "tax": "13311", + "total": "163297", + "fee": "8215", + "earnings": "141771", + "currency_code": "USD" + }, + "created_at": "2023-08-21T11:25:13.138521Z", + "updated_at": "2023-08-21T11:25:13.138521Z" + }, + { + "id": "adj_01h7jgzjqt2s8sab70e03ptkhv", + "action": "credit", + "credit_applied_to_balance": true, + "transaction_id": "txn_01h7jgd9bkwjscj3ae15g5d3vs", + "subscription_id": "sub_01h7ht5z5wdg9pz18jx1fagp8k", + "customer_id": "ctm_01h7hswb86rtps5ggbq7ybydcw", + "reason": "subscription_recurring", + "currency_code": "USD", + "status": "approved", + "items": [ + { + "id": "adjitm_01h7jgzjqt2s8sab70e43mth7k", + "item_id": "txnitm_01h7jgd9cp215v676v2eye6pf7", + "type": "proration", + "amount": "71984", + "proration": { + "rate": "0.99978", + "billing_period": { + "starts_at": "2023-12-11T08:43:21.861947Z", + "ends_at": "2024-01-11T08:33:04.443903Z" + } + }, + "totals": { + "subtotal": "59987", + "tax": "11997", + "total": "71984" + } + } + ], + "totals": { + "subtotal": "59987", + "tax": "11997", + "total": "71984", + "fee": "3631", + "earnings": "56356", + "currency_code": "USD" + }, + "payout_totals": { + "subtotal": "59987", + "tax": "11997", + "total": "71984", + "fee": "3631", + "earnings": "56356", + "currency_code": "USD" + }, + "created_at": "2023-08-11T14:46:04.811464Z", + "updated_at": "2023-08-11T14:46:05.13404Z" + }, + { + "id": "adj_01h7jf6ptkfsc93hzc20fgf8wy", + "action": "credit", + "credit_applied_to_balance": true, + "transaction_id": "txn_01h7je77vc1qmzxntem45ebb5q", + "subscription_id": "sub_01h7ht5z5wdg9pz18jx1fagp8k", + "customer_id": "ctm_01h7hswb86rtps5ggbq7ybydcw", + "reason": "subscription_recurring", + "currency_code": "USD", + "status": "approved", + "items": [ + { + "id": "adjitm_01h7jf6ptkfsc93hzc23je84yw", + "item_id": "txnitm_01h7je77web8mawdyntht1w855", + "type": "proration", + "amount": "35986", + "proration": { + "rate": "0.99961", + "billing_period": { + "starts_at": "2023-11-11T08:50:20.664672Z", + "ends_at": "2023-12-11T08:33:04.443903Z" + } + }, + "totals": { + "subtotal": "29988", + "tax": "5998", + "total": "35986" + } + } + ], + "totals": { + "subtotal": "29988", + "tax": "5998", + "total": "35986", + "fee": "1837", + "earnings": "28151", + "currency_code": "USD" + }, + "payout_totals": { + "subtotal": "29988", + "tax": "5998", + "total": "35986", + "fee": "1837", + "earnings": "28151", + "currency_code": "USD" + }, + "created_at": "2023-08-11T14:15:01.225381Z", + "updated_at": "2023-08-11T14:15:03.54843Z" + }, + { + "id": "adj_01h468w41ttb2j2bh8av74gwt1", + "action": "credit", + "credit_applied_to_balance": true, + "transaction_id": "txn_01h468crc3b3fe98a5ft53recb", + "subscription_id": "sub_01h468kv3jhs5jk330gszncsgt", + "customer_id": "ctm_01h468k5wr7j4r6kf0790k9g3d", + "reason": "subscription_recurring", + "currency_code": "AUD", + "status": "pending_approval", + "items": [ + { + "id": "adjitm_01h468w41ttb2j2bh8avxtnfwf", + "item_id": "txnitm_01h468k649s7zk3dyyh3pbys2w", + "type": "proration", + "amount": "54993", + "proration": { + "rate": "0.99988", + "billing_period": { + "starts_at": "2023-06-30T13:46:23.963147Z", + "ends_at": "2023-07-30T13:41:51.571658Z" + } + }, + "totals": { + "subtotal": "49994", + "tax": "4999", + "total": "54993" + } + }, + { + "id": "adjitm_01h468w41ttb2j2bh8awm8zwtg", + "item_id": "txnitm_01h468k649s7zk3dyyh7ennjwq", + "type": "proration", + "amount": "21997", + "proration": { + "rate": "0.99988", + "billing_period": { + "starts_at": "2023-06-30T13:46:23.969169Z", + "ends_at": "2023-07-30T13:41:51.571658Z" + } + }, + "totals": { + "subtotal": "19997", + "tax": "2000", + "total": "21997" + } + } + ], + "totals": { + "subtotal": "69991", + "tax": "6999", + "total": "76990", + "fee": "3897", + "earnings": "66094", + "currency_code": "AUD" + }, + "payout_totals": { + "subtotal": "45120", + "tax": "4513", + "total": "49633", + "fee": "2512", + "earnings": "42608", + "currency_code": "USD" + }, + "created_at": "2023-06-30T13:46:24.240375Z", + "updated_at": "2023-06-30T13:46:24.240376Z" + } + ], + "meta": { + "request_id": "fb879865-79fc-4547-9df7-0edd92289cd5", + "pagination": { + "per_page": 5, + "next": "https://api.paddle.com/adjustments?after=adj_01h468w41ttb2j2bh8av74gwt1", + "has_more": false, + "estimated_total": 5 + } + } +} diff --git a/tests/Functional/Resources/Adjustments/_fixtures/response/minimal_entity.json b/tests/Functional/Resources/Adjustments/_fixtures/response/minimal_entity.json new file mode 100644 index 0000000..44c8b79 --- /dev/null +++ b/tests/Functional/Resources/Adjustments/_fixtures/response/minimal_entity.json @@ -0,0 +1,47 @@ +{ + "data": { + "id": "adj_01h8c65c2ggq5nxswnnwv78e75", + "action": "refund", + "transaction_id": "txn_01h8bxpvx398a7zbawb77y0kp5", + "subscription_id": "sub_01h8bxswamxysj44zt5n48njwh", + "customer_id": "ctm_01h8441jn5pcwrfhwh78jqt8hk", + "reason": "error", + "currency_code": "USD", + "status": "pending_approval", + "items": [ + { + "id": "adjitm_01h8c65c2ggq5nxswnnz4kxkkr", + "item_id": "txnitm_01h8bxryv3065dyh6103p3yg28", + "type": "partial", + "amount": "100", + "proration": null, + "totals": { + "subtotal": "92", + "tax": "8", + "total": "100" + } + } + ], + "totals": { + "subtotal": "92", + "tax": "8", + "total": "100", + "fee": "5", + "earnings": "87", + "currency_code": "USD" + }, + "payout_totals": { + "subtotal": "92", + "tax": "8", + "total": "100", + "fee": "5", + "earnings": "87", + "currency_code": "USD" + }, + "created_at": "2023-08-21T13:57:15.489634Z", + "updated_at": "0001-01-01T00:00:00Z" + }, + "meta": { + "request_id": "4ec9ff32-c88a-4eef-93b6-d8b99b5fba4b" + } +} diff --git a/tests/Functional/Resources/Businesses/BusinessesClientTest.php b/tests/Functional/Resources/Businesses/BusinessesClientTest.php new file mode 100644 index 0000000..4f4a59c --- /dev/null +++ b/tests/Functional/Resources/Businesses/BusinessesClientTest.php @@ -0,0 +1,245 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** + * @test + * + * @dataProvider createOperationsProvider + */ + public function it_uses_expected_payload_on_create( + CreateOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->businesses->create('ctm_01h844p3h41s12zs5mn4axja51', $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/customers/ctm_01h844p3h41s12zs5mn4axja51/businesses', + urldecode((string) $request->getUri()), + ); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function createOperationsProvider(): \Generator + { + yield 'Basic Create' => [ + new CreateOperation( + name: 'ChatApp Inc.', + ), + new Response(200, body: self::readRawJsonFixture('response/minimal_entity')), + self::readRawJsonFixture('request/create_basic'), + ]; + + yield 'Create with Data' => [ + new CreateOperation( + name: 'ChatApp Inc.', + companyNumber: '555775291485', + taxIdentifier: '555952383', + contacts: [ + new Contacts( + 'Parker Jones', + 'parker@example.com', + ), + ], + customData: new CustomData([ + 'customer_reference_id' => 'abcd1234', + ]), + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/create_full'), + ]; + } + + /** + * @test + * + * @dataProvider updateOperationsProvider + */ + public function it_uses_expected_payload_on_update( + UpdateOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->businesses->update('ctm_01h844p3h41s12zs5mn4axja51', 'biz_01h84a7hr4pzhsajkm8tev89ev', $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('PATCH', $request->getMethod()); + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/customers/ctm_01h844p3h41s12zs5mn4axja51/businesses/biz_01h84a7hr4pzhsajkm8tev89ev', + urldecode((string) $request->getUri()), + ); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function updateOperationsProvider(): \Generator + { + yield 'Update Single' => [ + new UpdateOperation(name: 'ChatApp Inc.'), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_single'), + ]; + + yield 'Update Partial' => [ + new UpdateOperation( + name: 'ChatApp Inc.', + contacts: [ + new Contacts('Parker Jones', 'parker@example.com'), + new Contacts('Jo Riley', 'jo@example.com'), + new Contacts('Jesse Garcia', 'jo@example.com'), + ], + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_partial'), + ]; + + yield 'Update All' => [ + new UpdateOperation( + name: 'ChatApp Inc.', + companyNumber: '555775291485', + taxIdentifier: '555952383', + contacts: [ + new Contacts('Parker Jones', 'parker@example.com'), + new Contacts('Jo Riley', 'jo@example.com'), + new Contacts('Jesse Garcia', 'jo@example.com'), + ], + customData: new CustomData([ + 'customer_reference_id' => 'abcd1234', + ]), + status: Status::Active, + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_full'), + ]; + } + + /** + * @test + * + * @dataProvider listOperationsProvider + */ + public function list_hits_expected_uri( + ListOperation $operation, + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->businesses->list('ctm_01h8441jn5pcwrfhwh78jqt8hk', $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function listOperationsProvider(): \Generator + { + yield 'Default' => [ + new ListOperation(), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/customers/ctm_01h8441jn5pcwrfhwh78jqt8hk/businesses', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Default Paged' => [ + new ListOperation(new Pager()), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/customers/ctm_01h8441jn5pcwrfhwh78jqt8hk/businesses?order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Default Paged with After' => [ + new ListOperation(new Pager(after: 'biz_01h84a7hr4pzhsajkm8tev89ev')), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/customers/ctm_01h8441jn5pcwrfhwh78jqt8hk/businesses?after=biz_01h84a7hr4pzhsajkm8tev89ev&order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'NotificationStatus Filtered' => [ + new ListOperation(statuses: [Status::Archived]), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/customers/ctm_01h8441jn5pcwrfhwh78jqt8hk/businesses?status=archived', Environment::SANDBOX->baseUrl()), + ]; + + yield 'ID Filtered' => [ + new ListOperation(ids: ['biz_01h84a7hr4pzhsajkm8tev89ev']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/customers/ctm_01h8441jn5pcwrfhwh78jqt8hk/businesses?id=biz_01h84a7hr4pzhsajkm8tev89ev', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Multiple ID Filtered' => [ + new ListOperation(ids: ['biz_01h84a7hr4pzhsajkm8tev89ev', 'biz_01hf6pv0tmnw1cs0bcj2z8nmqx']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/customers/ctm_01h8441jn5pcwrfhwh78jqt8hk/businesses?id=biz_01h84a7hr4pzhsajkm8tev89ev,biz_01hf6pv0tmnw1cs0bcj2z8nmqx', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Search Filtered' => [ + new ListOperation(search: 'ChatApp'), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/customers/ctm_01h8441jn5pcwrfhwh78jqt8hk/businesses?search=ChatApp', Environment::SANDBOX->baseUrl()), + ]; + } + + /** @test */ + public function get_hits_expected_uri(): void + { + $response = new Response(200, body: self::readRawJsonFixture('response/full_entity')); + $this->mockClient->addResponse($response); + $this->client->businesses->get('ctm_01h844p3h41s12zs5mn4axja51', 'biz_01h84a7hr4pzhsajkm8tev89ev'); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals( + sprintf('%s/customers/ctm_01h844p3h41s12zs5mn4axja51/businesses/biz_01h84a7hr4pzhsajkm8tev89ev', Environment::SANDBOX->baseUrl()), + urldecode((string) $request->getUri()), + ); + } +} diff --git a/tests/Functional/Resources/Businesses/_fixtures/request/create_basic.json b/tests/Functional/Resources/Businesses/_fixtures/request/create_basic.json new file mode 100644 index 0000000..f765e15 --- /dev/null +++ b/tests/Functional/Resources/Businesses/_fixtures/request/create_basic.json @@ -0,0 +1,3 @@ +{ + "name": "ChatApp Inc." +} diff --git a/tests/Functional/Resources/Businesses/_fixtures/request/create_full.json b/tests/Functional/Resources/Businesses/_fixtures/request/create_full.json new file mode 100644 index 0000000..7a2f546 --- /dev/null +++ b/tests/Functional/Resources/Businesses/_fixtures/request/create_full.json @@ -0,0 +1,14 @@ +{ + "name": "ChatApp Inc.", + "company_number": "555775291485", + "tax_identifier": "555952383", + "contacts": [ + { + "name": "Parker Jones", + "email": "parker@example.com" + } + ], + "custom_data": { + "customer_reference_id": "abcd1234" + } +} diff --git a/tests/Functional/Resources/Businesses/_fixtures/request/update_full.json b/tests/Functional/Resources/Businesses/_fixtures/request/update_full.json new file mode 100644 index 0000000..3abe51d --- /dev/null +++ b/tests/Functional/Resources/Businesses/_fixtures/request/update_full.json @@ -0,0 +1,23 @@ +{ + "name": "ChatApp Inc.", + "company_number": "555775291485", + "tax_identifier": "555952383", + "contacts": [ + { + "name": "Parker Jones", + "email": "parker@example.com" + }, + { + "name": "Jo Riley", + "email": "jo@example.com" + }, + { + "name": "Jesse Garcia", + "email": "jo@example.com" + } + ], + "custom_data": { + "customer_reference_id": "abcd1234" + }, + "status": "active" +} diff --git a/tests/Functional/Resources/Businesses/_fixtures/request/update_partial.json b/tests/Functional/Resources/Businesses/_fixtures/request/update_partial.json new file mode 100644 index 0000000..1275397 --- /dev/null +++ b/tests/Functional/Resources/Businesses/_fixtures/request/update_partial.json @@ -0,0 +1,17 @@ +{ + "name": "ChatApp Inc.", + "contacts": [ + { + "name": "Parker Jones", + "email": "parker@example.com" + }, + { + "name": "Jo Riley", + "email": "jo@example.com" + }, + { + "name": "Jesse Garcia", + "email": "jo@example.com" + } + ] +} diff --git a/tests/Functional/Resources/Businesses/_fixtures/request/update_single.json b/tests/Functional/Resources/Businesses/_fixtures/request/update_single.json new file mode 100644 index 0000000..f765e15 --- /dev/null +++ b/tests/Functional/Resources/Businesses/_fixtures/request/update_single.json @@ -0,0 +1,3 @@ +{ + "name": "ChatApp Inc." +} diff --git a/tests/Functional/Resources/Businesses/_fixtures/response/full_entity.json b/tests/Functional/Resources/Businesses/_fixtures/response/full_entity.json new file mode 100644 index 0000000..fc518ef --- /dev/null +++ b/tests/Functional/Resources/Businesses/_fixtures/response/full_entity.json @@ -0,0 +1,23 @@ +{ + "data": { + "id": "biz_01h84a7hr4pzhsajkm8tev89ev", + "status": "active", + "name": "ChatApp Inc.", + "company_number": "555775291485", + "tax_identifier": "555952383", + "contacts": [ + { + "name": "Parker Jones", + "email": "parker@example.com" + } + ], + "custom_data": { + "customer_reference_id": "abcd1234" + }, + "created_at": "2023-08-18T12:34:25.668Z", + "updated_at": "2023-08-18T12:34:25.668Z" + }, + "meta": { + "request_id": "6f8eb0a1-9c91-4bc9-9e05-ed72987e5e24" + } +} diff --git a/tests/Functional/Resources/Businesses/_fixtures/response/list_default.json b/tests/Functional/Resources/Businesses/_fixtures/response/list_default.json new file mode 100644 index 0000000..59625fb --- /dev/null +++ b/tests/Functional/Resources/Businesses/_fixtures/response/list_default.json @@ -0,0 +1,31 @@ +{ + "data": [ + { + "id": "biz_01h84a7hr4pzhsajkm8tev89ev", + "status": "active", + "name": "ChatApp Inc.", + "company_number": "555775291485", + "tax_identifier": "555952383", + "contacts": [ + { + "name": "Parker Jones", + "email": "parker@example.com" + } + ], + "custom_data": { + "customer_reference_id": "abcd1234" + }, + "created_at": "2023-08-18T12:34:25.668Z", + "updated_at": "2023-08-18T12:34:25.668Z" + } + ], + "meta": { + "request_id": "2e2c747d-82d7-46de-a9ba-3c3ea02294e7", + "pagination": { + "per_page": 50, + "next": "https://api.paddle.com/customers/ctm_01h8441jn5pcwrfhwh78jqt8hk/businesses?after=biz_01h84a7hr4pzhsajkm8tev89ev", + "has_more": false, + "estimated_total": 1 + } + } +} diff --git a/tests/Functional/Resources/Businesses/_fixtures/response/minimal_entity.json b/tests/Functional/Resources/Businesses/_fixtures/response/minimal_entity.json new file mode 100644 index 0000000..a2581c9 --- /dev/null +++ b/tests/Functional/Resources/Businesses/_fixtures/response/minimal_entity.json @@ -0,0 +1,16 @@ +{ + "data": { + "id": "biz_01h84a7hr4pzhsajkm8tev89ev", + "status": "active", + "name": "ChatApp Inc.", + "company_number": null, + "tax_identifier": null, + "contacts": [], + "custom_data": null, + "created_at": "2023-08-18T12:34:25.668Z", + "updated_at": "2023-08-18T12:34:25.668Z" + }, + "meta": { + "request_id": "6f8eb0a1-9c91-4bc9-9e05-ed72987e5e24" + } +} diff --git a/tests/Functional/Resources/Customers/CustomersClientTest.php b/tests/Functional/Resources/Customers/CustomersClientTest.php new file mode 100644 index 0000000..88a9477 --- /dev/null +++ b/tests/Functional/Resources/Customers/CustomersClientTest.php @@ -0,0 +1,240 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** + * @test + * + * @dataProvider createOperationsProvider + */ + public function it_uses_expected_payload_on_create( + CreateOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->customers->create($operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals(Environment::SANDBOX->baseUrl() . '/customers', urldecode((string) $request->getUri())); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function createOperationsProvider(): \Generator + { + yield 'Basic Create' => [ + new CreateOperation('test2@example.com'), + new Response(200, body: self::readRawJsonFixture('response/minimal_entity')), + self::readRawJsonFixture('request/create_basic'), + ]; + + yield 'Create with Data' => [ + new CreateOperation( + email: 'test2@example.com', + name: 'Alex Wilson', + customData: new CustomData(['customer_reference_id' => 'abcd1234']), + locale: 'en', + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/create_full'), + ]; + } + + /** + * @test + * + * @dataProvider updateOperationsProvider + */ + public function it_uses_expected_payload_on_update( + UpdateOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->customers->update('ctm_01h844p3h41s12zs5mn4axja51', $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('PATCH', $request->getMethod()); + self::assertEquals(Environment::SANDBOX->baseUrl() . '/customers/ctm_01h844p3h41s12zs5mn4axja51', urldecode((string) $request->getUri())); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function updateOperationsProvider(): \Generator + { + yield 'Update Single' => [ + new UpdateOperation(name: 'Alex Wilson'), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_single'), + ]; + + yield 'Update Partial' => [ + new UpdateOperation(name: 'Alex Wilson', email: 'test1@example.com'), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_partial'), + ]; + + yield 'Update All' => [ + new UpdateOperation( + name: 'Alex Wilson', + email: 'test1@example.com', + locale: 'el', + customData: new CustomData(['customer_reference_id' => 'abcd1234']), + status: Status::Active, + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_full'), + ]; + } + + /** + * @test + * + * @dataProvider listOperationsProvider + */ + public function list_hits_expected_uri( + ListOperation $operation, + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->customers->list($operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function listOperationsProvider(): \Generator + { + yield 'Default' => [ + new ListOperation(), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/customers', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Default Paged' => [ + new ListOperation(new Pager()), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/customers?order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Default Paged with After' => [ + new ListOperation(new Pager(after: 'ctm_01h8441jn5pcwrfhwh78jqt8hk')), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/customers?after=ctm_01h8441jn5pcwrfhwh78jqt8hk&order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'NotificationStatus Filtered' => [ + new ListOperation(statuses: [Status::Archived]), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/customers?status=archived', Environment::SANDBOX->baseUrl()), + ]; + + yield 'ID Filtered' => [ + new ListOperation(ids: ['ctm_01h8441jn5pcwrfhwh78jqt8hk']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/customers?id=ctm_01h8441jn5pcwrfhwh78jqt8hk', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Multiple ID Filtered' => [ + new ListOperation(ids: ['ctm_01h8441jn5pcwrfhwh78jqt8hk', 'ctm_01h844p3h41s12zs5mn4axja51']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/customers?id=ctm_01h8441jn5pcwrfhwh78jqt8hk,ctm_01h844p3h41s12zs5mn4axja51', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Search Filtered' => [ + new ListOperation(search: 'Alex'), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/customers?search=Alex', Environment::SANDBOX->baseUrl()), + ]; + } + + /** @test */ + public function get_hits_expected_uri(): void + { + $response = new Response(200, body: self::readRawJsonFixture('response/full_entity')); + $this->mockClient->addResponse($response); + $this->client->customers->get('ctm_01h8441jn5pcwrfhwh78jqt8hk'); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals( + sprintf('%s/customers/ctm_01h8441jn5pcwrfhwh78jqt8hk', Environment::SANDBOX->baseUrl()), + urldecode((string) $request->getUri()), + ); + } + + /** + * @test + * + * @dataProvider creditBalancesOperationsProvider + */ + public function credit_balances_hits_expected_uri( + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->customers->creditBalances('ctm_01h8441jn5pcwrfhwh78jqt8hk'); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function creditBalancesOperationsProvider(): \Generator + { + yield 'Default' => [ + new Response(200, body: self::readRawJsonFixture('response/list_credit_balances')), + sprintf('%s/customers/ctm_01h8441jn5pcwrfhwh78jqt8hk/credit-balances', Environment::SANDBOX->baseUrl()), + ]; + } +} diff --git a/tests/Functional/Resources/Customers/_fixtures/request/create_basic.json b/tests/Functional/Resources/Customers/_fixtures/request/create_basic.json new file mode 100644 index 0000000..67ba735 --- /dev/null +++ b/tests/Functional/Resources/Customers/_fixtures/request/create_basic.json @@ -0,0 +1,3 @@ +{ + "email": "test2@example.com" +} diff --git a/tests/Functional/Resources/Customers/_fixtures/request/create_full.json b/tests/Functional/Resources/Customers/_fixtures/request/create_full.json new file mode 100644 index 0000000..17017ae --- /dev/null +++ b/tests/Functional/Resources/Customers/_fixtures/request/create_full.json @@ -0,0 +1,8 @@ +{ + "email": "test2@example.com", + "name": "Alex Wilson", + "custom_data": { + "customer_reference_id": "abcd1234" + }, + "locale": "en" +} diff --git a/tests/Functional/Resources/Customers/_fixtures/request/update_full.json b/tests/Functional/Resources/Customers/_fixtures/request/update_full.json new file mode 100644 index 0000000..ec631be --- /dev/null +++ b/tests/Functional/Resources/Customers/_fixtures/request/update_full.json @@ -0,0 +1,9 @@ +{ + "status": "active", + "email": "test1@example.com", + "name": "Alex Wilson", + "custom_data": { + "customer_reference_id": "abcd1234" + }, + "locale": "el" +} diff --git a/tests/Functional/Resources/Customers/_fixtures/request/update_partial.json b/tests/Functional/Resources/Customers/_fixtures/request/update_partial.json new file mode 100644 index 0000000..85fff14 --- /dev/null +++ b/tests/Functional/Resources/Customers/_fixtures/request/update_partial.json @@ -0,0 +1,4 @@ +{ + "name": "Alex Wilson", + "email": "test1@example.com" +} diff --git a/tests/Functional/Resources/Customers/_fixtures/request/update_single.json b/tests/Functional/Resources/Customers/_fixtures/request/update_single.json new file mode 100644 index 0000000..54ca580 --- /dev/null +++ b/tests/Functional/Resources/Customers/_fixtures/request/update_single.json @@ -0,0 +1,3 @@ +{ + "name": "Alex Wilson" +} diff --git a/tests/Functional/Resources/Customers/_fixtures/response/full_entity.json b/tests/Functional/Resources/Customers/_fixtures/response/full_entity.json new file mode 100644 index 0000000..e43fe2f --- /dev/null +++ b/tests/Functional/Resources/Customers/_fixtures/response/full_entity.json @@ -0,0 +1,18 @@ +{ + "data": { + "id": "ctm_01h844p3h41s12zs5mn4axja51", + "status": "active", + "custom_data": { + "customer_reference_id": "abcd1234" + }, + "name": "Alex Wilson", + "email": "test2@example.com", + "marketing_consent": false, + "locale": "en", + "created_at": "2023-08-18T10:57:31.172Z", + "updated_at": "2023-08-18T10:57:31.172Z" + }, + "meta": { + "request_id": "43210d8e-a945-4c20-885d-6cf08fd1fc78" + } +} diff --git a/tests/Functional/Resources/Customers/_fixtures/response/list_credit_balances.json b/tests/Functional/Resources/Customers/_fixtures/response/list_credit_balances.json new file mode 100644 index 0000000..0190f74 --- /dev/null +++ b/tests/Functional/Resources/Customers/_fixtures/response/list_credit_balances.json @@ -0,0 +1,16 @@ +{ + "data": [ + { + "customer_id": "ctm_01gw9m680k848184fpttwr0b7z", + "currency_code": "USD", + "balance": { + "available": "550", + "reserved": "900", + "used": "1300" + } + } + ], + "meta": { + "request_id": "32cf1966-ed49-47d6-a76a-a9b8f7843245" + } +} diff --git a/tests/Functional/Resources/Customers/_fixtures/response/list_default.json b/tests/Functional/Resources/Customers/_fixtures/response/list_default.json new file mode 100644 index 0000000..edacf04 --- /dev/null +++ b/tests/Functional/Resources/Customers/_fixtures/response/list_default.json @@ -0,0 +1,48 @@ +{ + "data": [ + { + "id": "ctm_01h844q4mznqpgqgm6evgw1w63", + "status": "active", + "custom_data": { + "customer_reference_id": "abcd1234" + }, + "name": "Jamie Price", + "email": "test3@example.com", + "marketing_consent": false, + "locale": "en", + "created_at": "2023-08-18T10:58:05.087Z", + "updated_at": "2023-08-18T10:58:05.087Z" + }, + { + "id": "ctm_01h844p3h41s12zs5mn4axja51", + "status": "active", + "custom_data": null, + "name": "Alex Wilson", + "email": "test2@example.com", + "marketing_consent": false, + "locale": "en", + "created_at": "2023-08-18T10:57:31.172Z", + "updated_at": "2023-08-18T10:57:31.172Z" + }, + { + "id": "ctm_01h8441jn5pcwrfhwh78jqt8hk", + "status": "active", + "custom_data": null, + "name": "Sam Miller", + "email": "sam@example.com", + "marketing_consent": false, + "locale": "en", + "created_at": "2023-08-18T10:46:18.533Z", + "updated_at": "2023-08-18T10:46:18.533Z" + } + ], + "meta": { + "request_id": "91d95663-e2df-4769-a81b-b6f340b782c9", + "pagination": { + "per_page": 50, + "next": "https://api.paddle.com/customers?after=ctm_01h8441jn5pcwrfhwh78jqt8hk", + "has_more": false, + "estimated_total": 3 + } + } +} diff --git a/tests/Functional/Resources/Customers/_fixtures/response/minimal_entity.json b/tests/Functional/Resources/Customers/_fixtures/response/minimal_entity.json new file mode 100644 index 0000000..0f082a7 --- /dev/null +++ b/tests/Functional/Resources/Customers/_fixtures/response/minimal_entity.json @@ -0,0 +1,16 @@ +{ + "data": { + "id": "ctm_01h844p3h41s12zs5mn4axja51", + "status": "active", + "custom_data": null, + "name": null, + "email": "test2@example.com", + "marketing_consent": false, + "locale": "en", + "created_at": "2023-11-02T13:41:44.366Z", + "updated_at": "2023-11-02T13:41:44.366Z" + }, + "meta": { + "request_id": "90ee95f7-d6af-4218-ae09-2514344dbc5b" + } +} diff --git a/tests/Functional/Resources/Discounts/DiscountsClientTest.php b/tests/Functional/Resources/Discounts/DiscountsClientTest.php new file mode 100644 index 0000000..464d1c8 --- /dev/null +++ b/tests/Functional/Resources/Discounts/DiscountsClientTest.php @@ -0,0 +1,246 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** + * @test + * + * @dataProvider createOperationsProvider + */ + public function it_uses_expected_payload_on_create( + CreateOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->discounts->create($operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals(Environment::SANDBOX->baseUrl() . '/discounts', urldecode((string) $request->getUri())); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function createOperationsProvider(): \Generator + { + yield 'Basic Create' => [ + new CreateOperation( + '10', + 'Nonprofit discount', + DiscountType::Percentage, + true, + true, + CurrencyCode::USD, + ), + new Response(200, body: self::readRawJsonFixture('response/minimal_entity')), + self::readRawJsonFixture('request/create_basic'), + ]; + + yield 'Create with Data' => [ + new CreateOperation( + '10', + 'Nonprofit discount', + DiscountType::Percentage, + true, + true, + CurrencyCode::USD, + 'ABCDE12345', + 5, + 1000, + ['pro_01gsz4t5hdjse780zja8vvr7jg', 'pro_01gsz4s0w61y0pp88528f1wvvb'], + '2025-01-01 10:00:00', + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/create_full'), + ]; + } + + /** + * @test + * + * @dataProvider updateOperationsProvider + */ + public function it_uses_expected_payload_on_update( + UpdateOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->discounts->update('dsc_01h83xenpcfjyhkqr4x214m02x', $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('PATCH', $request->getMethod()); + self::assertEquals(Environment::SANDBOX->baseUrl() . '/discounts/dsc_01h83xenpcfjyhkqr4x214m02x', urldecode((string) $request->getUri())); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function updateOperationsProvider(): \Generator + { + yield 'Update Single' => [ + new UpdateOperation(enabledForCheckout: false), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_single'), + ]; + + yield 'Update Partial' => [ + new UpdateOperation(enabledForCheckout: false, code: null), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_partial'), + ]; + + yield 'Update All' => [ + new UpdateOperation( + description: 'Nonprofit discount', + enabledForCheckout: true, + type: DiscountType::Percentage, + amount: '10', + recur: true, + currencyCode: CurrencyCode::USD, + code: 'ABCDE12345', + maximumRecurringIntervals: 5, + usageLimit: 1000, + restrictTo: [ + 'pro_01gsz4t5hdjse780zja8vvr7jg', + 'pro_01gsz4s0w61y0pp88528f1wvvb', + ], + expiresAt: '2025-01-01 10:00:00', + status: DiscountStatus::Active, + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_full'), + ]; + } + + /** + * @test + * + * @dataProvider listOperationsProvider + */ + public function list_hits_expected_uri( + ListOperation $operation, + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->discounts->list($operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function listOperationsProvider(): \Generator + { + yield 'Default' => [ + new ListOperation(), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/discounts', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Default Paged' => [ + new ListOperation(new Pager()), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/discounts?order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Default Paged with After' => [ + new ListOperation(new Pager(after: 'dsc_01h83xenpcfjyhkqr4x214m02x')), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/discounts?after=dsc_01h83xenpcfjyhkqr4x214m02x&order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'NotificationStatus Filtered' => [ + new ListOperation(statuses: [Status::Archived]), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/discounts?status=archived', Environment::SANDBOX->baseUrl()), + ]; + + yield 'ID Filtered' => [ + new ListOperation(ids: ['dsc_01h83xenpcfjyhkqr4x214m02x']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/discounts?id=dsc_01h83xenpcfjyhkqr4x214m02x', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Multiple ID Filtered' => [ + new ListOperation(ids: ['dsc_01h83xenpcfjyhkqr4x214m02x', 'dsc_01gtgraak4chyhnp47rrdv89ad']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/discounts?id=dsc_01h83xenpcfjyhkqr4x214m02x,dsc_01gtgraak4chyhnp47rrdv89ad', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Code Filtered' => [ + new ListOperation(codes: ['ABCDE12345']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/discounts?code=ABCDE12345', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Multiple Code Filtered' => [ + new ListOperation(codes: ['ABCDE12345', '54321EDCBA']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/discounts?code=ABCDE12345,54321EDCBA', Environment::SANDBOX->baseUrl()), + ]; + } + + /** @test */ + public function get_hits_expected_uri(): void + { + $response = new Response(200, body: self::readRawJsonFixture('response/full_entity')); + $this->mockClient->addResponse($response); + $this->client->discounts->get('dsc_01h83xenpcfjyhkqr4x214m02x'); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals( + sprintf('%s/discounts/dsc_01h83xenpcfjyhkqr4x214m02x', Environment::SANDBOX->baseUrl()), + urldecode((string) $request->getUri()), + ); + } +} diff --git a/tests/Functional/Resources/Discounts/_fixtures/request/create_basic.json b/tests/Functional/Resources/Discounts/_fixtures/request/create_basic.json new file mode 100644 index 0000000..7c4b001 --- /dev/null +++ b/tests/Functional/Resources/Discounts/_fixtures/request/create_basic.json @@ -0,0 +1,8 @@ +{ + "description": "Nonprofit discount", + "enabled_for_checkout": true, + "type": "percentage", + "amount": "10", + "recur": true, + "currency_code": "USD" +} diff --git a/tests/Functional/Resources/Discounts/_fixtures/request/create_full.json b/tests/Functional/Resources/Discounts/_fixtures/request/create_full.json new file mode 100644 index 0000000..9367b24 --- /dev/null +++ b/tests/Functional/Resources/Discounts/_fixtures/request/create_full.json @@ -0,0 +1,16 @@ +{ + "description": "Nonprofit discount", + "enabled_for_checkout": true, + "type": "percentage", + "amount": "10", + "recur": true, + "currency_code": "USD", + "code": "ABCDE12345", + "maximum_recurring_intervals": 5, + "usage_limit": 1000, + "restrict_to": [ + "pro_01gsz4t5hdjse780zja8vvr7jg", + "pro_01gsz4s0w61y0pp88528f1wvvb" + ], + "expires_at": "2025-01-01 10:00:00" +} diff --git a/tests/Functional/Resources/Discounts/_fixtures/request/update_full.json b/tests/Functional/Resources/Discounts/_fixtures/request/update_full.json new file mode 100644 index 0000000..4b4874b --- /dev/null +++ b/tests/Functional/Resources/Discounts/_fixtures/request/update_full.json @@ -0,0 +1,17 @@ +{ + "description": "Nonprofit discount", + "enabled_for_checkout": true, + "type": "percentage", + "amount": "10", + "recur": true, + "currency_code": "USD", + "code": "ABCDE12345", + "maximum_recurring_intervals": 5, + "usage_limit": 1000, + "restrict_to": [ + "pro_01gsz4t5hdjse780zja8vvr7jg", + "pro_01gsz4s0w61y0pp88528f1wvvb" + ], + "expires_at": "2025-01-01 10:00:00", + "status": "active" +} diff --git a/tests/Functional/Resources/Discounts/_fixtures/request/update_partial.json b/tests/Functional/Resources/Discounts/_fixtures/request/update_partial.json new file mode 100644 index 0000000..7703f3d --- /dev/null +++ b/tests/Functional/Resources/Discounts/_fixtures/request/update_partial.json @@ -0,0 +1,4 @@ +{ + "enabled_for_checkout": false, + "code": null +} diff --git a/tests/Functional/Resources/Discounts/_fixtures/request/update_single.json b/tests/Functional/Resources/Discounts/_fixtures/request/update_single.json new file mode 100644 index 0000000..8bc3fc7 --- /dev/null +++ b/tests/Functional/Resources/Discounts/_fixtures/request/update_single.json @@ -0,0 +1,3 @@ +{ + "enabled_for_checkout": false +} diff --git a/tests/Functional/Resources/Discounts/_fixtures/response/full_entity.json b/tests/Functional/Resources/Discounts/_fixtures/response/full_entity.json new file mode 100644 index 0000000..d965596 --- /dev/null +++ b/tests/Functional/Resources/Discounts/_fixtures/response/full_entity.json @@ -0,0 +1,26 @@ +{ + "data": { + "id": "dsc_01h83xenpcfjyhkqr4x214m02x", + "status": "active", + "description": "Nonprofit discount", + "enabled_for_checkout": true, + "code": "ABCDE12345", + "type": "percentage", + "amount": "10", + "currency_code": "USD", + "recur": true, + "maximum_recurring_intervals": 5, + "usage_limit": 1000, + "restrict_to": [ + "pro_01gsz4t5hdjse780zja8vvr7jg", + "pro_01gsz4s0w61y0pp88528f1wvvb" + ], + "expires_at": "2025-01-01 10:00:00", + "times_used": 0, + "created_at": "2023-08-18T08:51:07.596Z", + "updated_at": "2023-08-18T08:51:07.596Z" + }, + "meta": { + "request_id": "badf3eb7-3eb8-4d4a-92f9-bc798790b7bc" + } +} diff --git a/tests/Functional/Resources/Discounts/_fixtures/response/list_default.json b/tests/Functional/Resources/Discounts/_fixtures/response/list_default.json new file mode 100644 index 0000000..99957dc --- /dev/null +++ b/tests/Functional/Resources/Discounts/_fixtures/response/list_default.json @@ -0,0 +1,92 @@ +{ + "data": [ + { + "id": "dsc_01gtf15svsqzgp9325ss4ebmwt", + "status": "active", + "description": "Introductory offer: $10 off per user for Pro", + "enabled_for_checkout": true, + "code": "10OFFPRO", + "type": "flat_per_seat", + "amount": "1000", + "currency_code": "USD", + "recur": false, + "maximum_recurring_intervals": null, + "usage_limit": null, + "restrict_to": ["pri_01gsz8x8sawmvhz1pv30nge1ke"], + "expires_at": null, + "times_used": 0, + "custom_data": null, + "created_at": "2023-03-01T16:48:04.473Z", + "updated_at": "2023-08-18T09:22:14.563Z" + }, + { + "id": "dsc_01gtgraak4chyhnp47rrdv89ad", + "status": "active", + "description": "Introductory offer: 50% off first 3 months", + "enabled_for_checkout": true, + "code": "98XFAUR91R", + "type": "percentage", + "amount": "50", + "currency_code": null, + "recur": true, + "maximum_recurring_intervals": 3, + "usage_limit": null, + "restrict_to": [ + "pri_01gsz8ntc6z7npqqp6j4ys0w1w", + "pri_01gsz8x8sawmvhz1pv30nge1ke" + ], + "expires_at": null, + "times_used": 0, + "custom_data": null, + "created_at": "2023-03-02T08:51:44.356Z", + "updated_at": "2023-08-18T09:23:08.058Z" + }, + { + "id": "dsc_01gtgztp8fpchantd5g1wrksa3", + "status": "active", + "description": "Black Friday 2024", + "enabled_for_checkout": true, + "code": "BF2024", + "type": "percentage", + "amount": "10", + "currency_code": null, + "recur": false, + "maximum_recurring_intervals": null, + "usage_limit": 1000, + "restrict_to": null, + "expires_at": "2024-12-03T00:00:00Z", + "times_used": 0, + "custom_data": null, + "created_at": "2023-03-02T11:03:00.623Z", + "updated_at": "2023-08-18T09:40:44.463Z" + }, + { + "id": "dsc_01h83xenpcfjyhkqr4x214m02x", + "status": "active", + "description": "Nonprofit discount", + "enabled_for_checkout": false, + "code": null, + "type": "percentage", + "amount": "10", + "currency_code": null, + "recur": true, + "maximum_recurring_intervals": null, + "usage_limit": null, + "restrict_to": ["pri_01h83xenpcfjyhkqr4x214m02x"], + "expires_at": null, + "times_used": 0, + "custom_data": null, + "created_at": "2023-08-18T08:51:07.596Z", + "updated_at": "2023-08-18T09:05:17.05Z" + } + ], + "meta": { + "request_id": "f802da6d-096f-4cab-abd1-6c98330d4b09", + "pagination": { + "per_page": 50, + "next": "https://api.paddle.com/discounts?after=dsc_01h83xenpcfjyhkqr4x214m02x", + "has_more": false, + "estimated_total": 4 + } + } +} diff --git a/tests/Functional/Resources/Discounts/_fixtures/response/minimal_entity.json b/tests/Functional/Resources/Discounts/_fixtures/response/minimal_entity.json new file mode 100644 index 0000000..dde7e6c --- /dev/null +++ b/tests/Functional/Resources/Discounts/_fixtures/response/minimal_entity.json @@ -0,0 +1,27 @@ +{ + "data": { + "id": "dsc_01h83xenpcfjyhkqr4x214m02x", + "status": "active", + "description": "Nonprofit discount", + "enabled_for_checkout": true, + "code": "4Y3ZR24Q4R", + "type": "percentage", + "amount": "10", + "currency_code": "USD", + "recur": true, + "maximum_recurring_intervals": null, + "usage_limit": null, + "restrict_to": [ + "pro_01gsz4t5hdjse780zja8vvr7jg", + "pro_01gsz4s0w61y0pp88528f1wvvb" + ], + "expires_at": null, + "times_used": 0, + "custom_data": null, + "created_at": "2023-08-18T08:51:07.596Z", + "updated_at": "2023-08-18T08:51:07.596Z" + }, + "meta": { + "request_id": "badf3eb7-3eb8-4d4a-92f9-bc798790b7bc" + } +} diff --git a/tests/Functional/Resources/EventTypes/EventTypesClientTest.php b/tests/Functional/Resources/EventTypes/EventTypesClientTest.php new file mode 100644 index 0000000..dfa6eb0 --- /dev/null +++ b/tests/Functional/Resources/EventTypes/EventTypesClientTest.php @@ -0,0 +1,47 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** @test */ + public function list_hits_expected_uri(): void + { + $expectedUri = sprintf('%s/event-types', Environment::SANDBOX->baseUrl()); + + $this->mockClient->addResponse( + new Response(200, body: self::readRawJsonFixture('response/list_default')), + ); + $this->client->eventTypes->list(); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } +} diff --git a/tests/Functional/Resources/EventTypes/_fixtures/response/list_default.json b/tests/Functional/Resources/EventTypes/_fixtures/response/list_default.json new file mode 100644 index 0000000..d6cc464 --- /dev/null +++ b/tests/Functional/Resources/EventTypes/_fixtures/response/list_default.json @@ -0,0 +1,255 @@ +{ + "data": [ + { + "name": "transaction.billed", + "description": "Occurs when a transaction is billed. Its status field changes to billed and billed_at is populated.", + "group": "Transaction", + "available_versions": [ + 1 + ] + }, + { + "name": "transaction.canceled", + "description": "Occurs when a transaction is canceled. Its status field changes to canceled.", + "group": "Transaction", + "available_versions": [ + 1 + ] + }, + { + "name": "transaction.completed", + "description": "Occurs when a transaction is completed. Its status field changes to completed.", + "group": "Transaction", + "available_versions": [ + 1 + ] + }, + { + "name": "transaction.created", + "description": "Occurs when a transaction is created.", + "group": "Transaction", + "available_versions": [ + 1 + ] + }, + { + "name": "transaction.past_due", + "description": "Occurs when a transaction becomes past due. Its status field changes to past_due.", + "group": "Transaction", + "available_versions": [ + 1 + ] + }, + { + "name": "transaction.payment_failed", + "description": "Occurs when a payment fails for a transaction. The payments array is updated with details of the payment attempt.", + "group": "Transaction", + "available_versions": [ + 1 + ] + }, + { + "name": "transaction.ready", + "description": "Occurs when a transaction is ready to be billed. Its status field changes to ready.", + "group": "Transaction", + "available_versions": [ + 1 + ] + }, + { + "name": "transaction.updated", + "description": "Occurs when a transaction is updated.", + "group": "Transaction", + "available_versions": [ + 1 + ] + }, + { + "name": "subscription.activated", + "description": "Occurs when a subscription becomes active. Its status field changes to active. This means any trial period has elapsed and Paddle has successfully billed the customer.", + "group": "Subscription", + "available_versions": [ + 1 + ] + }, + { + "name": "subscription.canceled", + "description": "Occurs when a subscription is canceled. Its status field changes to canceled.", + "group": "Subscription", + "available_versions": [ + 1 + ] + }, + { + "name": "subscription.created", + "description": "Occurs when a subscription is created. subscription.trialing or subscription.activated typically follow.", + "group": "Subscription", + "available_versions": [ + 1 + ] + }, + { + "name": "subscription.imported", + "description": "Occurs when a subscription is imported.", + "group": "Subscription", + "available_versions": [ + 1 + ] + }, + { + "name": "subscription.past_due", + "description": "Occurs when a subscription has an unpaid transaction. Its status changes to past_due.", + "group": "Subscription", + "available_versions": [ + 1 + ] + }, + { + "name": "subscription.paused", + "description": "Occurs when a subscription is paused. Its status field changes to paused.", + "group": "Subscription", + "available_versions": [ + 1 + ] + }, + { + "name": "subscription.resumed", + "description": "Occurs when a subscription is resumed after being paused. Its status field changes to active.", + "group": "Subscription", + "available_versions": [ + 1 + ] + }, + { + "name": "subscription.trialing", + "description": "Occurs when a subscription enters trial period.", + "group": "Subscription", + "available_versions": [ + 1 + ] + }, + { + "name": "subscription.updated", + "description": "Occurs when a subscription is updated.", + "group": "Subscription", + "available_versions": [ + 1 + ] + }, + { + "name": "product.created", + "description": "Occurs when a product is created.", + "group": "Product", + "available_versions": [ + 1 + ] + }, + { + "name": "product.updated", + "description": "Occurs when a product is updated.", + "group": "Product", + "available_versions": [ + 1 + ] + }, + { + "name": "price.created", + "description": "Occurs when a price is created.", + "group": "Price", + "available_versions": [ + 1 + ] + }, + { + "name": "price.updated", + "description": "Occurs when a price is updated.", + "group": "Price", + "available_versions": [ + 1 + ] + }, + { + "name": "discount.created", + "description": "Occurs when a discount is created.", + "group": "Discount", + "available_versions": [ + 1 + ] + }, + { + "name": "discount.updated", + "description": "Occurs when a discount is updated.", + "group": "Discount", + "available_versions": [ + 1 + ] + }, + { + "name": "customer.created", + "description": "Occurs when a customer is created.", + "group": "Customer", + "available_versions": [ + 1 + ] + }, + { + "name": "customer.updated", + "description": "Occurs when a customer is updated.", + "group": "Customer", + "available_versions": [ + 1 + ] + }, + { + "name": "address.created", + "description": "Occurs when an address is created.", + "group": "Address", + "available_versions": [ + 1 + ] + }, + { + "name": "address.updated", + "description": "Occurs when an address is updated.", + "group": "Address", + "available_versions": [ + 1 + ] + }, + { + "name": "business.created", + "description": "Occurs when a business is created.", + "group": "Business", + "available_versions": [ + 1 + ] + }, + { + "name": "business.updated", + "description": "Occurs when a business is updated.", + "group": "Business", + "available_versions": [ + 1 + ] + }, + { + "name": "adjustment.created", + "description": "Occurs when an adjustment is created.", + "group": "Adjustment", + "available_versions": [ + 1 + ] + }, + { + "name": "adjustment.updated", + "description": "Occurs when an adjustment is updated, the only time an adjustment will be updated is when the status changes from pending to approved or from pending to rejected.", + "group": "Adjustment", + "available_versions": [ + 1 + ] + } + ], + "meta": { + "request_id": "83fad52c-fcd1-440e-a222-16b727e5262b" + } +} diff --git a/tests/Functional/Resources/Events/EventsClientTest.php b/tests/Functional/Resources/Events/EventsClientTest.php new file mode 100644 index 0000000..09956d1 --- /dev/null +++ b/tests/Functional/Resources/Events/EventsClientTest.php @@ -0,0 +1,80 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** + * @test + * + * @dataProvider listOperationsProvider + */ + public function list_hits_expected_uri( + ListOperation $operation, + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->events->list($operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function listOperationsProvider(): \Generator + { + yield 'Default' => [ + new ListOperation(), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/events', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Default Paged' => [ + new ListOperation(new Pager()), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/events?order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Default Paged with After' => [ + new ListOperation(new Pager(after: 'evt_01h83xenpcfjyhkqr4x214m02x')), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/events?after=evt_01h83xenpcfjyhkqr4x214m02x&order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + } +} diff --git a/tests/Functional/Resources/Events/_fixtures/response/list_default.json b/tests/Functional/Resources/Events/_fixtures/response/list_default.json new file mode 100644 index 0000000..54f36f6 --- /dev/null +++ b/tests/Functional/Resources/Events/_fixtures/response/list_default.json @@ -0,0 +1,2777 @@ +{ + "data": [ + { + "event_id": "evt_01hg0trvbgjfp0avfam8a2yzq1", + "event_type": "subscription.updated", + "occurred_at": "2023-11-24T14:12:06.640975Z", + "data": { + "id": "sub_01gyssnczp81czs49zcprm6hfv", + "status": "past_due", + "customer_id": "ctm_01gyssmfx5rnmk4dt8qx88v0ee", + "address_id": "add_01gyssmfzbaxg9e6tkazmt02ba", + "business_id": null, + "currency_code": "GBP", + "created_at": "2023-04-24T14:11:13.014122Z", + "updated_at": "2023-11-24T14:12:05.533Z", + "started_at": "2023-04-24T14:11:11.447004Z", + "first_billed_at": "2023-04-24T14:11:11.447004Z", + "next_billed_at": "2023-12-24T14:11:11.447004Z", + "paused_at": null, + "canceled_at": null, + "discount": null, + "collection_mode": "automatic", + "billing_details": null, + "current_billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + }, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "scheduled_change": null, + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "tax_mode": "account_setting", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3600", + "currency_code": null + }, + "description": "Monthly (per seat)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + } + }, + "status": "active", + "quantity": 10, + "recurring": true, + "created_at": "2023-04-24T14:11:13.014124Z", + "updated_at": "2023-11-24T14:12:05.528Z", + "trial_dates": null, + "next_billed_at": "2023-12-24T14:11:11.447004Z", + "previously_billed_at": "2023-11-24T14:11:11.447004Z" + }, + { + "price": { + "id": "pri_01gsz95g2zrkagg294kpstx54r", + "tax_mode": "account_setting", + "product_id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "unit_price": { + "amount": "30000", + "currency_code": null + }, + "description": "Monthly (recurring addon)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + } + }, + "status": "active", + "quantity": 1, + "recurring": true, + "created_at": "2023-04-24T14:11:13.014128Z", + "updated_at": "2023-11-24T14:12:05.53Z", + "trial_dates": null, + "next_billed_at": "2023-12-24T14:11:11.447004Z", + "previously_billed_at": "2023-11-24T14:11:11.447004Z" + } + ], + "custom_data": null, + "import_meta": null + } + }, + { + "event_id": "evt_01hg0trtbnd4jz0h6y6yg0jjv6", + "event_type": "transaction.past_due", + "occurred_at": "2023-11-24T14:12:05.621593Z", + "data": { + "id": "txn_01hg0trpqvp70evgmzj1648z5q", + "status": "past_due", + "customer_id": "ctm_01gyssmfx5rnmk4dt8qx88v0ee", + "address_id": "add_01gyssmfzbaxg9e6tkazmt02ba", + "business_id": null, + "custom_data": null, + "currency_code": "GBP", + "origin": "subscription_recurring", + "subscription_id": "sub_01gyssnczp81czs49zcprm6hfv", + "invoice_id": null, + "invoice_number": null, + "collection_mode": "automatic", + "discount_id": null, + "billing_details": null, + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + }, + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 10 + }, + "tax_mode": "account_setting", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "GBP" + }, + "custom_data": null, + "description": "Monthly (per seat)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [ + { + "unit_price": { + "amount": "4500", + "currency_code": "USD" + }, + "country_codes": [ + "US" + ] + }, + { + "unit_price": { + "amount": "8000", + "currency_code": "PLN" + }, + "country_codes": [ + "PL" + ] + }, + { + "unit_price": { + "amount": "100500", + "currency_code": "INR" + }, + "country_codes": [ + "IN" + ] + }, + { + "unit_price": { + "amount": "40000", + "currency_code": "UAH" + }, + "country_codes": [ + "UA" + ] + }, + { + "unit_price": { + "amount": "1400", + "currency_code": "USD" + }, + "country_codes": [ + "NG" + ] + } + ] + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + } + } + }, + { + "price": { + "id": "pri_01gsz95g2zrkagg294kpstx54r", + "status": "active", + "quantity": { + "maximum": 1, + "minimum": 1 + }, + "tax_mode": "account_setting", + "product_id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "unit_price": { + "amount": "25000", + "currency_code": "GBP" + }, + "custom_data": null, + "description": "Monthly (recurring addon)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [ + { + "unit_price": { + "amount": "28500", + "currency_code": "USD" + }, + "country_codes": [ + "US" + ] + }, + { + "unit_price": { + "amount": "66000", + "currency_code": "PLN" + }, + "country_codes": [ + "PL" + ] + }, + { + "unit_price": { + "amount": "837500", + "currency_code": "INR" + }, + "country_codes": [ + "IN" + ] + }, + { + "unit_price": { + "amount": "335000", + "currency_code": "UAH" + }, + "country_codes": [ + "UA" + ] + }, + { + "unit_price": { + "amount": "12000", + "currency_code": "USD" + }, + "country_codes": [ + "NG" + ] + } + ] + }, + "price_id": "pri_01gsz95g2zrkagg294kpstx54r", + "quantity": 1, + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + } + } + } + ], + "details": { + "totals": { + "fee": null, + "tax": "11000", + "total": "66000", + "credit": "0", + "balance": "66000", + "discount": "0", + "earnings": null, + "subtotal": "55000", + "grand_total": "66000", + "currency_code": "GBP", + "credit_to_balance": "0" + }, + "line_items": [ + { + "id": "txnitm_01hg0trpt1k1h3c98j8gyvf1ma", + "totals": { + "tax": "6000", + "total": "36000", + "discount": "0", + "subtotal": "30000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "status": "active", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "custom_data": { + "features": { + "crm": true, + "reports": true, + "data_retention": false + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of advanced reporting and a CRM that's right where you're chatting." + }, + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "tax_rate": "0.2", + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + } + }, + "unit_totals": { + "tax": "600", + "total": "3600", + "discount": "0", + "subtotal": "3000" + } + }, + { + "id": "txnitm_01hg0trpt1k1h3c98j8qv8k0kw", + "totals": { + "tax": "5000", + "total": "30000", + "discount": "0", + "subtotal": "25000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "name": "VIP support", + "status": "active", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "custom_data": null, + "description": "Get exclusive access to our expert team of product specialists, available to help you make the most of your ChatApp subscription.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz95g2zrkagg294kpstx54r", + "quantity": 1, + "tax_rate": "0.2", + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + } + }, + "unit_totals": { + "tax": "5000", + "total": "30000", + "discount": "0", + "subtotal": "25000" + } + } + ], + "payout_totals": null, + "tax_rates_used": [ + { + "totals": { + "tax": "11000", + "total": "66000", + "discount": "0", + "subtotal": "55000" + }, + "tax_rate": "0.2" + } + ], + "adjusted_totals": { + "fee": "0", + "tax": "11000", + "total": "66000", + "earnings": "0", + "subtotal": "55000", + "grand_total": "66000", + "currency_code": "GBP" + } + }, + "payments": [ + { + "amount": "66000", + "status": "error", + "created_at": "2023-11-24T14:12:02.096324Z", + "error_code": "authentication_failed", + "captured_at": null, + "method_details": { + "card": { + "type": "visa", + "last4": "3184", + "expiry_year": 2024, + "expiry_month": 1, + "cardholder_name": "Michael" + }, + "type": "card" + }, + "payment_attempt_id": "afa07161-601d-4a65-8c1a-6993ae1ae027", + "stored_payment_method_id": "d3218a38-1cff-44a7-8a47-6a809a5ae9ad" + } + ], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01hg0trpqvp70evgmzj1648z5q" + }, + "created_at": "2023-11-24T14:12:02.004032Z", + "updated_at": "2023-11-24T14:12:02.004032Z", + "billed_at": "2023-11-24T14:12:01.915193Z" + } + }, + { + "event_id": "evt_01hg0trtavsvbmrdaz5vhekm5w", + "event_type": "transaction.updated", + "occurred_at": "2023-11-24T14:12:05.596257Z", + "data": { + "id": "txn_01hg0trpqvp70evgmzj1648z5q", + "status": "past_due", + "customer_id": "ctm_01gyssmfx5rnmk4dt8qx88v0ee", + "address_id": "add_01gyssmfzbaxg9e6tkazmt02ba", + "business_id": null, + "custom_data": null, + "currency_code": "GBP", + "origin": "subscription_recurring", + "subscription_id": "sub_01gyssnczp81czs49zcprm6hfv", + "invoice_id": null, + "invoice_number": null, + "collection_mode": "automatic", + "discount_id": null, + "billing_details": null, + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + }, + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 10 + }, + "tax_mode": "account_setting", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "GBP" + }, + "custom_data": null, + "description": "Monthly (per seat)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [ + { + "unit_price": { + "amount": "4500", + "currency_code": "USD" + }, + "country_codes": [ + "US" + ] + }, + { + "unit_price": { + "amount": "8000", + "currency_code": "PLN" + }, + "country_codes": [ + "PL" + ] + }, + { + "unit_price": { + "amount": "100500", + "currency_code": "INR" + }, + "country_codes": [ + "IN" + ] + }, + { + "unit_price": { + "amount": "40000", + "currency_code": "UAH" + }, + "country_codes": [ + "UA" + ] + }, + { + "unit_price": { + "amount": "1400", + "currency_code": "USD" + }, + "country_codes": [ + "NG" + ] + } + ] + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + } + } + }, + { + "price": { + "id": "pri_01gsz95g2zrkagg294kpstx54r", + "status": "active", + "quantity": { + "maximum": 1, + "minimum": 1 + }, + "tax_mode": "account_setting", + "product_id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "unit_price": { + "amount": "25000", + "currency_code": "GBP" + }, + "custom_data": null, + "description": "Monthly (recurring addon)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [ + { + "unit_price": { + "amount": "28500", + "currency_code": "USD" + }, + "country_codes": [ + "US" + ] + }, + { + "unit_price": { + "amount": "66000", + "currency_code": "PLN" + }, + "country_codes": [ + "PL" + ] + }, + { + "unit_price": { + "amount": "837500", + "currency_code": "INR" + }, + "country_codes": [ + "IN" + ] + }, + { + "unit_price": { + "amount": "335000", + "currency_code": "UAH" + }, + "country_codes": [ + "UA" + ] + }, + { + "unit_price": { + "amount": "12000", + "currency_code": "USD" + }, + "country_codes": [ + "NG" + ] + } + ] + }, + "price_id": "pri_01gsz95g2zrkagg294kpstx54r", + "quantity": 1, + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + } + } + } + ], + "details": { + "totals": { + "fee": null, + "tax": "11000", + "total": "66000", + "credit": "0", + "balance": "66000", + "discount": "0", + "earnings": null, + "subtotal": "55000", + "grand_total": "66000", + "currency_code": "GBP", + "credit_to_balance": "0" + }, + "line_items": [ + { + "id": "txnitm_01hg0trpt1k1h3c98j8gyvf1ma", + "totals": { + "tax": "6000", + "total": "36000", + "discount": "0", + "subtotal": "30000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "status": "active", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "custom_data": { + "features": { + "crm": true, + "reports": true, + "data_retention": false + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of advanced reporting and a CRM that's right where you're chatting." + }, + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "tax_rate": "0.2", + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + } + }, + "unit_totals": { + "tax": "600", + "total": "3600", + "discount": "0", + "subtotal": "3000" + } + }, + { + "id": "txnitm_01hg0trpt1k1h3c98j8qv8k0kw", + "totals": { + "tax": "5000", + "total": "30000", + "discount": "0", + "subtotal": "25000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "name": "VIP support", + "status": "active", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "custom_data": null, + "description": "Get exclusive access to our expert team of product specialists, available to help you make the most of your ChatApp subscription.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz95g2zrkagg294kpstx54r", + "quantity": 1, + "tax_rate": "0.2", + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + } + }, + "unit_totals": { + "tax": "5000", + "total": "30000", + "discount": "0", + "subtotal": "25000" + } + } + ], + "payout_totals": null, + "tax_rates_used": [ + { + "totals": { + "tax": "11000", + "total": "66000", + "discount": "0", + "subtotal": "55000" + }, + "tax_rate": "0.2" + } + ], + "adjusted_totals": { + "fee": "0", + "tax": "11000", + "total": "66000", + "earnings": "0", + "subtotal": "55000", + "grand_total": "66000", + "currency_code": "GBP" + } + }, + "payments": [ + { + "amount": "66000", + "status": "error", + "created_at": "2023-11-24T14:12:02.096324Z", + "error_code": "authentication_failed", + "captured_at": null, + "method_details": { + "card": { + "type": "visa", + "last4": "3184", + "expiry_year": 2024, + "expiry_month": 1, + "cardholder_name": "Michael" + }, + "type": "card" + }, + "payment_attempt_id": "afa07161-601d-4a65-8c1a-6993ae1ae027", + "stored_payment_method_id": "d3218a38-1cff-44a7-8a47-6a809a5ae9ad" + } + ], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01hg0trpqvp70evgmzj1648z5q" + }, + "created_at": "2023-11-24T14:12:02.004032Z", + "updated_at": "2023-11-24T14:12:02.004032Z", + "billed_at": "2023-11-24T14:12:01.915193Z" + } + }, + { + "event_id": "evt_01hg0trtagdz34hgnyvdz31j9e", + "event_type": "transaction.payment_failed", + "occurred_at": "2023-11-24T14:12:05.584247Z", + "data": { + "id": "txn_01hg0trpqvp70evgmzj1648z5q", + "status": "billed", + "customer_id": "ctm_01gyssmfx5rnmk4dt8qx88v0ee", + "address_id": "add_01gyssmfzbaxg9e6tkazmt02ba", + "business_id": null, + "custom_data": null, + "currency_code": "GBP", + "origin": "subscription_recurring", + "subscription_id": "sub_01gyssnczp81czs49zcprm6hfv", + "invoice_id": null, + "invoice_number": null, + "collection_mode": "automatic", + "discount_id": null, + "billing_details": null, + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + }, + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 10 + }, + "tax_mode": "account_setting", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "GBP" + }, + "custom_data": null, + "description": "Monthly (per seat)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [ + { + "unit_price": { + "amount": "4500", + "currency_code": "USD" + }, + "country_codes": [ + "US" + ] + }, + { + "unit_price": { + "amount": "8000", + "currency_code": "PLN" + }, + "country_codes": [ + "PL" + ] + }, + { + "unit_price": { + "amount": "100500", + "currency_code": "INR" + }, + "country_codes": [ + "IN" + ] + }, + { + "unit_price": { + "amount": "40000", + "currency_code": "UAH" + }, + "country_codes": [ + "UA" + ] + }, + { + "unit_price": { + "amount": "1400", + "currency_code": "USD" + }, + "country_codes": [ + "NG" + ] + } + ] + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + } + } + }, + { + "price": { + "id": "pri_01gsz95g2zrkagg294kpstx54r", + "status": "active", + "quantity": { + "maximum": 1, + "minimum": 1 + }, + "tax_mode": "account_setting", + "product_id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "unit_price": { + "amount": "25000", + "currency_code": "GBP" + }, + "custom_data": null, + "description": "Monthly (recurring addon)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [ + { + "unit_price": { + "amount": "28500", + "currency_code": "USD" + }, + "country_codes": [ + "US" + ] + }, + { + "unit_price": { + "amount": "66000", + "currency_code": "PLN" + }, + "country_codes": [ + "PL" + ] + }, + { + "unit_price": { + "amount": "837500", + "currency_code": "INR" + }, + "country_codes": [ + "IN" + ] + }, + { + "unit_price": { + "amount": "335000", + "currency_code": "UAH" + }, + "country_codes": [ + "UA" + ] + }, + { + "unit_price": { + "amount": "12000", + "currency_code": "USD" + }, + "country_codes": [ + "NG" + ] + } + ] + }, + "price_id": "pri_01gsz95g2zrkagg294kpstx54r", + "quantity": 1, + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + } + } + } + ], + "details": { + "totals": { + "fee": null, + "tax": "11000", + "total": "66000", + "credit": "0", + "balance": "66000", + "discount": "0", + "earnings": null, + "subtotal": "55000", + "grand_total": "66000", + "currency_code": "GBP", + "credit_to_balance": "0" + }, + "line_items": [ + { + "id": "txnitm_01hg0trpt1k1h3c98j8gyvf1ma", + "totals": { + "tax": "6000", + "total": "36000", + "discount": "0", + "subtotal": "30000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "status": "active", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "custom_data": { + "features": { + "crm": true, + "reports": true, + "data_retention": false + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of advanced reporting and a CRM that's right where you're chatting." + }, + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "tax_rate": "0.2", + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + } + }, + "unit_totals": { + "tax": "600", + "total": "3600", + "discount": "0", + "subtotal": "3000" + } + }, + { + "id": "txnitm_01hg0trpt1k1h3c98j8qv8k0kw", + "totals": { + "tax": "5000", + "total": "30000", + "discount": "0", + "subtotal": "25000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "name": "VIP support", + "status": "active", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "custom_data": null, + "description": "Get exclusive access to our expert team of product specialists, available to help you make the most of your ChatApp subscription.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz95g2zrkagg294kpstx54r", + "quantity": 1, + "tax_rate": "0.2", + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + } + }, + "unit_totals": { + "tax": "5000", + "total": "30000", + "discount": "0", + "subtotal": "25000" + } + } + ], + "payout_totals": null, + "tax_rates_used": [ + { + "totals": { + "tax": "11000", + "total": "66000", + "discount": "0", + "subtotal": "55000" + }, + "tax_rate": "0.2" + } + ], + "adjusted_totals": { + "fee": "0", + "tax": "11000", + "total": "66000", + "earnings": "0", + "subtotal": "55000", + "grand_total": "66000", + "currency_code": "GBP" + } + }, + "payments": [ + { + "amount": "66000", + "status": "error", + "created_at": "2023-11-24T14:12:02.096324Z", + "error_code": "authentication_failed", + "captured_at": null, + "method_details": { + "card": { + "type": "visa", + "last4": "3184", + "expiry_year": 2024, + "expiry_month": 1, + "cardholder_name": "Michael" + }, + "type": "card" + }, + "payment_attempt_id": "afa07161-601d-4a65-8c1a-6993ae1ae027", + "stored_payment_method_id": "d3218a38-1cff-44a7-8a47-6a809a5ae9ad" + } + ], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01hg0trpqvp70evgmzj1648z5q" + }, + "created_at": "2023-11-24T14:12:02.004032Z", + "updated_at": "2023-11-24T14:12:02.004032Z", + "billed_at": "2023-11-24T14:12:01.915193Z" + } + }, + { + "event_id": "evt_01hg0trqj5q888jba20v662gsg", + "event_type": "transaction.billed", + "occurred_at": "2023-11-24T14:12:02.757703Z", + "data": { + "id": "txn_01hg0trpqvp70evgmzj1648z5q", + "status": "billed", + "customer_id": "ctm_01gyssmfx5rnmk4dt8qx88v0ee", + "address_id": "add_01gyssmfzbaxg9e6tkazmt02ba", + "business_id": null, + "custom_data": null, + "currency_code": "GBP", + "origin": "subscription_recurring", + "subscription_id": "sub_01gyssnczp81czs49zcprm6hfv", + "invoice_id": null, + "invoice_number": null, + "collection_mode": "automatic", + "discount_id": null, + "billing_details": null, + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + }, + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 10 + }, + "tax_mode": "account_setting", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "GBP" + }, + "custom_data": null, + "description": "Monthly (per seat)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [ + { + "unit_price": { + "amount": "4500", + "currency_code": "USD" + }, + "country_codes": [ + "US" + ] + }, + { + "unit_price": { + "amount": "8000", + "currency_code": "PLN" + }, + "country_codes": [ + "PL" + ] + }, + { + "unit_price": { + "amount": "100500", + "currency_code": "INR" + }, + "country_codes": [ + "IN" + ] + }, + { + "unit_price": { + "amount": "40000", + "currency_code": "UAH" + }, + "country_codes": [ + "UA" + ] + }, + { + "unit_price": { + "amount": "1400", + "currency_code": "USD" + }, + "country_codes": [ + "NG" + ] + } + ] + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + } + } + }, + { + "price": { + "id": "pri_01gsz95g2zrkagg294kpstx54r", + "status": "active", + "quantity": { + "maximum": 1, + "minimum": 1 + }, + "tax_mode": "account_setting", + "product_id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "unit_price": { + "amount": "25000", + "currency_code": "GBP" + }, + "custom_data": null, + "description": "Monthly (recurring addon)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [ + { + "unit_price": { + "amount": "28500", + "currency_code": "USD" + }, + "country_codes": [ + "US" + ] + }, + { + "unit_price": { + "amount": "66000", + "currency_code": "PLN" + }, + "country_codes": [ + "PL" + ] + }, + { + "unit_price": { + "amount": "837500", + "currency_code": "INR" + }, + "country_codes": [ + "IN" + ] + }, + { + "unit_price": { + "amount": "335000", + "currency_code": "UAH" + }, + "country_codes": [ + "UA" + ] + }, + { + "unit_price": { + "amount": "12000", + "currency_code": "USD" + }, + "country_codes": [ + "NG" + ] + } + ] + }, + "price_id": "pri_01gsz95g2zrkagg294kpstx54r", + "quantity": 1, + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + } + } + } + ], + "details": { + "totals": { + "fee": null, + "tax": "11000", + "total": "66000", + "credit": "0", + "balance": "66000", + "discount": "0", + "earnings": null, + "subtotal": "55000", + "grand_total": "66000", + "currency_code": "GBP", + "credit_to_balance": "0" + }, + "line_items": [ + { + "id": "txnitm_01hg0trpt1k1h3c98j8gyvf1ma", + "totals": { + "tax": "6000", + "total": "36000", + "discount": "0", + "subtotal": "30000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "status": "active", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "custom_data": { + "features": { + "crm": true, + "reports": true, + "data_retention": false + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of advanced reporting and a CRM that's right where you're chatting." + }, + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "tax_rate": "0.2", + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + } + }, + "unit_totals": { + "tax": "600", + "total": "3600", + "discount": "0", + "subtotal": "3000" + } + }, + { + "id": "txnitm_01hg0trpt1k1h3c98j8qv8k0kw", + "totals": { + "tax": "5000", + "total": "30000", + "discount": "0", + "subtotal": "25000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "name": "VIP support", + "status": "active", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "custom_data": null, + "description": "Get exclusive access to our expert team of product specialists, available to help you make the most of your ChatApp subscription.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz95g2zrkagg294kpstx54r", + "quantity": 1, + "tax_rate": "0.2", + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + } + }, + "unit_totals": { + "tax": "5000", + "total": "30000", + "discount": "0", + "subtotal": "25000" + } + } + ], + "payout_totals": null, + "tax_rates_used": [ + { + "totals": { + "tax": "11000", + "total": "66000", + "discount": "0", + "subtotal": "55000" + }, + "tax_rate": "0.2" + } + ], + "adjusted_totals": { + "fee": "0", + "tax": "11000", + "total": "66000", + "earnings": "0", + "subtotal": "55000", + "grand_total": "66000", + "currency_code": "GBP" + } + }, + "payments": [], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01hg0trpqvp70evgmzj1648z5q" + }, + "created_at": "2023-11-24T14:12:02.004032433Z", + "updated_at": "2023-11-24T14:12:02.004032433Z", + "billed_at": "2023-11-24T14:12:01.915193036Z" + } + }, + { + "event_id": "evt_01hg0trqf2gc8g6pyqy2xnzwg6", + "event_type": "transaction.created", + "occurred_at": "2023-11-24T14:12:02.664829Z", + "data": { + "id": "txn_01hg0trpqvp70evgmzj1648z5q", + "status": "billed", + "customer_id": "ctm_01gyssmfx5rnmk4dt8qx88v0ee", + "address_id": "add_01gyssmfzbaxg9e6tkazmt02ba", + "business_id": null, + "custom_data": null, + "currency_code": "GBP", + "origin": "subscription_recurring", + "subscription_id": "sub_01gyssnczp81czs49zcprm6hfv", + "invoice_id": null, + "invoice_number": null, + "collection_mode": "automatic", + "discount_id": null, + "billing_details": null, + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + }, + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 10 + }, + "tax_mode": "account_setting", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "GBP" + }, + "custom_data": null, + "description": "Monthly (per seat)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [ + { + "unit_price": { + "amount": "4500", + "currency_code": "USD" + }, + "country_codes": [ + "US" + ] + }, + { + "unit_price": { + "amount": "8000", + "currency_code": "PLN" + }, + "country_codes": [ + "PL" + ] + }, + { + "unit_price": { + "amount": "100500", + "currency_code": "INR" + }, + "country_codes": [ + "IN" + ] + }, + { + "unit_price": { + "amount": "40000", + "currency_code": "UAH" + }, + "country_codes": [ + "UA" + ] + }, + { + "unit_price": { + "amount": "1400", + "currency_code": "USD" + }, + "country_codes": [ + "NG" + ] + } + ] + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + } + } + }, + { + "price": { + "id": "pri_01gsz95g2zrkagg294kpstx54r", + "status": "active", + "quantity": { + "maximum": 1, + "minimum": 1 + }, + "tax_mode": "account_setting", + "product_id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "unit_price": { + "amount": "25000", + "currency_code": "GBP" + }, + "custom_data": null, + "description": "Monthly (recurring addon)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [ + { + "unit_price": { + "amount": "28500", + "currency_code": "USD" + }, + "country_codes": [ + "US" + ] + }, + { + "unit_price": { + "amount": "66000", + "currency_code": "PLN" + }, + "country_codes": [ + "PL" + ] + }, + { + "unit_price": { + "amount": "837500", + "currency_code": "INR" + }, + "country_codes": [ + "IN" + ] + }, + { + "unit_price": { + "amount": "335000", + "currency_code": "UAH" + }, + "country_codes": [ + "UA" + ] + }, + { + "unit_price": { + "amount": "12000", + "currency_code": "USD" + }, + "country_codes": [ + "NG" + ] + } + ] + }, + "price_id": "pri_01gsz95g2zrkagg294kpstx54r", + "quantity": 1, + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + } + } + } + ], + "details": { + "totals": { + "fee": null, + "tax": "11000", + "total": "66000", + "credit": "0", + "balance": "66000", + "discount": "0", + "earnings": null, + "subtotal": "55000", + "grand_total": "66000", + "currency_code": "GBP", + "credit_to_balance": "0" + }, + "line_items": [ + { + "id": "txnitm_01hg0trpt1k1h3c98j8gyvf1ma", + "totals": { + "tax": "6000", + "total": "36000", + "discount": "0", + "subtotal": "30000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "status": "active", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "custom_data": { + "features": { + "crm": true, + "reports": true, + "data_retention": false + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of advanced reporting and a CRM that's right where you're chatting." + }, + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "tax_rate": "0.2", + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + } + }, + "unit_totals": { + "tax": "600", + "total": "3600", + "discount": "0", + "subtotal": "3000" + } + }, + { + "id": "txnitm_01hg0trpt1k1h3c98j8qv8k0kw", + "totals": { + "tax": "5000", + "total": "30000", + "discount": "0", + "subtotal": "25000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "name": "VIP support", + "status": "active", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "custom_data": null, + "description": "Get exclusive access to our expert team of product specialists, available to help you make the most of your ChatApp subscription.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz95g2zrkagg294kpstx54r", + "quantity": 1, + "tax_rate": "0.2", + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + } + }, + "unit_totals": { + "tax": "5000", + "total": "30000", + "discount": "0", + "subtotal": "25000" + } + } + ], + "payout_totals": null, + "tax_rates_used": [ + { + "totals": { + "tax": "11000", + "total": "66000", + "discount": "0", + "subtotal": "55000" + }, + "tax_rate": "0.2" + } + ], + "adjusted_totals": { + "fee": "0", + "tax": "11000", + "total": "66000", + "earnings": "0", + "subtotal": "55000", + "grand_total": "66000", + "currency_code": "GBP" + } + }, + "payments": [], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01hg0trpqvp70evgmzj1648z5q" + }, + "created_at": "2023-11-24T14:12:02.004032433Z", + "updated_at": "2023-11-24T14:12:02.004032433Z", + "billed_at": "2023-11-24T14:12:01.915193036Z" + } + }, + { + "event_id": "evt_01hg0trpmmdkkdbk4p8czp4drm", + "event_type": "subscription.updated", + "occurred_at": "2023-11-24T14:12:01.813044Z", + "data": { + "id": "sub_01gyssnczp81czs49zcprm6hfv", + "status": "past_due", + "customer_id": "ctm_01gyssmfx5rnmk4dt8qx88v0ee", + "address_id": "add_01gyssmfzbaxg9e6tkazmt02ba", + "business_id": null, + "currency_code": "GBP", + "created_at": "2023-04-24T14:11:13.014122Z", + "updated_at": "2023-11-24T14:12:00.876Z", + "started_at": "2023-04-24T14:11:11.447004Z", + "first_billed_at": "2023-04-24T14:11:11.447004Z", + "next_billed_at": "2023-12-24T14:11:11.447004Z", + "paused_at": null, + "canceled_at": null, + "discount": null, + "collection_mode": "automatic", + "billing_details": null, + "current_billing_period": { + "ends_at": "2023-12-24T14:11:11.447004Z", + "starts_at": "2023-11-24T14:11:11.447004Z" + }, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "scheduled_change": null, + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "tax_mode": "account_setting", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3600", + "currency_code": null + }, + "description": "Monthly (per seat)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + } + }, + "status": "active", + "quantity": 10, + "recurring": true, + "created_at": "2023-04-24T14:11:13.014124Z", + "updated_at": "2023-11-24T14:12:00.877Z", + "trial_dates": null, + "next_billed_at": "2023-12-24T14:11:11.447004Z", + "previously_billed_at": "2023-11-24T14:11:11.447004Z" + }, + { + "price": { + "id": "pri_01gsz95g2zrkagg294kpstx54r", + "tax_mode": "account_setting", + "product_id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "unit_price": { + "amount": "30000", + "currency_code": null + }, + "description": "Monthly (recurring addon)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + } + }, + "status": "active", + "quantity": 1, + "recurring": true, + "created_at": "2023-04-24T14:11:13.014128Z", + "updated_at": "2023-11-24T14:12:00.88Z", + "trial_dates": null, + "next_billed_at": "2023-12-24T14:11:11.447004Z", + "previously_billed_at": "2023-11-24T14:11:11.447004Z" + } + ], + "custom_data": null, + "import_meta": null + } + }, + { + "event_id": "evt_01hfzvc6v4005wad5dcgtbewv9", + "event_type": "transaction.created", + "occurred_at": "2023-11-24T05:03:26.564980Z", + "data": { + "id": "txn_01hfzvc6e6zqc0eehgqhjsfx5b", + "status": "draft", + "customer_id": null, + "address_id": null, + "business_id": null, + "custom_data": null, + "currency_code": "USD", + "origin": "web", + "subscription_id": null, + "invoice_id": null, + "invoice_number": null, + "collection_mode": "automatic", + "discount_id": null, + "billing_details": null, + "billing_period": null, + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 1 + }, + "tax_mode": "account_setting", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "custom_data": { + "features": { + "crm": true, + "reports": true, + "data_retention": false + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of advanced reporting and a CRM that's right where you're chatting." + }, + "description": "Monthly (per seat)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [ + { + "unit_price": { + "amount": "5000", + "currency_code": "AUD" + }, + "country_codes": [ + "AU" + ] + } + ] + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "proration": null + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "status": "active", + "quantity": { + "maximum": 100, + "minimum": 1 + }, + "tax_mode": "account_setting", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "custom_data": null, + "description": "Monthly (recurring addon)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [ + { + "unit_price": { + "amount": "20000", + "currency_code": "AUD" + }, + "country_codes": [ + "AU", + "AT", + "BE" + ] + } + ] + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "proration": null + }, + { + "price": { + "id": "pri_01gsz98e27ak2tyhexptwc58yk", + "status": "active", + "quantity": { + "maximum": 1, + "minimum": 1 + }, + "tax_mode": "account_setting", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "custom_data": null, + "description": "One-time charge", + "trial_period": null, + "billing_cycle": null, + "unit_price_overrides": [ + { + "unit_price": { + "amount": "40000", + "currency_code": "AUD" + }, + "country_codes": [ + "AU" + ] + } + ] + }, + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "proration": null + } + ], + "details": { + "totals": { + "fee": null, + "tax": "3594", + "total": "63494", + "credit": "0", + "balance": "63494", + "discount": "0", + "earnings": null, + "subtotal": "59900", + "grand_total": "63494", + "currency_code": "USD", + "credit_to_balance": "0" + }, + "line_items": [ + { + "id": "txnitm_01hfzvc6gm4b7pkce40gtnxrr2", + "totals": { + "tax": "1800", + "total": "31800", + "discount": "0", + "subtotal": "30000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "status": "active", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "custom_data": { + "features": { + "crm": true, + "reports": true, + "data_retention": false + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of advanced reporting and a CRM that's right where you're chatting." + }, + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "tax_rate": "0.06", + "unit_totals": { + "tax": "180", + "total": "3180", + "discount": "0", + "subtotal": "3000" + } + }, + { + "id": "txnitm_01hfzvc6gm4b7pkce40pe6sj5p", + "totals": { + "tax": "600", + "total": "10600", + "discount": "0", + "subtotal": "10000" + }, + "item_id": null, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Voice rooms addon", + "status": "active", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/GcZzBjXRfiraensppgtQ_icon2.png", + "custom_data": null, + "description": "Create voice rooms in your chats to work in real time alongside your colleagues. Includes unlimited voice rooms and recording backup for compliance.", + "tax_category": "standard" + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "tax_rate": "0.06", + "unit_totals": { + "tax": "600", + "total": "10600", + "discount": "0", + "subtotal": "10000" + } + }, + { + "id": "txnitm_01hfzvc6gm4b7pkce40st7wfvh", + "totals": { + "tax": "1194", + "total": "21094", + "discount": "0", + "subtotal": "19900" + }, + "item_id": null, + "product": { + "id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "name": "Custom domains", + "status": "active", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "custom_data": { + "crm_id": "ABC" + }, + "description": "Make ChatApp truly your own with custom domains! Custom domains reinforce your brand identity and make it easy for your team to access ChatApp.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "tax_rate": "0.06", + "unit_totals": { + "tax": "1194", + "total": "21094", + "discount": "0", + "subtotal": "19900" + } + } + ], + "payout_totals": null, + "tax_rates_used": [ + { + "totals": { + "tax": "3594", + "total": "63494", + "discount": "0", + "subtotal": "59900" + }, + "tax_rate": "0.06" + } + ], + "adjusted_totals": { + "fee": "0", + "tax": "3594", + "total": "63494", + "earnings": "0", + "subtotal": "59900", + "grand_total": "63494", + "currency_code": "USD" + } + }, + "payments": [], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01hfzvc6e6zqc0eehgqhjsfx5b" + }, + "created_at": "2023-11-24T05:03:26.244748839Z", + "updated_at": "2023-11-24T05:03:26.244748839Z", + "billed_at": null + } + }, + { + "event_id": "evt_01hfyd0v4xpqdypnyf55gnn58g", + "event_type": "transaction.updated", + "occurred_at": "2023-11-23T15:33:19.645701Z", + "data": { + "id": "txn_01hfyd09vas8qwq6jw7k6yd9rg", + "status": "completed", + "customer_id": "ctm_01gyswd1xrzxsxghdtc2f8jhep", + "address_id": "add_01gyswev1m6gq9evx9akpw48d0", + "business_id": "biz_01gyswg3q9m0gzvh4sgy4zb7yw", + "custom_data": null, + "currency_code": "GBP", + "origin": "subscription_recurring", + "subscription_id": "sub_01gyswnfgtehe0f6mvggzza8qk", + "invoice_id": "inv_01hfyd0j9xfv64vfthhrbxmpe0", + "invoice_number": "325-10290", + "collection_mode": "automatic", + "discount_id": null, + "billing_details": null, + "billing_period": { + "ends_at": "2024-05-24T15:07:40.922338Z", + "starts_at": "2024-04-24T15:07:40.922338Z" + }, + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 10 + }, + "tax_mode": "account_setting", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "GBP" + }, + "custom_data": null, + "description": "Monthly (per seat)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [ + { + "unit_price": { + "amount": "4500", + "currency_code": "USD" + }, + "country_codes": [ + "US" + ] + }, + { + "unit_price": { + "amount": "8000", + "currency_code": "PLN" + }, + "country_codes": [ + "PL" + ] + }, + { + "unit_price": { + "amount": "100500", + "currency_code": "INR" + }, + "country_codes": [ + "IN" + ] + }, + { + "unit_price": { + "amount": "40000", + "currency_code": "UAH" + }, + "country_codes": [ + "UA" + ] + }, + { + "unit_price": { + "amount": "1400", + "currency_code": "USD" + }, + "country_codes": [ + "NG" + ] + } + ] + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2024-05-24T15:07:40.922338Z", + "starts_at": "2024-04-24T15:07:40.922338Z" + } + } + }, + { + "price": { + "id": "pri_01gsz95g2zrkagg294kpstx54r", + "status": "active", + "quantity": { + "maximum": 1, + "minimum": 1 + }, + "tax_mode": "account_setting", + "product_id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "unit_price": { + "amount": "25000", + "currency_code": "GBP" + }, + "custom_data": null, + "description": "Monthly (recurring addon)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [ + { + "unit_price": { + "amount": "28500", + "currency_code": "USD" + }, + "country_codes": [ + "US" + ] + }, + { + "unit_price": { + "amount": "66000", + "currency_code": "PLN" + }, + "country_codes": [ + "PL" + ] + }, + { + "unit_price": { + "amount": "837500", + "currency_code": "INR" + }, + "country_codes": [ + "IN" + ] + }, + { + "unit_price": { + "amount": "335000", + "currency_code": "UAH" + }, + "country_codes": [ + "UA" + ] + }, + { + "unit_price": { + "amount": "12000", + "currency_code": "USD" + }, + "country_codes": [ + "NG" + ] + } + ] + }, + "price_id": "pri_01gsz95g2zrkagg294kpstx54r", + "quantity": 1, + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2024-05-24T15:07:40.922338Z", + "starts_at": "2024-04-24T15:07:40.922338Z" + } + } + } + ], + "details": { + "totals": { + "fee": "3340", + "tax": "11000", + "total": "66000", + "credit": "0", + "balance": "0", + "discount": "0", + "earnings": "51660", + "subtotal": "55000", + "grand_total": "66000", + "currency_code": "GBP", + "credit_to_balance": "0" + }, + "line_items": [ + { + "id": "txnitm_01hfyd09xrkdkbaacs934g8v8p", + "totals": { + "tax": "6000", + "total": "36000", + "discount": "0", + "subtotal": "30000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "status": "active", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "custom_data": { + "features": { + "crm": true, + "reports": true, + "data_retention": false + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of advanced reporting and a CRM that's right where you're chatting." + }, + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "tax_rate": "0.2", + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2024-05-24T15:07:40.922338Z", + "starts_at": "2024-04-24T15:07:40.922338Z" + } + }, + "unit_totals": { + "tax": "600", + "total": "3600", + "discount": "0", + "subtotal": "3000" + } + }, + { + "id": "txnitm_01hfyd09xrkdkbaacs98bjfgfj", + "totals": { + "tax": "5000", + "total": "30000", + "discount": "0", + "subtotal": "25000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "name": "VIP support", + "status": "active", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "custom_data": null, + "description": "Get exclusive access to our expert team of product specialists, available to help you make the most of your ChatApp subscription.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz95g2zrkagg294kpstx54r", + "quantity": 1, + "tax_rate": "0.2", + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2024-05-24T15:07:40.922338Z", + "starts_at": "2024-04-24T15:07:40.922338Z" + } + }, + "unit_totals": { + "tax": "5000", + "total": "30000", + "discount": "0", + "subtotal": "25000" + } + } + ], + "payout_totals": { + "fee": "4102", + "tax": "13511", + "total": "81066", + "credit": "0", + "balance": "0", + "discount": "0", + "earnings": "63453", + "fee_rate": "0.05", + "subtotal": "67555", + "grand_total": "81066", + "currency_code": "USD", + "exchange_rate": "1.2282731999999998", + "credit_to_balance": "0" + }, + "tax_rates_used": [ + { + "totals": { + "tax": "11000", + "total": "66000", + "discount": "0", + "subtotal": "55000" + }, + "tax_rate": "0.2" + } + ], + "adjusted_totals": { + "fee": "3340", + "tax": "11000", + "total": "66000", + "earnings": "51660", + "subtotal": "55000", + "grand_total": "66000", + "currency_code": "GBP" + } + }, + "payments": [ + { + "amount": "66000", + "status": "captured", + "created_at": "2023-11-23T15:33:02.177841Z", + "error_code": null, + "captured_at": "2023-11-23T15:33:04.574587Z", + "method_details": { + "card": { + "type": "visa", + "last4": "4242", + "expiry_year": 2024, + "expiry_month": 1, + "cardholder_name": "Michael McGovern" + }, + "type": "card" + }, + "payment_attempt_id": "0bec49bd-dd4d-45e3-a91e-46992b09c5e2", + "stored_payment_method_id": "8009a718-30f7-4718-a7f6-4d3fdecf559b" + } + ], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01hfyd09vas8qwq6jw7k6yd9rg" + }, + "created_at": "2023-11-23T15:33:02.036155Z", + "updated_at": "2023-11-23T15:33:13.358282Z", + "billed_at": "2023-11-23T15:33:01.930479Z" + } + }, + { + "event_id": "evt_01hfyd0v4xppkwmjaca5xyzh5d", + "event_type": "transaction.completed", + "occurred_at": "2023-11-23T15:33:19.645134Z", + "data": { + "id": "txn_01hfyd09vas8qwq6jw7k6yd9rg", + "status": "completed", + "customer_id": "ctm_01gyswd1xrzxsxghdtc2f8jhep", + "address_id": "add_01gyswev1m6gq9evx9akpw48d0", + "business_id": "biz_01gyswg3q9m0gzvh4sgy4zb7yw", + "custom_data": null, + "currency_code": "GBP", + "origin": "subscription_recurring", + "subscription_id": "sub_01gyswnfgtehe0f6mvggzza8qk", + "invoice_id": "inv_01hfyd0j9xfv64vfthhrbxmpe0", + "invoice_number": "325-10290", + "collection_mode": "automatic", + "discount_id": null, + "billing_details": null, + "billing_period": { + "ends_at": "2024-05-24T15:07:40.922338Z", + "starts_at": "2024-04-24T15:07:40.922338Z" + }, + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 10 + }, + "tax_mode": "account_setting", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "GBP" + }, + "custom_data": null, + "description": "Monthly (per seat)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [ + { + "unit_price": { + "amount": "4500", + "currency_code": "USD" + }, + "country_codes": [ + "US" + ] + }, + { + "unit_price": { + "amount": "8000", + "currency_code": "PLN" + }, + "country_codes": [ + "PL" + ] + }, + { + "unit_price": { + "amount": "100500", + "currency_code": "INR" + }, + "country_codes": [ + "IN" + ] + }, + { + "unit_price": { + "amount": "40000", + "currency_code": "UAH" + }, + "country_codes": [ + "UA" + ] + }, + { + "unit_price": { + "amount": "1400", + "currency_code": "USD" + }, + "country_codes": [ + "NG" + ] + } + ] + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2024-05-24T15:07:40.922338Z", + "starts_at": "2024-04-24T15:07:40.922338Z" + } + } + }, + { + "price": { + "id": "pri_01gsz95g2zrkagg294kpstx54r", + "status": "active", + "quantity": { + "maximum": 1, + "minimum": 1 + }, + "tax_mode": "account_setting", + "product_id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "unit_price": { + "amount": "25000", + "currency_code": "GBP" + }, + "custom_data": null, + "description": "Monthly (recurring addon)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [ + { + "unit_price": { + "amount": "28500", + "currency_code": "USD" + }, + "country_codes": [ + "US" + ] + }, + { + "unit_price": { + "amount": "66000", + "currency_code": "PLN" + }, + "country_codes": [ + "PL" + ] + }, + { + "unit_price": { + "amount": "837500", + "currency_code": "INR" + }, + "country_codes": [ + "IN" + ] + }, + { + "unit_price": { + "amount": "335000", + "currency_code": "UAH" + }, + "country_codes": [ + "UA" + ] + }, + { + "unit_price": { + "amount": "12000", + "currency_code": "USD" + }, + "country_codes": [ + "NG" + ] + } + ] + }, + "price_id": "pri_01gsz95g2zrkagg294kpstx54r", + "quantity": 1, + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2024-05-24T15:07:40.922338Z", + "starts_at": "2024-04-24T15:07:40.922338Z" + } + } + } + ], + "details": { + "totals": { + "fee": "3340", + "tax": "11000", + "total": "66000", + "credit": "0", + "balance": "0", + "discount": "0", + "earnings": "51660", + "subtotal": "55000", + "grand_total": "66000", + "currency_code": "GBP", + "credit_to_balance": "0" + }, + "line_items": [ + { + "id": "txnitm_01hfyd09xrkdkbaacs934g8v8p", + "totals": { + "tax": "6000", + "total": "36000", + "discount": "0", + "subtotal": "30000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "status": "active", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "custom_data": { + "features": { + "crm": true, + "reports": true, + "data_retention": false + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of advanced reporting and a CRM that's right where you're chatting." + }, + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "tax_rate": "0.2", + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2024-05-24T15:07:40.922338Z", + "starts_at": "2024-04-24T15:07:40.922338Z" + } + }, + "unit_totals": { + "tax": "600", + "total": "3600", + "discount": "0", + "subtotal": "3000" + } + }, + { + "id": "txnitm_01hfyd09xrkdkbaacs98bjfgfj", + "totals": { + "tax": "5000", + "total": "30000", + "discount": "0", + "subtotal": "25000" + }, + "item_id": null, + "product": { + "id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "name": "VIP support", + "status": "active", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "custom_data": null, + "description": "Get exclusive access to our expert team of product specialists, available to help you make the most of your ChatApp subscription.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz95g2zrkagg294kpstx54r", + "quantity": 1, + "tax_rate": "0.2", + "proration": { + "rate": "1", + "billing_period": { + "ends_at": "2024-05-24T15:07:40.922338Z", + "starts_at": "2024-04-24T15:07:40.922338Z" + } + }, + "unit_totals": { + "tax": "5000", + "total": "30000", + "discount": "0", + "subtotal": "25000" + } + } + ], + "payout_totals": { + "fee": "4102", + "tax": "13511", + "total": "81066", + "credit": "0", + "balance": "0", + "discount": "0", + "earnings": "63453", + "fee_rate": "0.05", + "subtotal": "67555", + "grand_total": "81066", + "currency_code": "USD", + "exchange_rate": "1.2282731999999998", + "credit_to_balance": "0" + }, + "tax_rates_used": [ + { + "totals": { + "tax": "11000", + "total": "66000", + "discount": "0", + "subtotal": "55000" + }, + "tax_rate": "0.2" + } + ], + "adjusted_totals": { + "fee": "3340", + "tax": "11000", + "total": "66000", + "earnings": "51660", + "subtotal": "55000", + "grand_total": "66000", + "currency_code": "GBP" + } + }, + "payments": [ + { + "amount": "66000", + "status": "captured", + "created_at": "2023-11-23T15:33:02.177841Z", + "error_code": null, + "captured_at": "2023-11-23T15:33:04.574587Z", + "method_details": { + "card": { + "type": "visa", + "last4": "4242", + "expiry_year": 2024, + "expiry_month": 1, + "cardholder_name": "Michael McGovern" + }, + "type": "card" + }, + "payment_attempt_id": "0bec49bd-dd4d-45e3-a91e-46992b09c5e2", + "stored_payment_method_id": "8009a718-30f7-4718-a7f6-4d3fdecf559b" + } + ], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01hfyd09vas8qwq6jw7k6yd9rg" + }, + "created_at": "2023-11-23T15:33:02.036155Z", + "updated_at": "2023-11-23T15:33:19.238230688Z", + "billed_at": "2023-11-23T15:33:01.930479Z" + } + } + ], + "meta": { + "pagination": { + "per_page": 10, + "estimated_total": 5574, + "next": "http://sandbox-api.paddle.com/events?per_page=10&after=evt_01hfyd0v4xppkwmjaca5xyzh5d", + "has_more": true + }, + "request_id": "acca7502-1eaf-4a9e-9635-c6ab9844c8a5" + } +} diff --git a/tests/Functional/Resources/NotificationLogs/NotificationLogsClientTest.php b/tests/Functional/Resources/NotificationLogs/NotificationLogsClientTest.php new file mode 100644 index 0000000..cb25907 --- /dev/null +++ b/tests/Functional/Resources/NotificationLogs/NotificationLogsClientTest.php @@ -0,0 +1,84 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** + * @test + * + * @dataProvider listOperationsProvider + */ + public function list_hits_expected_uri( + ListOperation $operation, + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->notificationLogs->list(self::TEST_ID, $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function listOperationsProvider(): \Generator + { + yield 'Default' => [ + new ListOperation(), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/notifications/%s/logs', Environment::SANDBOX->baseUrl(), self::TEST_ID), + ]; + + yield 'Default Paged' => [ + new ListOperation(new Pager()), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/notifications/%s/logs?order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + self::TEST_ID, + ), + ]; + + yield 'Default Paged with After' => [ + new ListOperation(new Pager(after: 'pro_01gsz4s0w61y0pp88528f1wvvb')), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/notifications/%s/logs?after=pro_01gsz4s0w61y0pp88528f1wvvb&order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + self::TEST_ID, + ), + ]; + } +} diff --git a/tests/Functional/Resources/NotificationLogs/_fixtures/response/list_default.json b/tests/Functional/Resources/NotificationLogs/_fixtures/response/list_default.json new file mode 100644 index 0000000..1d99872 --- /dev/null +++ b/tests/Functional/Resources/NotificationLogs/_fixtures/response/list_default.json @@ -0,0 +1,83 @@ +{ + "data": [ + { + "id": "ntflog_01h8c0bswct46cwamynrzs6fwr", + "response_code": 200, + "response_content_type": "text/plain; charset=UTF-8", + "response_body": "", + "attempted_at": "2023-08-21T12:15:54.764232Z" + }, + { + "id": "ntflog_01h8c04xjj1ybcgsawp54h5zkr", + "response_code": 404, + "response_content_type": "application/json", + "response_body": "{\"success\":false,\"error\":{\"message\":\"Token not found\",\"id\":null}}", + "attempted_at": "2023-08-21T12:12:09.170559Z" + }, + { + "id": "ntflog_01h8bzyx2g57ywng4edfnpycya", + "response_code": 404, + "response_content_type": "application/json", + "response_body": "{\"success\":false,\"error\":{\"message\":\"Token not found\",\"id\":null}}", + "attempted_at": "2023-08-21T12:08:52.049462Z" + }, + { + "id": "ntflog_01h8bzsp45tvprvrra1jq5m2n5", + "response_code": 404, + "response_content_type": "application/json", + "response_body": "{\"success\":false,\"error\":{\"message\":\"Token not found\",\"id\":null}}", + "attempted_at": "2023-08-21T12:06:01.093936Z" + }, + { + "id": "ntflog_01h8bzn9vr2j0hav6bqjvmfhwj", + "response_code": 404, + "response_content_type": "application/json", + "response_body": "{\"success\":false,\"error\":{\"message\":\"Token not found\",\"id\":null}}", + "attempted_at": "2023-08-21T12:03:37.464996Z" + }, + { + "id": "ntflog_01h8bzhqdt790wtmepa10we4ed", + "response_code": 404, + "response_content_type": "application/json", + "response_body": "{\"success\":false,\"error\":{\"message\":\"Token not found\",\"id\":null}}", + "attempted_at": "2023-08-21T12:01:40.282965Z" + }, + { + "id": "ntflog_01h8bzey4xs3txscjw7td3793c", + "response_code": 404, + "response_content_type": "application/json", + "response_body": "{\"success\":false,\"error\":{\"message\":\"Token not found\",\"id\":null}}", + "attempted_at": "2023-08-21T12:00:08.861949Z" + }, + { + "id": "ntflog_01h8bzcx9gwtcvf000ngvacnnw", + "response_code": 404, + "response_content_type": "application/json", + "response_body": "{\"success\":false,\"error\":{\"message\":\"Token not found\",\"id\":null}}", + "attempted_at": "2023-08-21T11:59:02.448596Z" + }, + { + "id": "ntflog_01h8bzb80ns1dtwmjsx311ztcw", + "response_code": 404, + "response_content_type": "application/json", + "response_body": "{\"success\":false,\"error\":{\"message\":\"Token not found\",\"id\":null}}", + "attempted_at": "2023-08-21T11:58:07.893473Z" + }, + { + "id": "ntflog_01h8bzame8ykt2zw5a11mxs110", + "response_code": 404, + "response_content_type": "application/json", + "response_body": "{\"success\":false,\"error\":{\"message\":\"Token not found\",\"id\":null}}", + "attempted_at": "2023-08-21T11:57:47.848991Z" + } + ], + "meta": { + "pagination": { + "per_page": 50, + "estimated_total": 10, + "next": "http://api.paddle.com/notifications/ntf_01h8bzam1z32agrxjwhjgqk8w6/logs?after=ntflog_01h8bzame8ykt2zw5a11mxs110", + "has_more": false + }, + "request_id": "b12dc646-6131-4801-b76a-b717d1aff37c" + } +} diff --git a/tests/Functional/Resources/NotificationSettings/NotificationSettingsClientTest.php b/tests/Functional/Resources/NotificationSettings/NotificationSettingsClientTest.php new file mode 100644 index 0000000..f228766 --- /dev/null +++ b/tests/Functional/Resources/NotificationSettings/NotificationSettingsClientTest.php @@ -0,0 +1,238 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** + * @test + * + * @dataProvider createOperationsProvider + */ + public function it_uses_expected_payload_on_create( + CreateOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->notificationSettings->create($operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals(Environment::SANDBOX->baseUrl() . '/notification-settings', urldecode((string) $request->getUri())); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function createOperationsProvider(): \Generator + { + yield 'Basic Create' => [ + new CreateOperation( + description: 'Slack notifications', + type: NotificationSettingType::Url, + destination: 'https://hooks.slack.com/example', + includeSensitiveFields: false, + subscribedEvents: [ + EventTypeName::TransactionBilled, + EventTypeName::TransactionCanceled, + EventTypeName::TransactionCompleted, + EventTypeName::TransactionCreated, + EventTypeName::TransactionPaymentFailed, + EventTypeName::SubscriptionCreated, + ], + ), + new Response(200, body: self::readRawJsonFixture('response/minimal_entity')), + self::readRawJsonFixture('request/create_basic'), + ]; + + yield 'Create with Data' => [ + new CreateOperation( + description: 'Slack notifications', + type: NotificationSettingType::Url, + destination: 'https://hooks.slack.com/example', + includeSensitiveFields: false, + subscribedEvents: [ + EventTypeName::TransactionBilled, + EventTypeName::TransactionCanceled, + EventTypeName::TransactionCompleted, + EventTypeName::TransactionCreated, + EventTypeName::TransactionPaymentFailed, + EventTypeName::TransactionReady, + EventTypeName::TransactionUpdated, + EventTypeName::SubscriptionActivated, + EventTypeName::SubscriptionCreated, + EventTypeName::SubscriptionPastDue, + EventTypeName::SubscriptionPaused, + EventTypeName::SubscriptionResumed, + EventTypeName::SubscriptionTrialing, + EventTypeName::SubscriptionUpdated, + ], + apiVersion: 1, + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/create_full'), + ]; + } + + /** + * @test + * + * @dataProvider updateOperationsProvider + */ + public function it_uses_expected_payload_on_update( + UpdateOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->notificationSettings->update('ntfset_01gkpjp8bkm3tm53kdgkx6sms7', $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('PATCH', $request->getMethod()); + self::assertEquals(Environment::SANDBOX->baseUrl() . '/notification-settings/ntfset_01gkpjp8bkm3tm53kdgkx6sms7', urldecode((string) $request->getUri())); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function updateOperationsProvider(): \Generator + { + yield 'Update Single' => [ + new UpdateOperation(active: false), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_single'), + ]; + + yield 'Update Partial' => [ + new UpdateOperation(description: 'Slack notifications (old)', active: false), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_partial'), + ]; + + yield 'Update All' => [ + new UpdateOperation( + description: 'Slack notifications (old)', + destination: 'https://hooks.slack.com/example', + active: false, + apiVersion: 1, + includeSensitiveFields: false, + subscribedEvents: [ + EventTypeName::TransactionBilled, + EventTypeName::TransactionCanceled, + EventTypeName::TransactionCompleted, + EventTypeName::TransactionCreated, + EventTypeName::TransactionPaymentFailed, + EventTypeName::TransactionReady, + EventTypeName::TransactionUpdated, + EventTypeName::SubscriptionActivated, + EventTypeName::SubscriptionCreated, + EventTypeName::SubscriptionPastDue, + EventTypeName::SubscriptionPaused, + EventTypeName::SubscriptionResumed, + EventTypeName::SubscriptionTrialing, + EventTypeName::SubscriptionUpdated, + ], + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_full'), + ]; + } + + /** + * @test + * + * @dataProvider listOperationsProvider + */ + public function list_hits_expected_uri( + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->notificationSettings->list(); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function listOperationsProvider(): \Generator + { + yield 'Default' => [ + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/notification-settings', Environment::SANDBOX->baseUrl()), + ]; + } + + /** + * @test + * + * @dataProvider getRequestProvider + */ + public function get_hits_expected_uri( + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->notificationSettings->get('ntfset_01gkpjp8bkm3tm53kdgkx6sms7'); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function getRequestProvider(): \Generator + { + yield 'Default' => [ + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + sprintf('%s/notification-settings/ntfset_01gkpjp8bkm3tm53kdgkx6sms7', Environment::SANDBOX->baseUrl()), + ]; + } + + /** @test */ + public function delete_hits_expected_uri(): void + { + $expectedUri = sprintf( + '%s/notification-settings/ntfset_01gkpjp8bkm3tm53kdgkx6sms7', + Environment::SANDBOX->baseUrl(), + ); + + $this->mockClient->addResponse(new Response(204, body: '{}')); + $this->client->notificationSettings->delete('ntfset_01gkpjp8bkm3tm53kdgkx6sms7'); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('DELETE', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } +} diff --git a/tests/Functional/Resources/NotificationSettings/_fixtures/request/create_basic.json b/tests/Functional/Resources/NotificationSettings/_fixtures/request/create_basic.json new file mode 100644 index 0000000..76c50b0 --- /dev/null +++ b/tests/Functional/Resources/NotificationSettings/_fixtures/request/create_basic.json @@ -0,0 +1,14 @@ +{ + "description": "Slack notifications", + "type": "url", + "destination": "https://hooks.slack.com/example", + "include_sensitive_fields": false, + "subscribed_events": [ + "transaction.billed", + "transaction.canceled", + "transaction.completed", + "transaction.created", + "transaction.payment_failed", + "subscription.created" + ] +} diff --git a/tests/Functional/Resources/NotificationSettings/_fixtures/request/create_full.json b/tests/Functional/Resources/NotificationSettings/_fixtures/request/create_full.json new file mode 100644 index 0000000..4a24175 --- /dev/null +++ b/tests/Functional/Resources/NotificationSettings/_fixtures/request/create_full.json @@ -0,0 +1,23 @@ +{ + "description": "Slack notifications", + "type": "url", + "destination": "https://hooks.slack.com/example", + "include_sensitive_fields": false, + "subscribed_events": [ + "transaction.billed", + "transaction.canceled", + "transaction.completed", + "transaction.created", + "transaction.payment_failed", + "transaction.ready", + "transaction.updated", + "subscription.activated", + "subscription.created", + "subscription.past_due", + "subscription.paused", + "subscription.resumed", + "subscription.trialing", + "subscription.updated" + ], + "api_version": 1 +} diff --git a/tests/Functional/Resources/NotificationSettings/_fixtures/request/update_full.json b/tests/Functional/Resources/NotificationSettings/_fixtures/request/update_full.json new file mode 100644 index 0000000..a9267a4 --- /dev/null +++ b/tests/Functional/Resources/NotificationSettings/_fixtures/request/update_full.json @@ -0,0 +1,23 @@ +{ + "description": "Slack notifications (old)", + "destination": "https://hooks.slack.com/example", + "active": false, + "api_version": 1, + "include_sensitive_fields": false, + "subscribed_events": [ + "transaction.billed", + "transaction.canceled", + "transaction.completed", + "transaction.created", + "transaction.payment_failed", + "transaction.ready", + "transaction.updated", + "subscription.activated", + "subscription.created", + "subscription.past_due", + "subscription.paused", + "subscription.resumed", + "subscription.trialing", + "subscription.updated" + ] +} diff --git a/tests/Functional/Resources/NotificationSettings/_fixtures/request/update_partial.json b/tests/Functional/Resources/NotificationSettings/_fixtures/request/update_partial.json new file mode 100644 index 0000000..f3f9c60 --- /dev/null +++ b/tests/Functional/Resources/NotificationSettings/_fixtures/request/update_partial.json @@ -0,0 +1,4 @@ +{ + "description": "Slack notifications (old)", + "active": false +} diff --git a/tests/Functional/Resources/NotificationSettings/_fixtures/request/update_single.json b/tests/Functional/Resources/NotificationSettings/_fixtures/request/update_single.json new file mode 100644 index 0000000..bba3067 --- /dev/null +++ b/tests/Functional/Resources/NotificationSettings/_fixtures/request/update_single.json @@ -0,0 +1,3 @@ +{ + "active": false +} diff --git a/tests/Functional/Resources/NotificationSettings/_fixtures/response/full_entity.json b/tests/Functional/Resources/NotificationSettings/_fixtures/response/full_entity.json new file mode 100644 index 0000000..f19c141 --- /dev/null +++ b/tests/Functional/Resources/NotificationSettings/_fixtures/response/full_entity.json @@ -0,0 +1,107 @@ +{ + "data": { + "id": "ntfset_01gkpjp8bkm3tm53kdgkx6sms7", + "description": "Slack notifications", + "type": "url", + "destination": "https://hooks.slack.com/example", + "active": true, + "api_version": 1, + "include_sensitive_fields": false, + "subscribed_events": [ + { + "name": "transaction.billed", + "description": "Occurs when a transaction is billed.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.canceled", + "description": "Occurs when a transaction is canceled.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.completed", + "description": "Occurs when a transaction is completed.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.created", + "description": "Occurs when a transaction is created.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.payment_failed", + "description": "Occurs when a payment fails for a transaction.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.ready", + "description": "Occurs when a transaction is ready.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.updated", + "description": "Occurs when a transaction is updated.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "subscription.activated", + "description": "Occurs when a subscription is activated.", + "group": "Subscription", + "available_versions": [1] + }, + { + "name": "subscription.canceled", + "description": "Occurs when a subscription is canceled.", + "group": "Subscription", + "available_versions": [1] + }, + { + "name": "subscription.created", + "description": "Occurs when a subscription is created.", + "group": "Subscription", + "available_versions": [1] + }, + { + "name": "subscription.past_due", + "description": "Occurs when a subscription is past due.", + "group": "Subscription", + "available_versions": [1] + }, + { + "name": "subscription.paused", + "description": "Occurs when a subscription is paused.", + "group": "Subscription", + "available_versions": [1] + }, + { + "name": "subscription.resumed", + "description": "Occurs when a subscription is resumed.", + "group": "Subscription", + "available_versions": [1] + }, + { + "name": "subscription.trialing", + "description": "Occurs when a subscription is trialing.", + "group": "Subscription", + "available_versions": [1] + }, + { + "name": "subscription.updated", + "description": "Occurs when a subscription is updated.", + "group": "Subscription", + "available_versions": [1] + } + ], + "endpoint_secret_key": "pdl_ntfset_01gkpjp8bkm3tm53kdgkx6sms7_6h3qd3uFSi9YCD3OLYAShQI90XTI5vEI" + }, + "meta": { + "request_id": "fd55d51a-6242-4645-8572-af2a8b6f41b6" + } +} diff --git a/tests/Functional/Resources/NotificationSettings/_fixtures/response/list_default.json b/tests/Functional/Resources/NotificationSettings/_fixtures/response/list_default.json new file mode 100644 index 0000000..56b237e --- /dev/null +++ b/tests/Functional/Resources/NotificationSettings/_fixtures/response/list_default.json @@ -0,0 +1,211 @@ +{ + "data": [ + { + "id": "ntfset_01gkpjp8bkm3tm53kdgkx6sms7", + "description": "Slack notifications", + "type": "url", + "destination": "https://hooks.slack.com/example", + "active": true, + "api_version": 1, + "include_sensitive_fields": false, + "subscribed_events": [ + { + "name": "transaction.billed", + "description": "Occurs when a transaction is billed.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.canceled", + "description": "Occurs when a transaction is canceled.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.completed", + "description": "Occurs when a transaction is completed.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.created", + "description": "Occurs when a transaction is created.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.payment_failed", + "description": "Occurs when a payment fails for a transaction.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.ready", + "description": "Occurs when a transaction is ready.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.updated", + "description": "Occurs when a transaction is updated.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "subscription.activated", + "description": "Occurs when a subscription is activated.", + "group": "Subscription", + "available_versions": [1] + }, + { + "name": "subscription.canceled", + "description": "Occurs when a subscription is canceled.", + "group": "Subscription", + "available_versions": [1] + }, + { + "name": "subscription.created", + "description": "Occurs when a subscription is created.", + "group": "Subscription", + "available_versions": [1] + }, + { + "name": "subscription.past_due", + "description": "Occurs when a subscription is past due.", + "group": "Subscription", + "available_versions": [1] + }, + { + "name": "subscription.paused", + "description": "Occurs when a subscription is paused.", + "group": "Subscription", + "available_versions": [1] + }, + { + "name": "subscription.resumed", + "description": "Occurs when a subscription is resumed.", + "group": "Subscription", + "available_versions": [1] + }, + { + "name": "subscription.trialing", + "description": "Occurs when a subscription is trialing.", + "group": "Subscription", + "available_versions": [1] + }, + { + "name": "subscription.updated", + "description": "Occurs when a subscription is updated.", + "group": "Subscription", + "available_versions": [1] + } + ], + "endpoint_secret_key": "pdl_ntfset_01gkpjp8bkm3tm53kdgkx6sms7_6h3qd3uFSi9YCD3OLYAShQI90XTI5vEI" + }, + { + "id": "ntfset_01gkpop8bkm3tm53itgkx6klk7", + "description": "Discord notifications", + "type": "url", + "destination": "https://hooks.discord.com/example", + "active": true, + "api_version": 1, + "include_sensitive_fields": false, + "subscribed_events": [ + { + "name": "transaction.billed", + "description": "Occurs when a transaction is billed.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.canceled", + "description": "Occurs when a transaction is canceled.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.completed", + "description": "Occurs when a transaction is completed.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.created", + "description": "Occurs when a transaction is created.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.payment_failed", + "description": "Occurs when a payment fails for a transaction.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.ready", + "description": "Occurs when a transaction is ready.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.updated", + "description": "Occurs when a transaction is updated.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "subscription.activated", + "description": "Occurs when a subscription is activated.", + "group": "Subscription", + "available_versions": [1] + }, + { + "name": "subscription.canceled", + "description": "Occurs when a subscription is canceled.", + "group": "Subscription", + "available_versions": [1] + }, + { + "name": "subscription.created", + "description": "Occurs when a subscription is created.", + "group": "Subscription", + "available_versions": [1] + }, + { + "name": "subscription.past_due", + "description": "Occurs when a subscription is past due.", + "group": "Subscription", + "available_versions": [1] + }, + { + "name": "subscription.paused", + "description": "Occurs when a subscription is paused.", + "group": "Subscription", + "available_versions": [1] + }, + { + "name": "subscription.resumed", + "description": "Occurs when a subscription is resumed.", + "group": "Subscription", + "available_versions": [1] + }, + { + "name": "subscription.trialing", + "description": "Occurs when a subscription is trialing.", + "group": "Subscription", + "available_versions": [1] + }, + { + "name": "subscription.updated", + "description": "Occurs when a subscription is updated.", + "group": "Subscription", + "available_versions": [1] + } + ], + "endpoint_secret_key": "ntfset_01gkpop8bkm3tm53itgkx6klk7_6h3qd3uFSi9YCD3OLYAShQI90XTI5vEI" + } + ], + "meta": { + "request_id": "cf039cbb-6c2a-485d-b244-501894b797a6" + } +} diff --git a/tests/Functional/Resources/NotificationSettings/_fixtures/response/minimal_entity.json b/tests/Functional/Resources/NotificationSettings/_fixtures/response/minimal_entity.json new file mode 100644 index 0000000..bd2c92a --- /dev/null +++ b/tests/Functional/Resources/NotificationSettings/_fixtures/response/minimal_entity.json @@ -0,0 +1,53 @@ +{ + "data": { + "id": "ntfset_01gkpjp8bkm3tm53kdgkx6sms7", + "description": "Slack notifications", + "type": "url", + "destination": "https://hooks.slack.com/example", + "active": true, + "api_version": 1, + "include_sensitive_fields": false, + "subscribed_events": [ + { + "name": "transaction.billed", + "description": "Occurs when a transaction is billed.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.canceled", + "description": "Occurs when a transaction is canceled.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.completed", + "description": "Occurs when a transaction is completed.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.created", + "description": "Occurs when a transaction is created.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "transaction.payment_failed", + "description": "Occurs when a payment fails for a transaction.", + "group": "Transaction", + "available_versions": [1] + }, + { + "name": "subscription.created", + "description": "Occurs when a subscription is created.", + "group": "Subscription", + "available_versions": [1] + } + ], + "endpoint_secret_key": "pdl_ntfset_01gkpjp8bkm3tm53kdgkx6sms7_6h3qd3uFSi9YCD3OLYAShQI90XTI5vEI" + }, + "meta": { + "request_id": "fd55d51a-6242-4645-8572-af2a8b6f41b6" + } +} diff --git a/tests/Functional/Resources/Notifications/NotificationsClientTest.php b/tests/Functional/Resources/Notifications/NotificationsClientTest.php new file mode 100644 index 0000000..48b3a2d --- /dev/null +++ b/tests/Functional/Resources/Notifications/NotificationsClientTest.php @@ -0,0 +1,174 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** + * @test + * + * @dataProvider listOperationsProvider + */ + public function list_hits_expected_uri( + ListOperation $operation, + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->notifications->list($operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function listOperationsProvider(): \Generator + { + yield 'Default' => [ + new ListOperation(), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/notifications', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Default Paged' => [ + new ListOperation(new Pager()), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/notifications?order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Default Paged with After' => [ + new ListOperation(new Pager(after: 'nft_01h83xenpcfjyhkqr4x214m02x')), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/notifications?after=nft_01h83xenpcfjyhkqr4x214m02x&order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Notification Setting ID Filtered' => [ + new ListOperation(notificationSettingId: ['nftset_01h83xenpcfjyhkqr4x214m02']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/notifications?notification_setting_id=nftset_01h83xenpcfjyhkqr4x214m02', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Multiple Notification Setting ID Filtered' => [ + new ListOperation(notificationSettingId: ['nftset_01h83xenpcfjyhkqr4x214m02', 'nftset_01h8brhckjd6qk4n7e4py2340t']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/notifications?notification_setting_id=nftset_01h83xenpcfjyhkqr4x214m02,nftset_01h8brhckjd6qk4n7e4py2340t', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'NotificationStatus Filtered' => [ + new ListOperation(status: [NotificationStatus::Delivered]), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/notifications?status=delivered', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Multiple NotificationStatus Filtered' => [ + new ListOperation(status: [NotificationStatus::Delivered, NotificationStatus::NotAttempted]), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/notifications?status=delivered,not_attempted', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Filter Filtered' => [ + new ListOperation(filter: 'txn_01h83xenpcfjyhkqr4x214m02'), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/notifications?filter=txn_01h83xenpcfjyhkqr4x214m02', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Search Filtered' => [ + new ListOperation(search: 'transaction.created'), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/notifications?search=transaction.created', Environment::SANDBOX->baseUrl()), + ]; + + yield 'To Filtered' => [ + new ListOperation(to: new \DateTime('2023-12-25T00:00:00.000Z')), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/notifications?to=2023-12-25T00:00:00.000000Z', Environment::SANDBOX->baseUrl()), + ]; + + yield 'From Filtered' => [ + new ListOperation(to: new \DateTime('2023-12-25T00:00:00.000Z')), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/notifications?to=2023-12-25T00:00:00.000000Z', Environment::SANDBOX->baseUrl()), + ]; + + yield 'To and From Filtered' => [ + new ListOperation( + to: new \DateTime('2023-12-25T00:00:00.000Z'), + from: new \DateTime('2023-12-24T00:00:00.000Z'), + ), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/notifications?to=2023-12-25T00:00:00.000000Z&from=2023-12-24T00:00:00.000000Z', Environment::SANDBOX->baseUrl()), + ]; + } + + /** @test */ + public function get_hits_expected_uri(): void + { + $response = new Response(200, body: self::readRawJsonFixture('response/full_entity')); + $this->mockClient->addResponse($response); + $this->client->notifications->get('nft_01h8441jn5pcwrfhwh78jqt8hk'); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals( + sprintf('%s/notifications/nft_01h8441jn5pcwrfhwh78jqt8hk', Environment::SANDBOX->baseUrl()), + urldecode((string) $request->getUri()), + ); + } + + /** @test */ + public function replay_hits_expected_uri(): void + { + $response = new Response(200, body: self::readRawJsonFixture('response/replay')); + $this->mockClient->addResponse($response); + $replayId = $this->client->notifications->replay('nft_01h8441jn5pcwrfhwh78jqt8hk'); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals( + sprintf('%s/notifications/nft_01h8441jn5pcwrfhwh78jqt8hk', Environment::SANDBOX->baseUrl()), + urldecode((string) $request->getUri()), + ); + self::assertSame('ntf_01h46h1s2zabpkdks7yt4vkgkc', $replayId); + } +} diff --git a/tests/Functional/Resources/Notifications/_fixtures/response/full_entity.json b/tests/Functional/Resources/Notifications/_fixtures/response/full_entity.json new file mode 100644 index 0000000..751309c --- /dev/null +++ b/tests/Functional/Resources/Notifications/_fixtures/response/full_entity.json @@ -0,0 +1,47 @@ +{ + "data": { + "id": "ntf_01h8bzam1z32agrxjwhjgqk8w6", + "type": "business.updated", + "status": "failed", + "payload": { + "data": { + "id": "biz_01h84a7hr4pzhsajkm8tev89ev", + "name": "ChatApp Inc.", + "status": "active", + "contacts": [ + { + "name": "Parker Jones", + "email": "parker@example.com" + }, + { + "name": "Jo Riley", + "email": "jo@example.com" + }, + { + "name": "Jesse Garcia", + "email": "jo@example.com" + } + ], + "created_at": "2023-08-18T12:34:25.668Z", + "updated_at": "2023-08-21T11:57:47.03542Z", + "company_number": "555775291485", + "tax_identifier": null + }, + "event_id": "evt_01h8bzakzx3hm2fmen703n5q45", + "event_type": "business.updated", + "occurred_at": "2023-08-21T11:57:47.390028Z", + "notification_id": "ntf_01h8bzam1z32agrxjwhjgqk8w6" + }, + "occurred_at": "2023-08-21T11:57:47.390028Z", + "delivered_at": null, + "replayed_at": null, + "origin": "event", + "last_attempt_at": "2023-08-21T12:15:53.620185Z", + "retry_at": null, + "times_attempted": 10, + "notification_setting_id": "ntfset_01h126ps19rp06wn89v9797mab" + }, + "meta": { + "request_id": "d14081ea-0062-491e-9035-0010f2216046" + } +} diff --git a/tests/Functional/Resources/Notifications/_fixtures/response/list_default.json b/tests/Functional/Resources/Notifications/_fixtures/response/list_default.json new file mode 100644 index 0000000..c90cbb7 --- /dev/null +++ b/tests/Functional/Resources/Notifications/_fixtures/response/list_default.json @@ -0,0 +1,458 @@ +{ + "data": [ + { + "id": "ntf_01h8bzam1z32agrxjwhjgqk8w6", + "type": "business.updated", + "status": "failed", + "payload": { + "data": { + "id": "biz_01h84a7hr4pzhsajkm8tev89ev", + "name": "ChatApp Inc.", + "status": "active", + "contacts": [ + { + "name": "Parker Jones", + "email": "parker@example.com" + }, + { + "name": "Jo Riley", + "email": "jo@example.com" + }, + { + "name": "Jesse Garcia", + "email": "jo@example.com" + } + ], + "created_at": "2023-08-18T12:34:25.668Z", + "updated_at": "2023-08-21T11:57:47.03542Z", + "company_number": "555775291485", + "tax_identifier": null + }, + "event_id": "evt_01h8bzakzx3hm2fmen703n5q45", + "event_type": "business.updated", + "occurred_at": "2023-08-21T11:57:47.390028Z", + "notification_id": "ntf_01h8bzam1z32agrxjwhjgqk8w6" + }, + "occurred_at": "2023-08-21T11:57:47.390028Z", + "delivered_at": null, + "replayed_at": null, + "origin": "event", + "last_attempt_at": "2023-08-21T12:15:53.620185Z", + "retry_at": null, + "times_attempted": 10, + "notification_setting_id": "ntfset_01h126ps19rp06wn89v9797mab" + }, + { + "id": "ntf_01h8brhd934tvsvf8s5q7thd2r", + "type": "transaction.created", + "status": "delivered", + "payload": { + "data": { + "id": "txn_01h8brhckjd6qk4n7e4py2340t", + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "status": "active", + "quantity": { + "maximum": 999, + "minimum": 10 + }, + "tax_mode": "account_setting", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "description": "Monthly (per seat)", + "name": "Monthly (per seat)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [ + { + "unit_price": { + "amount": "5000", + "currency_code": "AUD" + }, + "country_codes": [ + "AU" + ] + } + ] + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "proration": null + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "status": "active", + "quantity": { + "maximum": 100, + "minimum": 1 + }, + "tax_mode": "account_setting", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "description": "Monthly (recurring addon)", + "name": "Monthly (recurring addon)", + "trial_period": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "unit_price_overrides": [ + { + "unit_price": { + "amount": "20000", + "currency_code": "AUD" + }, + "country_codes": [ + "AU" + ] + } + ] + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "proration": null + }, + { + "price": { + "id": "pri_01gsz98e27ak2tyhexptwc58yk", + "status": "active", + "quantity": { + "maximum": 1, + "minimum": 1 + }, + "tax_mode": "account_setting", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "description": "One-time charge", + "name": "One-time charge", + "trial_period": null, + "billing_cycle": null, + "unit_price_overrides": [ + { + "unit_price": { + "amount": "40000", + "currency_code": "AUD" + }, + "country_codes": [ + "AU" + ] + } + ] + }, + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "proration": null + } + ], + "origin": "web", + "status": "draft", + "details": { + "totals": { + "fee": null, + "tax": "11980", + "total": "71880", + "credit": "0", + "balance": "71880", + "discount": "0", + "earnings": null, + "subtotal": "59900", + "grand_total": "71880", + "currency_code": "USD" + }, + "line_items": [ + { + "id": "txnitm_01h8brhcmxzjp7zy6zxa4rmb6w", + "totals": { + "tax": "6000", + "total": "36000", + "discount": "0", + "subtotal": "30000" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "status": "active", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "tax_rate": "0.2", + "unit_totals": { + "tax": "600", + "total": "3600", + "discount": "0", + "subtotal": "3000" + } + }, + { + "id": "txnitm_01h8brhcmxzjp7zy6zxbxpks7d", + "totals": { + "tax": "2000", + "total": "12000", + "discount": "0", + "subtotal": "10000" + }, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Voice rooms addon", + "status": "active", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/GcZzBjXRfiraensppgtQ_icon2.png", + "description": "Create voice rooms in your chats to work in real time alongside your colleagues. Includes unlimited voice rooms and recording backup for compliance.", + "tax_category": "standard" + }, + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "tax_rate": "0.2", + "unit_totals": { + "tax": "2000", + "total": "12000", + "discount": "0", + "subtotal": "10000" + } + }, + { + "id": "txnitm_01h8brhcmxzjp7zy6zxdm1d0wt", + "totals": { + "tax": "3980", + "total": "23880", + "discount": "0", + "subtotal": "19900" + }, + "product": { + "id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "name": "Custom domains", + "status": "active", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "description": "Make ChatApp truly your own with custom domains! Custom domains reinforce your brand identity and make it easy for your team to access ChatApp.", + "tax_category": "standard" + }, + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "tax_rate": "0.2", + "unit_totals": { + "tax": "3980", + "total": "23880", + "discount": "0", + "subtotal": "19900" + } + } + ], + "payout_totals": null, + "tax_rates_used": [ + { + "totals": { + "tax": "11980", + "total": "71880", + "discount": "0", + "subtotal": "59900" + }, + "tax_rate": "0.2" + } + ] + }, + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01h8brhckjd6qk4n7e4py2340t" + }, + "payments": [], + "billed_at": null, + "address_id": null, + "created_at": "2023-08-21T09:59:09.237822398Z", + "invoice_id": null, + "updated_at": "2023-08-21T09:59:09.237822398Z", + "business_id": null, + "custom_data": null, + "customer_id": null, + "discount_id": null, + "currency_code": "USD", + "billing_period": null, + "invoice_number": null, + "billing_details": null, + "collection_mode": "automatic", + "subscription_id": null + }, + "event_id": "evt_01h8brhd6mj4frv1dg3cghcrs3", + "event_type": "transaction.created", + "occurred_at": "2023-08-21T09:59:09.781003Z", + "notification_id": "ntf_01h8brhd934tvsvf8s5q7thd2r" + }, + "occurred_at": "2023-08-21T09:59:09.781003Z", + "delivered_at": "2023-08-21T09:59:10.316373Z", + "replayed_at": null, + "origin": "event", + "last_attempt_at": "2023-08-21T09:59:09.902732Z", + "retry_at": null, + "times_attempted": 1, + "notification_setting_id": "ntfset_01h7zcdzf04a7wvyja9k9p1n3p" + }, + { + "id": "ntf_01h8bkrfe7w1vwf8xmytwn51e7", + "type": "product.updated", + "status": "failed", + "payload": { + "data": { + "id": "pro_01h7zcgmdc6tmwtjehp3sh7azf", + "name": "ChatApp for Schools", + "status": "archived", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "created_at": "2023-08-16T14:38:08.3Z", + "custom_data": { + "features": { + "crm": false, + "reports": true, + "data_retention": true + } + }, + "description": "Spend more time engaging with students with ChataApp Education. Includes features from our Pro plan, plus tools to help educators track student progress.", + "tax_category": "standard" + }, + "event_id": "evt_01h8bkrfbwtaram1w8m7qekytx", + "event_type": "product.updated", + "occurred_at": "2023-08-21T08:35:38.493036Z", + "notification_id": "ntf_01h8bkrfe7w1vwf8xmytwn51e7" + }, + "occurred_at": "2023-08-21T08:35:38.493036Z", + "delivered_at": null, + "replayed_at": null, + "origin": "event", + "last_attempt_at": "2023-08-21T08:53:50.759289Z", + "retry_at": null, + "times_attempted": 10, + "notification_setting_id": "ntfset_01gt21c5pdx9q1e4mh1xrsjjn6" + }, + { + "id": "ntf_01h84cka91b65gy90qhe2tw2q6", + "type": "subscription.trialing", + "status": "delivered", + "payload": { + "data": { + "id": "sub_01h84ck8sg4ebkpzqb9x2mtjjf", + "items": [ + { + "price": { + "id": "pri_01h84cdy3xatsp16afda2gekzy", + "tax_mode": "account_setting", + "product_id": "pro_01h84cd36f900f3wmpdfamgv8w", + "unit_price": { + "amount": "0", + "currency_code": "USD" + }, + "description": "Annual plan", + "trial_period": { + "interval": "day", + "frequency": 10 + }, + "billing_cycle": { + "interval": "year", + "frequency": 1 + } + }, + "status": "trialing", + "quantity": 1, + "recurring": true, + "created_at": "2023-08-18T13:15:46.864164Z", + "updated_at": "2023-08-18T13:15:46.864164Z", + "trial_dates": { + "ends_at": "2023-08-28T13:15:46.864158Z", + "starts_at": "2023-08-18T13:15:46.864158Z" + }, + "next_billed_at": "2023-08-28T13:15:46.864158Z", + "previously_billed_at": null + } + ], + "status": "trialing", + "paused_at": null, + "address_id": "add_01h84cjfy5411jpjes4hmafqry", + "created_at": "2023-08-18T13:15:46.864163Z", + "started_at": "2023-08-18T13:15:46.864158Z", + "updated_at": "2023-08-18T13:15:46.864163Z", + "business_id": null, + "canceled_at": null, + "custom_data": null, + "customer_id": "ctm_01h84cjfwmdph1k8kgsyjt3k7g", + "billing_cycle": { + "interval": "year", + "frequency": 1 + }, + "currency_code": "USD", + "next_billed_at": "2023-08-28T13:15:46.864158Z", + "billing_details": null, + "collection_mode": "automatic", + "first_billed_at": null, + "scheduled_change": null, + "current_billing_period": { + "ends_at": "2023-08-28T13:15:46.864158Z", + "starts_at": "2023-08-18T13:15:46.864158Z" + }, + "import_meta": null + }, + "event_id": "evt_01h84cka4p40e737vm1ajb2bc5", + "event_type": "subscription.trialing", + "occurred_at": "2023-08-18T13:15:48.246292Z", + "notification_id": "ntf_01h84cka91b65gy90qhe2tw2q6" + }, + "occurred_at": "2023-08-18T13:15:48.246292Z", + "delivered_at": "2023-08-18T13:15:49.043358Z", + "replayed_at": null, + "origin": "event", + "last_attempt_at": "2023-08-18T13:15:48.446347Z", + "retry_at": null, + "times_attempted": 1, + "notification_setting_id": "ntfset_01h7hssm6xememp4cx2h562hvq" + }, + { + "id": "ntf_01h8441jz6fr97hv7zemswj8cw", + "type": "customer.created", + "status": "delivered", + "payload": { + "data": { + "id": "ctm_01h8441jn5pcwrfhwh78jqt8hk", + "name": "Sam Miller", + "email": "sam@example.com", + "locale": "en", + "status": "active", + "created_at": "2023-08-18T10:46:18.533Z", + "updated_at": "2023-08-18T10:46:18.533Z", + "marketing_consent": false + }, + "event_id": "evt_01h8441jx8x1q971q9ksksqh82", + "event_type": "customer.created", + "occurred_at": "2023-08-18T10:46:18.792661Z", + "notification_id": "ntf_01h8441jz6fr97hv7zemswj8cw" + }, + "occurred_at": "2023-08-18T10:46:18.792661Z", + "delivered_at": "2023-08-18T10:46:19.396422Z", + "replayed_at": null, + "origin": "event", + "last_attempt_at": "2023-08-18T10:46:18.887423Z", + "retry_at": null, + "times_attempted": 1, + "notification_setting_id": "ntfset_01h7zcdzf04a7wvyja9k9p1n3p" + } + ], + "meta": { + "pagination": { + "per_page": 50, + "estimated_total": 5, + "next": "http://api.paddle.com/notifications?after=ntf_01h8441jz6fr97hv7zemswj8cw", + "has_more": false + }, + "request_id": "21279f36-3eee-4828-943b-e2a9a6df4fce" + } +} diff --git a/tests/Functional/Resources/Notifications/_fixtures/response/replay.json b/tests/Functional/Resources/Notifications/_fixtures/response/replay.json new file mode 100644 index 0000000..a6fc2b4 --- /dev/null +++ b/tests/Functional/Resources/Notifications/_fixtures/response/replay.json @@ -0,0 +1,8 @@ +{ + "data": { + "notification_id": "ntf_01h46h1s2zabpkdks7yt4vkgkc" + }, + "meta": { + "request_id": "cfe92cac-86a1-49fe-ac50-20620dcd024f" + } +} diff --git a/tests/Functional/Resources/Prices/PricesClientTest.php b/tests/Functional/Resources/Prices/PricesClientTest.php new file mode 100644 index 0000000..27ca528 --- /dev/null +++ b/tests/Functional/Resources/Prices/PricesClientTest.php @@ -0,0 +1,283 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** + * @test + * + * @dataProvider createOperationsProvider + */ + public function it_uses_expected_payload_on_create( + CreateOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->prices->create($operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals(Environment::SANDBOX->baseUrl() . '/prices', urldecode((string) $request->getUri())); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function createOperationsProvider(): \Generator + { + yield 'Basic Create' => [ + new CreateOperation( + description: 'Monthly (per seat)', + productId: 'pro_01h7zcgmdc6tmwtjehp3sh7azf', + unitPrice: new Money('500', CurrencyCode::USD), + ), + new Response(200, body: self::readRawJsonFixture('response/minimal_entity')), + self::readRawJsonFixture('request/create_basic'), + ]; + + yield 'Create with Data' => [ + new CreateOperation( + description: 'Weekly (per seat)', + productId: 'pro_01gsz4t5hdjse780zja8vvr7jg', + unitPrice: new Money('1000', CurrencyCode::GBP), + name: 'Weekly', + unitPriceOverrides: [ + new UnitPriceOverride( + [CountryCode::CA, CountryCode::US], + new Money('5000', CurrencyCode::USD), + ), + ], + taxMode: TaxMode::AccountSetting, + trialPeriod: new TimePeriod(Interval::Week, 1), + billingCycle: new TimePeriod(Interval::Year, 1), + quantity: new PriceQuantity(1, 1), + customData: new CustomData(['foo' => 'bar']), + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/create_full'), + ]; + } + + /** + * @test + * + * @dataProvider updateOperationsProvider + */ + public function it_uses_expected_payload_on_update( + UpdateOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->prices->update('pro_01h7zcgmdc6tmwtjehp3sh7azf', $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('PATCH', $request->getMethod()); + self::assertEquals(Environment::SANDBOX->baseUrl() . '/prices/pro_01h7zcgmdc6tmwtjehp3sh7azf', urldecode((string) $request->getUri())); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function updateOperationsProvider(): \Generator + { + yield 'Update Single' => [ + new UpdateOperation(name: 'Annually'), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_single'), + ]; + + yield 'Update Partial' => [ + new UpdateOperation(name: 'Annually', unitPrice: new Money('100000', CurrencyCode::GBP)), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_partial'), + ]; + + yield 'Update All' => [ + new UpdateOperation( + name: 'Annually', + description: 'Annually (per seat)', + unitPrice: new Money('100000', CurrencyCode::GBP), + unitPriceOverrides: [new UnitPriceOverride([CountryCode::US], new Money('200000', CurrencyCode::USD))], + quantity: new PriceQuantity(1, 10), + trialPeriod: new TimePeriod(Interval::Month, 1), + billingCycle: new TimePeriod(Interval::Year, 1), + taxMode: TaxMode::External, + customData: new CustomData(['features' => ['reports' => true]]), + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_full'), + ]; + } + + /** + * @test + * + * @dataProvider listOperationsProvider + */ + public function list_hits_expected_uri( + ListOperation $operation, + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->prices->list($operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function listOperationsProvider(): \Generator + { + yield 'Default' => [ + new ListOperation(), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/prices', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Default Paged' => [ + new ListOperation(new Pager()), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/prices?order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Default Paged with After' => [ + new ListOperation(new Pager(after: 'pro_01gsz4s0w61y0pp88528f1wvvb')), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/prices?after=pro_01gsz4s0w61y0pp88528f1wvvb&order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'NotificationStatus Filtered' => [ + new ListOperation(statuses: [Status::Archived]), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/prices?status=archived', Environment::SANDBOX->baseUrl()), + ]; + + yield 'ID Filtered' => [ + new ListOperation(ids: ['pri_01gsz4s0w61y0pp88528f1wvvb']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/prices?id=pri_01gsz4s0w61y0pp88528f1wvvb', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Multiple ID Filtered' => [ + new ListOperation(ids: ['pri_01he6hp8cg49jjf1pdjf6d5yw1', 'pri_01h1vjes1y163xfj1rh1tkfb65']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/prices?id=pri_01he6hp8cg49jjf1pdjf6d5yw1,pri_01h1vjes1y163xfj1rh1tkfb65', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Product ID Filtered' => [ + new ListOperation(productIds: ['pro_01gsz4s0w61y0pp88528f1wvvb']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/prices?product_id=pro_01gsz4s0w61y0pp88528f1wvvb', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Multiple Product ID Filtered' => [ + new ListOperation(productIds: ['pro_01he6hp8cg49jjf1pdjf6d5yw1', 'pro_01h1vjes1y163xfj1rh1tkfb65']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/prices?product_id=pro_01he6hp8cg49jjf1pdjf6d5yw1,pro_01h1vjes1y163xfj1rh1tkfb65', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Recurring Filtered' => [ + new ListOperation(recurring: true), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/prices?recurring=true', Environment::SANDBOX->baseUrl()), + ]; + + yield 'With Includes' => [ + new ListOperation(includes: [Includes::Product]), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/prices?include=product', Environment::SANDBOX->baseUrl()), + ]; + } + + /** + * @test + * + * @dataProvider getRequestProvider + * + * @param Includes[] $includes + */ + public function get_hits_expected_uri( + array $includes, + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->prices->get('pri_01h7zcgmdc6tmwtjehp3sh7azf', $includes); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function getRequestProvider(): \Generator + { + yield 'Without Includes' => [ + [], + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + sprintf('%s/prices/pri_01h7zcgmdc6tmwtjehp3sh7azf', Environment::SANDBOX->baseUrl()), + ]; + + yield 'With Includes' => [ + [Includes::Product], + new Response(200, body: self::readRawJsonFixture('response/full_entity_with_includes')), + sprintf('%s/prices/pri_01h7zcgmdc6tmwtjehp3sh7azf?include=product', Environment::SANDBOX->baseUrl()), + ]; + } +} diff --git a/tests/Functional/Resources/Prices/_fixtures/request/create_basic.json b/tests/Functional/Resources/Prices/_fixtures/request/create_basic.json new file mode 100644 index 0000000..abccd60 --- /dev/null +++ b/tests/Functional/Resources/Prices/_fixtures/request/create_basic.json @@ -0,0 +1,8 @@ +{ + "description": "Monthly (per seat)", + "product_id": "pro_01h7zcgmdc6tmwtjehp3sh7azf", + "unit_price": { + "amount": "500", + "currency_code": "USD" + } +} diff --git a/tests/Functional/Resources/Prices/_fixtures/request/create_full.json b/tests/Functional/Resources/Prices/_fixtures/request/create_full.json new file mode 100644 index 0000000..02f4482 --- /dev/null +++ b/tests/Functional/Resources/Prices/_fixtures/request/create_full.json @@ -0,0 +1,37 @@ +{ + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "description": "Weekly (per seat)", + "name": "Weekly", + "billing_cycle": { + "interval": "year", + "frequency": 1 + }, + "trial_period": { + "interval": "week", + "frequency": 1 + }, + "tax_mode": "account_setting", + "unit_price": { + "amount": "1000", + "currency_code": "GBP" + }, + "unit_price_overrides": [ + { + "country_codes": [ + "CA", + "US" + ], + "unit_price": { + "amount": "5000", + "currency_code": "USD" + } + } + ], + "custom_data": { + "foo": "bar" + }, + "quantity": { + "minimum": 1, + "maximum": 1 + } +} diff --git a/tests/Functional/Resources/Prices/_fixtures/request/update_full.json b/tests/Functional/Resources/Prices/_fixtures/request/update_full.json new file mode 100644 index 0000000..28fc2d3 --- /dev/null +++ b/tests/Functional/Resources/Prices/_fixtures/request/update_full.json @@ -0,0 +1,35 @@ +{ + "description": "Annually (per seat)", + "name": "Annually", + "billing_cycle": { + "interval": "year", + "frequency": 1 + }, + "trial_period": { + "interval": "month", + "frequency": 1 + }, + "tax_mode": "external", + "unit_price": { + "amount": "100000", + "currency_code": "GBP" + }, + "unit_price_overrides": [ + { + "country_codes": [ + "US" + ], + "unit_price": { + "amount": "200000", + "currency_code": "USD" + } + } + ], + "custom_data": { + "features": {"reports": true} + }, + "quantity": { + "minimum": 1, + "maximum": 10 + } +} diff --git a/tests/Functional/Resources/Prices/_fixtures/request/update_partial.json b/tests/Functional/Resources/Prices/_fixtures/request/update_partial.json new file mode 100644 index 0000000..aeb102a --- /dev/null +++ b/tests/Functional/Resources/Prices/_fixtures/request/update_partial.json @@ -0,0 +1,7 @@ +{ + "name": "Annually", + "unit_price": { + "amount": "100000", + "currency_code": "GBP" + } +} diff --git a/tests/Functional/Resources/Prices/_fixtures/request/update_single.json b/tests/Functional/Resources/Prices/_fixtures/request/update_single.json new file mode 100644 index 0000000..f5dd563 --- /dev/null +++ b/tests/Functional/Resources/Prices/_fixtures/request/update_single.json @@ -0,0 +1,3 @@ +{ + "name": "Annually" +} diff --git a/tests/Functional/Resources/Prices/_fixtures/response/full_entity.json b/tests/Functional/Resources/Prices/_fixtures/response/full_entity.json new file mode 100644 index 0000000..167053b --- /dev/null +++ b/tests/Functional/Resources/Prices/_fixtures/response/full_entity.json @@ -0,0 +1,44 @@ +{ + "data": { + "id": "pri_01he6hp8cg49jjf1pdjf6d5yw1", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "description": "Weekly (per seat)", + "name": "Weekly", + "billing_cycle": { + "interval": "year", + "frequency": 1 + }, + "trial_period": { + "interval": "week", + "frequency": 1 + }, + "tax_mode": "account_setting", + "unit_price": { + "amount": "1000", + "currency_code": "GBP" + }, + "unit_price_overrides": [ + { + "country_codes": [ + "CA", + "US" + ], + "unit_price": { + "amount": "5000", + "currency_code": "USD" + } + } + ], + "custom_data": { + "foo": "bar" + }, + "status": "active", + "quantity": { + "minimum": 1, + "maximum": 1 + } + }, + "meta": { + "request_id": "c5f89594-9551-47b5-a946-6030942b6080" + } +} diff --git a/tests/Functional/Resources/Prices/_fixtures/response/full_entity_with_includes.json b/tests/Functional/Resources/Prices/_fixtures/response/full_entity_with_includes.json new file mode 100644 index 0000000..5b27a30 --- /dev/null +++ b/tests/Functional/Resources/Prices/_fixtures/response/full_entity_with_includes.json @@ -0,0 +1,65 @@ +{ + "data": { + "id": "pri_01he6hp8cg49jjf1pdjf6d5yw1", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "description": "Weekly (per seat)", + "name": null, + "billing_cycle": { + "interval": "week", + "frequency": 1 + }, + "trial_period": { + "interval": "week", + "frequency": 1 + }, + "tax_mode": "account_setting", + "unit_price": { + "amount": "1000", + "currency_code": "GBP" + }, + "unit_price_overrides": [ + { + "country_codes": [ + "CA", + "US" + ], + "unit_price": { + "amount": "5000", + "currency_code": "USD" + } + } + ], + "custom_data": { + "foo": "bar" + }, + "status": "active", + "quantity": { + "minimum": 1, + "maximum": 1 + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "tax_category": "standard", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/2nmP8MQSret0aWeDemRw_icon1.png", + "custom_data": { + "features": { + "crm": true, + "data_retention": false, + "reports": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of advanced reporting and a CRM that's right where you're chatting." + }, + "status": "active", + "created_at": "2023-02-23T12:43:46.605Z" + } + }, + "meta": { + "request_id": "aad79720-fb1b-4cf5-a770-15505251eb9e" + } +} diff --git a/tests/Functional/Resources/Prices/_fixtures/response/list_default.json b/tests/Functional/Resources/Prices/_fixtures/response/list_default.json new file mode 100644 index 0000000..a1aa9f9 --- /dev/null +++ b/tests/Functional/Resources/Prices/_fixtures/response/list_default.json @@ -0,0 +1,534 @@ +{ + "data": [ + { + "id": "pri_01he6hp8cg49jjf1pdjf6d5yw1", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "description": "Weekly (per seat)", + "name": null, + "billing_cycle": { + "interval": "week", + "frequency": 1 + }, + "trial_period": { + "interval": "week", + "frequency": 1 + }, + "tax_mode": "account_setting", + "unit_price": { + "amount": "1000", + "currency_code": "GBP" + }, + "unit_price_overrides": [ + { + "country_codes": [ + "CA", + "US" + ], + "unit_price": { + "amount": "5000", + "currency_code": "USD" + } + } + ], + "custom_data": { + "foo": "bar" + }, + "status": "active", + "quantity": { + "minimum": 1, + "maximum": 1 + } + }, + { + "id": "pri_01he6fm50df5cjmwdy3v3p1z1t", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "description": "Monthly (per seat)", + "name": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "500", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "custom_data": null, + "status": "active", + "quantity": { + "minimum": 1, + "maximum": 100 + } + }, + { + "id": "pri_01he5kxqey1k8ankgef29cj4bv", + "product_id": "pro_01he5kwnnvgdv2chtpgavk2rf8", + "description": "Base subscription", + "name": "Base subscription", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "0", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "custom_data": null, + "status": "active", + "quantity": { + "minimum": 1, + "maximum": 100 + } + }, + { + "id": "pri_01h982194qx6az312q4jc7pb7y", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "description": "One-time charge", + "name": null, + "billing_cycle": null, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "20000000", + "currency_code": "CNY" + }, + "unit_price_overrides": [], + "custom_data": null, + "status": "active", + "quantity": { + "minimum": 1, + "maximum": 100 + } + }, + { + "id": "pri_01h95tv9jar7paw64xf2f9vdpt", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "description": "One-time charge", + "name": null, + "billing_cycle": null, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "15000", + "currency_code": "EUR" + }, + "unit_price_overrides": [], + "custom_data": null, + "status": "active", + "quantity": { + "minimum": 1, + "maximum": 100 + } + }, + { + "id": "pri_01h1vjg3sqjj1y9tvazkdqe5vt", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "description": "Annual (recurring addon)", + "name": null, + "billing_cycle": { + "interval": "year", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "100000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "custom_data": null, + "status": "active", + "quantity": { + "minimum": 1, + "maximum": 1 + } + }, + { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "description": "Monthly (recurring addon)", + "name": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": [ + "AU", + "AT", + "BE" + ], + "unit_price": { + "amount": "20000", + "currency_code": "AUD" + } + } + ], + "custom_data": null, + "status": "active", + "quantity": { + "minimum": 1, + "maximum": 100 + } + }, + { + "id": "pri_01gvne87kv8vbqa9jkfbmgtsed", + "product_id": "pro_01gsz4vmqbjk3x4vvtafffd540", + "description": "Monthly", + "name": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "5000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "custom_data": null, + "status": "active", + "quantity": { + "minimum": 1, + "maximum": 100 + } + }, + { + "id": "pri_01gsz98e27ak2tyhexptwc58yk", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "description": "One-time charge", + "name": null, + "billing_cycle": null, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": [ + "AU" + ], + "unit_price": { + "amount": "40000", + "currency_code": "AUD" + } + } + ], + "custom_data": null, + "status": "active", + "quantity": { + "minimum": 1, + "maximum": 1 + } + }, + { + "id": "pri_01gsz96z29d88jrmsf2ztbfgjg", + "product_id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "description": "Annual (recurring addon)", + "name": null, + "billing_cycle": { + "interval": "year", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "300000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "custom_data": null, + "status": "active", + "quantity": { + "minimum": 1, + "maximum": 1 + } + }, + { + "id": "pri_01gsz95g2zrkagg294kpstx54r", + "product_id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "description": "Monthly (recurring addon)", + "name": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "25000", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": [ + "US" + ], + "unit_price": { + "amount": "28500", + "currency_code": "USD" + } + }, + { + "country_codes": [ + "PL" + ], + "unit_price": { + "amount": "66000", + "currency_code": "PLN" + } + }, + { + "country_codes": [ + "IN" + ], + "unit_price": { + "amount": "837500", + "currency_code": "INR" + } + }, + { + "country_codes": [ + "UA" + ], + "unit_price": { + "amount": "335000", + "currency_code": "UAH" + } + }, + { + "country_codes": [ + "NG" + ], + "unit_price": { + "amount": "12000", + "currency_code": "USD" + } + } + ], + "custom_data": null, + "status": "active", + "quantity": { + "minimum": 1, + "maximum": 1 + } + }, + { + "id": "pri_01gsz91wy9k1yn7kx82aafwvea", + "product_id": "pro_01gsz4vmqbjk3x4vvtafffd540", + "description": "Annual (per seat)", + "name": null, + "billing_cycle": { + "interval": "year", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "50000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "custom_data": null, + "status": "active", + "quantity": { + "minimum": 1, + "maximum": 100 + } + }, + { + "id": "pri_01gsz8z1q1n00f12qt82y31smh", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "description": "Annual (per seat)", + "name": null, + "billing_cycle": { + "interval": "year", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "30000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "custom_data": { + "features": { + "crm": true, + "data_retention": false, + "reports": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of advanced reporting and a CRM that's right where you're chatting." + }, + "status": "active", + "quantity": { + "minimum": 1, + "maximum": 999 + } + }, + { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "description": "Monthly (per seat)", + "name": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": [ + "AU" + ], + "unit_price": { + "amount": "5000", + "currency_code": "AUD" + } + } + ], + "custom_data": { + "features": { + "crm": true, + "data_retention": false, + "reports": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of advanced reporting and a CRM that's right where you're chatting." + }, + "status": "active", + "quantity": { + "minimum": 1, + "maximum": 999 + } + }, + { + "id": "pri_01gsz8s48pyr4mbhvv2xfggesg", + "product_id": "pro_01gsz4s0w61y0pp88528f1wvvb", + "description": "Annual (per seat)", + "name": null, + "billing_cycle": { + "interval": "year", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": [ + "IN" + ], + "unit_price": { + "amount": "360000", + "currency_code": "INR" + } + }, + { + "country_codes": [ + "UA" + ], + "unit_price": { + "amount": "140000", + "currency_code": "UAH" + } + }, + { + "country_codes": [ + "PL" + ], + "unit_price": { + "amount": "28000", + "currency_code": "PLN" + } + }, + { + "country_codes": [ + "US" + ], + "unit_price": { + "amount": "15000", + "currency_code": "USD" + } + }, + { + "country_codes": [ + "NG" + ], + "unit_price": { + "amount": "5000", + "currency_code": "USD" + } + } + ], + "custom_data": null, + "status": "active", + "quantity": { + "minimum": 1, + "maximum": 100 + } + }, + { + "id": "pri_01gsz8ntc6z7npqqp6j4ys0w1w", + "product_id": "pro_01gsz4s0w61y0pp88528f1wvvb", + "description": "Monthly (per seat)", + "name": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "1000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "custom_data": null, + "status": "active", + "quantity": { + "minimum": 1, + "maximum": 100 + } + } + ], + "meta": { + "request_id": "a36e7258-c04d-483f-b50a-8d3a45eab392", + "pagination": { + "per_page": 50, + "next": "https://sandbox-api.paddle.com/prices?after=pri_01gsz8ntc6z7npqqp6j4ys0w1w", + "has_more": false, + "estimated_total": 16 + } + } +} diff --git a/tests/Functional/Resources/Prices/_fixtures/response/minimal_entity.json b/tests/Functional/Resources/Prices/_fixtures/response/minimal_entity.json new file mode 100644 index 0000000..41b807c --- /dev/null +++ b/tests/Functional/Resources/Prices/_fixtures/response/minimal_entity.json @@ -0,0 +1,28 @@ +{ + "data": { + "id": "pri_01he6fm50df5cjmwdy3v3p1z1t", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "description": "Monthly (per seat)", + "name": null, + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "500", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "custom_data": null, + "status": "active", + "quantity": { + "minimum": 1, + "maximum": 100 + } + }, + "meta": { + "request_id": "119db08f-5a82-4993-9dfa-72ec694af98b" + } +} diff --git a/tests/Functional/Resources/PricingPreviews/PricingPreviewsClientTest.php b/tests/Functional/Resources/PricingPreviews/PricingPreviewsClientTest.php new file mode 100644 index 0000000..c45f22a --- /dev/null +++ b/tests/Functional/Resources/PricingPreviews/PricingPreviewsClientTest.php @@ -0,0 +1,102 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** + * @test + * + * @dataProvider createOperationsProvider + */ + public function it_uses_expected_payload_on_preview_prices( + PreviewPricesOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->pricingPreviews->previewPrices($operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals(Environment::SANDBOX->baseUrl() . '/pricing-preview', urldecode((string) $request->getUri())); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function createOperationsProvider(): \Generator + { + yield 'Minimal' => [ + new PreviewPricesOperation( + [ + new PricePreviewItem('pri_01gsz8z1q1n00f12qt82y31smh', 20), + ], + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/preview_prices_minimal'), + ]; + + yield 'Multiple' => [ + new PreviewPricesOperation( + [ + new PricePreviewItem('pri_01gsz8z1q1n00f12qt82y31smh', 20), + new PricePreviewItem('pri_01gsz98e27ak2tyhexptwc58yk', 1), + ], + currencyCode: CurrencyCode::USD, + discountId: 'dsc_01gtgztp8fpchantd5g1wrksa3', + customerIpAddress: '34.232.58.13', + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/preview_prices_multiple'), + ]; + + yield 'Full' => [ + new PreviewPricesOperation( + [ + new PricePreviewItem('pri_01gsz8z1q1n00f12qt82y31smh', 20), + new PricePreviewItem('pri_01gsz98e27ak2tyhexptwc58yk', 1), + ], + customerId: 'ctm_01h25m0sar5845yv5j8zj5xwe1', + addressId: 'add_01h848pep46enq8y372x7maj0p', + businessId: 'biz_01hfvpm3fj1my86qqs1c32mzsp', + currencyCode: CurrencyCode::USD, + discountId: 'dsc_01gtgztp8fpchantd5g1wrksa3', + address: new AddressPreview('20149', CountryCode::US), + customerIpAddress: '34.232.58.13', + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/preview_prices_full'), + ]; + } +} diff --git a/tests/Functional/Resources/PricingPreviews/_fixtures/request/preview_prices_full.json b/tests/Functional/Resources/PricingPreviews/_fixtures/request/preview_prices_full.json new file mode 100644 index 0000000..c454553 --- /dev/null +++ b/tests/Functional/Resources/PricingPreviews/_fixtures/request/preview_prices_full.json @@ -0,0 +1,22 @@ +{ + "items": [ + { + "quantity": 20, + "price_id": "pri_01gsz8z1q1n00f12qt82y31smh" + }, + { + "quantity": 1, + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk" + } + ], + "customer_id": "ctm_01h25m0sar5845yv5j8zj5xwe1", + "address_id": "add_01h848pep46enq8y372x7maj0p", + "business_id": "biz_01hfvpm3fj1my86qqs1c32mzsp", + "currency_code": "USD", + "discount_id": "dsc_01gtgztp8fpchantd5g1wrksa3", + "address": { + "postal_code": "20149", + "country_code": "US" + }, + "customer_ip_address": "34.232.58.13" +} diff --git a/tests/Functional/Resources/PricingPreviews/_fixtures/request/preview_prices_minimal.json b/tests/Functional/Resources/PricingPreviews/_fixtures/request/preview_prices_minimal.json new file mode 100644 index 0000000..35a9243 --- /dev/null +++ b/tests/Functional/Resources/PricingPreviews/_fixtures/request/preview_prices_minimal.json @@ -0,0 +1,8 @@ +{ + "items": [ + { + "quantity": 20, + "price_id": "pri_01gsz8z1q1n00f12qt82y31smh" + } + ] +} diff --git a/tests/Functional/Resources/PricingPreviews/_fixtures/request/preview_prices_multiple.json b/tests/Functional/Resources/PricingPreviews/_fixtures/request/preview_prices_multiple.json new file mode 100644 index 0000000..2211227 --- /dev/null +++ b/tests/Functional/Resources/PricingPreviews/_fixtures/request/preview_prices_multiple.json @@ -0,0 +1,15 @@ +{ + "items": [ + { + "quantity": 20, + "price_id": "pri_01gsz8z1q1n00f12qt82y31smh" + }, + { + "quantity": 1, + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk" + } + ], + "currency_code": "USD", + "discount_id": "dsc_01gtgztp8fpchantd5g1wrksa3", + "customer_ip_address": "34.232.58.13" +} diff --git a/tests/Functional/Resources/PricingPreviews/_fixtures/response/full_entity.json b/tests/Functional/Resources/PricingPreviews/_fixtures/response/full_entity.json new file mode 100644 index 0000000..bb52ec4 --- /dev/null +++ b/tests/Functional/Resources/PricingPreviews/_fixtures/response/full_entity.json @@ -0,0 +1,195 @@ +{ + "data": { + "customer_id": null, + "address_id": null, + "business_id": null, + "currency_code": "USD", + "address": { + "postal_code": "20149", + "country_code": "US" + }, + "customer_ip_address": "34.232.58.13", + "discount_id": "dsc_01gtgztp8fpchantd5g1wrksa3", + "details": { + "line_items": [ + { + "price": { + "id": "pri_01gsz8z1q1n00f12qt82y31smh", + "description": "Annual (per seat)", + "name": "Annual (per seat)", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "year", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "30000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 10, + "maximum": 999 + }, + "status": "active", + "custom_data": null + }, + "quantity": 20, + "tax_rate": "0", + "unit_totals": { + "subtotal": "30000", + "discount": "3000", + "tax": "0", + "total": "27000" + }, + "formatted_unit_totals": { + "subtotal": "$300.00", + "discount": "$30.00", + "tax": "$0.00", + "total": "$270.00" + }, + "totals": { + "subtotal": "600000", + "discount": "60000", + "tax": "0", + "total": "540000" + }, + "formatted_totals": { + "subtotal": "$6,000.00", + "discount": "$600.00", + "tax": "$0.00", + "total": "$5,400.00" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active", + "custom_data": null + }, + "discounts": [ + { + "discount": { + "id": "dsc_01gtgztp8fpchantd5g1wrksa3", + "status": "active", + "description": "Black Friday 2024", + "enabled_for_checkout": false, + "code": "BF2024", + "type": "percentage", + "amount": "10", + "currency_code": null, + "recur": false, + "maximum_recurring_intervals": null, + "usage_limit": null, + "restrict_to": null, + "expires_at": "2024-12-03T00:00:00Z", + "times_used": 0, + "created_at": "2023-03-02T11:03:00.623Z", + "updated_at": "2023-03-08T12:24:29.706Z" + }, + "total": "60000", + "formatted_total": "$600.00" + } + ] + }, + { + "price": { + "id": "pri_01gsz98e27ak2tyhexptwc58yk", + "description": "One-time charge", + "name": "One-time charge", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "billing_cycle": null, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": ["AU"], + "unit_price": { + "amount": "40000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 1, + "maximum": 1 + }, + "status": "active", + "custom_data": null + }, + "quantity": 1, + "tax_rate": "0", + "unit_totals": { + "subtotal": "19900", + "discount": "1990", + "tax": "0", + "total": "17910" + }, + "formatted_unit_totals": { + "subtotal": "$199.00", + "discount": "$19.90", + "tax": "$0.00", + "total": "$179.10" + }, + "totals": { + "subtotal": "19900", + "discount": "1990", + "tax": "0", + "total": "17910" + }, + "formatted_totals": { + "subtotal": "$199.00", + "discount": "$19.90", + "tax": "$0.00", + "total": "$179.10" + }, + "product": { + "id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "name": "Custom domains", + "description": "Make ChatApp truly your own with custom domains! Custom domains reinforce your brand identity and make it easy for your team to access ChatApp.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "status": "active", + "custom_data": null + }, + "discounts": [ + { + "discount": { + "id": "dsc_01gtgztp8fpchantd5g1wrksa3", + "status": "active", + "description": "Black Friday 2024", + "enabled_for_checkout": false, + "code": "BF2024", + "type": "percentage", + "amount": "10", + "currency_code": null, + "recur": false, + "maximum_recurring_intervals": null, + "usage_limit": null, + "restrict_to": null, + "expires_at": "2024-12-03T00:00:00Z", + "times_used": 0, + "created_at": "2023-03-02T11:03:00.623Z", + "updated_at": "2023-03-08T12:24:29.706Z" + }, + "total": "1990", + "formatted_total": "$19.90" + } + ] + } + ] + }, + "available_payment_methods": ["apple_pay"] + }, + "meta": { + "request_id": "1b2b31c3-f266-4996-8d4d-e5fd495e2069" + } +} diff --git a/tests/Functional/Resources/Products/ProductsClientTest.php b/tests/Functional/Resources/Products/ProductsClientTest.php new file mode 100644 index 0000000..65ecb96 --- /dev/null +++ b/tests/Functional/Resources/Products/ProductsClientTest.php @@ -0,0 +1,258 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** + * @test + * + * @dataProvider createOperationsProvider + */ + public function it_uses_expected_payload_on_create( + CreateOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->products->create($operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals(Environment::SANDBOX->baseUrl() . '/products', urldecode((string) $request->getUri())); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function createOperationsProvider(): \Generator + { + yield 'Basic Create' => [ + new CreateOperation( + name: 'ChatApp Basic', + taxCategory: TaxCategory::Standard, + ), + new Response(200, body: self::readRawJsonFixture('response/minimal_entity')), + self::readRawJsonFixture('request/create_basic'), + ]; + + yield 'Create with Data' => [ + new CreateOperation( + name: 'ChatApp Full', + taxCategory: TaxCategory::Standard, + description: 'Spend more time engaging with students with ChataApp Education.', + imageUrl: 'https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png', + customData: new CustomData([ + 'features' => [ + 'reports' => true, + 'crm' => false, + 'data_retention' => true, + ], + ]), + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/create_full'), + ]; + } + + /** + * @test + * + * @dataProvider updateOperationsProvider + */ + public function it_uses_expected_payload_on_update( + UpdateOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->products->update('pro_01h7zcgmdc6tmwtjehp3sh7azf', $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('PATCH', $request->getMethod()); + self::assertEquals(Environment::SANDBOX->baseUrl() . '/products/pro_01h7zcgmdc6tmwtjehp3sh7azf', urldecode((string) $request->getUri())); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function updateOperationsProvider(): \Generator + { + yield 'Update Single' => [ + new UpdateOperation(name: 'ChatApp Pro'), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_single'), + ]; + + yield 'Update Partial' => [ + new UpdateOperation(name: 'ChatApp Pro', taxCategory: TaxCategory::Saas), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_partial'), + ]; + + yield 'Update All' => [ + new UpdateOperation( + name: 'ChatApp Pro', + taxCategory: TaxCategory::Saas, + description: 'Spend more time engaging with students with ChatApp Pro.', + imageUrl: 'https://paddle-sandbox.s3.amazonaws.com/pro.png', + customData: new CustomData([ + 'features' => [ + 'reports' => true, + 'crm' => true, + 'data_retention' => true, + ], + ]), + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_full'), + ]; + } + + /** + * @test + * + * @dataProvider listOperationsProvider + */ + public function list_hits_expected_uri( + ListOperation $operation, + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->products->list($operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function listOperationsProvider(): \Generator + { + yield 'Default' => [ + new ListOperation(), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/products', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Default Paged' => [ + new ListOperation(new Pager()), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/products?order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Default Paged with After' => [ + new ListOperation(new Pager(after: 'pro_01gsz4s0w61y0pp88528f1wvvb')), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/products?after=pro_01gsz4s0w61y0pp88528f1wvvb&order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'NotificationStatus Filtered' => [ + new ListOperation(statuses: [Status::Archived]), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/products?status=archived', Environment::SANDBOX->baseUrl()), + ]; + + yield 'ID Filtered' => [ + new ListOperation(ids: ['pro_01gsz4s0w61y0pp88528f1wvvb']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/products?id=pro_01gsz4s0w61y0pp88528f1wvvb', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Multiple ID Filtered' => [ + new ListOperation(ids: ['pro_01gsz4s0w61y0pp88528f1wvvb', 'pro_01h1vjes1y163xfj1rh1tkfb65']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/products?id=pro_01gsz4s0w61y0pp88528f1wvvb,pro_01h1vjes1y163xfj1rh1tkfb65', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Tax Category Filtered' => [ + new ListOperation(taxCategories: [TaxCategory::DigitalGoods, TaxCategory::Standard]), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/products?tax_category=digital-goods,standard', Environment::SANDBOX->baseUrl()), + ]; + + yield 'With Includes' => [ + new ListOperation(includes: [Includes::Prices]), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/products?include=prices', Environment::SANDBOX->baseUrl()), + ]; + } + + /** + * @test + * + * @dataProvider getRequestProvider + * + * @param Includes[] $includes + */ + public function get_hits_expected_uri( + array $includes, + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->products->get('pro_01h7zcgmdc6tmwtjehp3sh7azf', $includes); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function getRequestProvider(): \Generator + { + yield 'Without Includes' => [ + [], + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + sprintf('%s/products/pro_01h7zcgmdc6tmwtjehp3sh7azf', Environment::SANDBOX->baseUrl()), + ]; + + yield 'With Includes' => [ + [Includes::Prices], + new Response(200, body: self::readRawJsonFixture('response/full_entity_with_includes')), + sprintf('%s/products/pro_01h7zcgmdc6tmwtjehp3sh7azf?include=prices', Environment::SANDBOX->baseUrl()), + ]; + } +} diff --git a/tests/Functional/Resources/Products/_fixtures/request/create_basic.json b/tests/Functional/Resources/Products/_fixtures/request/create_basic.json new file mode 100644 index 0000000..6eadacd --- /dev/null +++ b/tests/Functional/Resources/Products/_fixtures/request/create_basic.json @@ -0,0 +1,4 @@ +{ + "name": "ChatApp Basic", + "tax_category": "standard" +} diff --git a/tests/Functional/Resources/Products/_fixtures/request/create_full.json b/tests/Functional/Resources/Products/_fixtures/request/create_full.json new file mode 100644 index 0000000..5f19e52 --- /dev/null +++ b/tests/Functional/Resources/Products/_fixtures/request/create_full.json @@ -0,0 +1,13 @@ +{ + "name": "ChatApp Full", + "tax_category": "standard", + "description": "Spend more time engaging with students with ChataApp Education.", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "custom_data": { + "features": { + "reports": true, + "crm": false, + "data_retention": true + } + } +} diff --git a/tests/Functional/Resources/Products/_fixtures/request/update_full.json b/tests/Functional/Resources/Products/_fixtures/request/update_full.json new file mode 100644 index 0000000..a350e19 --- /dev/null +++ b/tests/Functional/Resources/Products/_fixtures/request/update_full.json @@ -0,0 +1,13 @@ +{ + "name": "ChatApp Pro", + "tax_category": "saas", + "description": "Spend more time engaging with students with ChatApp Pro.", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/pro.png", + "custom_data": { + "features": { + "reports": true, + "crm": true, + "data_retention": true + } + } +} diff --git a/tests/Functional/Resources/Products/_fixtures/request/update_partial.json b/tests/Functional/Resources/Products/_fixtures/request/update_partial.json new file mode 100644 index 0000000..dac61ae --- /dev/null +++ b/tests/Functional/Resources/Products/_fixtures/request/update_partial.json @@ -0,0 +1,4 @@ +{ + "name": "ChatApp Pro", + "tax_category": "saas" +} diff --git a/tests/Functional/Resources/Products/_fixtures/request/update_single.json b/tests/Functional/Resources/Products/_fixtures/request/update_single.json new file mode 100644 index 0000000..b2d2822 --- /dev/null +++ b/tests/Functional/Resources/Products/_fixtures/request/update_single.json @@ -0,0 +1,3 @@ +{ + "name": "ChatApp Pro" +} diff --git a/tests/Functional/Resources/Products/_fixtures/response/full_entity.json b/tests/Functional/Resources/Products/_fixtures/response/full_entity.json new file mode 100644 index 0000000..31966a0 --- /dev/null +++ b/tests/Functional/Resources/Products/_fixtures/response/full_entity.json @@ -0,0 +1,21 @@ +{ + "data": { + "id": "pro_01h7zcgmdc6tmwtjehp3sh7azf", + "name": "ChatApp Full", + "tax_category": "standard", + "description": "Spend more time engaging with students with ChataApp Education. Includes features from our Pro plan, plus tools to help educators track student progress.", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "custom_data": { + "features": { + "crm": false, + "data_retention": true, + "reports": true + } + }, + "status": "active", + "created_at": "2023-08-16T14:38:08.3Z" + }, + "meta": { + "request_id": "ed738012-b8b3-4b4a-8761-ca708a01e400" + } +} diff --git a/tests/Functional/Resources/Products/_fixtures/response/full_entity_with_includes.json b/tests/Functional/Resources/Products/_fixtures/response/full_entity_with_includes.json new file mode 100644 index 0000000..e2cf7ae --- /dev/null +++ b/tests/Functional/Resources/Products/_fixtures/response/full_entity_with_includes.json @@ -0,0 +1,46 @@ +{ + "data": { + "id": "pro_01h7zcgmdc6tmwtjehp3sh7azf", + "name": "ChatApp Full", + "tax_category": "standard", + "description": "Spend more time engaging with students with ChataApp Education. Includes features from our Pro plan, plus tools to help educators track student progress.", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "custom_data": { + "features": { + "crm": false, + "data_retention": true, + "reports": true + } + }, + "status": "active", + "created_at": "2023-08-16T14:38:08.3Z", + "prices": [ + { + "id": "pri_01he5kxqey1k8ankgef29cj4bv", + "product_id": "pro_01he5kwnnvgdv2chtpgavk2rf8", + "description": "Base subscription", + "name": "Base subscription", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "10", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "custom_data": null, + "status": "active", + "quantity": { + "minimum": 1, + "maximum": 100 + } + } + ] + }, + "meta": { + "request_id": "ed738012-b8b3-4b4a-8761-ca708a01e400" + } +} diff --git a/tests/Functional/Resources/Products/_fixtures/response/list_default.json b/tests/Functional/Resources/Products/_fixtures/response/list_default.json new file mode 100644 index 0000000..a033e7f --- /dev/null +++ b/tests/Functional/Resources/Products/_fixtures/response/list_default.json @@ -0,0 +1,86 @@ +{ + "data": [ + { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Voice rooms addon", + "tax_category": "standard", + "description": "Create voice rooms in your chats to work in real time alongside your colleagues. Includes unlimited voice rooms and recording backup for compliance.", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/GcZzBjXRfiraensppgtQ_icon2.png", + "custom_data": null, + "status": "active", + "created_at": "2023-06-01T13:30:50.302Z" + }, + { + "id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "name": "Custom domains", + "tax_category": "standard", + "description": "Make ChatApp truly your own with custom domains! Custom domains reinforce your brand identity and make it easy for your team to access ChatApp.", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "custom_data": { + "crm_id": "ABC" + }, + "status": "active", + "created_at": "2023-02-23T14:01:02.441Z" + }, + { + "id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "name": "VIP support", + "tax_category": "standard", + "description": "Get exclusive access to our expert team of product specialists, available to help you make the most of your ChatApp subscription.", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "custom_data": null, + "status": "active", + "created_at": "2023-02-23T13:58:17.615Z" + }, + { + "id": "pro_01gsz4vmqbjk3x4vvtafffd540", + "name": "ChatApp Enterprise", + "tax_category": "standard", + "description": "The ultimate solution for businesses that require top-of-the-line features and customizations. Includes all the features of the Pro plan, plus personalized onboarding, dedicated account management, and the ability to pay via invoice.", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "custom_data": null, + "status": "active", + "created_at": "2023-02-23T12:44:34.923Z" + }, + { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "tax_category": "standard", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "custom_data": { + "features": { + "crm": true, + "data_retention": false, + "reports": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of advanced reporting and a CRM that's right where you're chatting." + }, + "status": "active", + "created_at": "2023-02-23T12:43:46.605Z" + }, + { + "id": "pro_01gsz4s0w61y0pp88528f1wvvb", + "name": "ChatApp Basic", + "tax_category": "standard", + "description": "Ideal for small teams who want to stay connected and organized. Access to all the essential features, including unlimited messaging, file sharing, and integrations with your favorite tools.", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "custom_data": null, + "status": "active", + "created_at": "2023-02-23T12:43:09.062Z" + } + ], + "meta": { + "request_id": "e2923a4b-6230-485f-bd7f-64cc4db2454b", + "pagination": { + "per_page": 50, + "next": "https://api.paddle.com/products?after=pro_01gsz4s0w61y0pp88528f1wvvb", + "has_more": false, + "estimated_total": 6 + } + } +} diff --git a/tests/Functional/Resources/Products/_fixtures/response/minimal_entity.json b/tests/Functional/Resources/Products/_fixtures/response/minimal_entity.json new file mode 100644 index 0000000..05ddaf3 --- /dev/null +++ b/tests/Functional/Resources/Products/_fixtures/response/minimal_entity.json @@ -0,0 +1,15 @@ +{ + "data": { + "id": "pro_01h7zcgmdc6tmwtjehp3sh7azf", + "name": "ChatApp Basic", + "tax_category": "standard", + "description": null, + "image_url": null, + "custom_data": null, + "status": "active", + "created_at": "2023-08-16T14:38:08.3Z" + }, + "meta": { + "request_id": "ed738012-b8b3-4b4a-8761-ca708a01e400" + } +} diff --git a/tests/Functional/Resources/Reports/ReportsClientTest.php b/tests/Functional/Resources/Reports/ReportsClientTest.php new file mode 100644 index 0000000..82d900e --- /dev/null +++ b/tests/Functional/Resources/Reports/ReportsClientTest.php @@ -0,0 +1,167 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** + * @test + * + * @dataProvider createOperationsProvider + */ + public function it_uses_expected_payload_on_create( + CreateOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->reports->create($operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/reports', + urldecode((string) $request->getUri()), + ); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function createOperationsProvider(): \Generator + { + yield 'Basic Create' => [ + new CreateOperation( + type: ReportType::Transactions, + ), + new Response(201, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/create_basic'), + ]; + + yield 'Create with filters' => [ + new CreateOperation( + type: ReportType::Transactions, + filters: [new ReportFilters(name: ReportName::UpdatedAt, operator: ReportOperator::Lt, value: '2023-12-30')], + ), + new Response(201, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/create_full'), + ]; + } + + /** + * @test + * + * @dataProvider listOperationsProvider + */ + public function list_hits_expected_uri( + ListOperation $operation, + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->reports->list($operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function listOperationsProvider(): \Generator + { + yield 'Default' => [ + new ListOperation(), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/reports', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Default Paged' => [ + new ListOperation(new Pager()), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/reports?order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Default Paged with After' => [ + new ListOperation(new Pager(after: 'rep_01hhq4c3b03g3x2kpkj8aecjv6')), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/reports?after=rep_01hhq4c3b03g3x2kpkj8aecjv6&order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Status Filtered' => [ + new ListOperation(statuses: [ReportStatus::Ready]), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/reports?status=ready', Environment::SANDBOX->baseUrl()), + ]; + } + + /** @test */ + public function get_hits_expected_uri(): void + { + $response = new Response(200, body: self::readRawJsonFixture('response/full_entity')); + $this->mockClient->addResponse($response); + $this->client->reports->get('rep_01hhq4c3b03g3x2kpkj8aecjv6'); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals( + sprintf('%s/reports/rep_01hhq4c3b03g3x2kpkj8aecjv6', Environment::SANDBOX->baseUrl()), + urldecode((string) $request->getUri()), + ); + } + + /** @test */ + public function get_report_csv_hits_expected_uri(): void + { + $response = new Response(200, body: self::readRawJsonFixture('response/report_csv_entity')); + $this->mockClient->addResponse($response); + $this->client->reports->getReportCsv('rep_01hhq4c3b03g3x2kpkj8aecjv6'); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals( + sprintf('%s/reports/rep_01hhq4c3b03g3x2kpkj8aecjv6/download-url', Environment::SANDBOX->baseUrl()), + urldecode((string) $request->getUri()), + ); + } +} diff --git a/tests/Functional/Resources/Reports/_fixtures/request/create_basic.json b/tests/Functional/Resources/Reports/_fixtures/request/create_basic.json new file mode 100644 index 0000000..8778cb5 --- /dev/null +++ b/tests/Functional/Resources/Reports/_fixtures/request/create_basic.json @@ -0,0 +1,3 @@ +{ + "type": "transactions" +} diff --git a/tests/Functional/Resources/Reports/_fixtures/request/create_full.json b/tests/Functional/Resources/Reports/_fixtures/request/create_full.json new file mode 100644 index 0000000..fe16d15 --- /dev/null +++ b/tests/Functional/Resources/Reports/_fixtures/request/create_full.json @@ -0,0 +1,10 @@ +{ + "type": "transactions", + "filters": [ + { + "name": "updated_at", + "value": "2023-12-30", + "operator": "lt" + } + ] +} \ No newline at end of file diff --git a/tests/Functional/Resources/Reports/_fixtures/response/full_entity.json b/tests/Functional/Resources/Reports/_fixtures/response/full_entity.json new file mode 100644 index 0000000..d3bc23b --- /dev/null +++ b/tests/Functional/Resources/Reports/_fixtures/response/full_entity.json @@ -0,0 +1,21 @@ +{ + "data": { + "id": "rep_01hhq4c3b03g3x2kpkj8aecjv6", + "type": "transactions", + "rows": null, + "status": "pending", + "filters": [ + { + "name": "updated_at", + "value": "2023-12-30", + "operator": "lt" + } + ], + "expires_at": null, + "created_at": "2024-01-05T16:18:53.92Z", + "updated_at": "2024-01-05T16:18:53.92Z" + }, + "meta": { + "request_id": "70d20619-e988-41ce-81f3-0c1a6a6136e2" + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/Reports/_fixtures/response/list_default.json b/tests/Functional/Resources/Reports/_fixtures/response/list_default.json new file mode 100644 index 0000000..4ed4ee9 --- /dev/null +++ b/tests/Functional/Resources/Reports/_fixtures/response/list_default.json @@ -0,0 +1,36 @@ +{ + "data": [ + { + "id": "rep_01hhq4c3b03g3x2kpkj8aecjv6", + "type": "transactions", + "rows": 10, + "status": "ready", + "filters": [ + { + "name": "updated_at", + "value": "2023-12-30", + "operator": "lt" + }, + { + "name": "collection_mode", + "value": [ + "manual" + ], + "operator": null + } + ], + "expires_at": "2024-02-05T16:18:53.92Z", + "created_at": "2024-01-05T16:18:53.92Z", + "updated_at": "2024-01-05T16:18:53.92Z" + } + ], + "meta": { + "request_id": "31858add-0308-4c26-90fb-266e16be0c8c", + "pagination": { + "per_page": 50, + "next": "https://api.paddle.com/reports?after=rep_01hhq4c3b03g3x2kpkj8aecjv6", + "has_more": false, + "estimated_total": 1 + } + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/Reports/_fixtures/response/report_csv_entity.json b/tests/Functional/Resources/Reports/_fixtures/response/report_csv_entity.json new file mode 100644 index 0000000..18377ab --- /dev/null +++ b/tests/Functional/Resources/Reports/_fixtures/response/report_csv_entity.json @@ -0,0 +1,8 @@ +{ + "data": { + "url": "https://reports.paddle.com/transactions-10889-2023-12-05-17-44-51.csv" + }, + "meta": { + "request_id": "f34d4a9c-2088-447d-a3a1-1da5ce74f507" + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/Subscriptions/SubscriptionsClientTest.php b/tests/Functional/Resources/Subscriptions/SubscriptionsClientTest.php new file mode 100644 index 0000000..c417f3a --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/SubscriptionsClientTest.php @@ -0,0 +1,617 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** + * @test + * + * @dataProvider updateOperationsProvider + */ + public function it_uses_expected_payload_on_update( + UpdateOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->subscriptions->update('sub_01h8bx8fmywym11t6swgzba704', $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('PATCH', $request->getMethod()); + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/subscriptions/sub_01h8bx8fmywym11t6swgzba704', + urldecode((string) $request->getUri()), + ); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function updateOperationsProvider(): \Generator + { + yield 'Update Single' => [ + new UpdateOperation(prorationBillingMode: SubscriptionProrationBillingMode::ProratedNextBillingPeriod), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_single'), + ]; + + yield 'Update Partial' => [ + new UpdateOperation(prorationBillingMode: SubscriptionProrationBillingMode::FullImmediately, scheduledChange: null), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_partial'), + ]; + + yield 'Update All' => [ + new UpdateOperation( + customerId: 'ctm_01h8441jn5pcwrfhwh78jqt8hk', + addressId: 'add_01h848pep46enq8y372x7maj0p', + businessId: null, + currencyCode: CurrencyCode::GBP, + nextBilledAt: new \DateTimeImmutable('2023-11-06 14:00:00'), + discount: new SubscriptionDiscount( + 'dsc_01h848pep46enq8y372x7maj0p', + SubscriptionEffectiveFrom::NextBillingPeriod, + ), + collectionMode: CollectionMode::Automatic, + billingDetails: null, + scheduledChange: null, + items: [ + new SubscriptionItems('pri_01gsz91wy9k1yn7kx82aafwvea', 1), + new SubscriptionItems('pri_01gsz91wy9k1yn7kx82bafwvea', 5), + ], + prorationBillingMode: SubscriptionProrationBillingMode::FullImmediately, + customData: new CustomData(['early_access' => true]), + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_full'), + ]; + } + + /** + * @test + * + * @dataProvider listOperationsProvider + */ + public function list_hits_expected_uri( + ListOperation $operation, + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->subscriptions->list($operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function listOperationsProvider(): \Generator + { + yield 'Default' => [ + new ListOperation(), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/subscriptions', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Default Paged' => [ + new ListOperation(new Pager()), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/subscriptions?order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Default Paged with After' => [ + new ListOperation(new Pager(after: 'sub_01h848pep46enq8y372x7maj0p')), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/subscriptions?after=sub_01h848pep46enq8y372x7maj0p&order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'NotificationStatus Filtered' => [ + new ListOperation(statuses: [SubscriptionStatus::Paused]), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/subscriptions?status=paused', Environment::SANDBOX->baseUrl()), + ]; + + yield 'ID Filtered' => [ + new ListOperation(ids: ['sub_01h848pep46enq8y372x7maj0p']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/subscriptions?id=sub_01h848pep46enq8y372x7maj0p', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Multiple ID Filtered' => [ + new ListOperation(ids: ['sub_01h8494f4w5rwfp8b12yqh8fp1', 'sub_01h848pep46enq8y372x7maj0p']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/subscriptions?id=sub_01h8494f4w5rwfp8b12yqh8fp1,sub_01h848pep46enq8y372x7maj0p', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Collection Mode Filtered' => [ + new ListOperation(collectionMode: CollectionMode::Automatic), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/subscriptions?collection_mode=automatic', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Address ID Filtered' => [ + new ListOperation(addressIds: ['add_01h8494f4w5rwfp8b12yqh8fp1']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/subscriptions?address_id=add_01h8494f4w5rwfp8b12yqh8fp1', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Multiple Address ID Filtered' => [ + new ListOperation(addressIds: ['add_01h8494f4w5rwfp8b12yqh8fp1', 'add_01h848pep46enq8y372x7maj0p']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/subscriptions?address_id=add_01h8494f4w5rwfp8b12yqh8fp1,add_01h848pep46enq8y372x7maj0p', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Price ID Filtered' => [ + new ListOperation(priceIds: ['pri_01h8494f4w5rwfp8b12yqh8fp1']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/subscriptions?price_id=pri_01h8494f4w5rwfp8b12yqh8fp1', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Multiple Price ID Filtered' => [ + new ListOperation(priceIds: ['pri_01h8494f4w5rwfp8b12yqh8fp1', 'pri_01h848pep46enq8y372x7maj0p']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/subscriptions?price_id=pri_01h8494f4w5rwfp8b12yqh8fp1,pri_01h848pep46enq8y372x7maj0p', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Scheduled Change Action Filtered' => [ + new ListOperation(scheduledChangeActions: [SubscriptionScheduledChangeAction::Pause]), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/subscriptions?scheduled_change_action=pause', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Multiple Scheduled Change Action Filtered' => [ + new ListOperation(scheduledChangeActions: [ + SubscriptionScheduledChangeAction::Pause, + SubscriptionScheduledChangeAction::Cancel, + ]), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/subscriptions?scheduled_change_action=pause,cancel', Environment::SANDBOX->baseUrl()), + ]; + } + + /** + * @test + * + * @dataProvider getRequestProvider + * + * @param Includes[] $includes + */ + public function get_hits_expected_uri( + array $includes, + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->subscriptions->get('sub_01h7zcgmdc6tmwtjehp3sh7azf', $includes); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function getRequestProvider(): \Generator + { + yield 'Without Includes' => [ + [], + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + sprintf('%s/subscriptions/sub_01h7zcgmdc6tmwtjehp3sh7azf', Environment::SANDBOX->baseUrl()), + ]; + + yield 'With Includes' => [ + [Includes::NextTransaction], + new Response(200, body: self::readRawJsonFixture('response/full_entity_with_includes')), + sprintf('%s/subscriptions/sub_01h7zcgmdc6tmwtjehp3sh7azf?include=next_transaction', Environment::SANDBOX->baseUrl()), + ]; + } + + /** + * @test + * + * @dataProvider pauseOperationsProvider + */ + public function pause_uses_expected_payload( + PauseOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->subscriptions->pause('sub_01h8bx8fmywym11t6swgzba704', $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/subscriptions/sub_01h8bx8fmywym11t6swgzba704/pause', + urldecode((string) $request->getUri()), + ); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function pauseOperationsProvider(): \Generator + { + yield 'Update None' => [ + new PauseOperation(), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/pause_none'), + ]; + + yield 'Update Single' => [ + new PauseOperation(SubscriptionEffectiveFrom::NextBillingPeriod), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/pause_single'), + ]; + + yield 'Update All' => [ + new PauseOperation( + SubscriptionEffectiveFrom::NextBillingPeriod, + new \DateTime('2023-10-09T16:30:00Z'), + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/pause_full'), + ]; + } + + /** + * @test + * + * @dataProvider resumeOperationsProvider + */ + public function resume_uses_expected_payload( + ResumeOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->subscriptions->resume('sub_01h8bx8fmywym11t6swgzba704', $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/subscriptions/sub_01h8bx8fmywym11t6swgzba704/resume', + urldecode((string) $request->getUri()), + ); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function resumeOperationsProvider(): \Generator + { + yield 'Update None' => [ + new ResumeOperation(), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/resume_none'), + ]; + + yield 'Update Single As Enum' => [ + new ResumeOperation(SubscriptionEffectiveFrom::NextBillingPeriod), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/resume_single_as_enum'), + ]; + + yield 'Update Single As Date' => [ + new ResumeOperation(new \DateTime('2023-10-09T16:30:00Z')), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/resume_single_as_date'), + ]; + } + + /** + * @test + * + * @dataProvider cancelOperationsProvider + */ + public function cancel_uses_expected_payload( + CancelOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->subscriptions->cancel('sub_01h8bx8fmywym11t6swgzba704', $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/subscriptions/sub_01h8bx8fmywym11t6swgzba704/cancel', + urldecode((string) $request->getUri()), + ); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function cancelOperationsProvider(): \Generator + { + yield 'Update None' => [ + new CancelOperation(), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/cancel_none'), + ]; + + yield 'Update Single' => [ + new CancelOperation(SubscriptionEffectiveFrom::NextBillingPeriod), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/cancel_single'), + ]; + } + + /** + * @test + * + * @dataProvider getPaymentMethodChangeTransactionRequestProvider + */ + public function get_payment_method_change_transaction_hits_expected_uri( + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->subscriptions->getPaymentMethodChangeTransaction('sub_01h7zcgmdc6tmwtjehp3sh7azf'); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function getPaymentMethodChangeTransactionRequestProvider(): \Generator + { + yield 'Basic' => [ + new Response(200, body: self::readRawJsonFixture('response/get_payment_method_change_transaction_entity')), + sprintf('%s/subscriptions/sub_01h7zcgmdc6tmwtjehp3sh7azf/update-payment-method-transaction', Environment::SANDBOX->baseUrl()), + ]; + } + + /** + * @test + * + * @dataProvider activateOperationsProvider + */ + public function activate_uses_expected_payload( + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->subscriptions->activate('sub_01h8bx8fmywym11t6swgzba704'); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/subscriptions/sub_01h8bx8fmywym11t6swgzba704/activate', + urldecode((string) $request->getUri()), + ); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function activateOperationsProvider(): \Generator + { + yield 'Update' => [ + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + '{}', + ]; + } + + /** + * @test + * + * @dataProvider createOneTimeChargeOperationsProvider + */ + public function create_one_time_charge_uses_expected_payload( + CreateOneTimeChargeOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->subscriptions->createOneTimeCharge('sub_01h8bx8fmywym11t6swgzba704', $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/subscriptions/sub_01h8bx8fmywym11t6swgzba704/charge', + urldecode((string) $request->getUri()), + ); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function createOneTimeChargeOperationsProvider(): \Generator + { + yield 'Update Minimal' => [ + new CreateOneTimeChargeOperation( + SubscriptionEffectiveFrom::NextBillingPeriod, + [ + new SubscriptionItems('pri_01gsz98e27ak2tyhexptwc58yk', 1), + ], + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/create_one_time_charge_minimal'), + ]; + + yield 'Update Full' => [ + new CreateOneTimeChargeOperation( + SubscriptionEffectiveFrom::Immediately, + [ + new SubscriptionItems('pri_01gsz98e27ak2tyhexptwc58yk', 1), + new SubscriptionItems('pri_01h7zdqstxe6djaefkqbkjy4k2', 10), + new SubscriptionItems('pri_01h7zd9mzfq79850w4ryc39v38', 845), + ], + SubscriptionOnPaymentFailure::ApplyChange, + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/create_one_time_charge_full'), + ]; + } + + /** + * @test + * + * @dataProvider previewUpdateOperationsProvider + */ + public function it_uses_expected_payload_on_preview_update( + PreviewUpdateOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->subscriptions->previewUpdate('sub_01h8bx8fmywym11t6swgzba704', $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('PATCH', $request->getMethod()); + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/subscriptions/sub_01h8bx8fmywym11t6swgzba704/preview', + urldecode((string) $request->getUri()), + ); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function previewUpdateOperationsProvider(): \Generator + { + yield 'Preview Update Single' => [ + new PreviewUpdateOperation( + prorationBillingMode: SubscriptionProrationBillingMode::ProratedNextBillingPeriod, + ), + new Response(200, body: self::readRawJsonFixture('response/preview_update_full_entity')), + self::readRawJsonFixture('request/preview_update_single'), + ]; + + yield 'Preview Update Partial' => [ + new PreviewUpdateOperation( + prorationBillingMode: SubscriptionProrationBillingMode::FullImmediately, + scheduledChange: null, + ), + new Response(200, body: self::readRawJsonFixture('response/preview_update_full_entity')), + self::readRawJsonFixture('request/preview_update_partial'), + ]; + + yield 'Preview Update All' => [ + new PreviewUpdateOperation( + customerId: 'ctm_01h8441jn5pcwrfhwh78jqt8hk', + addressId: 'add_01h848pep46enq8y372x7maj0p', + businessId: null, + currencyCode: CurrencyCode::GBP, + nextBilledAt: new \DateTimeImmutable('2023-11-06 14:00:00'), + discount: new SubscriptionDiscount( + 'dsc_01h848pep46enq8y372x7maj0p', + SubscriptionEffectiveFrom::NextBillingPeriod, + ), + collectionMode: CollectionMode::Automatic, + billingDetails: null, + scheduledChange: null, + items: [ + new SubscriptionItems('pri_01gsz91wy9k1yn7kx82aafwvea', 1), + new SubscriptionItems('pri_01gsz91wy9k1yn7kx82bafwvea', 5), + ], + prorationBillingMode: SubscriptionProrationBillingMode::FullImmediately, + customData: new CustomData(['early_access' => true]), + ), + new Response(200, body: self::readRawJsonFixture('response/preview_update_full_entity')), + self::readRawJsonFixture('request/preview_update_full'), + ]; + } + + /** + * @test + * + * @dataProvider previewOneTimeChargeOperationsProvider + */ + public function preview_one_time_charge_uses_expected_payload( + PreviewOneTimeChargeOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->subscriptions->previewOneTimeCharge('sub_01h8bx8fmywym11t6swgzba704', $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/subscriptions/sub_01h8bx8fmywym11t6swgzba704/charge/preview', + urldecode((string) $request->getUri()), + ); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function previewOneTimeChargeOperationsProvider(): \Generator + { + yield 'Update Minimal' => [ + new PreviewOneTimeChargeOperation( + SubscriptionEffectiveFrom::NextBillingPeriod, + [ + new SubscriptionItems('pri_01gsz98e27ak2tyhexptwc58yk', 1), + ], + ), + new Response(200, body: self::readRawJsonFixture('response/preview_update_full_entity')), + self::readRawJsonFixture('request/preview_one_time_charge_minimal'), + ]; + + yield 'Update Full' => [ + new PreviewOneTimeChargeOperation( + SubscriptionEffectiveFrom::Immediately, + [ + new SubscriptionItems('pri_01gsz98e27ak2tyhexptwc58yk', 1), + new SubscriptionItems('pri_01h7zdqstxe6djaefkqbkjy4k2', 10), + new SubscriptionItems('pri_01h7zd9mzfq79850w4ryc39v38', 845), + ], + ), + new Response(200, body: self::readRawJsonFixture('response/preview_update_full_entity')), + self::readRawJsonFixture('request/preview_one_time_charge_full'), + ]; + } +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/request/cancel_none.json b/tests/Functional/Resources/Subscriptions/_fixtures/request/cancel_none.json new file mode 100644 index 0000000..9837641 --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/request/cancel_none.json @@ -0,0 +1,3 @@ +{ + "effective_from": null +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/request/cancel_single.json b/tests/Functional/Resources/Subscriptions/_fixtures/request/cancel_single.json new file mode 100644 index 0000000..b7da520 --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/request/cancel_single.json @@ -0,0 +1,3 @@ +{ + "effective_from": "next_billing_period" +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/request/create_one_time_charge_full.json b/tests/Functional/Resources/Subscriptions/_fixtures/request/create_one_time_charge_full.json new file mode 100644 index 0000000..da463e4 --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/request/create_one_time_charge_full.json @@ -0,0 +1,18 @@ +{ + "effective_from": "immediately", + "items": [ + { + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1 + }, + { + "price_id": "pri_01h7zdqstxe6djaefkqbkjy4k2", + "quantity": 10 + }, + { + "price_id": "pri_01h7zd9mzfq79850w4ryc39v38", + "quantity": 845 + } + ], + "on_payment_failure": "apply_change" +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/request/create_one_time_charge_minimal.json b/tests/Functional/Resources/Subscriptions/_fixtures/request/create_one_time_charge_minimal.json new file mode 100644 index 0000000..807d664 --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/request/create_one_time_charge_minimal.json @@ -0,0 +1,9 @@ +{ + "effective_from": "next_billing_period", + "items": [ + { + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1 + } + ] +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/request/pause_full.json b/tests/Functional/Resources/Subscriptions/_fixtures/request/pause_full.json new file mode 100644 index 0000000..e389e55 --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/request/pause_full.json @@ -0,0 +1,4 @@ +{ + "effective_from": "next_billing_period", + "resume_at": "2023-10-09T16:30:00.000000Z" +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/request/pause_none.json b/tests/Functional/Resources/Subscriptions/_fixtures/request/pause_none.json new file mode 100644 index 0000000..e203ae2 --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/request/pause_none.json @@ -0,0 +1,4 @@ +{ + "effective_from": null, + "resume_at": null +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/request/pause_single.json b/tests/Functional/Resources/Subscriptions/_fixtures/request/pause_single.json new file mode 100644 index 0000000..118bb6b --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/request/pause_single.json @@ -0,0 +1,4 @@ +{ + "effective_from": "next_billing_period", + "resume_at": null +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/request/preview_one_time_charge_full.json b/tests/Functional/Resources/Subscriptions/_fixtures/request/preview_one_time_charge_full.json new file mode 100644 index 0000000..6844f1e --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/request/preview_one_time_charge_full.json @@ -0,0 +1,17 @@ +{ + "effective_from": "immediately", + "items": [ + { + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1 + }, + { + "price_id": "pri_01h7zdqstxe6djaefkqbkjy4k2", + "quantity": 10 + }, + { + "price_id": "pri_01h7zd9mzfq79850w4ryc39v38", + "quantity": 845 + } + ] +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/request/preview_one_time_charge_minimal.json b/tests/Functional/Resources/Subscriptions/_fixtures/request/preview_one_time_charge_minimal.json new file mode 100644 index 0000000..807d664 --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/request/preview_one_time_charge_minimal.json @@ -0,0 +1,9 @@ +{ + "effective_from": "next_billing_period", + "items": [ + { + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1 + } + ] +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/request/preview_update_full.json b/tests/Functional/Resources/Subscriptions/_fixtures/request/preview_update_full.json new file mode 100644 index 0000000..0201ca7 --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/request/preview_update_full.json @@ -0,0 +1,22 @@ +{ + "customer_id": "ctm_01h8441jn5pcwrfhwh78jqt8hk", + "address_id": "add_01h848pep46enq8y372x7maj0p", + "business_id": null, + "currency_code": "GBP", + "next_billed_at": "2023-11-06T14:00:00.000000Z", + "discount": { + "id": "dsc_01h848pep46enq8y372x7maj0p", + "effective_from": "next_billing_period" + }, + "collection_mode": "automatic", + "billing_details": null, + "scheduled_change": null, + "items": [ + { "price_id": "pri_01gsz91wy9k1yn7kx82aafwvea", "quantity": 1 }, + { "price_id": "pri_01gsz91wy9k1yn7kx82bafwvea", "quantity": 5 } + ], + "proration_billing_mode": "full_immediately", + "custom_data": { + "early_access": true + } +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/request/preview_update_partial.json b/tests/Functional/Resources/Subscriptions/_fixtures/request/preview_update_partial.json new file mode 100644 index 0000000..b0f443a --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/request/preview_update_partial.json @@ -0,0 +1,4 @@ +{ + "proration_billing_mode": "full_immediately", + "scheduled_change": null +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/request/preview_update_single.json b/tests/Functional/Resources/Subscriptions/_fixtures/request/preview_update_single.json new file mode 100644 index 0000000..32e6110 --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/request/preview_update_single.json @@ -0,0 +1,3 @@ +{ + "proration_billing_mode": "prorated_next_billing_period" +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/request/resume_none.json b/tests/Functional/Resources/Subscriptions/_fixtures/request/resume_none.json new file mode 100644 index 0000000..9837641 --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/request/resume_none.json @@ -0,0 +1,3 @@ +{ + "effective_from": null +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/request/resume_single_as_date.json b/tests/Functional/Resources/Subscriptions/_fixtures/request/resume_single_as_date.json new file mode 100644 index 0000000..723f7fd --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/request/resume_single_as_date.json @@ -0,0 +1,3 @@ +{ + "effective_from": "2023-10-09T16:30:00.000000Z" +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/request/resume_single_as_enum.json b/tests/Functional/Resources/Subscriptions/_fixtures/request/resume_single_as_enum.json new file mode 100644 index 0000000..b7da520 --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/request/resume_single_as_enum.json @@ -0,0 +1,3 @@ +{ + "effective_from": "next_billing_period" +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/request/update_full.json b/tests/Functional/Resources/Subscriptions/_fixtures/request/update_full.json new file mode 100644 index 0000000..0201ca7 --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/request/update_full.json @@ -0,0 +1,22 @@ +{ + "customer_id": "ctm_01h8441jn5pcwrfhwh78jqt8hk", + "address_id": "add_01h848pep46enq8y372x7maj0p", + "business_id": null, + "currency_code": "GBP", + "next_billed_at": "2023-11-06T14:00:00.000000Z", + "discount": { + "id": "dsc_01h848pep46enq8y372x7maj0p", + "effective_from": "next_billing_period" + }, + "collection_mode": "automatic", + "billing_details": null, + "scheduled_change": null, + "items": [ + { "price_id": "pri_01gsz91wy9k1yn7kx82aafwvea", "quantity": 1 }, + { "price_id": "pri_01gsz91wy9k1yn7kx82bafwvea", "quantity": 5 } + ], + "proration_billing_mode": "full_immediately", + "custom_data": { + "early_access": true + } +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/request/update_partial.json b/tests/Functional/Resources/Subscriptions/_fixtures/request/update_partial.json new file mode 100644 index 0000000..b0f443a --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/request/update_partial.json @@ -0,0 +1,4 @@ +{ + "proration_billing_mode": "full_immediately", + "scheduled_change": null +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/request/update_single.json b/tests/Functional/Resources/Subscriptions/_fixtures/request/update_single.json new file mode 100644 index 0000000..32e6110 --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/request/update_single.json @@ -0,0 +1,3 @@ +{ + "proration_billing_mode": "prorated_next_billing_period" +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/response/full_entity.json b/tests/Functional/Resources/Subscriptions/_fixtures/response/full_entity.json new file mode 100644 index 0000000..0572a49 --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/response/full_entity.json @@ -0,0 +1,72 @@ +{ + "data": { + "id": "sub_01h8bx8fmywym11t6swgzba704", + "status": "active", + "customer_id": "ctm_01h8441jn5pcwrfhwh78jqt8hk", + "address_id": "add_01h848pep46enq8y372x7maj0p", + "business_id": null, + "currency_code": "USD", + "created_at": "2023-08-21T11:21:40.254Z", + "updated_at": "2023-08-21T11:25:13Z", + "started_at": "2023-08-21T11:21:38.547106Z", + "first_billed_at": "2023-08-21T11:21:38.547106Z", + "next_billed_at": "2024-08-21T11:25:12.462Z", + "paused_at": null, + "canceled_at": null, + "collection_mode": "manual", + "billing_details": { + "enable_checkout": false, + "purchase_order_number": "PO-123", + "additional_information": "Welcome to Enterprise! Contact your dedicated account manager if you have any problems.", + "payment_terms": { + "frequency": 2, + "interval": "week" + } + }, + "current_billing_period": { + "starts_at": "2023-08-21T11:25:12.462Z", + "ends_at": "2024-08-21T11:25:12.462Z" + }, + "billing_cycle": { + "frequency": 1, + "interval": "year" + }, + "scheduled_change": null, + "items": [ + { + "status": "active", + "quantity": 85, + "recurring": true, + "created_at": "2023-08-21T11:25:12.476Z", + "updated_at": "2023-08-21T11:25:12.476Z", + "previously_billed_at": "2023-08-21T11:25:12.476Z", + "next_billed_at": "2024-08-21T11:25:12.462Z", + "trial_dates": null, + "price": { + "id": "pri_01gsz91wy9k1yn7kx82aafwvea", + "product_id": "pro_01gsz4vmqbjk3x4vvtafffd540", + "description": "Annual (per seat)", + "tax_mode": "account_setting", + "billing_cycle": { + "frequency": 1, + "interval": "year" + }, + "trial_period": null, + "unit_price": { + "amount": "54437", + "currency_code": "USD" + } + } + } + ], + "custom_data": null, + "management_urls": { + "update_payment_method": null, + "cancel": "https://buyer-portal.paddle.com/subscriptions/sub_01h8bx8fmywym11t6swgzba704/cancel" + }, + "discount": null + }, + "meta": { + "request_id": "1431ce43-0e7f-4ed7-b4c6-c2c860b9a1ff" + } +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/response/full_entity_with_includes.json b/tests/Functional/Resources/Subscriptions/_fixtures/response/full_entity_with_includes.json new file mode 100644 index 0000000..b894b83 --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/response/full_entity_with_includes.json @@ -0,0 +1,222 @@ +{ + "data": { + "id": "sub_01heb0tj9w00j33bfbq3469cy0", + "status": "active", + "customer_id": "ctm_01heaqwvb9kv4dv4v67m738dta", + "address_id": "add_01heaqx50ad2sbsmf68saya8v2", + "business_id": null, + "currency_code": "USD", + "created_at": "2023-11-03T16:38:55.036Z", + "updated_at": "2023-11-24T16:38:53.111897Z", + "started_at": "2023-11-03T16:38:53.111897Z", + "first_billed_at": "2023-11-03T16:38:53.111897Z", + "next_billed_at": "2023-12-03T16:38:53.111897Z", + "paused_at": null, + "canceled_at": null, + "collection_mode": "automatic", + "billing_details": null, + "current_billing_period": { + "starts_at": "2023-11-03T16:38:53.111897Z", + "ends_at": "2023-12-03T16:38:53.111897Z" + }, + "billing_cycle": { + "frequency": 1, + "interval": "month" + }, + "next_transaction": { + "billing_period": { + "starts_at": "2023-12-03T16:38:53.111897Z", + "ends_at": "2024-01-03T16:38:53.111897Z" + }, + "details": { + "tax_rates_used": [ + { + "tax_rate": "0.2", + "totals": { + "subtotal": "41667", + "discount": "0", + "tax": "8333", + "total": "50000" + } + } + ], + "totals": { + "subtotal": "41667", + "tax": "8333", + "discount": "0", + "total": "50000", + "fee": null, + "credit": "0", + "credit_to_balance": "0", + "balance": "50000", + "grand_total": "50000", + "earnings": null, + "currency_code": "USD", + "exchange_rate": "1" + }, + "line_items": [ + { + "item_id": null, + "price_id": "pri_01heb0e913gej56emc8tjrkaq1", + "quantity": 5, + "totals": { + "subtotal": "16667", + "tax": "3333", + "discount": "0", + "total": "20000" + }, + "product": { + "id": "pro_01heb0cesy14mwnpyps6ee3hdc", + "name": " Basic Plan Product", + "description": "This is a description of my fav product", + "tax_category": "standard", + "image_url": "http://example.com", + "status": "active" + }, + "tax_rate": "0.2", + "unit_totals": { + "subtotal": "3333", + "discount": "0", + "tax": "667", + "total": "4000" + }, + "proration": { + "rate": "1", + "billing_period": { + "starts_at": "2023-12-03T16:38:53.111897Z", + "ends_at": "2024-01-03T16:38:53.111897Z" + } + } + }, + { + "item_id": null, + "price_id": "pri_01heb0fkqcnj94ykp5c3r8e0ja", + "quantity": 10, + "totals": { + "subtotal": "25000", + "tax": "5000", + "discount": "0", + "total": "30000" + }, + "product": { + "id": "pro_01heb0eqkjnw4pwgshghq87qga", + "name": "Recurring Addon Test", + "description": "", + "tax_category": "standard", + "image_url": "", + "status": "active" + }, + "tax_rate": "0.2", + "unit_totals": { + "subtotal": "2500", + "discount": "0", + "tax": "500", + "total": "3000" + }, + "proration": { + "rate": "1", + "billing_period": { + "starts_at": "2023-12-03T16:38:53.111897Z", + "ends_at": "2024-01-03T16:38:53.111897Z" + } + } + } + ] + }, + "adjustments": [ + { + "transaction_id": "txn_01h8bxpvx398a7zbawb77y0kp5", + "items": [ + { + "item_id": "txnitm_01h8bxryv3065dyh6103p3yg28", + "type": "proration", + "amount": "97987", + "proration": { + "rate": "1", + "billing_period": { + "starts_at": "2023-08-21T11:31:59.284Z", + "ends_at": "2023-09-21T11:31:08.689295Z" + } + }, + "totals": { + "subtotal": "90000", + "tax": "7987", + "total": "97987" + } + } + ], + "totals": { + "subtotal": "90000", + "tax": "7987", + "total": "97987", + "fee": "4949", + "earnings": "85051", + "currency_code": "USD" + } + } + ] + }, + "scheduled_change": null, + "items": [ + { + "status": "active", + "quantity": 5, + "recurring": true, + "created_at": "2023-11-03T16:38:55.036Z", + "updated_at": "2023-11-03T16:38:55.036Z", + "previously_billed_at": "2023-11-03T16:38:53.111897Z", + "next_billed_at": "2023-12-03T16:38:53.111897Z", + "trial_dates": null, + "price": { + "id": "pri_01heb0e913gej56emc8tjrkaqa", + "product_id": "pro_01heb0cesy14mwnpyps6ee3hda", + "description": "basic plan", + "tax_mode": "internal", + "billing_cycle": { + "frequency": 1, + "interval": "month" + }, + "trial_period": null, + "unit_price": { + "amount": "4000", + "currency_code": "USD" + } + } + }, + { + "status": "active", + "quantity": 10, + "recurring": true, + "created_at": "2023-11-03T16:38:55.036Z", + "updated_at": "2023-11-03T16:38:55.036Z", + "previously_billed_at": "2023-11-03T16:38:53.111897Z", + "next_billed_at": "2023-12-03T16:38:53.111897Z", + "trial_dates": null, + "price": { + "id": "pri_01heb0fkqcnj94ykp5c3r8e0ja", + "product_id": "pro_01heb0eqkjnw4pwgshghq87qga", + "description": "recurring addon", + "tax_mode": "internal", + "billing_cycle": { + "frequency": 1, + "interval": "month" + }, + "trial_period": null, + "unit_price": { + "amount": "3000", + "currency_code": "USD" + } + } + } + ], + "custom_data": null, + "management_urls": { + "update_payment_method": "https://buyer-portal.paddle.com/subscriptions/sub_01heb0tj9w00j33bfbq3469cy0/update-payment-method?token=pga_eyJhbGciOiJFZERTQSIsImtpZCI6Imp3a18wMWhkazBuOHF3OG55NTJ5cGNocGNhazA1ayIsInR5cCI6IkpXVCJ9.eyJpZCI6InBnYV8wMWhlanBja2s1Z2trYmZhcWhhYXgyNnJlYSIsInNlbGxlci1pZCI6IjUwNiIsInR5cGUiOiJzdGFuZGFyZCIsInZlcnNpb24iOiIxIiwidXNhZ2UiOiJtYW5hZ2VtZW50X3VybCIsInNjb3BlIjoiY3VzdG9tZXIuc3Vic2NyaXB0aW9uLXBheW1lbnQudXBkYXRlIGN1c3RvbWVyLnN1YnNjcmlwdGlvbi1wYXltZW50LnJlYWQgY3VzdG9tZXIuc3Vic2NyaXB0aW9uLWNhbmNlbC5jcmVhdGUgY3VzdG9tZXIuc3Vic2NyaXB0aW9uLnJlYWQiLCJpc3MiOiJndWVzdGFjY2Vzcy1zZXJ2aWNlIiwic3ViIjoiY3RtXzAxaGVhcXd2YjlrdjRkdjR2NjdtNzM4ZHR2IiwiZXhwIjoxNzMyNzE3OTEyLCJpYXQiOjE2OTkyODcwMjd9.YX52k0BnP5KqXmaCKK7MBsvXMnxXDw0q_ZYKV25zkjLSzmAYG0ME1flRqj8b5WcoeFJLI8z0UPAhtFqys30XCA", + "cancel": "https://buyer-portal.paddle.com/subscriptions/sub_01heb0tj9w00j33bfbq3469cy0/cancel?token=pga_eyJhbGciOiJFZERTQSIsImtpZCI6Imp3a18wMWhkazBuOHF3OG55NTJ5cGNocGNhazA1ayIsInR5cCI6IkpXVCJ9.eyJpZCI6InBnYV8wMWhlanBja2s1Z2trYmZhcWhhYXgyNnJlYSIsInNlbGxlci1pZCI6IjUwNiIsInR5cGUiOiJzdGFuZGFyZCIsInZlcnNpb24iOiIxIiwidXNhZ2UiOiJtYW5hZ2VtZW50X3VybCIsInNjb3BlIjoiY3VzdG9tZXIuc3Vic2NyaXB0aW9uLXBheW1lbnQudXBkYXRlIGN1c3RvbWVyLnN1YnNjcmlwdGlvbi1wYXltZW50LnJlYWQgY3VzdG9tZXIuc3Vic2NyaXB0aW9uLWNhbmNlbC5jcmVhdGUgY3VzdG9tZXIuc3Vic2NyaXB0aW9uLnJlYWQiLCJpc3MiOiJndWVzdGFjY2Vzcy1zZXJ2aWNlIiwic3ViIjoiY3RtXzAxaGVhcXd2YjlrdjRkdjR2NjdtNzM4ZHR2IiwiZXhwIjoxNzMyNzE3OTEyLCJpYXQiOjE2OTkyODcwMjd9.YX52k0BnP5KqXmaCKK7MBsvXMnxXDw0q_ZYKV25zkjLSzmAYG0ME1flRqj8b5WcoeFJLI8z0UPAhtFqys30XCA" + }, + "discount": null + }, + "meta": { + "request_id": "1e465507-6d78-4553-9186-8ac8dfa02318" + } +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/response/get_payment_method_change_transaction_entity.json b/tests/Functional/Resources/Subscriptions/_fixtures/response/get_payment_method_change_transaction_entity.json new file mode 100644 index 0000000..769c81f --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/response/get_payment_method_change_transaction_entity.json @@ -0,0 +1,170 @@ +{ + "data": { + "id": "txn_01h8by3n3w1zn9fsq9c93afsq3", + "status": "ready", + "customer_id": "ctm_01h8441jn5pcwrfhwh78jqt8hk", + "address_id": "add_01h848pep46enq8y372x7maj0p", + "business_id": null, + "custom_data": null, + "origin": "subscription_payment_method_change", + "collection_mode": "automatic", + "subscription_id": "sub_01h8bxswamxysj44zt5n48njwh", + "invoice_id": null, + "invoice_number": null, + "discount_id": null, + "billing_details": null, + "billing_period": { + "starts_at": "2023-08-21T11:31:08.689295Z", + "ends_at": "2023-08-21T11:31:08.689295Z" + }, + "currency_code": "USD", + "created_at": "2023-08-21T11:36:30.96803057Z", + "updated_at": "2023-08-21T11:36:30.96803057Z", + "billed_at": null, + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "description": "Monthly (per seat)", + "name": "Monthly (per seat)", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": ["AU"], + "unit_price": { + "amount": "5000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 10, + "maximum": 999 + }, + "status": "active" + }, + "quantity": 30, + "proration": { + "rate": "0", + "billing_period": { + "starts_at": "2023-08-21T11:31:08.689295Z", + "ends_at": "2023-09-21T11:31:08.689295Z" + } + } + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0.08875", + "totals": { + "subtotal": "0", + "discount": "0", + "tax": "0", + "total": "0" + } + } + ], + "totals": { + "subtotal": "0", + "tax": "0", + "discount": "0", + "total": "0", + "fee": null, + "credit": "0", + "balance": "0", + "grand_total": "0", + "earnings": null, + "currency_code": "USD" + }, + "adjusted_totals": { + "subtotal": "0", + "tax": "0", + "total": "0", + "grand_total": "0", + "fee": "0", + "earnings": "0", + "currency_code": "USD" + }, + "payout_totals": null, + "adjusted_payout_totals": null, + "line_items": [ + { + "id": "txnitm_01h8by3nfc02fjtbda5gta732d", + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 30, + "totals": { + "subtotal": "0", + "tax": "0", + "discount": "0", + "total": "0" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active" + }, + "tax_rate": "0.08875", + "unit_totals": { + "subtotal": "0", + "discount": "0", + "tax": "0", + "total": "0" + }, + "proration": { + "rate": "0", + "billing_period": { + "starts_at": "2023-08-21T11:31:08.689295Z", + "ends_at": "2023-09-21T11:31:08.689295Z" + } + } + } + ] + }, + "payments": [], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01h8by3n3w1zn9fsq9c93afsq3" + }, + "customer": { + "id": "ctm_01h8441jn5pcwrfhwh78jqt8hk", + "name": "Sam Miller", + "email": "sam@example.com", + "locale": "en", + "marketing_consent": false, + "custom_data": null, + "status": "active", + "created_at": "2023-11-15T11:15:44.673Z", + "updated_at": "2023-11-15T11:15:44.673Z" + }, + "address": { + "id": "add_01h848pep46enq8y372x7maj0p", + "description": "Head Office", + "first_line": "4050 Jefferson Plaza, 41st Floor", + "second_line": "", + "city": "New York", + "postal_code": "10021", + "region": "NY", + "country_code": "US", + "status": "active", + "custom_data": null, + "created_at": "2023-11-15T11:15:44.673Z", + "updated_at": "2023-11-15T11:15:44.673Z" + } + }, + "meta": { + "request_id": "e4747324-738b-4707-ac7a-7eaabb7c7a26" + } +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/response/list_default.json b/tests/Functional/Resources/Subscriptions/_fixtures/response/list_default.json new file mode 100644 index 0000000..09c7106 --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/response/list_default.json @@ -0,0 +1,452 @@ +{ + "data": [ + { + "id": "sub_01h8bqcrwp0vjd1p3bv20y7323", + "status": "active", + "customer_id": "ctm_01h8441jn5pcwrfhwh78jqt8hk", + "address_id": "add_01h848pep46enq8y372x7maj0p", + "business_id": null, + "currency_code": "USD", + "created_at": "2023-08-21T09:39:09.334Z", + "updated_at": "2023-08-21T09:39:09.334Z", + "started_at": "2023-08-01T00:00:00Z", + "first_billed_at": "2023-08-01T00:00:00Z", + "next_billed_at": "2024-08-01T00:00:00Z", + "paused_at": null, + "canceled_at": null, + "collection_mode": "manual", + "billing_details": { + "enable_checkout": false, + "purchase_order_number": "PO-123", + "additional_information": null, + "payment_terms": { + "frequency": 2, + "interval": "week" + } + }, + "current_billing_period": { + "starts_at": "2023-08-01T00:00:00Z", + "ends_at": "2024-08-01T00:00:00Z" + }, + "billing_cycle": { + "frequency": 1, + "interval": "year" + }, + "scheduled_change": null, + "items": [ + { + "status": "active", + "quantity": 1, + "recurring": true, + "created_at": "2023-08-21T09:39:09.334Z", + "updated_at": "2023-08-21T09:39:09.334Z", + "previously_billed_at": "2023-08-01T00:00:00Z", + "next_billed_at": "2024-08-01T00:00:00Z", + "trial_dates": null, + "price": { + "id": "pri_01gsz96z29d88jrmsf2ztbfgjg", + "product_id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "description": "Annual (recurring addon)", + "tax_mode": "account_setting", + "billing_cycle": { + "frequency": 1, + "interval": "year" + }, + "trial_period": null, + "unit_price": { + "amount": "326625", + "currency_code": "USD" + } + } + }, + { + "status": "active", + "quantity": 20, + "recurring": true, + "created_at": "2023-08-21T09:39:09.334Z", + "updated_at": "2023-08-21T09:39:09.334Z", + "previously_billed_at": "2023-08-01T00:00:00Z", + "next_billed_at": "2024-08-01T00:00:00Z", + "trial_dates": null, + "price": { + "id": "pri_01gsz91wy9k1yn7kx82aafwvea", + "product_id": "pro_01gsz4vmqbjk3x4vvtafffd540", + "description": "Annual (per seat)", + "tax_mode": "account_setting", + "billing_cycle": { + "frequency": 1, + "interval": "year" + }, + "trial_period": null, + "unit_price": { + "amount": "54437", + "currency_code": "USD" + } + } + } + ], + "custom_data": null, + "management_urls": { + "update_payment_method": null, + "cancel": "https://buyer-portal.paddle.com/subscriptions/sub_01h8bqcrwp0vjd1p3bv20y7323/cancel" + }, + "discount": null + }, + { + "id": "sub_01h84qezffcfrjx275ve304rt0", + "status": "active", + "customer_id": "ctm_01h82et3vctdaapjzerb3xrkex", + "address_id": "add_01h84qdjewtveve149393qfqcz", + "business_id": null, + "currency_code": "USD", + "created_at": "2023-08-18T16:25:40.591Z", + "updated_at": "2023-08-18T16:25:40.591Z", + "started_at": "2023-08-18T16:25:38.870962Z", + "first_billed_at": "2023-08-18T16:25:38.870962Z", + "next_billed_at": "2023-09-18T16:25:38.870962Z", + "paused_at": null, + "canceled_at": null, + "collection_mode": "automatic", + "billing_details": null, + "current_billing_period": { + "starts_at": "2023-08-18T16:25:38.870962Z", + "ends_at": "2023-09-18T16:25:38.870962Z" + }, + "billing_cycle": { + "frequency": 1, + "interval": "month" + }, + "scheduled_change": null, + "items": [ + { + "status": "active", + "quantity": 10, + "recurring": true, + "created_at": "2023-08-18T16:25:40.591Z", + "updated_at": "2023-08-18T16:25:40.591Z", + "previously_billed_at": "2023-08-18T16:25:38.870962Z", + "next_billed_at": "2023-09-18T16:25:38.870962Z", + "trial_dates": null, + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "description": "Monthly (per seat)", + "tax_mode": "account_setting", + "billing_cycle": { + "frequency": 1, + "interval": "month" + }, + "trial_period": null, + "unit_price": { + "amount": "3570", + "currency_code": "USD" + } + } + }, + { + "status": "active", + "quantity": 1, + "recurring": true, + "created_at": "2023-08-18T16:25:40.591Z", + "updated_at": "2023-08-18T16:25:40.591Z", + "previously_billed_at": "2023-08-18T16:25:38.870962Z", + "next_billed_at": "2023-09-18T16:25:38.870962Z", + "trial_dates": null, + "price": { + "id": "pri_01gsz95g2zrkagg294kpstx54r", + "product_id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "description": "Monthly (recurring addon)", + "tax_mode": "account_setting", + "billing_cycle": { + "frequency": 1, + "interval": "month" + }, + "trial_period": null, + "unit_price": { + "amount": "29750", + "currency_code": "USD" + } + } + } + ], + "custom_data": null, + "management_urls": { + "update_payment_method": "https://buyer-portal.paddle.com/subscriptions/sub_01h84qezffcfrjx275ve304rt0/update-payment-method", + "cancel": "https://buyer-portal.paddle.com/subscriptions/sub_01h84qezffcfrjx275ve304rt0/cancel" + }, + "discount": null + }, + { + "id": "sub_01h84ck8sg4ebkpzqb9x2mtjjf", + "status": "trialing", + "customer_id": "ctm_01h84cjfwmdph1k8kgsyjt3k7g", + "address_id": "add_01h84cjfy5411jpjes4hmafqry", + "business_id": null, + "currency_code": "USD", + "created_at": "2023-08-18T13:15:46.864163Z", + "updated_at": "2023-08-18T13:15:46.864163Z", + "started_at": "2023-08-18T13:15:46.864158Z", + "first_billed_at": null, + "next_billed_at": "2023-08-28T13:15:46.864158Z", + "paused_at": null, + "canceled_at": null, + "collection_mode": "automatic", + "billing_details": null, + "current_billing_period": { + "starts_at": "2023-08-18T13:15:46.864158Z", + "ends_at": "2023-08-28T13:15:46.864158Z" + }, + "billing_cycle": { + "frequency": 1, + "interval": "year" + }, + "scheduled_change": null, + "items": [ + { + "status": "trialing", + "quantity": 1, + "recurring": true, + "created_at": "2023-08-18T13:15:46.864164Z", + "updated_at": "2023-08-18T13:15:46.864164Z", + "previously_billed_at": null, + "next_billed_at": "2023-08-28T13:15:46.864158Z", + "trial_dates": { + "starts_at": "2023-08-18T13:15:46.864158Z", + "ends_at": "2023-08-28T13:15:46.864158Z" + }, + "price": { + "id": "pri_01h84cdy3xatsp16afda2gekzy", + "product_id": "pro_01h84cd36f900f3wmpdfamgv8w", + "description": "Annual plan", + "tax_mode": "account_setting", + "billing_cycle": { + "frequency": 1, + "interval": "year" + }, + "trial_period": { + "frequency": 10, + "interval": "day" + }, + "unit_price": { + "amount": "0", + "currency_code": "USD" + } + } + } + ], + "custom_data": null, + "management_urls": { + "update_payment_method": "https://buyer-portal.paddle.com/subscriptions/sub_01h84ck8sg4ebkpzqb9x2mtjjf/update-payment-method", + "cancel": "https://buyer-portal.paddle.com/subscriptions/sub_01h84ck8sg4ebkpzqb9x2mtjjf/cancel" + }, + "discount": null + }, + { + "id": "sub_01h7ht5z5wdg9pz18jx1fagp8k", + "status": "canceled", + "customer_id": "ctm_01h7hswb86rtps5ggbq7ybydcw", + "address_id": "add_01h7hswbfjqez51ezfhk7s6400", + "business_id": "biz_01h7ht30avfmwa4kvkfeg4ef8e", + "currency_code": "USD", + "created_at": "2023-08-11T08:07:36.892822Z", + "updated_at": "2024-01-11T08:34:01.798065Z", + "started_at": "2023-08-11T08:07:35.449123Z", + "first_billed_at": "2023-08-11T08:07:35.449123Z", + "next_billed_at": null, + "paused_at": null, + "canceled_at": "2024-01-11T08:34:01.787929Z", + "collection_mode": "automatic", + "billing_details": null, + "current_billing_period": null, + "billing_cycle": { + "frequency": 1, + "interval": "month" + }, + "scheduled_change": null, + "items": [ + { + "status": "active", + "quantity": 10, + "recurring": true, + "created_at": "2023-08-11T08:07:36.892823Z", + "updated_at": "2024-01-11T08:34:01.799627Z", + "previously_billed_at": "2023-12-11T08:33:04.443903Z", + "next_billed_at": null, + "trial_dates": null, + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "description": "Monthly (per seat)", + "tax_mode": "account_setting", + "billing_cycle": { + "frequency": 1, + "interval": "month" + }, + "trial_period": null, + "unit_price": { + "amount": "3240", + "currency_code": "USD" + } + } + }, + { + "status": "active", + "quantity": 1, + "recurring": true, + "created_at": "2023-08-11T08:07:36.892827Z", + "updated_at": "2024-01-11T08:34:01.800924Z", + "previously_billed_at": "2023-12-11T08:33:04.443903Z", + "next_billed_at": null, + "trial_dates": null, + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "description": "Monthly (recurring addon)", + "tax_mode": "account_setting", + "billing_cycle": { + "frequency": 1, + "interval": "month" + }, + "trial_period": null, + "unit_price": { + "amount": "10800", + "currency_code": "USD" + } + } + }, + { + "status": "active", + "quantity": 1, + "recurring": true, + "created_at": "2023-11-11T08:50:20.669523Z", + "updated_at": "2024-01-11T08:34:01.803004Z", + "previously_billed_at": "2023-12-11T08:33:04.443903Z", + "next_billed_at": null, + "trial_dates": null, + "price": { + "id": "pri_01gsz95g2zrkagg294kpstx54r", + "product_id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "description": "Monthly (recurring addon)", + "tax_mode": "account_setting", + "billing_cycle": { + "frequency": 1, + "interval": "month" + }, + "trial_period": null, + "unit_price": { + "amount": "30000", + "currency_code": "USD" + } + } + } + ], + "custom_data": null, + "management_urls": { + "update_payment_method": "https://buyer-portal.paddle.com/subscriptions/sub_01h7ht5z5wdg9pz18jx1fagp8k/update-payment-method", + "cancel": "https://buyer-portal.paddle.com/subscriptions/sub_01h7ht5z5wdg9pz18jx1fagp8k/cancel" + }, + "discount": null + }, + { + "id": "sub_01h468kv3jhs5jk330gszncsgt", + "status": "past_due", + "customer_id": "ctm_01h468k5wr7j4r6kf0790k9g3d", + "address_id": "add_01h468k5ypbrag01dq2ebsmv1d", + "business_id": null, + "currency_code": "USD", + "created_at": "2023-06-30T13:41:52.88257Z", + "updated_at": "2023-07-31T13:46:53.802622Z", + "started_at": "2023-06-30T13:41:51.571658Z", + "first_billed_at": "2023-06-30T13:41:51.571658Z", + "next_billed_at": "2024-06-30T13:46:23.954808Z", + "paused_at": null, + "canceled_at": null, + "collection_mode": "manual", + "billing_details": { + "enable_checkout": true, + "purchase_order_number": "PO-1030", + "additional_information": "Contact your account manager if you have any problems.", + "payment_terms": { + "frequency": 30, + "interval": "day" + } + }, + "current_billing_period": { + "starts_at": "2023-06-30T13:46:23.954808Z", + "ends_at": "2024-06-30T13:46:23.954808Z" + }, + "billing_cycle": { + "frequency": 1, + "interval": "year" + }, + "scheduled_change": null, + "items": [ + { + "status": "active", + "quantity": 1, + "recurring": true, + "created_at": "2023-06-30T13:46:23.970324Z", + "updated_at": "2023-07-31T13:46:53.803785Z", + "previously_billed_at": "2023-06-30T13:46:23.970324Z", + "next_billed_at": "2024-06-30T13:46:23.954808Z", + "trial_dates": null, + "price": { + "id": "pri_01h1vjg3sqjj1y9tvazkdqe5vt", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "description": "Annual (recurring addon)", + "tax_mode": "account_setting", + "billing_cycle": { + "frequency": 1, + "interval": "year" + }, + "trial_period": null, + "unit_price": { + "amount": "110000", + "currency_code": "USD" + } + } + }, + { + "status": "active", + "quantity": 50, + "recurring": true, + "created_at": "2023-06-30T13:46:23.970345Z", + "updated_at": "2023-07-31T13:46:53.804572Z", + "previously_billed_at": "2023-06-30T13:46:23.970345Z", + "next_billed_at": "2024-06-30T13:46:23.954808Z", + "trial_dates": null, + "price": { + "id": "pri_01gsz8z1q1n00f12qt82y31smh", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "description": "Annual (per seat)", + "tax_mode": "account_setting", + "billing_cycle": { + "frequency": 1, + "interval": "year" + }, + "trial_period": null, + "unit_price": { + "amount": "33000", + "currency_code": "USD" + } + } + } + ], + "custom_data": null, + "management_urls": { + "update_payment_method": null, + "cancel": "https://buyer-portal.paddle.com/subscriptions/sub_01h468kv3jhs5jk330gszncsgt/cancel" + }, + "discount": null + } + ], + "meta": { + "request_id": "d254d835-62dc-4371-a1f5-4894e40de65c", + "pagination": { + "per_page": 50, + "next": "https://api.paddle.com/subscriptions?after=sub_01h468kv3jhs5jk330gszncsgt", + "has_more": false, + "estimated_total": 5 + } + } +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/response/preview_charge_full_entity.json b/tests/Functional/Resources/Subscriptions/_fixtures/response/preview_charge_full_entity.json new file mode 100644 index 0000000..57031a9 --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/response/preview_charge_full_entity.json @@ -0,0 +1,402 @@ +{ + "data": { + "id": "sub_01heb0tj9w00j33bfbq3469cy0", + "status": "active", + "customer_id": "ctm_01heaqwvb9kv4dv4v67m738dta", + "address_id": "add_01heaqx50ad2sbsmf68saya8v2", + "business_id": null, + "currency_code": "USD", + "created_at": "2023-11-03T16:38:55.036Z", + "updated_at": "2023-11-24T16:38:53.111897Z", + "started_at": "2023-11-03T16:38:53.111897Z", + "first_billed_at": "2023-11-03T16:38:53.111897Z", + "next_billed_at": "2023-12-03T16:38:53.111897Z", + "paused_at": null, + "canceled_at": null, + "collection_mode": "automatic", + "billing_details": null, + "current_billing_period": { + "starts_at": "2023-11-03T16:38:53.111897Z", + "ends_at": "2023-12-03T16:38:53.111897Z" + }, + "billing_cycle": { + "frequency": 1, + "interval": "month" + }, + "next_transaction": { + "billing_period": { + "starts_at": "2023-12-03T16:38:53.111897Z", + "ends_at": "2024-01-03T16:38:53.111897Z" + }, + "details": { + "tax_rates_used": [ + { + "tax_rate": "0.2", + "totals": { + "subtotal": "41667", + "discount": "0", + "tax": "8333", + "total": "50000" + } + } + ], + "totals": { + "subtotal": "41667", + "tax": "8333", + "discount": "0", + "total": "50000", + "fee": null, + "credit": "0", + "credit_to_balance": "0", + "balance": "50000", + "grand_total": "50000", + "earnings": null, + "currency_code": "USD", + "exchange_rate": "1" + }, + "line_items": [ + { + "item_id": null, + "price_id": "pri_01heb0e913gej56emc8tjrkaq1", + "quantity": 5, + "totals": { + "subtotal": "16667", + "tax": "3333", + "discount": "0", + "total": "20000" + }, + "product": { + "id": "pro_01heb0cesy14mwnpyps6ee3hdc", + "name": " Basic Plan Product", + "description": "This is a description of my fav product", + "tax_category": "standard", + "image_url": "http://example.com", + "status": "active" + }, + "tax_rate": "0.2", + "unit_totals": { + "subtotal": "3333", + "discount": "0", + "tax": "667", + "total": "4000" + }, + "proration": { + "rate": "1", + "billing_period": { + "starts_at": "2023-12-03T16:38:53.111897Z", + "ends_at": "2024-01-03T16:38:53.111897Z" + } + } + }, + { + "item_id": null, + "price_id": "pri_01heb0fkqcnj94ykp5c3r8e0ja", + "quantity": 10, + "totals": { + "subtotal": "25000", + "tax": "5000", + "discount": "0", + "total": "30000" + }, + "product": { + "id": "pro_01heb0eqkjnw4pwgshghq87qga", + "name": "Recurring Addon Test", + "description": "", + "tax_category": "standard", + "image_url": "", + "status": "active" + }, + "tax_rate": "0.2", + "unit_totals": { + "subtotal": "2500", + "discount": "0", + "tax": "500", + "total": "3000" + }, + "proration": { + "rate": "1", + "billing_period": { + "starts_at": "2023-12-03T16:38:53.111897Z", + "ends_at": "2024-01-03T16:38:53.111897Z" + } + } + } + ] + }, + "adjustments": [] + }, + "scheduled_change": null, + "items": [ + { + "status": "active", + "quantity": 5, + "recurring": true, + "created_at": "2023-11-03T16:38:55.036Z", + "updated_at": "2023-11-03T16:38:55.036Z", + "previously_billed_at": "2023-11-03T16:38:53.111897Z", + "next_billed_at": "2023-12-03T16:38:53.111897Z", + "trial_dates": null, + "price": { + "id": "pri_01heb0e913gej56emc8tjrkaqa", + "product_id": "pro_01heb0cesy14mwnpyps6ee3hda", + "description": "basic plan", + "tax_mode": "internal", + "billing_cycle": { + "frequency": 1, + "interval": "month" + }, + "trial_period": null, + "unit_price": { + "amount": "4000", + "currency_code": "USD" + } + } + }, + { + "status": "active", + "quantity": 10, + "recurring": true, + "created_at": "2023-11-03T16:38:55.036Z", + "updated_at": "2023-11-03T16:38:55.036Z", + "previously_billed_at": "2023-11-03T16:38:53.111897Z", + "next_billed_at": "2023-12-03T16:38:53.111897Z", + "trial_dates": null, + "price": { + "id": "pri_01heb0fkqcnj94ykp5c3r8e0ja", + "product_id": "pro_01heb0eqkjnw4pwgshghq87qga", + "description": "recurring addon", + "tax_mode": "internal", + "billing_cycle": { + "frequency": 1, + "interval": "month" + }, + "trial_period": null, + "unit_price": { + "amount": "3000", + "currency_code": "USD" + } + } + } + ], + "custom_data": null, + "management_urls": { + "update_payment_method": "https://buyer-portal.paddle.com/subscriptions/sub_01heb0tj9w00j33bfbq3469cy0/update-payment-method?token=pga_eyJhbGciOiJFZERTQSIsImtpZCI6Imp3a18wMWhkazBuOHF3OG55NTJ5cGNocGNhazA1ayIsInR5cCI6IkpXVCJ9.eyJpZCI6InBnYV8wMWhlanBja2s1Z2trYmZhcWhhYXgyNnJlYSIsInNlbGxlci1pZCI6IjUwNiIsInR5cGUiOiJzdGFuZGFyZCIsInZlcnNpb24iOiIxIiwidXNhZ2UiOiJtYW5hZ2VtZW50X3VybCIsInNjb3BlIjoiY3VzdG9tZXIuc3Vic2NyaXB0aW9uLXBheW1lbnQudXBkYXRlIGN1c3RvbWVyLnN1YnNjcmlwdGlvbi1wYXltZW50LnJlYWQgY3VzdG9tZXIuc3Vic2NyaXB0aW9uLWNhbmNlbC5jcmVhdGUgY3VzdG9tZXIuc3Vic2NyaXB0aW9uLnJlYWQiLCJpc3MiOiJndWVzdGFjY2Vzcy1zZXJ2aWNlIiwic3ViIjoiY3RtXzAxaGVhcXd2YjlrdjRkdjR2NjdtNzM4ZHR2IiwiZXhwIjoxNzMyNzE3OTEyLCJpYXQiOjE2OTkyODcwMjd9.YX52k0BnP5KqXmaCKK7MBsvXMnxXDw0q_ZYKV25zkjLSzmAYG0ME1flRqj8b5WcoeFJLI8z0UPAhtFqys30XCA", + "cancel": "https://buyer-portal.paddle.com/subscriptions/sub_01heb0tj9w00j33bfbq3469cy0/cancel?token=pga_eyJhbGciOiJFZERTQSIsImtpZCI6Imp3a18wMWhkazBuOHF3OG55NTJ5cGNocGNhazA1ayIsInR5cCI6IkpXVCJ9.eyJpZCI6InBnYV8wMWhlanBja2s1Z2trYmZhcWhhYXgyNnJlYSIsInNlbGxlci1pZCI6IjUwNiIsInR5cGUiOiJzdGFuZGFyZCIsInZlcnNpb24iOiIxIiwidXNhZ2UiOiJtYW5hZ2VtZW50X3VybCIsInNjb3BlIjoiY3VzdG9tZXIuc3Vic2NyaXB0aW9uLXBheW1lbnQudXBkYXRlIGN1c3RvbWVyLnN1YnNjcmlwdGlvbi1wYXltZW50LnJlYWQgY3VzdG9tZXIuc3Vic2NyaXB0aW9uLWNhbmNlbC5jcmVhdGUgY3VzdG9tZXIuc3Vic2NyaXB0aW9uLnJlYWQiLCJpc3MiOiJndWVzdGFjY2Vzcy1zZXJ2aWNlIiwic3ViIjoiY3RtXzAxaGVhcXd2YjlrdjRkdjR2NjdtNzM4ZHR2IiwiZXhwIjoxNzMyNzE3OTEyLCJpYXQiOjE2OTkyODcwMjd9.YX52k0BnP5KqXmaCKK7MBsvXMnxXDw0q_ZYKV25zkjLSzmAYG0ME1flRqj8b5WcoeFJLI8z0UPAhtFqys30XCA" + }, + "discount": null, + "recurring_transaction_details": { + "tax_rates_used": [ + { + "tax_rate": "0.08875", + "totals": { + "subtotal": "178500", + "discount": "0", + "tax": "15841", + "total": "194341" + } + } + ], + "totals": { + "subtotal": "178500", + "tax": "15841", + "discount": "0", + "total": "194341", + "fee": null, + "credit": "0", + "balance": "194341", + "grand_total": "194341", + "earnings": null, + "currency_code": "USD", + "exchange_rate": "1" + }, + "line_items": [ + { + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 50, + "totals": { + "subtotal": "150000", + "tax": "13312", + "discount": "0", + "total": "163312" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active" + }, + "tax_rate": "0.08875", + "unit_totals": { + "subtotal": "3000", + "discount": "0", + "tax": "266", + "total": "3266" + } + }, + { + "price_id": "pri_01gsz95g2zrkagg294kpstx54r", + "quantity": 1, + "totals": { + "subtotal": "28500", + "tax": "2529", + "discount": "0", + "total": "31029" + }, + "product": { + "id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "name": "VIP support", + "description": "Get exclusive access to our expert team of product specialists, available to help you make the most of your ChatApp subscription.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "status": "active" + }, + "tax_rate": "0.08875", + "unit_totals": { + "subtotal": "28500", + "discount": "0", + "tax": "2529", + "total": "31029" + } + } + ] + }, + "immediate_transaction": { + "billing_period": { + "starts_at": "2023-12-03T16:38:53.111897Z", + "ends_at": "2024-01-03T16:38:53.111897Z" + }, + "details": { + "tax_rates_used": [ + { + "tax_rate": "0.2", + "totals": { + "subtotal": "41667", + "discount": "0", + "tax": "8333", + "total": "50000" + } + } + ], + "totals": { + "subtotal": "41667", + "tax": "8333", + "discount": "0", + "total": "50000", + "fee": null, + "credit": "0", + "credit_to_balance": "0", + "balance": "50000", + "grand_total": "50000", + "earnings": null, + "currency_code": "USD", + "exchange_rate": "1" + }, + "line_items": [ + { + "item_id": null, + "price_id": "pri_01heb0e913gej56emc8tjrkaq1", + "quantity": 5, + "totals": { + "subtotal": "16667", + "tax": "3333", + "discount": "0", + "total": "20000" + }, + "product": { + "id": "pro_01heb0cesy14mwnpyps6ee3hdc", + "name": " Basic Plan Product", + "description": "This is a description of my fav product", + "tax_category": "standard", + "image_url": "http://example.com", + "status": "active" + }, + "tax_rate": "0.2", + "unit_totals": { + "subtotal": "3333", + "discount": "0", + "tax": "667", + "total": "4000" + }, + "proration": { + "rate": "1", + "billing_period": { + "starts_at": "2023-12-03T16:38:53.111897Z", + "ends_at": "2024-01-03T16:38:53.111897Z" + } + } + }, + { + "item_id": null, + "price_id": "pri_01heb0fkqcnj94ykp5c3r8e0ja", + "quantity": 10, + "totals": { + "subtotal": "25000", + "tax": "5000", + "discount": "0", + "total": "30000" + }, + "product": { + "id": "pro_01heb0eqkjnw4pwgshghq87qga", + "name": "Recurring Addon Test", + "description": "", + "tax_category": "standard", + "image_url": "", + "status": "active" + }, + "tax_rate": "0.2", + "unit_totals": { + "subtotal": "2500", + "discount": "0", + "tax": "500", + "total": "3000" + }, + "proration": { + "rate": "1", + "billing_period": { + "starts_at": "2023-12-03T16:38:53.111897Z", + "ends_at": "2024-01-03T16:38:53.111897Z" + } + } + } + ] + }, + "adjustments": [ + { + "transaction_id": "txn_01h8bxpvx398a7zbawb77y0kp5", + "items": [ + { + "item_id": "txnitm_01h8bxryv3065dyh6103p3yg28", + "type": "proration", + "amount": "97987", + "proration": { + "rate": "1", + "billing_period": { + "starts_at": "2023-08-21T11:31:59.284Z", + "ends_at": "2023-09-21T11:31:08.689295Z" + } + }, + "totals": { + "subtotal": "90000", + "tax": "7987", + "total": "97987" + } + } + ], + "totals": { + "subtotal": "90000", + "tax": "7987", + "total": "97987", + "fee": "4949", + "earnings": "85051", + "currency_code": "USD" + } + } + ] + } + }, + "meta": { + "request_id": "1e465507-6d78-4553-9186-8ac8dfa02318" + } +} diff --git a/tests/Functional/Resources/Subscriptions/_fixtures/response/preview_update_full_entity.json b/tests/Functional/Resources/Subscriptions/_fixtures/response/preview_update_full_entity.json new file mode 100644 index 0000000..d038d62 --- /dev/null +++ b/tests/Functional/Resources/Subscriptions/_fixtures/response/preview_update_full_entity.json @@ -0,0 +1,402 @@ +{ + "data": { + "id": "sub_01heb0tj9w00j33bfbq3469cy0", + "status": "active", + "customer_id": "ctm_01heaqwvb9kv4dv4v67m738dta", + "address_id": "add_01heaqx50ad2sbsmf68saya8v2", + "business_id": null, + "currency_code": "USD", + "created_at": "2023-11-03T16:38:55.036Z", + "updated_at": "2023-11-24T16:38:53.111897Z", + "started_at": "2023-11-03T16:38:53.111897Z", + "first_billed_at": "2023-11-03T16:38:53.111897Z", + "next_billed_at": "2023-12-03T16:38:53.111897Z", + "paused_at": null, + "canceled_at": null, + "collection_mode": "automatic", + "billing_details": null, + "current_billing_period": { + "starts_at": "2023-11-03T16:38:53.111897Z", + "ends_at": "2023-12-03T16:38:53.111897Z" + }, + "billing_cycle": { + "frequency": 1, + "interval": "month" + }, + "next_transaction": { + "billing_period": { + "starts_at": "2023-12-03T16:38:53.111897Z", + "ends_at": "2024-01-03T16:38:53.111897Z" + }, + "details": { + "tax_rates_used": [ + { + "tax_rate": "0.2", + "totals": { + "subtotal": "41667", + "discount": "0", + "tax": "8333", + "total": "50000" + } + } + ], + "totals": { + "subtotal": "41667", + "tax": "8333", + "discount": "0", + "total": "50000", + "fee": null, + "credit": "0", + "credit_to_balance": "0", + "balance": "50000", + "grand_total": "50000", + "earnings": null, + "currency_code": "USD", + "exchange_rate": "1" + }, + "line_items": [ + { + "item_id": null, + "price_id": "pri_01heb0e913gej56emc8tjrkaq1", + "quantity": 5, + "totals": { + "subtotal": "16667", + "tax": "3333", + "discount": "0", + "total": "20000" + }, + "product": { + "id": "pro_01heb0cesy14mwnpyps6ee3hdc", + "name": " Basic Plan Product", + "description": "This is a description of my fav product", + "tax_category": "standard", + "image_url": "http://example.com", + "status": "active" + }, + "tax_rate": "0.2", + "unit_totals": { + "subtotal": "3333", + "discount": "0", + "tax": "667", + "total": "4000" + }, + "proration": { + "rate": "1", + "billing_period": { + "starts_at": "2023-12-03T16:38:53.111897Z", + "ends_at": "2024-01-03T16:38:53.111897Z" + } + } + }, + { + "item_id": null, + "price_id": "pri_01heb0fkqcnj94ykp5c3r8e0ja", + "quantity": 10, + "totals": { + "subtotal": "25000", + "tax": "5000", + "discount": "0", + "total": "30000" + }, + "product": { + "id": "pro_01heb0eqkjnw4pwgshghq87qga", + "name": "Recurring Addon Test", + "description": "", + "tax_category": "standard", + "image_url": "", + "status": "active" + }, + "tax_rate": "0.2", + "unit_totals": { + "subtotal": "2500", + "discount": "0", + "tax": "500", + "total": "3000" + }, + "proration": { + "rate": "1", + "billing_period": { + "starts_at": "2023-12-03T16:38:53.111897Z", + "ends_at": "2024-01-03T16:38:53.111897Z" + } + } + } + ] + }, + "adjustments": [] + }, + "scheduled_change": null, + "items": [ + { + "status": "active", + "quantity": 5, + "recurring": true, + "created_at": "2023-11-03T16:38:55.036Z", + "updated_at": "2023-11-03T16:38:55.036Z", + "previously_billed_at": "2023-11-03T16:38:53.111897Z", + "next_billed_at": "2023-12-03T16:38:53.111897Z", + "trial_dates": null, + "price": { + "id": "pri_01heb0e913gej56emc8tjrkaqa", + "product_id": "pro_01heb0cesy14mwnpyps6ee3hda", + "description": "basic plan", + "tax_mode": "internal", + "billing_cycle": { + "frequency": 1, + "interval": "month" + }, + "trial_period": null, + "unit_price": { + "amount": "4000", + "currency_code": "USD" + } + } + }, + { + "status": "active", + "quantity": 10, + "recurring": true, + "created_at": "2023-11-03T16:38:55.036Z", + "updated_at": "2023-11-03T16:38:55.036Z", + "previously_billed_at": "2023-11-03T16:38:53.111897Z", + "next_billed_at": "2023-12-03T16:38:53.111897Z", + "trial_dates": null, + "price": { + "id": "pri_01heb0fkqcnj94ykp5c3r8e0ja", + "product_id": "pro_01heb0eqkjnw4pwgshghq87qga", + "description": "recurring addon", + "tax_mode": "internal", + "billing_cycle": { + "frequency": 1, + "interval": "month" + }, + "trial_period": null, + "unit_price": { + "amount": "3000", + "currency_code": "USD" + } + } + } + ], + "custom_data": null, + "management_urls": { + "update_payment_method": "https://buyer-portal.paddle.com/subscriptions/sub_01heb0tj9w00j33bfbq3469cy0/update-payment-method?token=pga_eyJhbGciOiJFZERTQSIsImtpZCI6Imp3a18wMWhkazBuOHF3OG55NTJ5cGNocGNhazA1ayIsInR5cCI6IkpXVCJ9.eyJpZCI6InBnYV8wMWhlanBja2s1Z2trYmZhcWhhYXgyNnJlYSIsInNlbGxlci1pZCI6IjUwNiIsInR5cGUiOiJzdGFuZGFyZCIsInZlcnNpb24iOiIxIiwidXNhZ2UiOiJtYW5hZ2VtZW50X3VybCIsInNjb3BlIjoiY3VzdG9tZXIuc3Vic2NyaXB0aW9uLXBheW1lbnQudXBkYXRlIGN1c3RvbWVyLnN1YnNjcmlwdGlvbi1wYXltZW50LnJlYWQgY3VzdG9tZXIuc3Vic2NyaXB0aW9uLWNhbmNlbC5jcmVhdGUgY3VzdG9tZXIuc3Vic2NyaXB0aW9uLnJlYWQiLCJpc3MiOiJndWVzdGFjY2Vzcy1zZXJ2aWNlIiwic3ViIjoiY3RtXzAxaGVhcXd2YjlrdjRkdjR2NjdtNzM4ZHR2IiwiZXhwIjoxNzMyNzE3OTEyLCJpYXQiOjE2OTkyODcwMjd9.YX52k0BnP5KqXmaCKK7MBsvXMnxXDw0q_ZYKV25zkjLSzmAYG0ME1flRqj8b5WcoeFJLI8z0UPAhtFqys30XCA", + "cancel": "https://buyer-portal.paddle.com/subscriptions/sub_01heb0tj9w00j33bfbq3469cy0/cancel?token=pga_eyJhbGciOiJFZERTQSIsImtpZCI6Imp3a18wMWhkazBuOHF3OG55NTJ5cGNocGNhazA1ayIsInR5cCI6IkpXVCJ9.eyJpZCI6InBnYV8wMWhlanBja2s1Z2trYmZhcWhhYXgyNnJlYSIsInNlbGxlci1pZCI6IjUwNiIsInR5cGUiOiJzdGFuZGFyZCIsInZlcnNpb24iOiIxIiwidXNhZ2UiOiJtYW5hZ2VtZW50X3VybCIsInNjb3BlIjoiY3VzdG9tZXIuc3Vic2NyaXB0aW9uLXBheW1lbnQudXBkYXRlIGN1c3RvbWVyLnN1YnNjcmlwdGlvbi1wYXltZW50LnJlYWQgY3VzdG9tZXIuc3Vic2NyaXB0aW9uLWNhbmNlbC5jcmVhdGUgY3VzdG9tZXIuc3Vic2NyaXB0aW9uLnJlYWQiLCJpc3MiOiJndWVzdGFjY2Vzcy1zZXJ2aWNlIiwic3ViIjoiY3RtXzAxaGVhcXd2YjlrdjRkdjR2NjdtNzM4ZHR2IiwiZXhwIjoxNzMyNzE3OTEyLCJpYXQiOjE2OTkyODcwMjd9.YX52k0BnP5KqXmaCKK7MBsvXMnxXDw0q_ZYKV25zkjLSzmAYG0ME1flRqj8b5WcoeFJLI8z0UPAhtFqys30XCA" + }, + "discount": null, + "recurring_transaction_details": { + "tax_rates_used": [ + { + "tax_rate": "0.08875", + "totals": { + "subtotal": "178500", + "discount": "0", + "tax": "15841", + "total": "194341" + } + } + ], + "totals": { + "subtotal": "178500", + "tax": "15841", + "discount": "0", + "total": "194341", + "fee": null, + "credit": "0", + "balance": "194341", + "grand_total": "194341", + "earnings": null, + "currency_code": "USD", + "exchange_rate": "1" + }, + "line_items": [ + { + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 50, + "totals": { + "subtotal": "150000", + "tax": "13312", + "discount": "0", + "total": "163312" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active" + }, + "tax_rate": "0.08875", + "unit_totals": { + "subtotal": "3000", + "discount": "0", + "tax": "266", + "total": "3266" + } + }, + { + "price_id": "pri_01gsz95g2zrkagg294kpstx54r", + "quantity": 1, + "totals": { + "subtotal": "28500", + "tax": "2529", + "discount": "0", + "total": "31029" + }, + "product": { + "id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "name": "VIP support", + "description": "Get exclusive access to our expert team of product specialists, available to help you make the most of your ChatApp subscription.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "status": "active" + }, + "tax_rate": "0.08875", + "unit_totals": { + "subtotal": "28500", + "discount": "0", + "tax": "2529", + "total": "31029" + } + } + ] + }, + "immediate_transaction": { + "billing_period": { + "starts_at": "2023-12-03T16:38:53.111897Z", + "ends_at": "2024-01-03T16:38:53.111897Z" + }, + "details": { + "tax_rates_used": [ + { + "tax_rate": "0.2", + "totals": { + "subtotal": "41667", + "discount": "0", + "tax": "8333", + "total": "50000" + } + } + ], + "totals": { + "subtotal": "41667", + "tax": "8333", + "discount": "0", + "total": "50000", + "fee": null, + "credit": "0", + "credit_to_balance": "0", + "balance": "50000", + "grand_total": "50000", + "earnings": null, + "currency_code": "USD", + "exchange_rate": "1" + }, + "line_items": [ + { + "item_id": null, + "price_id": "pri_01heb0e913gej56emc8tjrkaq1", + "quantity": 5, + "totals": { + "subtotal": "16667", + "tax": "3333", + "discount": "0", + "total": "20000" + }, + "product": { + "id": "pro_01heb0cesy14mwnpyps6ee3hdc", + "name": " Basic Plan Product", + "description": "This is a description of my fav product", + "tax_category": "standard", + "image_url": "http://example.com", + "status": "active" + }, + "tax_rate": "0.2", + "unit_totals": { + "subtotal": "3333", + "discount": "0", + "tax": "667", + "total": "4000" + }, + "proration": { + "rate": "1", + "billing_period": { + "starts_at": "2023-12-03T16:38:53.111897Z", + "ends_at": "2024-01-03T16:38:53.111897Z" + } + } + }, + { + "item_id": null, + "price_id": "pri_01heb0fkqcnj94ykp5c3r8e0ja", + "quantity": 10, + "totals": { + "subtotal": "25000", + "tax": "5000", + "discount": "0", + "total": "30000" + }, + "product": { + "id": "pro_01heb0eqkjnw4pwgshghq87qga", + "name": "Recurring Addon Test", + "description": "", + "tax_category": "standard", + "image_url": "", + "status": "active" + }, + "tax_rate": "0.2", + "unit_totals": { + "subtotal": "2500", + "discount": "0", + "tax": "500", + "total": "3000" + }, + "proration": { + "rate": "1", + "billing_period": { + "starts_at": "2023-12-03T16:38:53.111897Z", + "ends_at": "2024-01-03T16:38:53.111897Z" + } + } + } + ] + }, + "adjustments": [ + { + "transaction_id": "txn_01h8bxpvx398a7zbawb77y0kp5", + "items": [ + { + "item_id": "txnitm_01h8bxryv3065dyh6103p3yg28", + "type": "proration", + "amount": "97987", + "proration": { + "rate": "1", + "billing_period": { + "starts_at": "2023-08-21T11:31:59.284Z", + "ends_at": "2023-09-21T11:31:08.689295Z" + } + }, + "totals": { + "subtotal": "90000", + "tax": "7987", + "total": "97987" + } + } + ], + "totals": { + "subtotal": "90000", + "tax": "7987", + "total": "97987", + "fee": "4949", + "earnings": "85051", + "currency_code": "USD" + } + } + ] + } + }, + "meta": { + "request_id": "1e465507-6d78-4553-9186-8ac8dfa02318" + } +} diff --git a/tests/Functional/Resources/Transactions/TransactionsClientTest.php b/tests/Functional/Resources/Transactions/TransactionsClientTest.php new file mode 100644 index 0000000..480319a --- /dev/null +++ b/tests/Functional/Resources/Transactions/TransactionsClientTest.php @@ -0,0 +1,495 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** @test */ + public function it_can_paginate(): void + { + $this->mockClient->addResponse(new Response(200, body: self::readRawJsonFixture('response/list_paginated_page_one'))); + $this->mockClient->addResponse(new Response(200, body: self::readRawJsonFixture('response/list_paginated_page_two'))); + + $collection = $this->client->transactions->list(); + + $request = $this->mockClient->getLastRequest(); + + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/transactions', + urldecode((string) $request->getUri()), + ); + + foreach ($collection as $transaction) { + self::assertInstanceOf(TransactionWithIncludes::class, $transaction); + } + + $request = $this->mockClient->getLastRequest(); + + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/transactions?after=txn_01h69ddtrb11km0wk46dn607ya', + urldecode((string) $request->getUri()), + ); + } + + /** @test */ + public function it_can_include_on_create(): void + { + $operation = new CreateOperation( + items: [ + new TransactionCreateItem('pri_01he5kxqey1k8ankgef29cj4bv', 1), + ], + ); + + $this->mockClient->addResponse(new Response(200, body: self::readRawJsonFixture('response/minimal_entity'))); + $this->client->transactions->create($operation, [Includes::Customer, Includes::Business]); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/transactions?include=customer,business', + urldecode((string) $request->getUri()), + ); + } + + /** + * @test + * + * @dataProvider createOperationsProvider + */ + public function it_uses_expected_payload_on_create( + CreateOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->transactions->create($operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals(Environment::SANDBOX->baseUrl() . '/transactions', urldecode((string) $request->getUri())); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function createOperationsProvider(): \Generator + { + yield 'Basic Create' => [ + new CreateOperation( + items: [ + new TransactionCreateItem('pri_01he5kxqey1k8ankgef29cj4bv', 1), + ], + ), + new Response(200, body: self::readRawJsonFixture('response/minimal_entity')), + self::readRawJsonFixture('request/create_basic'), + ]; + + yield 'Create with non catalog price' => [ + new CreateOperation( + items: [ + new TransactionCreateItemWithPrice( + new TransactionNonCatalogPrice( + 'Annual (per seat)', + 'Annual (per seat)', + new TimePeriod(Interval::Year, 1), + null, + TaxMode::AccountSetting, + new Money('30000', CurrencyCode::USD), + [], + new PriceQuantity(10, 999), + null, + 'pro_01gsz4t5hdjse780zja8vvr7jg', + ), + 20, + ), + ], + ), + new Response(200, body: self::readRawJsonFixture('response/minimal_entity')), + self::readRawJsonFixture('request/create_with_non_catalog_price'), + ]; + + yield 'Create Manually Collected' => [ + new CreateOperation( + items: [ + new TransactionCreateItem('pri_01gsz8x8sawmvhz1pv30nge1ke', 1), + ], + status: StatusTransaction::Billed, + customerId: 'ctm_01he849dseyj0zgrc589eeb1c7', + addressId: 'add_01hen28ebw1ew99y295jhd4n3n', + businessId: 'biz_01hen2ng2290g84twtefdn5s00', + currencyCode: CurrencyCode::GBP, + collectionMode: CollectionMode::Manual, + discountId: 'dsc_01hen7bjzh12m0v2peer15d9qt', + billingDetails: new BillingDetails( + enableCheckout: true, + paymentTerms: new TimePeriod(Interval::Month, 1), + purchaseOrderNumber: '10009', + ), + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/create_manual'), + ]; + } + + /** + * @test + * + * @dataProvider updateOperationsProvider + */ + public function it_uses_expected_payload_on_update( + UpdateOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->transactions->update('txn_01h7zcgmdc6tmwtjehp3sh7azf', $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('PATCH', $request->getMethod()); + self::assertEquals(Environment::SANDBOX->baseUrl() . '/transactions/txn_01h7zcgmdc6tmwtjehp3sh7azf', urldecode((string) $request->getUri())); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function updateOperationsProvider(): \Generator + { + yield 'Update Single' => [ + new UpdateOperation(status: StatusTransaction::Billed), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_single'), + ]; + + yield 'Update Partial' => [ + new UpdateOperation( + status: StatusTransaction::Billed, + customData: new CustomData(['completed_by' => 'Frank T']), + ), + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + self::readRawJsonFixture('request/update_partial'), + ]; + } + + /** + * @test + * + * @dataProvider listOperationsProvider + */ + public function list_hits_expected_uri( + ListOperation $operation, + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->transactions->list($operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function listOperationsProvider(): \Generator + { + yield 'Default' => [ + new ListOperation(), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/transactions', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Default Paged' => [ + new ListOperation(new Pager()), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/transactions?order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Default Paged with After' => [ + new ListOperation(new Pager(after: 'txn_01hen7bxc1p8ep4yk7n5jbzk9r')), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/transactions?after=txn_01hen7bxc1p8ep4yk7n5jbzk9r&order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'NotificationStatus Filtered' => [ + new ListOperation(statuses: [StatusTransaction::Billed]), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/transactions?status=billed', Environment::SANDBOX->baseUrl()), + ]; + + yield 'NotificationStatus Filtered Multiple' => [ + new ListOperation(statuses: [StatusTransaction::Billed, StatusTransaction::Completed]), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/transactions?status=billed,completed', Environment::SANDBOX->baseUrl()), + ]; + + yield 'ID Filtered' => [ + new ListOperation(ids: ['txn_01gsz4s0w61y0pp88528f1wvvb']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/transactions?id=txn_01gsz4s0w61y0pp88528f1wvvb', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Multiple ID Filtered' => [ + new ListOperation(ids: ['txn_01gsz4s0w61y0pp88528f1wvvb', 'txn_01h1vjes1y163xfj1rh1tkfb65']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf( + '%s/transactions?id=txn_01gsz4s0w61y0pp88528f1wvvb,txn_01h1vjes1y163xfj1rh1tkfb65', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'Collection Mode Filtered' => [ + new ListOperation(collectionMode: CollectionMode::Automatic), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/transactions?collection_mode=automatic', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Billed At Filtered No Comparator' => [ + new ListOperation(billedAt: new DateComparison(new \DateTimeImmutable('2023-11-06 14:00:00'))), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/transactions?billed_at=2023-11-06T14:00:00.000000Z', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Billed At Filtered With Comparator' => [ + new ListOperation(billedAt: new DateComparison(new \DateTimeImmutable('2023-11-06 14:00:00'), Comparator::GT)), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/transactions?billed_at[GT]=2023-11-06T14:00:00.000000Z', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Invoice Number Filtered' => [ + new ListOperation(invoiceNumbers: ['inv_01gsz4s0w61y0pp88528f1wvvb']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/transactions?invoice_number=inv_01gsz4s0w61y0pp88528f1wvvb', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Invoice Number Filtered multiple' => [ + new ListOperation(invoiceNumbers: ['inv_01gsz4s0w61y0pp88528f1wvvb', 'inv_01h1vjes1y163xfj1rh1tkfb65']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/transactions?invoice_number=inv_01gsz4s0w61y0pp88528f1wvvb,inv_01h1vjes1y163xfj1rh1tkfb65', Environment::SANDBOX->baseUrl()), + ]; + + yield 'NotificationSubscription ID Filtered' => [ + new ListOperation(subscriptionIds: ['sub_01gsz4s0w61y0pp88528f1wvvb']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/transactions?subscription_id=sub_01gsz4s0w61y0pp88528f1wvvb', Environment::SANDBOX->baseUrl()), + ]; + + yield 'NotificationSubscription ID Filtered Multiple' => [ + new ListOperation(subscriptionIds: ['sub_01gsz4s0w61y0pp88528f1wvvb', 'sub_01h1vjes1y163xfj1rh1tkfb65']), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/transactions?subscription_id=sub_01gsz4s0w61y0pp88528f1wvvb,sub_01h1vjes1y163xfj1rh1tkfb65', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Updated At Filtered No Comparator' => [ + new ListOperation(updatedAt: new DateComparison(new \DateTimeImmutable('2023-11-06 14:00:00'))), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/transactions?updated_at=2023-11-06T14:00:00.000000Z', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Updated At Filtered With Comparator' => [ + new ListOperation(updatedAt: new DateComparison(new \DateTimeImmutable('2023-11-06 14:00:00'), Comparator::GT)), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/transactions?updated_at[GT]=2023-11-06T14:00:00.000000Z', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Created At Filtered No Comparator' => [ + new ListOperation(createdAt: new DateComparison(new \DateTimeImmutable('2023-11-06 14:00:00'))), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/transactions?created_at=2023-11-06T14:00:00.000000Z', Environment::SANDBOX->baseUrl()), + ]; + + yield 'Created At Filtered With Comparator' => [ + new ListOperation(createdAt: new DateComparison(new \DateTimeImmutable('2023-11-06 14:00:00'), Comparator::GT)), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/transactions?created_at[GT]=2023-11-06T14:00:00.000000Z', Environment::SANDBOX->baseUrl()), + ]; + + yield 'With Includes' => [ + new ListOperation(includes: [Includes::Customer]), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/transactions?include=customer', Environment::SANDBOX->baseUrl()), + ]; + + yield 'With Includes Multiple' => [ + new ListOperation(includes: [Includes::Customer, Includes::Address, Includes::Discount]), + new Response(200, body: self::readRawJsonFixture('response/list_default')), + sprintf('%s/transactions?include=customer,address,discount', Environment::SANDBOX->baseUrl()), + ]; + } + + /** + * @test + * + * @dataProvider getRequestProvider + * + * @param Includes[] $includes + */ + public function get_hits_expected_uri( + array $includes, + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->transactions->get('txn_01hen7bxc1p8ep4yk7n5jbzk9r', $includes); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function getRequestProvider(): \Generator + { + yield 'Without Includes' => [ + [], + new Response(200, body: self::readRawJsonFixture('response/full_entity')), + sprintf('%s/transactions/txn_01hen7bxc1p8ep4yk7n5jbzk9r', Environment::SANDBOX->baseUrl()), + ]; + + yield 'With Includes' => [ + [Includes::Customer, Includes::Address, Includes::Business, Includes::Discount], + new Response(200, body: self::readRawJsonFixture('response/full_entity_with_includes')), + sprintf('%s/transactions/txn_01hen7bxc1p8ep4yk7n5jbzk9r?include=customer,address,business,discount', Environment::SANDBOX->baseUrl()), + ]; + } + + /** + * @test + * + * @dataProvider previewOperationsProvider + */ + public function it_uses_expected_payload_on_preview( + PreviewOperation $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->transactions->preview($operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals(Environment::SANDBOX->baseUrl() . '/transactions/preview', urldecode((string) $request->getUri())); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function previewOperationsProvider(): \Generator + { + yield 'Basic Preview' => [ + new PreviewOperation( + items: [ + new TransactionItemPreviewWithPriceId('pri_01he5kxqey1k8ankgef29cj4bv', 1, true), + ], + ), + new Response(200, body: self::readRawJsonFixture('response/preview_entity')), + self::readRawJsonFixture('request/preview_basic'), + ]; + + yield 'Preview with non catalog price' => [ + new PreviewOperation( + items: [ + new TransactionItemPreviewWithNonCatalogPrice( + new TransactionNonCatalogPrice( + 'Annual (per seat)', + 'Annual (per seat)', + new TimePeriod(Interval::Year, 1), + null, + TaxMode::AccountSetting, + new Money('30000', CurrencyCode::USD), + [], + new PriceQuantity(10, 999), + null, + 'pro_01gsz4t5hdjse780zja8vvr7jg', + ), + 20, + true, + ), + ], + ), + new Response(200, body: self::readRawJsonFixture('response/preview_entity')), + self::readRawJsonFixture('request/preview_with_non_catalog_price'), + ]; + } + + /** + * @test + * + * @dataProvider getInvoicePDFOperationsProvider + */ + public function get_invoice_pdf_hits_expected_uri( + ResponseInterface $response, + string $expectedUri, + ): void { + $this->mockClient->addResponse($response); + $this->client->transactions->getInvoicePDF('txn_01hen7bxc1p8ep4yk7n5jbzk9r'); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function getInvoicePDFOperationsProvider(): \Generator + { + yield 'Default' => [ + new Response(200, body: self::readRawJsonFixture('response/get_invoice_pdf_default')), + sprintf('%s/transactions/txn_01hen7bxc1p8ep4yk7n5jbzk9r/preview', Environment::SANDBOX->baseUrl()), + ]; + } +} diff --git a/tests/Functional/Resources/Transactions/_fixtures/request/create_basic.json b/tests/Functional/Resources/Transactions/_fixtures/request/create_basic.json new file mode 100644 index 0000000..2cf2d62 --- /dev/null +++ b/tests/Functional/Resources/Transactions/_fixtures/request/create_basic.json @@ -0,0 +1,8 @@ +{ + "items": [ + { + "quantity": 1, + "price_id": "pri_01he5kxqey1k8ankgef29cj4bv" + } + ] +} diff --git a/tests/Functional/Resources/Transactions/_fixtures/request/create_manual.json b/tests/Functional/Resources/Transactions/_fixtures/request/create_manual.json new file mode 100644 index 0000000..a672c56 --- /dev/null +++ b/tests/Functional/Resources/Transactions/_fixtures/request/create_manual.json @@ -0,0 +1,24 @@ +{ + "items": [ + { + "quantity": 1, + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke" + } + ], + "status": "billed", + "customer_id": "ctm_01he849dseyj0zgrc589eeb1c7", + "address_id": "add_01hen28ebw1ew99y295jhd4n3n", + "business_id": "biz_01hen2ng2290g84twtefdn5s00", + "currency_code": "GBP", + "collection_mode": "manual", + "discount_id": "dsc_01hen7bjzh12m0v2peer15d9qt", + "billing_details": { + "additional_information": null, + "enable_checkout": true, + "purchase_order_number": "10009", + "payment_terms": { + "interval": "month", + "frequency": 1 + } + } +} diff --git a/tests/Functional/Resources/Transactions/_fixtures/request/create_with_non_catalog_price.json b/tests/Functional/Resources/Transactions/_fixtures/request/create_with_non_catalog_price.json new file mode 100644 index 0000000..eb64ad5 --- /dev/null +++ b/tests/Functional/Resources/Transactions/_fixtures/request/create_with_non_catalog_price.json @@ -0,0 +1,28 @@ +{ + "items": [ + { + "quantity": 20, + "price": { + "description": "Annual (per seat)", + "name": "Annual (per seat)", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "year", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "30000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 10, + "maximum": 999 + }, + "custom_data": null + } + } + ] +} diff --git a/tests/Functional/Resources/Transactions/_fixtures/request/preview_basic.json b/tests/Functional/Resources/Transactions/_fixtures/request/preview_basic.json new file mode 100644 index 0000000..6e85568 --- /dev/null +++ b/tests/Functional/Resources/Transactions/_fixtures/request/preview_basic.json @@ -0,0 +1,9 @@ +{ + "items": [ + { + "quantity": 1, + "price_id": "pri_01he5kxqey1k8ankgef29cj4bv", + "include_in_totals": true + } + ] +} diff --git a/tests/Functional/Resources/Transactions/_fixtures/request/preview_with_non_catalog_price.json b/tests/Functional/Resources/Transactions/_fixtures/request/preview_with_non_catalog_price.json new file mode 100644 index 0000000..31196b4 --- /dev/null +++ b/tests/Functional/Resources/Transactions/_fixtures/request/preview_with_non_catalog_price.json @@ -0,0 +1,29 @@ +{ + "items": [ + { + "quantity": 20, + "price": { + "description": "Annual (per seat)", + "name": "Annual (per seat)", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "year", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "30000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 10, + "maximum": 999 + }, + "custom_data": null + }, + "include_in_totals": true + } + ] +} diff --git a/tests/Functional/Resources/Transactions/_fixtures/request/update_partial.json b/tests/Functional/Resources/Transactions/_fixtures/request/update_partial.json new file mode 100644 index 0000000..0995ce0 --- /dev/null +++ b/tests/Functional/Resources/Transactions/_fixtures/request/update_partial.json @@ -0,0 +1,6 @@ +{ + "status": "billed", + "custom_data": { + "completed_by": "Frank T" + } +} diff --git a/tests/Functional/Resources/Transactions/_fixtures/request/update_single.json b/tests/Functional/Resources/Transactions/_fixtures/request/update_single.json new file mode 100644 index 0000000..dc837b5 --- /dev/null +++ b/tests/Functional/Resources/Transactions/_fixtures/request/update_single.json @@ -0,0 +1,3 @@ +{ + "status": "billed" +} diff --git a/tests/Functional/Resources/Transactions/_fixtures/response/full_entity.json b/tests/Functional/Resources/Transactions/_fixtures/response/full_entity.json new file mode 100644 index 0000000..c3658e3 --- /dev/null +++ b/tests/Functional/Resources/Transactions/_fixtures/response/full_entity.json @@ -0,0 +1,165 @@ +{ + "data": { + "id": "txn_01hen7bxc1p8ep4yk7n5jbzk9r", + "status": "billed", + "customer_id": "ctm_01he849dseyj0zgrc589eeb1c7", + "address_id": "add_01hen28ebw1ew99y295jhd4n3n", + "business_id": "biz_01hen2ng2290g84twtefdn5s00", + "custom_data": null, + "origin": "api", + "collection_mode": "manual", + "subscription_id": "sub_01hen7byqyfeh7d0cw0qg8tphh", + "invoice_id": "inv_01hen7bys9g6d2xkj9n208e2yw", + "invoice_number": "325-10261", + "billing_details": { + "enable_checkout": true, + "payment_terms": { + "interval": "month", + "frequency": 1 + }, + "purchase_order_number": "10009", + "additional_information": null + }, + "billing_period": { + "starts_at": "2023-11-07T15:45:40.606Z", + "ends_at": "2023-12-07T15:45:40.606Z" + }, + "currency_code": "GBP", + "discount_id": "dsc_01hen7bjzh12m0v2peer15d9qt", + "created_at": "2023-11-07T15:45:39.297512Z", + "updated_at": "2023-11-07T15:45:45.086499Z", + "billed_at": "2023-11-07T15:45:39.201442Z", + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "description": "Monthly (per seat)", + "name": null, + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": [ + "AU" + ], + "unit_price": { + "amount": "5000", + "currency_code": "AUD" + } + } + ], + "custom_data": { + "features": { + "crm": true, + "data_retention": false, + "reports": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of advanced reporting and a CRM that's right where you're chatting." + }, + "quantity": { + "minimum": 1, + "maximum": 999 + }, + "status": "active" + }, + "quantity": 1 + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0.2", + "totals": { + "subtotal": "2439", + "discount": "10", + "tax": "486", + "total": "2915" + } + } + ], + "totals": { + "subtotal": "2439", + "tax": "486", + "discount": "10", + "total": "2915", + "grand_total": "2915", + "fee": null, + "credit": "0", + "balance": "2915", + "earnings": null, + "currency_code": "GBP" + }, + "adjusted_totals": { + "subtotal": "2429", + "tax": "486", + "total": "2915", + "grand_total": "2915", + "fee": "0", + "earnings": "0", + "currency_code": "GBP" + }, + "payout_totals": null, + "adjusted_payout_totals": null, + "line_items": [ + { + "id": "txnitm_01hen7bxecbsbdd65s8qhyv7jw", + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 1, + "totals": { + "subtotal": "2439", + "tax": "486", + "discount": "10", + "total": "2915" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "custom_data": { + "features": { + "crm": true, + "data_retention": false, + "reports": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of advanced reporting and a CRM that's right where you're chatting." + }, + "status": "active" + }, + "tax_rate": "0.2", + "unit_totals": { + "subtotal": "2439", + "tax": "486", + "discount": "10", + "total": "2915" + } + } + ] + }, + "payments": [], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01hen7bxc1p8ep4yk7n5jbzk9r" + } + }, + "meta": { + "request_id": "0daa7c59-f2eb-41b6-bf2e-bb3b070873a5" + } +} diff --git a/tests/Functional/Resources/Transactions/_fixtures/response/full_entity_with_includes.json b/tests/Functional/Resources/Transactions/_fixtures/response/full_entity_with_includes.json new file mode 100644 index 0000000..50f4372 --- /dev/null +++ b/tests/Functional/Resources/Transactions/_fixtures/response/full_entity_with_includes.json @@ -0,0 +1,219 @@ +{ + "data": { + "id": "txn_01hen7bxc1p8ep4yk7n5jbzk9r", + "status": "billed", + "customer_id": "ctm_01he849dseyj0zgrc589eeb1c7", + "address_id": "add_01hen28ebw1ew99y295jhd4n3n", + "business_id": "biz_01hen2ng2290g84twtefdn5s00", + "custom_data": null, + "origin": "api", + "collection_mode": "manual", + "subscription_id": "sub_01hen7byqyfeh7d0cw0qg8tphh", + "invoice_id": "inv_01hen7bys9g6d2xkj9n208e2yw", + "invoice_number": "325-10261", + "billing_details": { + "enable_checkout": true, + "payment_terms": { + "interval": "month", + "frequency": 1 + }, + "purchase_order_number": "10009", + "additional_information": null + }, + "billing_period": { + "starts_at": "2023-11-07T15:45:40.606Z", + "ends_at": "2023-12-07T15:45:40.606Z" + }, + "currency_code": "GBP", + "discount_id": "dsc_01hen7bjzh12m0v2peer15d9qt", + "created_at": "2023-11-07T15:45:39.297512Z", + "updated_at": "2023-11-07T15:45:45.086499Z", + "billed_at": "2023-11-07T15:45:39.201442Z", + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "description": "Monthly (per seat)", + "name": null, + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": [ + "AU" + ], + "unit_price": { + "amount": "5000", + "currency_code": "AUD" + } + } + ], + "custom_data": { + "features": { + "crm": true, + "data_retention": false, + "reports": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of advanced reporting and a CRM that's right where you're chatting." + }, + "quantity": { + "minimum": 1, + "maximum": 999 + }, + "status": "active" + }, + "quantity": 1 + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0.2", + "totals": { + "subtotal": "2439", + "discount": "10", + "tax": "486", + "total": "2915" + } + } + ], + "totals": { + "subtotal": "2439", + "tax": "486", + "discount": "10", + "total": "2915", + "grand_total": "2915", + "fee": null, + "credit": "0", + "balance": "2915", + "earnings": null, + "currency_code": "GBP" + }, + "adjusted_totals": { + "subtotal": "2429", + "tax": "486", + "total": "2915", + "grand_total": "2915", + "fee": "0", + "earnings": "0", + "currency_code": "GBP" + }, + "payout_totals": null, + "adjusted_payout_totals": null, + "line_items": [ + { + "id": "txnitm_01hen7bxecbsbdd65s8qhyv7jw", + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 1, + "totals": { + "subtotal": "2439", + "tax": "486", + "discount": "10", + "total": "2915" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "custom_data": { + "features": { + "crm": true, + "data_retention": false, + "reports": true + }, + "suggested_addons": [ + "pro_01h1vjes1y163xfj1rh1tkfb65", + "pro_01gsz97mq9pa4fkyy0wqenepkz" + ], + "upgrade_description": "Move from Basic to Pro to take advantage of advanced reporting and a CRM that's right where you're chatting." + }, + "status": "active" + }, + "tax_rate": "0.2", + "unit_totals": { + "subtotal": "2439", + "tax": "486", + "discount": "10", + "total": "2915" + } + } + ] + }, + "payments": [], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01hen7bxc1p8ep4yk7n5jbzk9r" + }, + "customer": { + "id": "ctm_01he849dseyj0zgrc589eeb1c7", + "name": "Michael ", + "email": "michael.woodward+sdk-testing@paddle.com", + "locale": "en", + "marketing_consent": false, + "custom_data": null, + "status": "active", + "created_at": "2023-11-02T13:41:44.366Z", + "updated_at": "2023-11-02T13:41:44.366Z" + }, + "business": { + "id": "biz_01hen2ng2290g84twtefdn5s00", + "name": "Michael's Bizniz", + "company_number": "", + "tax_identifier": "", + "status": "active", + "contacts": [], + "custom_data": null, + "created_at": "2023-11-07T14:23:30.37Z", + "updated_at": "2023-11-07T14:23:30.37Z" + }, + "address": { + "id": "add_01hen28ebw1ew99y295jhd4n3n", + "description": "", + "first_line": "1 Manual Collection Drive", + "second_line": "", + "city": "Derby", + "postal_code": "DE11AB", + "region": "Derbyshire", + "country_code": "GB", + "status": "active", + "custom_data": null, + "created_at": "2023-11-07T14:16:22.652Z", + "updated_at": "2023-11-07T14:16:22.652Z" + }, + "discount": { + "id": "dsc_01hen7bjzh12m0v2peer15d9qt", + "status": "active", + "description": "Michaels discount", + "enabled_for_checkout": true, + "code": "QY3XXXMS3E", + "type": "flat", + "amount": "10", + "currency_code": "GBP", + "recur": false, + "maximum_recurring_intervals": null, + "usage_limit": null, + "restrict_to": null, + "expires_at": null, + "times_used": 0, + "created_at": "2023-11-07T15:45:28.561Z", + "updated_at": "2023-11-07T15:45:28.561Z" + } + }, + "meta": { + "request_id": "f3dd61c8-045a-4975-95c3-a361ff2abcc6" + } +} diff --git a/tests/Functional/Resources/Transactions/_fixtures/response/get_invoice_pdf_default.json b/tests/Functional/Resources/Transactions/_fixtures/response/get_invoice_pdf_default.json new file mode 100644 index 0000000..659518b --- /dev/null +++ b/tests/Functional/Resources/Transactions/_fixtures/response/get_invoice_pdf_default.json @@ -0,0 +1,8 @@ +{ + "data": { + "url": "https://paddle-invoice-service-pdfs.s3.amazonaws.com/invoices/00000/f64658f0-d8ef-41cb-a916-d24d9b1e33bf/invoice_325-10001_Bluth.pdf" + }, + "meta": { + "request_id": "f34d4a9c-2088-447d-a3a1-1da5ce74f507" + } +} diff --git a/tests/Functional/Resources/Transactions/_fixtures/response/list_default.json b/tests/Functional/Resources/Transactions/_fixtures/response/list_default.json new file mode 100644 index 0000000..49d0839 --- /dev/null +++ b/tests/Functional/Resources/Transactions/_fixtures/response/list_default.json @@ -0,0 +1,1236 @@ +{ + "data": [ + { + "id": "txn_01h8bm0f0gwa622zpcvw49hwc1", + "status": "ready", + "customer_id": "ctm_01h8441jn5pcwrfhwh78jqt8hk", + "address_id": "add_01h848pep46enq8y372x7maj0p", + "business_id": null, + "custom_data": null, + "origin": "api", + "collection_mode": "manual", + "subscription_id": null, + "invoice_id": "inv_01h8bm0gtxhgd7f37f4xx8b93m", + "invoice_number": null, + "billing_details": { + "enable_checkout": false, + "payment_terms": { + "interval": "day", + "frequency": 14 + }, + "purchase_order_number": "PO-123", + "additional_information": null + }, + "billing_period": { + "starts_at": "2023-08-01T00:00:00Z", + "ends_at": "2024-07-31T23:59:00Z" + }, + "currency_code": "USD", + "discount_id": "dsc_01gtgztp8fpchantd5g1wrksa3", + "created_at": "2023-08-21T08:40:00.766226Z", + "updated_at": "2023-08-21T08:46:37.414122Z", + "billed_at": null, + "items": [ + { + "price": { + "id": "pri_01gsz8z1q1n00f12qt82y31smh", + "description": "Annual (per seat)", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "year", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "30000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 10, + "maximum": 999 + }, + "status": "active" + }, + "quantity": 20 + }, + { + "price": { + "id": "pri_01gsz96z29d88jrmsf2ztbfgjg", + "description": "Annual (recurring addon)", + "product_id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "billing_cycle": { + "interval": "year", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "300000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 1, + "maximum": 1 + }, + "status": "active" + }, + "quantity": 1 + }, + { + "price": { + "id": "pri_01gsz98e27ak2tyhexptwc58yk", + "description": "One-time charge", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "billing_cycle": null, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": [ + "AU" + ], + "unit_price": { + "amount": "40000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 1, + "maximum": 1 + }, + "status": "active" + }, + "quantity": 1 + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0.08875", + "totals": { + "subtotal": "919900", + "discount": "91990", + "tax": "73477", + "total": "901387" + } + } + ], + "totals": { + "subtotal": "919900", + "tax": "73477", + "discount": "91990", + "total": "901387", + "grand_total": "901387", + "fee": null, + "credit": "0", + "balance": "901387", + "earnings": null, + "currency_code": "USD" + }, + "adjusted_totals": { + "subtotal": "827910", + "tax": "73477", + "total": "901387", + "grand_total": "901387", + "fee": null, + "earnings": null, + "currency_code": "USD" + }, + "payout_totals": null, + "adjusted_payout_totals": null, + "line_items": [ + { + "id": "txnitm_01h8bmcjtsx04v0c7f1ctdeh6w", + "price_id": "pri_01gsz8z1q1n00f12qt82y31smh", + "quantity": 20, + "totals": { + "subtotal": "600000", + "tax": "47925", + "discount": "60000", + "total": "587925" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active" + }, + "tax_rate": "0.08875", + "unit_totals": { + "subtotal": "30000", + "tax": "2396", + "discount": "3000", + "total": "29396" + } + }, + { + "id": "txnitm_01h8bmcjtsx04v0c7f1da1pg6d", + "price_id": "pri_01gsz96z29d88jrmsf2ztbfgjg", + "quantity": 1, + "totals": { + "subtotal": "300000", + "tax": "23962", + "discount": "30000", + "total": "293962" + }, + "product": { + "id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "name": "VIP support", + "description": "Get exclusive access to our expert team of product specialists, available to help you make the most of your ChatApp subscription.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "status": "active" + }, + "tax_rate": "0.08875", + "unit_totals": { + "subtotal": "300000", + "tax": "23962", + "discount": "30000", + "total": "293962" + } + }, + { + "id": "txnitm_01h8bmcjtsx04v0c7f1drwqcgg", + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "totals": { + "subtotal": "19900", + "tax": "1590", + "discount": "1990", + "total": "19500" + }, + "product": { + "id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "name": "Custom domains", + "description": "Make ChatApp truly your own with custom domains! Custom domains reinforce your brand identity and make it easy for your team to access ChatApp.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "status": "active" + }, + "tax_rate": "0.08875", + "unit_totals": { + "subtotal": "19900", + "tax": "1590", + "discount": "1990", + "total": "19500" + } + } + ] + }, + "payments": [], + "checkout": { + "url": null + } + }, + { + "id": "txn_01h8bh3jn3a1kfwk4kdw6rf3gp", + "status": "draft", + "customer_id": null, + "address_id": null, + "business_id": null, + "custom_data": null, + "origin": "web", + "collection_mode": "automatic", + "subscription_id": null, + "invoice_id": null, + "invoice_number": null, + "billing_details": null, + "billing_period": null, + "currency_code": "USD", + "discount_id": null, + "created_at": "2023-08-21T07:49:16.634436Z", + "updated_at": "2023-08-21T07:49:16.634436Z", + "billed_at": null, + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "description": "Monthly (per seat)", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": [ + "AU" + ], + "unit_price": { + "amount": "5000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 10, + "maximum": 999 + }, + "status": "active" + }, + "quantity": 10 + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "description": "Monthly (recurring addon)", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": [ + "AU" + ], + "unit_price": { + "amount": "20000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 1, + "maximum": 100 + }, + "status": "active" + }, + "quantity": 1 + }, + { + "price": { + "id": "pri_01gsz98e27ak2tyhexptwc58yk", + "description": "One-time charge", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "billing_cycle": null, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": [ + "AU" + ], + "unit_price": { + "amount": "40000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 1, + "maximum": 1 + }, + "status": "active" + }, + "quantity": 1 + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0.21", + "totals": { + "subtotal": "59900", + "discount": "0", + "tax": "12579", + "total": "72479" + } + } + ], + "totals": { + "subtotal": "59900", + "tax": "12579", + "discount": "0", + "total": "72479", + "grand_total": "72479", + "fee": null, + "credit": "0", + "balance": "72479", + "earnings": null, + "currency_code": "USD" + }, + "adjusted_totals": { + "subtotal": "59900", + "tax": "12579", + "total": "72479", + "grand_total": "72479", + "fee": null, + "earnings": null, + "currency_code": "USD" + }, + "payout_totals": null, + "adjusted_payout_totals": null, + "line_items": [ + { + "id": "txnitm_01h8bh3jp5rbngqb2d0gdazcht", + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "totals": { + "subtotal": "30000", + "tax": "6300", + "discount": "0", + "total": "36300" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active" + }, + "tax_rate": "0.21", + "unit_totals": { + "subtotal": "3000", + "tax": "630", + "discount": "0", + "total": "3630" + } + }, + { + "id": "txnitm_01h8bh3jp5rbngqb2d0kxnjk2x", + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "totals": { + "subtotal": "10000", + "tax": "2100", + "discount": "0", + "total": "12100" + }, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Voice rooms addon", + "description": "Create voice rooms in your chats to work in real time alongside your colleagues. Includes unlimited voice rooms and recording backup for compliance.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/GcZzBjXRfiraensppgtQ_icon2.png", + "status": "active" + }, + "tax_rate": "0.21", + "unit_totals": { + "subtotal": "10000", + "tax": "2100", + "discount": "0", + "total": "12100" + } + }, + { + "id": "txnitm_01h8bh3jp5rbngqb2d0mr5051a", + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "totals": { + "subtotal": "19900", + "tax": "4179", + "discount": "0", + "total": "24079" + }, + "product": { + "id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "name": "Custom domains", + "description": "Make ChatApp truly your own with custom domains! Custom domains reinforce your brand identity and make it easy for your team to access ChatApp.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "status": "active" + }, + "tax_rate": "0.21", + "unit_totals": { + "subtotal": "19900", + "tax": "4179", + "discount": "0", + "total": "24079" + } + } + ] + }, + "payments": [], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01h8bh3jn3a1kfwk4kdw6rf3gp" + } + }, + { + "id": "txn_01h8bh19ag3brhyvakme2c91pa", + "status": "canceled", + "customer_id": "ctm_01h8bh2k8wb98f7dc1s5j3j43b", + "address_id": "add_01h8bh2ka9pcvm3bc3mfbq34wp", + "business_id": null, + "custom_data": null, + "origin": "web", + "collection_mode": "automatic", + "subscription_id": null, + "invoice_id": null, + "invoice_number": null, + "billing_details": null, + "billing_period": null, + "currency_code": "USD", + "discount_id": null, + "created_at": "2023-08-21T07:48:01.560677Z", + "updated_at": "2023-08-21T08:54:11.273239Z", + "billed_at": null, + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "description": "Monthly (per seat)", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": [ + "AU" + ], + "unit_price": { + "amount": "5000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 10, + "maximum": 999 + }, + "status": "active" + }, + "quantity": 10 + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "description": "Monthly (recurring addon)", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": [ + "AU" + ], + "unit_price": { + "amount": "20000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 1, + "maximum": 100 + }, + "status": "active" + }, + "quantity": 1 + }, + { + "price": { + "id": "pri_01gsz98e27ak2tyhexptwc58yk", + "description": "One-time charge", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "billing_cycle": null, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": [ + "AU" + ], + "unit_price": { + "amount": "40000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 1, + "maximum": 1 + }, + "status": "active" + }, + "quantity": 1 + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0", + "totals": { + "subtotal": "59900", + "discount": "0", + "tax": "0", + "total": "59900" + } + } + ], + "totals": { + "subtotal": "59900", + "tax": "0", + "discount": "0", + "total": "59900", + "grand_total": "59900", + "fee": null, + "credit": "0", + "balance": "59900", + "earnings": null, + "currency_code": "USD" + }, + "adjusted_totals": { + "subtotal": "59900", + "tax": "0", + "total": "59900", + "grand_total": "59900", + "fee": null, + "earnings": null, + "currency_code": "USD" + }, + "payout_totals": null, + "adjusted_payout_totals": null, + "line_items": [ + { + "id": "txnitm_01h8bh2kdzx5jtcn24nfxp8mge", + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "totals": { + "subtotal": "30000", + "tax": "0", + "discount": "0", + "total": "30000" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active" + }, + "tax_rate": "0", + "unit_totals": { + "subtotal": "3000", + "tax": "0", + "discount": "0", + "total": "3000" + } + }, + { + "id": "txnitm_01h8bh2kdzx5jtcn24njfzq7pw", + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "totals": { + "subtotal": "10000", + "tax": "0", + "discount": "0", + "total": "10000" + }, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Voice rooms addon", + "description": "Create voice rooms in your chats to work in real time alongside your colleagues. Includes unlimited voice rooms and recording backup for compliance.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/GcZzBjXRfiraensppgtQ_icon2.png", + "status": "active" + }, + "tax_rate": "0", + "unit_totals": { + "subtotal": "10000", + "tax": "0", + "discount": "0", + "total": "10000" + } + }, + { + "id": "txnitm_01h8bh2kdzx5jtcn24np0zysg4", + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "totals": { + "subtotal": "19900", + "tax": "0", + "discount": "0", + "total": "19900" + }, + "product": { + "id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "name": "Custom domains", + "description": "Make ChatApp truly your own with custom domains! Custom domains reinforce your brand identity and make it easy for your team to access ChatApp.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "status": "active" + }, + "tax_rate": "0", + "unit_totals": { + "subtotal": "19900", + "tax": "0", + "discount": "0", + "total": "19900" + } + } + ] + }, + "payments": [], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01h8bh19ag3brhyvakme2c91pa" + } + }, + { + "id": "txn_01h857x99rw3vy424gsy6bgtfs", + "status": "completed", + "customer_id": "ctm_01h1xmrqekzftg1ebbnhwbae9y", + "address_id": "add_01h1xmrqgcrcjbqtd7nj1kdy1a", + "business_id": null, + "custom_data": null, + "origin": "subscription_recurring", + "collection_mode": "automatic", + "subscription_id": "sub_01h1xmtadr8n1zck73f3drhq2a", + "invoice_id": "inv_01h857xe4rqr9d1qs0qb1by8nc", + "invoice_number": "325-10134", + "billing_details": null, + "billing_period": { + "starts_at": "2023-10-01T00:00:00Z", + "ends_at": "2023-11-01T00:00:00Z" + }, + "currency_code": "USD", + "discount_id": null, + "created_at": "2023-08-18T21:13:07.033262Z", + "updated_at": "2023-08-18T21:13:14.83528Z", + "billed_at": "2023-08-18T21:13:06.616004Z", + "items": [ + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "description": "Monthly", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 1, + "maximum": 100 + }, + "status": "active" + }, + "quantity": 1 + }, + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "description": "Monthly (per seat)", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 10, + "maximum": 999 + }, + "status": "active" + }, + "quantity": 10 + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0", + "totals": { + "subtotal": "40000", + "discount": "0", + "tax": "0", + "total": "40000" + } + } + ], + "totals": { + "subtotal": "40000", + "tax": "0", + "discount": "0", + "total": "40000", + "grand_total": "40000", + "fee": "2050", + "credit": "0", + "balance": "0", + "earnings": "37950", + "currency_code": "USD" + }, + "adjusted_totals": { + "subtotal": "40000", + "tax": "0", + "total": "40000", + "grand_total": "40000", + "fee": "2050", + "earnings": "37950", + "currency_code": "USD" + }, + "payout_totals": { + "subtotal": "40000", + "tax": "0", + "discount": "0", + "total": "40000", + "credit": "0", + "balance": "0", + "grand_total": "40000", + "fee": "2050", + "earnings": "37950", + "currency_code": "USD" + }, + "adjusted_payout_totals": { + "subtotal": "40000", + "tax": "0", + "total": "40000", + "fee": "2050", + "chargeback_fee": { + "amount": "0", + "original": null + }, + "earnings": "37950", + "currency_code": "USD" + }, + "line_items": [ + { + "id": "txnitm_01h857x9nvtq3vmc89yccnxym6", + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "totals": { + "subtotal": "10000", + "tax": "0", + "discount": "0", + "total": "10000" + }, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Voice rooms addon", + "description": "Create voice rooms in your chats to work in real time alongside your colleagues. Includes unlimited voice rooms and recording backup for compliance.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/GcZzBjXRfiraensppgtQ_icon2.png", + "status": "active" + }, + "tax_rate": "0", + "unit_totals": { + "subtotal": "10000", + "tax": "0", + "discount": "0", + "total": "10000" + } + }, + { + "id": "txnitm_01h857x9nvtq3vmc89ydbye62d", + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "totals": { + "subtotal": "30000", + "tax": "0", + "discount": "0", + "total": "30000" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active" + }, + "tax_rate": "0", + "unit_totals": { + "subtotal": "3000", + "tax": "0", + "discount": "0", + "total": "3000" + } + } + ] + }, + "payments": [ + { + "payment_attempt_id": "1f8e8302-b6fa-4290-b457-4c201eb3d53f", + "stored_payment_method_id": "5f85a637-efa7-4c6b-88ec-9cc05301bb48", + "amount": "40000", + "status": "captured", + "error_code": null, + "method_details": { + "type": "card", + "card": { + "type": "visa", + "last4": "4242", + "expiry_month": 1, + "expiry_year": 2024, + "cardholder_name": "Joe Bloggs" + } + }, + "created_at": "2023-08-18T21:13:07.18821Z", + "captured_at": "2023-08-18T21:13:09.477933Z" + } + ], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01h857x99rw3vy424gsy6bgtfs" + } + }, + { + "id": "txn_01h7zcz6dhp2tc5mcd7qbnf8sp", + "status": "past_due", + "customer_id": "ctm_01gvcz30f4d77tfnn60snnyxfd", + "address_id": "add_01gvczbeepz72bfgsvbcmy1vpg", + "business_id": null, + "custom_data": {}, + "origin": "subscription_recurring", + "collection_mode": "manual", + "subscription_id": "sub_01gvne45dvdhg5gdxrz6hh511r", + "invoice_id": "inv_01h7zcz74ckz5yygwfkk2kphkt", + "invoice_number": "325-10122", + "billing_details": { + "enable_checkout": false, + "payment_terms": { + "interval": "week", + "frequency": 2 + }, + "purchase_order_number": "", + "additional_information": "" + }, + "billing_period": { + "starts_at": "2023-08-16T14:45:30.683929Z", + "ends_at": "2023-09-16T14:45:30.683929Z" + }, + "currency_code": "USD", + "discount_id": null, + "created_at": "2023-08-16T14:46:05.86701Z", + "updated_at": "2023-08-19T14:47:05.97537Z", + "billed_at": "2023-08-16T14:46:05.489912Z", + "items": [ + { + "price": { + "id": "pri_01gvne87kv8vbqa9jkfbmgtsed", + "description": "Monthly", + "product_id": "pro_01gsz4vmqbjk3x4vvtafffd540", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "5000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 1, + "maximum": 100 + }, + "status": "active" + }, + "quantity": 1 + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0", + "totals": { + "subtotal": "5000", + "discount": "0", + "tax": "0", + "total": "5000" + } + } + ], + "totals": { + "subtotal": "5000", + "tax": "0", + "discount": "0", + "total": "5000", + "grand_total": "5000", + "fee": null, + "credit": "0", + "balance": "5000", + "earnings": null, + "currency_code": "USD" + }, + "adjusted_totals": { + "subtotal": "5000", + "tax": "0", + "total": "5000", + "grand_total": "5000", + "fee": null, + "earnings": null, + "currency_code": "USD" + }, + "payout_totals": null, + "adjusted_payout_totals": null, + "line_items": [ + { + "id": "txnitm_01h7zcz6rq1ttphe4wmz0nnr4b", + "price_id": "pri_01gvne87kv8vbqa9jkfbmgtsed", + "quantity": 1, + "totals": { + "subtotal": "5000", + "tax": "0", + "discount": "0", + "total": "5000" + }, + "product": { + "id": "pro_01gsz4vmqbjk3x4vvtafffd540", + "name": "ChatApp Enterprise", + "description": "The ultimate solution for businesses that require top-of-the-line features and customizations. Includes all the features of the Pro plan, plus personalized onboarding, dedicated account management, and the ability to pay via invoice.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active" + }, + "tax_rate": "0", + "unit_totals": { + "subtotal": "5000", + "tax": "0", + "discount": "0", + "total": "5000" + } + } + ] + }, + "payments": [], + "checkout": { + "url": null + } + }, + { + "id": "txn_01h69ddtrb11km0wk46dn607ya", + "status": "billed", + "customer_id": "ctm_01h3w5erbfqppdcmwchhss3cv1", + "address_id": "add_01h3w5ercxtvj5wnkeam6nmwyy", + "business_id": null, + "custom_data": null, + "origin": "subscription_recurring", + "collection_mode": "manual", + "subscription_id": "sub_01h3w5fhbrzyqgk0dkhz19e526", + "invoice_id": "inv_01h69ddw9rqsdfn6qjhfqcfdbg", + "invoice_number": "325-10101", + "billing_details": { + "enable_checkout": true, + "payment_terms": { + "interval": "day", + "frequency": 30 + }, + "purchase_order_number": "PO-1030", + "additional_information": "Contact your account manager if you have any problems." + }, + "billing_period": { + "starts_at": "2023-07-26T15:34:40.309508Z", + "ends_at": "2023-08-26T15:34:40.309508Z" + }, + "currency_code": "USD", + "discount_id": null, + "created_at": "2023-07-26T15:35:06.134251Z", + "updated_at": "2023-07-26T15:35:11.182344Z", + "billed_at": "2023-07-26T15:35:05.739403Z", + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "description": "Monthly (per seat)", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 10, + "maximum": 999 + }, + "status": "active" + }, + "quantity": 10 + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "description": "Monthly (recurring addon)", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 1, + "maximum": 100 + }, + "status": "active" + }, + "quantity": 1 + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0.08875", + "totals": { + "subtotal": "40000", + "discount": "0", + "tax": "3549", + "total": "43549" + } + } + ], + "totals": { + "subtotal": "40000", + "tax": "3549", + "discount": "0", + "total": "43549", + "grand_total": "43549", + "fee": null, + "credit": "0", + "balance": "43549", + "earnings": null, + "currency_code": "USD" + }, + "adjusted_totals": { + "subtotal": "40000", + "tax": "3549", + "total": "43549", + "grand_total": "43549", + "fee": null, + "earnings": null, + "currency_code": "USD" + }, + "payout_totals": null, + "adjusted_payout_totals": null, + "line_items": [ + { + "id": "txnitm_01h69ddv3p1pv24rtr6vxfvh63", + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "totals": { + "subtotal": "30000", + "tax": "2662", + "discount": "0", + "total": "32662" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active" + }, + "tax_rate": "0.08875", + "unit_totals": { + "subtotal": "3000", + "tax": "266", + "discount": "0", + "total": "3266" + } + }, + { + "id": "txnitm_01h69ddv3p1pv24rtr6wrrn41v", + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "totals": { + "subtotal": "10000", + "tax": "887", + "discount": "0", + "total": "10887" + }, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Voice rooms addon", + "description": "Create voice rooms in your chats to work in real time alongside your colleagues. Includes unlimited voice rooms and recording backup for compliance.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/GcZzBjXRfiraensppgtQ_icon2.png", + "status": "active" + }, + "tax_rate": "0.08875", + "unit_totals": { + "subtotal": "10000", + "tax": "887", + "discount": "0", + "total": "10887" + } + } + ] + }, + "payments": [], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01h69ddtrb11km0wk46dn607ya" + } + } + ], + "meta": { + "request_id": "ed3e36f7-9eb5-4579-a995-3c5121b68129", + "pagination": { + "per_page": 30, + "next": "https://api.paddle.com/transactions?after=txn_01h69ddtrb11km0wk46dn607ya", + "has_more": false, + "estimated_total": 6 + } + } +} diff --git a/tests/Functional/Resources/Transactions/_fixtures/response/list_paginated_page_one.json b/tests/Functional/Resources/Transactions/_fixtures/response/list_paginated_page_one.json new file mode 100644 index 0000000..f11a52a --- /dev/null +++ b/tests/Functional/Resources/Transactions/_fixtures/response/list_paginated_page_one.json @@ -0,0 +1,1222 @@ +{ + "data": [ + { + "id": "txn_01h8bm0f0gwa622zpcvw49hwc1", + "status": "ready", + "customer_id": "ctm_01h8441jn5pcwrfhwh78jqt8hk", + "address_id": "add_01h848pep46enq8y372x7maj0p", + "business_id": null, + "custom_data": null, + "origin": "api", + "collection_mode": "manual", + "subscription_id": null, + "invoice_id": "inv_01h8bm0gtxhgd7f37f4xx8b93m", + "invoice_number": null, + "billing_details": { + "enable_checkout": false, + "payment_terms": { + "interval": "day", + "frequency": 14 + }, + "purchase_order_number": "PO-123", + "additional_information": null + }, + "billing_period": { + "starts_at": "2023-08-01T00:00:00Z", + "ends_at": "2024-07-31T23:59:00Z" + }, + "currency_code": "USD", + "discount_id": "dsc_01gtgztp8fpchantd5g1wrksa3", + "created_at": "2023-08-21T08:40:00.766226Z", + "updated_at": "2023-08-21T08:46:37.414122Z", + "billed_at": null, + "items": [ + { + "price": { + "id": "pri_01gsz8z1q1n00f12qt82y31smh", + "description": "Annual (per seat)", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "year", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "30000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 10, + "maximum": 999 + }, + "status": "active" + }, + "quantity": 20 + }, + { + "price": { + "id": "pri_01gsz96z29d88jrmsf2ztbfgjg", + "description": "Annual (recurring addon)", + "product_id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "billing_cycle": { + "interval": "year", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "300000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 1, + "maximum": 1 + }, + "status": "active" + }, + "quantity": 1 + }, + { + "price": { + "id": "pri_01gsz98e27ak2tyhexptwc58yk", + "description": "One-time charge", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "billing_cycle": null, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": ["AU"], + "unit_price": { + "amount": "40000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 1, + "maximum": 1 + }, + "status": "active" + }, + "quantity": 1 + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0.08875", + "totals": { + "subtotal": "919900", + "discount": "91990", + "tax": "73477", + "total": "901387" + } + } + ], + "totals": { + "subtotal": "919900", + "tax": "73477", + "discount": "91990", + "total": "901387", + "grand_total": "901387", + "fee": null, + "credit": "0", + "balance": "901387", + "earnings": null, + "currency_code": "USD" + }, + "adjusted_totals": { + "subtotal": "827910", + "tax": "73477", + "total": "901387", + "grand_total": "901387", + "fee": null, + "earnings": null, + "currency_code": "USD" + }, + "payout_totals": null, + "adjusted_payout_totals": null, + "line_items": [ + { + "id": "txnitm_01h8bmcjtsx04v0c7f1ctdeh6w", + "price_id": "pri_01gsz8z1q1n00f12qt82y31smh", + "quantity": 20, + "totals": { + "subtotal": "600000", + "tax": "47925", + "discount": "60000", + "total": "587925" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active" + }, + "tax_rate": "0.08875", + "unit_totals": { + "subtotal": "30000", + "tax": "2396", + "discount": "3000", + "total": "29396" + } + }, + { + "id": "txnitm_01h8bmcjtsx04v0c7f1da1pg6d", + "price_id": "pri_01gsz96z29d88jrmsf2ztbfgjg", + "quantity": 1, + "totals": { + "subtotal": "300000", + "tax": "23962", + "discount": "30000", + "total": "293962" + }, + "product": { + "id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "name": "VIP support", + "description": "Get exclusive access to our expert team of product specialists, available to help you make the most of your ChatApp subscription.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "status": "active" + }, + "tax_rate": "0.08875", + "unit_totals": { + "subtotal": "300000", + "tax": "23962", + "discount": "30000", + "total": "293962" + } + }, + { + "id": "txnitm_01h8bmcjtsx04v0c7f1drwqcgg", + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "totals": { + "subtotal": "19900", + "tax": "1590", + "discount": "1990", + "total": "19500" + }, + "product": { + "id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "name": "Custom domains", + "description": "Make ChatApp truly your own with custom domains! Custom domains reinforce your brand identity and make it easy for your team to access ChatApp.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "status": "active" + }, + "tax_rate": "0.08875", + "unit_totals": { + "subtotal": "19900", + "tax": "1590", + "discount": "1990", + "total": "19500" + } + } + ] + }, + "payments": [], + "checkout": { + "url": null + } + }, + { + "id": "txn_01h8bh3jn3a1kfwk4kdw6rf3gp", + "status": "draft", + "customer_id": null, + "address_id": null, + "business_id": null, + "custom_data": null, + "origin": "web", + "collection_mode": "automatic", + "subscription_id": null, + "invoice_id": null, + "invoice_number": null, + "billing_details": null, + "billing_period": null, + "currency_code": "USD", + "discount_id": null, + "created_at": "2023-08-21T07:49:16.634436Z", + "updated_at": "2023-08-21T07:49:16.634436Z", + "billed_at": null, + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "description": "Monthly (per seat)", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": ["AU"], + "unit_price": { + "amount": "5000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 10, + "maximum": 999 + }, + "status": "active" + }, + "quantity": 10 + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "description": "Monthly (recurring addon)", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": ["AU"], + "unit_price": { + "amount": "20000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 1, + "maximum": 100 + }, + "status": "active" + }, + "quantity": 1 + }, + { + "price": { + "id": "pri_01gsz98e27ak2tyhexptwc58yk", + "description": "One-time charge", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "billing_cycle": null, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": ["AU"], + "unit_price": { + "amount": "40000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 1, + "maximum": 1 + }, + "status": "active" + }, + "quantity": 1 + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0.21", + "totals": { + "subtotal": "59900", + "discount": "0", + "tax": "12579", + "total": "72479" + } + } + ], + "totals": { + "subtotal": "59900", + "tax": "12579", + "discount": "0", + "total": "72479", + "grand_total": "72479", + "fee": null, + "credit": "0", + "balance": "72479", + "earnings": null, + "currency_code": "USD" + }, + "adjusted_totals": { + "subtotal": "59900", + "tax": "12579", + "total": "72479", + "grand_total": "72479", + "fee": null, + "earnings": null, + "currency_code": "USD" + }, + "payout_totals": null, + "adjusted_payout_totals": null, + "line_items": [ + { + "id": "txnitm_01h8bh3jp5rbngqb2d0gdazcht", + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "totals": { + "subtotal": "30000", + "tax": "6300", + "discount": "0", + "total": "36300" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active" + }, + "tax_rate": "0.21", + "unit_totals": { + "subtotal": "3000", + "tax": "630", + "discount": "0", + "total": "3630" + } + }, + { + "id": "txnitm_01h8bh3jp5rbngqb2d0kxnjk2x", + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "totals": { + "subtotal": "10000", + "tax": "2100", + "discount": "0", + "total": "12100" + }, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Voice rooms addon", + "description": "Create voice rooms in your chats to work in real time alongside your colleagues. Includes unlimited voice rooms and recording backup for compliance.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/GcZzBjXRfiraensppgtQ_icon2.png", + "status": "active" + }, + "tax_rate": "0.21", + "unit_totals": { + "subtotal": "10000", + "tax": "2100", + "discount": "0", + "total": "12100" + } + }, + { + "id": "txnitm_01h8bh3jp5rbngqb2d0mr5051a", + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "totals": { + "subtotal": "19900", + "tax": "4179", + "discount": "0", + "total": "24079" + }, + "product": { + "id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "name": "Custom domains", + "description": "Make ChatApp truly your own with custom domains! Custom domains reinforce your brand identity and make it easy for your team to access ChatApp.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "status": "active" + }, + "tax_rate": "0.21", + "unit_totals": { + "subtotal": "19900", + "tax": "4179", + "discount": "0", + "total": "24079" + } + } + ] + }, + "payments": [], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01h8bh3jn3a1kfwk4kdw6rf3gp" + } + }, + { + "id": "txn_01h8bh19ag3brhyvakme2c91pa", + "status": "canceled", + "customer_id": "ctm_01h8bh2k8wb98f7dc1s5j3j43b", + "address_id": "add_01h8bh2ka9pcvm3bc3mfbq34wp", + "business_id": null, + "custom_data": null, + "origin": "web", + "collection_mode": "automatic", + "subscription_id": null, + "invoice_id": null, + "invoice_number": null, + "billing_details": null, + "billing_period": null, + "currency_code": "USD", + "discount_id": null, + "created_at": "2023-08-21T07:48:01.560677Z", + "updated_at": "2023-08-21T08:54:11.273239Z", + "billed_at": null, + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "description": "Monthly (per seat)", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": ["AU"], + "unit_price": { + "amount": "5000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 10, + "maximum": 999 + }, + "status": "active" + }, + "quantity": 10 + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "description": "Monthly (recurring addon)", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": ["AU"], + "unit_price": { + "amount": "20000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 1, + "maximum": 100 + }, + "status": "active" + }, + "quantity": 1 + }, + { + "price": { + "id": "pri_01gsz98e27ak2tyhexptwc58yk", + "description": "One-time charge", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "billing_cycle": null, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": ["AU"], + "unit_price": { + "amount": "40000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 1, + "maximum": 1 + }, + "status": "active" + }, + "quantity": 1 + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0", + "totals": { + "subtotal": "59900", + "discount": "0", + "tax": "0", + "total": "59900" + } + } + ], + "totals": { + "subtotal": "59900", + "tax": "0", + "discount": "0", + "total": "59900", + "grand_total": "59900", + "fee": null, + "credit": "0", + "balance": "59900", + "earnings": null, + "currency_code": "USD" + }, + "adjusted_totals": { + "subtotal": "59900", + "tax": "0", + "total": "59900", + "grand_total": "59900", + "fee": null, + "earnings": null, + "currency_code": "USD" + }, + "payout_totals": null, + "adjusted_payout_totals": null, + "line_items": [ + { + "id": "txnitm_01h8bh2kdzx5jtcn24nfxp8mge", + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "totals": { + "subtotal": "30000", + "tax": "0", + "discount": "0", + "total": "30000" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active" + }, + "tax_rate": "0", + "unit_totals": { + "subtotal": "3000", + "tax": "0", + "discount": "0", + "total": "3000" + } + }, + { + "id": "txnitm_01h8bh2kdzx5jtcn24njfzq7pw", + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "totals": { + "subtotal": "10000", + "tax": "0", + "discount": "0", + "total": "10000" + }, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Voice rooms addon", + "description": "Create voice rooms in your chats to work in real time alongside your colleagues. Includes unlimited voice rooms and recording backup for compliance.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/GcZzBjXRfiraensppgtQ_icon2.png", + "status": "active" + }, + "tax_rate": "0", + "unit_totals": { + "subtotal": "10000", + "tax": "0", + "discount": "0", + "total": "10000" + } + }, + { + "id": "txnitm_01h8bh2kdzx5jtcn24np0zysg4", + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "totals": { + "subtotal": "19900", + "tax": "0", + "discount": "0", + "total": "19900" + }, + "product": { + "id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "name": "Custom domains", + "description": "Make ChatApp truly your own with custom domains! Custom domains reinforce your brand identity and make it easy for your team to access ChatApp.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "status": "active" + }, + "tax_rate": "0", + "unit_totals": { + "subtotal": "19900", + "tax": "0", + "discount": "0", + "total": "19900" + } + } + ] + }, + "payments": [], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01h8bh19ag3brhyvakme2c91pa" + } + }, + { + "id": "txn_01h857x99rw3vy424gsy6bgtfs", + "status": "completed", + "customer_id": "ctm_01h1xmrqekzftg1ebbnhwbae9y", + "address_id": "add_01h1xmrqgcrcjbqtd7nj1kdy1a", + "business_id": null, + "custom_data": null, + "origin": "subscription_recurring", + "collection_mode": "automatic", + "subscription_id": "sub_01h1xmtadr8n1zck73f3drhq2a", + "invoice_id": "inv_01h857xe4rqr9d1qs0qb1by8nc", + "invoice_number": "325-10134", + "billing_details": null, + "billing_period": { + "starts_at": "2023-10-01T00:00:00Z", + "ends_at": "2023-11-01T00:00:00Z" + }, + "currency_code": "USD", + "discount_id": null, + "created_at": "2023-08-18T21:13:07.033262Z", + "updated_at": "2023-08-18T21:13:14.83528Z", + "billed_at": "2023-08-18T21:13:06.616004Z", + "items": [ + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "description": "Monthly", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 1, + "maximum": 100 + }, + "status": "active" + }, + "quantity": 1 + }, + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "description": "Monthly (per seat)", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 10, + "maximum": 999 + }, + "status": "active" + }, + "quantity": 10 + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0", + "totals": { + "subtotal": "40000", + "discount": "0", + "tax": "0", + "total": "40000" + } + } + ], + "totals": { + "subtotal": "40000", + "tax": "0", + "discount": "0", + "total": "40000", + "grand_total": "40000", + "fee": "2050", + "credit": "0", + "balance": "0", + "earnings": "37950", + "currency_code": "USD" + }, + "adjusted_totals": { + "subtotal": "40000", + "tax": "0", + "total": "40000", + "grand_total": "40000", + "fee": "2050", + "earnings": "37950", + "currency_code": "USD" + }, + "payout_totals": { + "subtotal": "40000", + "tax": "0", + "discount": "0", + "total": "40000", + "credit": "0", + "balance": "0", + "grand_total": "40000", + "fee": "2050", + "earnings": "37950", + "currency_code": "USD" + }, + "adjusted_payout_totals": { + "subtotal": "40000", + "tax": "0", + "total": "40000", + "fee": "2050", + "chargeback_fee": { + "amount": "0", + "original": null + }, + "earnings": "37950", + "currency_code": "USD" + }, + "line_items": [ + { + "id": "txnitm_01h857x9nvtq3vmc89yccnxym6", + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "totals": { + "subtotal": "10000", + "tax": "0", + "discount": "0", + "total": "10000" + }, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Voice rooms addon", + "description": "Create voice rooms in your chats to work in real time alongside your colleagues. Includes unlimited voice rooms and recording backup for compliance.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/GcZzBjXRfiraensppgtQ_icon2.png", + "status": "active" + }, + "tax_rate": "0", + "unit_totals": { + "subtotal": "10000", + "tax": "0", + "discount": "0", + "total": "10000" + } + }, + { + "id": "txnitm_01h857x9nvtq3vmc89ydbye62d", + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "totals": { + "subtotal": "30000", + "tax": "0", + "discount": "0", + "total": "30000" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active" + }, + "tax_rate": "0", + "unit_totals": { + "subtotal": "3000", + "tax": "0", + "discount": "0", + "total": "3000" + } + } + ] + }, + "payments": [ + { + "payment_attempt_id": "1f8e8302-b6fa-4290-b457-4c201eb3d53f", + "stored_payment_method_id": "5f85a637-efa7-4c6b-88ec-9cc05301bb48", + "amount": "40000", + "status": "captured", + "error_code": null, + "method_details": { + "type": "card", + "card": { + "type": "visa", + "last4": "4242", + "expiry_month": 1, + "expiry_year": 2024, + "cardholder_name": "Joe Bloggs" + } + }, + "created_at": "2023-08-18T21:13:07.18821Z", + "captured_at": "2023-08-18T21:13:09.477933Z" + } + ], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01h857x99rw3vy424gsy6bgtfs" + } + }, + { + "id": "txn_01h7zcz6dhp2tc5mcd7qbnf8sp", + "status": "past_due", + "customer_id": "ctm_01gvcz30f4d77tfnn60snnyxfd", + "address_id": "add_01gvczbeepz72bfgsvbcmy1vpg", + "business_id": null, + "custom_data": {}, + "origin": "subscription_recurring", + "collection_mode": "manual", + "subscription_id": "sub_01gvne45dvdhg5gdxrz6hh511r", + "invoice_id": "inv_01h7zcz74ckz5yygwfkk2kphkt", + "invoice_number": "325-10122", + "billing_details": { + "enable_checkout": false, + "payment_terms": { + "interval": "week", + "frequency": 2 + }, + "purchase_order_number": "", + "additional_information": "" + }, + "billing_period": { + "starts_at": "2023-08-16T14:45:30.683929Z", + "ends_at": "2023-09-16T14:45:30.683929Z" + }, + "currency_code": "USD", + "discount_id": null, + "created_at": "2023-08-16T14:46:05.86701Z", + "updated_at": "2023-08-19T14:47:05.97537Z", + "billed_at": "2023-08-16T14:46:05.489912Z", + "items": [ + { + "price": { + "id": "pri_01gvne87kv8vbqa9jkfbmgtsed", + "description": "Monthly", + "product_id": "pro_01gsz4vmqbjk3x4vvtafffd540", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "5000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 1, + "maximum": 100 + }, + "status": "active" + }, + "quantity": 1 + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0", + "totals": { + "subtotal": "5000", + "discount": "0", + "tax": "0", + "total": "5000" + } + } + ], + "totals": { + "subtotal": "5000", + "tax": "0", + "discount": "0", + "total": "5000", + "grand_total": "5000", + "fee": null, + "credit": "0", + "balance": "5000", + "earnings": null, + "currency_code": "USD" + }, + "adjusted_totals": { + "subtotal": "5000", + "tax": "0", + "total": "5000", + "grand_total": "5000", + "fee": null, + "earnings": null, + "currency_code": "USD" + }, + "payout_totals": null, + "adjusted_payout_totals": null, + "line_items": [ + { + "id": "txnitm_01h7zcz6rq1ttphe4wmz0nnr4b", + "price_id": "pri_01gvne87kv8vbqa9jkfbmgtsed", + "quantity": 1, + "totals": { + "subtotal": "5000", + "tax": "0", + "discount": "0", + "total": "5000" + }, + "product": { + "id": "pro_01gsz4vmqbjk3x4vvtafffd540", + "name": "ChatApp Enterprise", + "description": "The ultimate solution for businesses that require top-of-the-line features and customizations. Includes all the features of the Pro plan, plus personalized onboarding, dedicated account management, and the ability to pay via invoice.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active" + }, + "tax_rate": "0", + "unit_totals": { + "subtotal": "5000", + "tax": "0", + "discount": "0", + "total": "5000" + } + } + ] + }, + "payments": [], + "checkout": { + "url": null + } + }, + { + "id": "txn_01h69ddtrb11km0wk46dn607ya", + "status": "billed", + "customer_id": "ctm_01h3w5erbfqppdcmwchhss3cv1", + "address_id": "add_01h3w5ercxtvj5wnkeam6nmwyy", + "business_id": null, + "custom_data": null, + "origin": "subscription_recurring", + "collection_mode": "manual", + "subscription_id": "sub_01h3w5fhbrzyqgk0dkhz19e526", + "invoice_id": "inv_01h69ddw9rqsdfn6qjhfqcfdbg", + "invoice_number": "325-10101", + "billing_details": { + "enable_checkout": true, + "payment_terms": { + "interval": "day", + "frequency": 30 + }, + "purchase_order_number": "PO-1030", + "additional_information": "Contact your account manager if you have any problems." + }, + "billing_period": { + "starts_at": "2023-07-26T15:34:40.309508Z", + "ends_at": "2023-08-26T15:34:40.309508Z" + }, + "currency_code": "USD", + "discount_id": null, + "created_at": "2023-07-26T15:35:06.134251Z", + "updated_at": "2023-07-26T15:35:11.182344Z", + "billed_at": "2023-07-26T15:35:05.739403Z", + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "description": "Monthly (per seat)", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 10, + "maximum": 999 + }, + "status": "active" + }, + "quantity": 10 + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "description": "Monthly (recurring addon)", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 1, + "maximum": 100 + }, + "status": "active" + }, + "quantity": 1 + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0.08875", + "totals": { + "subtotal": "40000", + "discount": "0", + "tax": "3549", + "total": "43549" + } + } + ], + "totals": { + "subtotal": "40000", + "tax": "3549", + "discount": "0", + "total": "43549", + "grand_total": "43549", + "fee": null, + "credit": "0", + "balance": "43549", + "earnings": null, + "currency_code": "USD" + }, + "adjusted_totals": { + "subtotal": "40000", + "tax": "3549", + "total": "43549", + "grand_total": "43549", + "fee": null, + "earnings": null, + "currency_code": "USD" + }, + "payout_totals": null, + "adjusted_payout_totals": null, + "line_items": [ + { + "id": "txnitm_01h69ddv3p1pv24rtr6vxfvh63", + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "totals": { + "subtotal": "30000", + "tax": "2662", + "discount": "0", + "total": "32662" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active" + }, + "tax_rate": "0.08875", + "unit_totals": { + "subtotal": "3000", + "tax": "266", + "discount": "0", + "total": "3266" + } + }, + { + "id": "txnitm_01h69ddv3p1pv24rtr6wrrn41v", + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "totals": { + "subtotal": "10000", + "tax": "887", + "discount": "0", + "total": "10887" + }, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Voice rooms addon", + "description": "Create voice rooms in your chats to work in real time alongside your colleagues. Includes unlimited voice rooms and recording backup for compliance.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/GcZzBjXRfiraensppgtQ_icon2.png", + "status": "active" + }, + "tax_rate": "0.08875", + "unit_totals": { + "subtotal": "10000", + "tax": "887", + "discount": "0", + "total": "10887" + } + } + ] + }, + "payments": [], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01h69ddtrb11km0wk46dn607ya" + } + } + ], + "meta": { + "request_id": "ed3e36f7-9eb5-4579-a995-3c5121b68129", + "pagination": { + "per_page": 30, + "next": "https://api.paddle.com/transactions?after=txn_01h69ddtrb11km0wk46dn607ya", + "has_more": true, + "estimated_total": 6 + } + } +} diff --git a/tests/Functional/Resources/Transactions/_fixtures/response/list_paginated_page_two.json b/tests/Functional/Resources/Transactions/_fixtures/response/list_paginated_page_two.json new file mode 100644 index 0000000..6a13c4e --- /dev/null +++ b/tests/Functional/Resources/Transactions/_fixtures/response/list_paginated_page_two.json @@ -0,0 +1,1222 @@ +{ + "data": [ + { + "id": "txn_01h8bm0f0gwa622zpcvw49hwc1", + "status": "ready", + "customer_id": "ctm_01h8441jn5pcwrfhwh78jqt8hk", + "address_id": "add_01h848pep46enq8y372x7maj0p", + "business_id": null, + "custom_data": null, + "origin": "api", + "collection_mode": "manual", + "subscription_id": null, + "invoice_id": "inv_01h8bm0gtxhgd7f37f4xx8b93m", + "invoice_number": null, + "billing_details": { + "enable_checkout": false, + "payment_terms": { + "interval": "day", + "frequency": 14 + }, + "purchase_order_number": "PO-123", + "additional_information": null + }, + "billing_period": { + "starts_at": "2023-08-01T00:00:00Z", + "ends_at": "2024-07-31T23:59:00Z" + }, + "currency_code": "USD", + "discount_id": "dsc_01gtgztp8fpchantd5g1wrksa3", + "created_at": "2023-08-21T08:40:00.766226Z", + "updated_at": "2023-08-21T08:46:37.414122Z", + "billed_at": null, + "items": [ + { + "price": { + "id": "pri_01gsz8z1q1n00f12qt82y31smh", + "description": "Annual (per seat)", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "year", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "30000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 10, + "maximum": 999 + }, + "status": "active" + }, + "quantity": 20 + }, + { + "price": { + "id": "pri_01gsz96z29d88jrmsf2ztbfgjg", + "description": "Annual (recurring addon)", + "product_id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "billing_cycle": { + "interval": "year", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "300000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 1, + "maximum": 1 + }, + "status": "active" + }, + "quantity": 1 + }, + { + "price": { + "id": "pri_01gsz98e27ak2tyhexptwc58yk", + "description": "One-time charge", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "billing_cycle": null, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": ["AU"], + "unit_price": { + "amount": "40000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 1, + "maximum": 1 + }, + "status": "active" + }, + "quantity": 1 + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0.08875", + "totals": { + "subtotal": "919900", + "discount": "91990", + "tax": "73477", + "total": "901387" + } + } + ], + "totals": { + "subtotal": "919900", + "tax": "73477", + "discount": "91990", + "total": "901387", + "grand_total": "901387", + "fee": null, + "credit": "0", + "balance": "901387", + "earnings": null, + "currency_code": "USD" + }, + "adjusted_totals": { + "subtotal": "827910", + "tax": "73477", + "total": "901387", + "grand_total": "901387", + "fee": null, + "earnings": null, + "currency_code": "USD" + }, + "payout_totals": null, + "adjusted_payout_totals": null, + "line_items": [ + { + "id": "txnitm_01h8bmcjtsx04v0c7f1ctdeh6w", + "price_id": "pri_01gsz8z1q1n00f12qt82y31smh", + "quantity": 20, + "totals": { + "subtotal": "600000", + "tax": "47925", + "discount": "60000", + "total": "587925" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active" + }, + "tax_rate": "0.08875", + "unit_totals": { + "subtotal": "30000", + "tax": "2396", + "discount": "3000", + "total": "29396" + } + }, + { + "id": "txnitm_01h8bmcjtsx04v0c7f1da1pg6d", + "price_id": "pri_01gsz96z29d88jrmsf2ztbfgjg", + "quantity": 1, + "totals": { + "subtotal": "300000", + "tax": "23962", + "discount": "30000", + "total": "293962" + }, + "product": { + "id": "pro_01gsz92krfzy3hcx5h5rtgnfwz", + "name": "VIP support", + "description": "Get exclusive access to our expert team of product specialists, available to help you make the most of your ChatApp subscription.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "status": "active" + }, + "tax_rate": "0.08875", + "unit_totals": { + "subtotal": "300000", + "tax": "23962", + "discount": "30000", + "total": "293962" + } + }, + { + "id": "txnitm_01h8bmcjtsx04v0c7f1drwqcgg", + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "totals": { + "subtotal": "19900", + "tax": "1590", + "discount": "1990", + "total": "19500" + }, + "product": { + "id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "name": "Custom domains", + "description": "Make ChatApp truly your own with custom domains! Custom domains reinforce your brand identity and make it easy for your team to access ChatApp.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "status": "active" + }, + "tax_rate": "0.08875", + "unit_totals": { + "subtotal": "19900", + "tax": "1590", + "discount": "1990", + "total": "19500" + } + } + ] + }, + "payments": [], + "checkout": { + "url": null + } + }, + { + "id": "txn_01h8bh3jn3a1kfwk4kdw6rf3gp", + "status": "draft", + "customer_id": null, + "address_id": null, + "business_id": null, + "custom_data": null, + "origin": "web", + "collection_mode": "automatic", + "subscription_id": null, + "invoice_id": null, + "invoice_number": null, + "billing_details": null, + "billing_period": null, + "currency_code": "USD", + "discount_id": null, + "created_at": "2023-08-21T07:49:16.634436Z", + "updated_at": "2023-08-21T07:49:16.634436Z", + "billed_at": null, + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "description": "Monthly (per seat)", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": ["AU"], + "unit_price": { + "amount": "5000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 10, + "maximum": 999 + }, + "status": "active" + }, + "quantity": 10 + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "description": "Monthly (recurring addon)", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": ["AU"], + "unit_price": { + "amount": "20000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 1, + "maximum": 100 + }, + "status": "active" + }, + "quantity": 1 + }, + { + "price": { + "id": "pri_01gsz98e27ak2tyhexptwc58yk", + "description": "One-time charge", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "billing_cycle": null, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": ["AU"], + "unit_price": { + "amount": "40000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 1, + "maximum": 1 + }, + "status": "active" + }, + "quantity": 1 + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0.21", + "totals": { + "subtotal": "59900", + "discount": "0", + "tax": "12579", + "total": "72479" + } + } + ], + "totals": { + "subtotal": "59900", + "tax": "12579", + "discount": "0", + "total": "72479", + "grand_total": "72479", + "fee": null, + "credit": "0", + "balance": "72479", + "earnings": null, + "currency_code": "USD" + }, + "adjusted_totals": { + "subtotal": "59900", + "tax": "12579", + "total": "72479", + "grand_total": "72479", + "fee": null, + "earnings": null, + "currency_code": "USD" + }, + "payout_totals": null, + "adjusted_payout_totals": null, + "line_items": [ + { + "id": "txnitm_01h8bh3jp5rbngqb2d0gdazcht", + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "totals": { + "subtotal": "30000", + "tax": "6300", + "discount": "0", + "total": "36300" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active" + }, + "tax_rate": "0.21", + "unit_totals": { + "subtotal": "3000", + "tax": "630", + "discount": "0", + "total": "3630" + } + }, + { + "id": "txnitm_01h8bh3jp5rbngqb2d0kxnjk2x", + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "totals": { + "subtotal": "10000", + "tax": "2100", + "discount": "0", + "total": "12100" + }, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Voice rooms addon", + "description": "Create voice rooms in your chats to work in real time alongside your colleagues. Includes unlimited voice rooms and recording backup for compliance.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/GcZzBjXRfiraensppgtQ_icon2.png", + "status": "active" + }, + "tax_rate": "0.21", + "unit_totals": { + "subtotal": "10000", + "tax": "2100", + "discount": "0", + "total": "12100" + } + }, + { + "id": "txnitm_01h8bh3jp5rbngqb2d0mr5051a", + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "totals": { + "subtotal": "19900", + "tax": "4179", + "discount": "0", + "total": "24079" + }, + "product": { + "id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "name": "Custom domains", + "description": "Make ChatApp truly your own with custom domains! Custom domains reinforce your brand identity and make it easy for your team to access ChatApp.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "status": "active" + }, + "tax_rate": "0.21", + "unit_totals": { + "subtotal": "19900", + "tax": "4179", + "discount": "0", + "total": "24079" + } + } + ] + }, + "payments": [], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01h8bh3jn3a1kfwk4kdw6rf3gp" + } + }, + { + "id": "txn_01h8bh19ag3brhyvakme2c91pa", + "status": "canceled", + "customer_id": "ctm_01h8bh2k8wb98f7dc1s5j3j43b", + "address_id": "add_01h8bh2ka9pcvm3bc3mfbq34wp", + "business_id": null, + "custom_data": null, + "origin": "web", + "collection_mode": "automatic", + "subscription_id": null, + "invoice_id": null, + "invoice_number": null, + "billing_details": null, + "billing_period": null, + "currency_code": "USD", + "discount_id": null, + "created_at": "2023-08-21T07:48:01.560677Z", + "updated_at": "2023-08-21T08:54:11.273239Z", + "billed_at": null, + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "description": "Monthly (per seat)", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": ["AU"], + "unit_price": { + "amount": "5000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 10, + "maximum": 999 + }, + "status": "active" + }, + "quantity": 10 + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "description": "Monthly (recurring addon)", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": ["AU"], + "unit_price": { + "amount": "20000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 1, + "maximum": 100 + }, + "status": "active" + }, + "quantity": 1 + }, + { + "price": { + "id": "pri_01gsz98e27ak2tyhexptwc58yk", + "description": "One-time charge", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "billing_cycle": null, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": ["AU"], + "unit_price": { + "amount": "40000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 1, + "maximum": 1 + }, + "status": "active" + }, + "quantity": 1 + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0", + "totals": { + "subtotal": "59900", + "discount": "0", + "tax": "0", + "total": "59900" + } + } + ], + "totals": { + "subtotal": "59900", + "tax": "0", + "discount": "0", + "total": "59900", + "grand_total": "59900", + "fee": null, + "credit": "0", + "balance": "59900", + "earnings": null, + "currency_code": "USD" + }, + "adjusted_totals": { + "subtotal": "59900", + "tax": "0", + "total": "59900", + "grand_total": "59900", + "fee": null, + "earnings": null, + "currency_code": "USD" + }, + "payout_totals": null, + "adjusted_payout_totals": null, + "line_items": [ + { + "id": "txnitm_01h8bh2kdzx5jtcn24nfxp8mge", + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "totals": { + "subtotal": "30000", + "tax": "0", + "discount": "0", + "total": "30000" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active" + }, + "tax_rate": "0", + "unit_totals": { + "subtotal": "3000", + "tax": "0", + "discount": "0", + "total": "3000" + } + }, + { + "id": "txnitm_01h8bh2kdzx5jtcn24njfzq7pw", + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "totals": { + "subtotal": "10000", + "tax": "0", + "discount": "0", + "total": "10000" + }, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Voice rooms addon", + "description": "Create voice rooms in your chats to work in real time alongside your colleagues. Includes unlimited voice rooms and recording backup for compliance.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/GcZzBjXRfiraensppgtQ_icon2.png", + "status": "active" + }, + "tax_rate": "0", + "unit_totals": { + "subtotal": "10000", + "tax": "0", + "discount": "0", + "total": "10000" + } + }, + { + "id": "txnitm_01h8bh2kdzx5jtcn24np0zysg4", + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "totals": { + "subtotal": "19900", + "tax": "0", + "discount": "0", + "total": "19900" + }, + "product": { + "id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "name": "Custom domains", + "description": "Make ChatApp truly your own with custom domains! Custom domains reinforce your brand identity and make it easy for your team to access ChatApp.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "status": "active" + }, + "tax_rate": "0", + "unit_totals": { + "subtotal": "19900", + "tax": "0", + "discount": "0", + "total": "19900" + } + } + ] + }, + "payments": [], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01h8bh19ag3brhyvakme2c91pa" + } + }, + { + "id": "txn_01h857x99rw3vy424gsy6bgtfs", + "status": "completed", + "customer_id": "ctm_01h1xmrqekzftg1ebbnhwbae9y", + "address_id": "add_01h1xmrqgcrcjbqtd7nj1kdy1a", + "business_id": null, + "custom_data": null, + "origin": "subscription_recurring", + "collection_mode": "automatic", + "subscription_id": "sub_01h1xmtadr8n1zck73f3drhq2a", + "invoice_id": "inv_01h857xe4rqr9d1qs0qb1by8nc", + "invoice_number": "325-10134", + "billing_details": null, + "billing_period": { + "starts_at": "2023-10-01T00:00:00Z", + "ends_at": "2023-11-01T00:00:00Z" + }, + "currency_code": "USD", + "discount_id": null, + "created_at": "2023-08-18T21:13:07.033262Z", + "updated_at": "2023-08-18T21:13:14.83528Z", + "billed_at": "2023-08-18T21:13:06.616004Z", + "items": [ + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "description": "Monthly", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 1, + "maximum": 100 + }, + "status": "active" + }, + "quantity": 1 + }, + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "description": "Monthly (per seat)", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 10, + "maximum": 999 + }, + "status": "active" + }, + "quantity": 10 + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0", + "totals": { + "subtotal": "40000", + "discount": "0", + "tax": "0", + "total": "40000" + } + } + ], + "totals": { + "subtotal": "40000", + "tax": "0", + "discount": "0", + "total": "40000", + "grand_total": "40000", + "fee": "2050", + "credit": "0", + "balance": "0", + "earnings": "37950", + "currency_code": "USD" + }, + "adjusted_totals": { + "subtotal": "40000", + "tax": "0", + "total": "40000", + "grand_total": "40000", + "fee": "2050", + "earnings": "37950", + "currency_code": "USD" + }, + "payout_totals": { + "subtotal": "40000", + "tax": "0", + "discount": "0", + "total": "40000", + "credit": "0", + "balance": "0", + "grand_total": "40000", + "fee": "2050", + "earnings": "37950", + "currency_code": "USD" + }, + "adjusted_payout_totals": { + "subtotal": "40000", + "tax": "0", + "total": "40000", + "fee": "2050", + "chargeback_fee": { + "amount": "0", + "original": null + }, + "earnings": "37950", + "currency_code": "USD" + }, + "line_items": [ + { + "id": "txnitm_01h857x9nvtq3vmc89yccnxym6", + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "totals": { + "subtotal": "10000", + "tax": "0", + "discount": "0", + "total": "10000" + }, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Voice rooms addon", + "description": "Create voice rooms in your chats to work in real time alongside your colleagues. Includes unlimited voice rooms and recording backup for compliance.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/GcZzBjXRfiraensppgtQ_icon2.png", + "status": "active" + }, + "tax_rate": "0", + "unit_totals": { + "subtotal": "10000", + "tax": "0", + "discount": "0", + "total": "10000" + } + }, + { + "id": "txnitm_01h857x9nvtq3vmc89ydbye62d", + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "totals": { + "subtotal": "30000", + "tax": "0", + "discount": "0", + "total": "30000" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active" + }, + "tax_rate": "0", + "unit_totals": { + "subtotal": "3000", + "tax": "0", + "discount": "0", + "total": "3000" + } + } + ] + }, + "payments": [ + { + "payment_attempt_id": "1f8e8302-b6fa-4290-b457-4c201eb3d53f", + "stored_payment_method_id": "5f85a637-efa7-4c6b-88ec-9cc05301bb48", + "amount": "40000", + "status": "captured", + "error_code": null, + "method_details": { + "type": "card", + "card": { + "type": "visa", + "last4": "4242", + "expiry_month": 1, + "expiry_year": 2024, + "cardholder_name": "Joe Bloggs" + } + }, + "created_at": "2023-08-18T21:13:07.18821Z", + "captured_at": "2023-08-18T21:13:09.477933Z" + } + ], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01h857x99rw3vy424gsy6bgtfs" + } + }, + { + "id": "txn_01h7zcz6dhp2tc5mcd7qbnf8sp", + "status": "past_due", + "customer_id": "ctm_01gvcz30f4d77tfnn60snnyxfd", + "address_id": "add_01gvczbeepz72bfgsvbcmy1vpg", + "business_id": null, + "custom_data": {}, + "origin": "subscription_recurring", + "collection_mode": "manual", + "subscription_id": "sub_01gvne45dvdhg5gdxrz6hh511r", + "invoice_id": "inv_01h7zcz74ckz5yygwfkk2kphkt", + "invoice_number": "325-10122", + "billing_details": { + "enable_checkout": false, + "payment_terms": { + "interval": "week", + "frequency": 2 + }, + "purchase_order_number": "", + "additional_information": "" + }, + "billing_period": { + "starts_at": "2023-08-16T14:45:30.683929Z", + "ends_at": "2023-09-16T14:45:30.683929Z" + }, + "currency_code": "USD", + "discount_id": null, + "created_at": "2023-08-16T14:46:05.86701Z", + "updated_at": "2023-08-19T14:47:05.97537Z", + "billed_at": "2023-08-16T14:46:05.489912Z", + "items": [ + { + "price": { + "id": "pri_01gvne87kv8vbqa9jkfbmgtsed", + "description": "Monthly", + "product_id": "pro_01gsz4vmqbjk3x4vvtafffd540", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "5000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 1, + "maximum": 100 + }, + "status": "active" + }, + "quantity": 1 + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0", + "totals": { + "subtotal": "5000", + "discount": "0", + "tax": "0", + "total": "5000" + } + } + ], + "totals": { + "subtotal": "5000", + "tax": "0", + "discount": "0", + "total": "5000", + "grand_total": "5000", + "fee": null, + "credit": "0", + "balance": "5000", + "earnings": null, + "currency_code": "USD" + }, + "adjusted_totals": { + "subtotal": "5000", + "tax": "0", + "total": "5000", + "grand_total": "5000", + "fee": null, + "earnings": null, + "currency_code": "USD" + }, + "payout_totals": null, + "adjusted_payout_totals": null, + "line_items": [ + { + "id": "txnitm_01h7zcz6rq1ttphe4wmz0nnr4b", + "price_id": "pri_01gvne87kv8vbqa9jkfbmgtsed", + "quantity": 1, + "totals": { + "subtotal": "5000", + "tax": "0", + "discount": "0", + "total": "5000" + }, + "product": { + "id": "pro_01gsz4vmqbjk3x4vvtafffd540", + "name": "ChatApp Enterprise", + "description": "The ultimate solution for businesses that require top-of-the-line features and customizations. Includes all the features of the Pro plan, plus personalized onboarding, dedicated account management, and the ability to pay via invoice.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active" + }, + "tax_rate": "0", + "unit_totals": { + "subtotal": "5000", + "tax": "0", + "discount": "0", + "total": "5000" + } + } + ] + }, + "payments": [], + "checkout": { + "url": null + } + }, + { + "id": "txn_01h69ddtrb11km0wk46dn607ya", + "status": "billed", + "customer_id": "ctm_01h3w5erbfqppdcmwchhss3cv1", + "address_id": "add_01h3w5ercxtvj5wnkeam6nmwyy", + "business_id": null, + "custom_data": null, + "origin": "subscription_recurring", + "collection_mode": "manual", + "subscription_id": "sub_01h3w5fhbrzyqgk0dkhz19e526", + "invoice_id": "inv_01h69ddw9rqsdfn6qjhfqcfdbg", + "invoice_number": "325-10101", + "billing_details": { + "enable_checkout": true, + "payment_terms": { + "interval": "day", + "frequency": 30 + }, + "purchase_order_number": "PO-1030", + "additional_information": "Contact your account manager if you have any problems." + }, + "billing_period": { + "starts_at": "2023-07-26T15:34:40.309508Z", + "ends_at": "2023-08-26T15:34:40.309508Z" + }, + "currency_code": "USD", + "discount_id": null, + "created_at": "2023-07-26T15:35:06.134251Z", + "updated_at": "2023-07-26T15:35:11.182344Z", + "billed_at": "2023-07-26T15:35:05.739403Z", + "items": [ + { + "price": { + "id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "description": "Monthly (per seat)", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "3000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 10, + "maximum": 999 + }, + "status": "active" + }, + "quantity": 10 + }, + { + "price": { + "id": "pri_01h1vjfevh5etwq3rb416a23h2", + "description": "Monthly (recurring addon)", + "product_id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "10000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 1, + "maximum": 100 + }, + "status": "active" + }, + "quantity": 1 + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0.08875", + "totals": { + "subtotal": "40000", + "discount": "0", + "tax": "3549", + "total": "43549" + } + } + ], + "totals": { + "subtotal": "40000", + "tax": "3549", + "discount": "0", + "total": "43549", + "grand_total": "43549", + "fee": null, + "credit": "0", + "balance": "43549", + "earnings": null, + "currency_code": "USD" + }, + "adjusted_totals": { + "subtotal": "40000", + "tax": "3549", + "total": "43549", + "grand_total": "43549", + "fee": null, + "earnings": null, + "currency_code": "USD" + }, + "payout_totals": null, + "adjusted_payout_totals": null, + "line_items": [ + { + "id": "txnitm_01h69ddv3p1pv24rtr6vxfvh63", + "price_id": "pri_01gsz8x8sawmvhz1pv30nge1ke", + "quantity": 10, + "totals": { + "subtotal": "30000", + "tax": "2662", + "discount": "0", + "total": "32662" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active" + }, + "tax_rate": "0.08875", + "unit_totals": { + "subtotal": "3000", + "tax": "266", + "discount": "0", + "total": "3266" + } + }, + { + "id": "txnitm_01h69ddv3p1pv24rtr6wrrn41v", + "price_id": "pri_01h1vjfevh5etwq3rb416a23h2", + "quantity": 1, + "totals": { + "subtotal": "10000", + "tax": "887", + "discount": "0", + "total": "10887" + }, + "product": { + "id": "pro_01h1vjes1y163xfj1rh1tkfb65", + "name": "Voice rooms addon", + "description": "Create voice rooms in your chats to work in real time alongside your colleagues. Includes unlimited voice rooms and recording backup for compliance.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/GcZzBjXRfiraensppgtQ_icon2.png", + "status": "active" + }, + "tax_rate": "0.08875", + "unit_totals": { + "subtotal": "10000", + "tax": "887", + "discount": "0", + "total": "10887" + } + } + ] + }, + "payments": [], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01h69ddtrb11km0wk46dn607ya" + } + } + ], + "meta": { + "request_id": "ed3e36f7-9eb5-4579-a995-3c5121b68129", + "pagination": { + "per_page": 6, + "next": "https://api.paddle.com/transactions?after=txn_01h69ddtrb11km0wk46dn607ya", + "has_more": false, + "estimated_total": 12 + } + } +} diff --git a/tests/Functional/Resources/Transactions/_fixtures/response/minimal_entity.json b/tests/Functional/Resources/Transactions/_fixtures/response/minimal_entity.json new file mode 100644 index 0000000..785c3ee --- /dev/null +++ b/tests/Functional/Resources/Transactions/_fixtures/response/minimal_entity.json @@ -0,0 +1,122 @@ +{ + "data": { + "id": "txn_01hempgzm2hjaxbxhbyya4c8m5", + "status": "draft", + "customer_id": null, + "address_id": null, + "business_id": null, + "custom_data": null, + "origin": "api", + "collection_mode": "automatic", + "subscription_id": null, + "invoice_id": null, + "invoice_number": null, + "billing_details": null, + "billing_period": null, + "currency_code": "USD", + "discount_id": null, + "created_at": "2023-11-07T10:51:19.601401071Z", + "updated_at": "2023-11-07T10:51:19.601401071Z", + "billed_at": null, + "items": [ + { + "price": { + "id": "pri_01he5kxqey1k8ankgef29cj4bv", + "description": "Base subscription", + "name": "Base subscription", + "product_id": "pro_01he5kwnnvgdv2chtpgavk2rf8", + "billing_cycle": { + "interval": "month", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "0", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "custom_data": null, + "quantity": { + "minimum": 1, + "maximum": 100 + }, + "status": "active" + }, + "quantity": 1 + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0", + "totals": { + "subtotal": "0", + "discount": "0", + "tax": "0", + "total": "0" + } + } + ], + "totals": { + "subtotal": "0", + "tax": "0", + "discount": "0", + "total": "0", + "grand_total": "0", + "fee": null, + "credit": "0", + "balance": "0", + "earnings": null, + "currency_code": "USD" + }, + "adjusted_totals": { + "subtotal": "0", + "tax": "0", + "total": "0", + "grand_total": "0", + "fee": "0", + "earnings": "0", + "currency_code": "USD" + }, + "payout_totals": null, + "adjusted_payout_totals": null, + "line_items": [ + { + "id": "txnitm_01hempgzn6v3f2d6fgtekt9nwc", + "price_id": "pri_01he5kxqey1k8ankgef29cj4bv", + "quantity": 1, + "totals": { + "subtotal": "0", + "tax": "0", + "discount": "0", + "total": "0" + }, + "product": { + "id": "pro_01he5kwnnvgdv2chtpgavk2rf8", + "name": "Fire Transfer", + "description": "Transfer data quickly with Fire Transfer. It's blazin' fast!", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/XBWZPsQoSc6YyViK5ocI_fire.png", + "custom_data": null, + "status": "active" + }, + "tax_rate": "0", + "unit_totals": { + "subtotal": "0", + "tax": "0", + "discount": "0", + "total": "0" + } + } + ] + }, + "payments": [], + "checkout": { + "url": "https://magnificent-entremet-7ae0c6.netlify.app/default/overlay?_ptxn=txn_01hempgzm2hjaxbxhbyya4c8m5" + } + }, + "meta": { + "request_id": "0a1e62a8-df0f-4944-a65c-bd1bbaa316fd" + } +} diff --git a/tests/Functional/Resources/Transactions/_fixtures/response/preview_entity.json b/tests/Functional/Resources/Transactions/_fixtures/response/preview_entity.json new file mode 100644 index 0000000..2ffb71d --- /dev/null +++ b/tests/Functional/Resources/Transactions/_fixtures/response/preview_entity.json @@ -0,0 +1,161 @@ +{ + "data": { + "customer_id": null, + "address_id": null, + "business_id": null, + "currency_code": "USD", + "address": { + "postal_code": "", + "country_code": "US" + }, + "customer_ip_address": null, + "discount_id": "dsc_01gtgztp8fpchantd5g1wrksa3", + "items": [ + { + "price": { + "id": "pri_01gsz8z1q1n00f12qt82y31smh", + "description": "Annual (per seat)", + "name": "Annual (per seat)", + "product_id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "billing_cycle": { + "interval": "year", + "frequency": 1 + }, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "30000", + "currency_code": "USD" + }, + "unit_price_overrides": [], + "quantity": { + "minimum": 10, + "maximum": 999 + }, + "status": "active", + "custom_data": null + }, + "quantity": 20, + "proration": null, + "include_in_totals": true + }, + { + "price": { + "id": "pri_01gsz98e27ak2tyhexptwc58yk", + "description": "One-time charge", + "name": "One-time charge", + "product_id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "billing_cycle": null, + "trial_period": null, + "tax_mode": "account_setting", + "unit_price": { + "amount": "19900", + "currency_code": "USD" + }, + "unit_price_overrides": [ + { + "country_codes": ["AU"], + "unit_price": { + "amount": "40000", + "currency_code": "AUD" + } + } + ], + "quantity": { + "minimum": 1, + "maximum": 1 + }, + "status": "active", + "custom_data": null + }, + "quantity": 1, + "proration": null, + "include_in_totals": false + } + ], + "details": { + "tax_rates_used": [ + { + "tax_rate": "0", + "totals": { + "subtotal": "600000", + "discount": "60000", + "tax": "0", + "total": "540000" + } + } + ], + "totals": { + "subtotal": "600000", + "tax": "0", + "discount": "60000", + "total": "540000", + "grand_total": "540000", + "fee": null, + "credit": "0", + "balance": "540000", + "earnings": null, + "currency_code": "USD" + }, + "line_items": [ + { + "price_id": "pri_01gsz8z1q1n00f12qt82y31smh", + "quantity": 20, + "totals": { + "subtotal": "600000", + "tax": "0", + "discount": "60000", + "total": "540000" + }, + "product": { + "id": "pro_01gsz4t5hdjse780zja8vvr7jg", + "name": "ChatApp Pro", + "description": "Everything in basic, plus access to a suite of powerful tools and features designed to take your team's productivity to the next level.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/2nmP8MQSret0aWeDemRw_icon1.png", + "status": "active", + "custom_data": null + }, + "tax_rate": "0", + "unit_totals": { + "subtotal": "30000", + "tax": "0", + "discount": "3000", + "total": "27000" + } + }, + { + "price_id": "pri_01gsz98e27ak2tyhexptwc58yk", + "quantity": 1, + "totals": { + "subtotal": "19900", + "tax": "0", + "discount": "1990", + "total": "17910" + }, + "product": { + "id": "pro_01gsz97mq9pa4fkyy0wqenepkz", + "name": "Custom domains", + "description": "Make ChatApp truly your own with custom domains! Custom domains reinforce your brand identity and make it easy for your team to access ChatApp.", + "tax_category": "standard", + "image_url": "https://paddle-sandbox.s3.amazonaws.com/user/10889/SW3OevDQ92dUHSkN5a2x_icon3.png", + "status": "active", + "custom_data": null + }, + "tax_rate": "0", + "unit_totals": { + "subtotal": "19900", + "tax": "0", + "discount": "1990", + "total": "17910" + } + } + ] + }, + "ignore_trials": false, + "available_payment_methods": ["apple_pay"] + }, + "meta": { + "request_id": "9e6a1195-d86f-4110-a0ee-981b5500a6a8" + } +} diff --git a/tests/Unit/Notifications/VerifierTest.php b/tests/Unit/Notifications/VerifierTest.php new file mode 100644 index 0000000..3db9f0e --- /dev/null +++ b/tests/Unit/Notifications/VerifierTest.php @@ -0,0 +1,37 @@ +createRequest('POST', '/paddle/notifications') + ->withHeader( + PaddleSignature::HEADER, + 'ts=1696195954;h1=dummy;h1=d96299976a6eb066f484d7fde011ac56fc32b38fc9940bc419d6e537fdc6ef02', + ) + ->withBody( + Psr17FactoryDiscovery::findStreamFactory()->createStream('hello-world'), + ); + + $secrets = [ + new Secret('pdl_ntf_01hbpjmytsa32fhr36nqgc3kgb_TjIG2BXbm83HPXqNfziwe506sBEdqL/4'), + new Secret('pdl_ntf_01hbpjmytsa32fhr36nqgc3kgb_vB/yIOnTOCWIvpBadM5jzBZPHc7OmdSo'), + // The correct key + new Secret('pdl_ntf_01hbpjmytsa32fhr36nqgc3kgb_WvRO0eL4Bj9rgYYIBZY6wZhG4EHy9jzh'), + ]; + + self::assertTrue((new Verifier(null))->verify($request, ...$secrets)); + } +} diff --git a/tests/Utils/Assertions/AssertsDeepMatchesData.php b/tests/Utils/Assertions/AssertsDeepMatchesData.php new file mode 100644 index 0000000..1dbc897 --- /dev/null +++ b/tests/Utils/Assertions/AssertsDeepMatchesData.php @@ -0,0 +1,51 @@ + $value) { + // snake case -> camel case + $prop = preg_replace_callback('/_(.?)/', fn (array $matches) => strtoupper($matches[1]), $prop); + + Assert::assertArrayHasKey($prop, get_object_vars($object), "{$prop} is missing from object"); + + self::assertMatch($value, $object->{$prop}); + } + } + + private static function assertMatch(mixed $expected, mixed $value): void + { + switch (true) { + case is_object($value) && is_array($expected): + self::assertDeepMatchesData($value, $expected); + break; + case is_object($value) && enum_exists($value::class): + self::assertEnumMatch($value, $expected); + break; + case is_array($value) && is_array($expected): + foreach ($value as $key => $item) { + Assert::assertArrayHasKey($key, $expected, "{$key} is missing from array"); + self::assertMatch($expected[$key], $item); + } + break; + default: + Assert::assertEquals($expected, $value); + } + } + + private static function assertEnumMatch(object $enum, mixed $value): void + { + Assert::assertEquals( + $value, + $enum->value, + sprintf('Enum value "%s" does not match expected value "%s"', $enum->value, $value), + ); + } +} diff --git a/tests/Utils/ReadsFixtures.php b/tests/Utils/ReadsFixtures.php new file mode 100644 index 0000000..a210f6a --- /dev/null +++ b/tests/Utils/ReadsFixtures.php @@ -0,0 +1,37 @@ +