Repository: Viincenttt/MollieApi Branch: development Commit: d9e4389a22d1 Files: 494 Total size: 1.2 MB Directory structure: gitextract_xm20k__5/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ └── dotnetcore.yml ├── .gitignore ├── AGENTS.md ├── Directory.Build.props ├── LICENSE ├── MollieApi.sln ├── README.md ├── mollie.publickey.txt ├── samples/ │ └── Mollie.WebApplication.Blazor/ │ ├── App.razor │ ├── Framework/ │ │ ├── StaticStringListBuilder.cs │ │ └── Validators/ │ │ ├── DecimalPlacesAttribute.cs │ │ └── StaticStringListAttribute.cs │ ├── Models/ │ │ ├── Customer/ │ │ │ └── CreateCustomerModel.cs │ │ ├── Mandate/ │ │ │ └── CreateMandateModel.cs │ │ ├── Order/ │ │ │ ├── CreateOrderBillingAddressModel.cs │ │ │ ├── CreateOrderLineModel.cs │ │ │ └── CreateOrderModel.cs │ │ ├── Payment/ │ │ │ └── CreatePaymentModel.cs │ │ ├── PaymentLink/ │ │ │ └── CreatePaymentModel.cs │ │ ├── Subscription/ │ │ │ ├── CreateSubscriptionModel.cs │ │ │ └── IntervalPeriod.cs │ │ └── Webhook/ │ │ └── CreateWebhookModel.cs │ ├── Mollie.WebApplication.Blazor.csproj │ ├── Pages/ │ │ ├── Customer/ │ │ │ ├── Create.razor │ │ │ └── Overview.razor │ │ ├── Error.cshtml │ │ ├── Error.cshtml.cs │ │ ├── Index.razor │ │ ├── Mandate/ │ │ │ ├── Create.razor │ │ │ └── Overview.razor │ │ ├── Order/ │ │ │ ├── Components/ │ │ │ │ ├── OrderAddressEditor.razor │ │ │ │ └── OrderLineEditor.razor │ │ │ ├── Create.razor │ │ │ └── Overview.razor │ │ ├── Payment/ │ │ │ ├── Create.razor │ │ │ └── Overview.razor │ │ ├── PaymentLink/ │ │ │ ├── Create.razor │ │ │ └── Overview.razor │ │ ├── PaymentMethod/ │ │ │ └── Overview.razor │ │ ├── Subscription/ │ │ │ ├── Create.razor │ │ │ └── Overview.razor │ │ ├── Terminal/ │ │ │ └── Overview.razor │ │ ├── Webhook/ │ │ │ ├── Components/ │ │ │ │ └── EventTypeEditor.razor │ │ │ ├── Create.razor │ │ │ └── Overview.razor │ │ └── _Host.cshtml │ ├── Program.cs │ ├── Properties/ │ │ └── launchSettings.json │ ├── Shared/ │ │ ├── ApiExceptionDisplay.razor │ │ ├── MainLayout.razor │ │ ├── MainLayout.razor.css │ │ ├── NavMenu.razor │ │ ├── NavMenu.razor.css │ │ └── OverviewNavigation.razor │ ├── Webhooks/ │ │ ├── Classic/ │ │ │ └── PaymentController.cs │ │ └── Nextgen/ │ │ ├── Controllers/ │ │ │ └── PaymentLinkController.cs │ │ └── MinimalApi/ │ │ └── WebhookHandler.cs │ ├── _Imports.razor │ ├── appsettings.Development.json │ ├── appsettings.json │ └── wwwroot/ │ └── css/ │ ├── open-iconic/ │ │ ├── FONT-LICENSE │ │ ├── ICON-LICENSE │ │ ├── README.md │ │ └── font/ │ │ └── fonts/ │ │ └── open-iconic.otf │ └── site.css ├── src/ │ ├── Mollie.Api/ │ │ ├── Client/ │ │ │ ├── Abstract/ │ │ │ │ ├── IBalanceClient.cs │ │ │ │ ├── IBalanceTransferClient.cs │ │ │ │ ├── IBaseMollieClient.cs │ │ │ │ ├── ICapabilityClient.cs │ │ │ │ ├── ICaptureClient.cs │ │ │ │ ├── IChargebackClient.cs │ │ │ │ ├── IClientClient.cs │ │ │ │ ├── IClientLinkClient.cs │ │ │ │ ├── IConnectClient.cs │ │ │ │ ├── ICustomerClient.cs │ │ │ │ ├── IInvoiceClient.cs │ │ │ │ ├── IMandateClient.cs │ │ │ │ ├── IOnboardingClient.cs │ │ │ │ ├── IOrderClient.cs │ │ │ │ ├── IOrganizationClient.cs │ │ │ │ ├── IPaymentClient.cs │ │ │ │ ├── IPaymentLinkClient.cs │ │ │ │ ├── IPaymentMethodClient.cs │ │ │ │ ├── IPermissionClient.cs │ │ │ │ ├── IProfileClient.cs │ │ │ │ ├── IRefundClient.cs │ │ │ │ ├── ISalesInvoiceClient.cs │ │ │ │ ├── ISessionClient.cs │ │ │ │ ├── ISettlementClient.cs │ │ │ │ ├── IShipmentClient.cs │ │ │ │ ├── ISubscriptionClient.cs │ │ │ │ ├── ITerminalClient.cs │ │ │ │ ├── IWalletClient.cs │ │ │ │ ├── IWebhookClient.cs │ │ │ │ └── IWebhookEventClient.cs │ │ │ ├── BalanceClient.cs │ │ │ ├── BalanceTransferClient.cs │ │ │ ├── BaseMollieClient.cs │ │ │ ├── CapabilityClient.cs │ │ │ ├── CaptureClient.cs │ │ │ ├── ChargebackClient.cs │ │ │ ├── ClientClient.cs │ │ │ ├── ClientLinkClient.cs │ │ │ ├── ConnectClient.cs │ │ │ ├── CustomerClient.cs │ │ │ ├── InvoiceClient.cs │ │ │ ├── MandateClient.cs │ │ │ ├── MollieApiException.cs │ │ │ ├── OauthBaseMollieClient.cs │ │ │ ├── OnboardingClient.cs │ │ │ ├── OrderClient.cs │ │ │ ├── OrganizationClient.cs │ │ │ ├── PaymentClient.cs │ │ │ ├── PaymentLinkClient.cs │ │ │ ├── PaymentMethodClient.cs │ │ │ ├── PermissionClient.cs │ │ │ ├── ProfileClient.cs │ │ │ ├── RefundClient.cs │ │ │ ├── SalesInvoiceClient.cs │ │ │ ├── SessionClient.cs │ │ │ ├── SettlementClient.cs │ │ │ ├── ShipmentClient.cs │ │ │ ├── SubscriptionClient.cs │ │ │ ├── TerminalClient.cs │ │ │ ├── WalletClient.cs │ │ │ ├── WebhookClient.cs │ │ │ └── WebhookEventClient.cs │ │ ├── DependencyInjection.cs │ │ ├── Extensions/ │ │ │ ├── DateTimeExtensions.cs │ │ │ ├── DictionaryExtensions.cs │ │ │ ├── HttpClientExtensions.cs │ │ │ └── ListExtensions.cs │ │ ├── Framework/ │ │ │ ├── Authentication/ │ │ │ │ ├── Abstract/ │ │ │ │ │ └── IMollieSecretManager.cs │ │ │ │ └── DefaultMollieSecretManager.cs │ │ │ ├── Factories/ │ │ │ │ ├── BalanceReportResponseFactory.cs │ │ │ │ ├── BalanceTransactionFactory.cs │ │ │ │ ├── ITypeFactory.cs │ │ │ │ ├── MandateResponseFactory.cs │ │ │ │ └── PaymentResponseFactory.cs │ │ │ ├── Idempotency/ │ │ │ │ └── AsyncLocalVariable.cs │ │ │ ├── JsonConverterService.cs │ │ │ └── MollieHttpRetryPolicies.cs │ │ ├── JsonConverters/ │ │ │ ├── CollectionToCommaSeparatedListConverter.cs │ │ │ ├── DateJsonConverter.cs │ │ │ ├── Iso8601DateTimeConverter.cs │ │ │ ├── ListResponseJsonConverter.cs │ │ │ ├── MicrosecondEpochConverter.cs │ │ │ ├── PolymorphicConverter.cs │ │ │ ├── RawJsonConverter.cs │ │ │ ├── SettlementPeriodConverter.cs │ │ │ ├── StringToDecimalConverter.cs │ │ │ └── WebhookEventEntityJsonConverter.cs │ │ ├── Models/ │ │ │ ├── AddressObject.cs │ │ │ ├── Amount.cs │ │ │ ├── ApplicationFee.cs │ │ │ ├── Balance/ │ │ │ │ └── Response/ │ │ │ │ ├── BalanceReport/ │ │ │ │ │ ├── BalanceReportAmount.cs │ │ │ │ │ ├── BalanceReportAmountWithSubtotals.cs │ │ │ │ │ ├── BalanceReportLinks.cs │ │ │ │ │ ├── BalanceReportResponse.cs │ │ │ │ │ ├── BalanceReportSubtotals.cs │ │ │ │ │ ├── ReportGrouping.cs │ │ │ │ │ └── Specific/ │ │ │ │ │ ├── StatusBalance/ │ │ │ │ │ │ ├── StatusBalanceAvailableBalance.cs │ │ │ │ │ │ ├── StatusBalanceReportResponse.cs │ │ │ │ │ │ ├── StatusBalancesPendingBalance.cs │ │ │ │ │ │ └── StatusBalancesTotal.cs │ │ │ │ │ └── TransactionCategories/ │ │ │ │ │ ├── TransactionCategoriesReportResponse.cs │ │ │ │ │ ├── TransactionCategoriesSummaryBalances.cs │ │ │ │ │ ├── TransactionCategoriesTotal.cs │ │ │ │ │ └── TransactionCategoriesTransaction.cs │ │ │ │ ├── BalanceResponse.cs │ │ │ │ ├── BalanceResponseLinks.cs │ │ │ │ ├── BalanceResponseStatus.cs │ │ │ │ ├── BalanceTransaction/ │ │ │ │ │ ├── BalanceTransactionContextType.cs │ │ │ │ │ ├── BalanceTransactionResponse.cs │ │ │ │ │ └── Specific/ │ │ │ │ │ ├── CaptureBalanceTransactionResponse.cs │ │ │ │ │ ├── ChargebackBalanceTransactionResponse.cs │ │ │ │ │ ├── InvoiceBalanceTransactionResponse.cs │ │ │ │ │ ├── PaymentBalanceTransactionResponse.cs │ │ │ │ │ ├── RefundBalanceTransactionResponse.cs │ │ │ │ │ └── SettlementBalanceTransactionResponse.cs │ │ │ │ └── BalanceTransferDestination.cs │ │ │ ├── BalanceTransfer/ │ │ │ │ ├── BalanceTransferParty.cs │ │ │ │ ├── Request/ │ │ │ │ │ └── BalanceTransferRequest.cs │ │ │ │ └── Response/ │ │ │ │ ├── BalanceTransferResponse.cs │ │ │ │ └── BalanceTransferStatusReason.cs │ │ │ ├── Capability/ │ │ │ │ ├── CapabilityRequirementStatus.cs │ │ │ │ ├── CapabilityStatus.cs │ │ │ │ ├── CapabilityStatusReason.cs │ │ │ │ └── Response/ │ │ │ │ ├── CapabilityRequirement.cs │ │ │ │ ├── CapabilityRequirementLinks.cs │ │ │ │ ├── CapabilityResponse.cs │ │ │ │ └── CapabilityResponseLinks.cs │ │ │ ├── Capture/ │ │ │ │ ├── CaptureMode.cs │ │ │ │ ├── Request/ │ │ │ │ │ └── CaptureRequest.cs │ │ │ │ └── Response/ │ │ │ │ ├── CaptureResponse.cs │ │ │ │ └── CaptureResponseLinks.cs │ │ │ ├── Chargeback/ │ │ │ │ └── Response/ │ │ │ │ ├── ChargebackResponse.cs │ │ │ │ ├── ChargebackResponseLinks.cs │ │ │ │ └── ChargebackResponseReason.cs │ │ │ ├── Client/ │ │ │ │ └── Response/ │ │ │ │ ├── ClientCommissionResponse.cs │ │ │ │ ├── ClientEmbeddedResponse.cs │ │ │ │ ├── ClientResponse.cs │ │ │ │ └── ClientResponseLinks.cs │ │ │ ├── ClientLink/ │ │ │ │ ├── Request/ │ │ │ │ │ ├── ClientLinkOwner.cs │ │ │ │ │ └── ClientLinkRequest.cs │ │ │ │ └── Response/ │ │ │ │ ├── ClientLinkResponse.cs │ │ │ │ └── ClientLinkResponseLinks.cs │ │ │ ├── CompanyEntityType.cs │ │ │ ├── CompanyObject.cs │ │ │ ├── Connect/ │ │ │ │ ├── Request/ │ │ │ │ │ ├── AppPermissions.cs │ │ │ │ │ ├── RevokeTokenRequest.cs │ │ │ │ │ ├── TokenRequest.cs │ │ │ │ │ └── TokenType.cs │ │ │ │ └── Response/ │ │ │ │ └── TokenResponse.cs │ │ │ ├── Currency.cs │ │ │ ├── Customer/ │ │ │ │ ├── Request/ │ │ │ │ │ └── CustomerRequest.cs │ │ │ │ └── Response/ │ │ │ │ ├── CustomerResponse.cs │ │ │ │ └── CustomerResponseLinks.cs │ │ │ ├── Error/ │ │ │ │ └── MollieErrorMessage.cs │ │ │ ├── IEntity.cs │ │ │ ├── IProfileRequest.cs │ │ │ ├── ITestModeRequest.cs │ │ │ ├── Invoice/ │ │ │ │ └── Response/ │ │ │ │ ├── InvoiceLine.cs │ │ │ │ ├── InvoiceResponse.cs │ │ │ │ ├── InvoiceResponseLinks.cs │ │ │ │ └── InvoiceStatus.cs │ │ │ ├── Issuer/ │ │ │ │ └── Response/ │ │ │ │ ├── IssuerResponse.cs │ │ │ │ └── IssuerResponseImage.cs │ │ │ ├── List/ │ │ │ │ └── Response/ │ │ │ │ ├── ListResponse.cs │ │ │ │ └── ListResponseLinks.cs │ │ │ ├── Mandate/ │ │ │ │ ├── Request/ │ │ │ │ │ ├── MandateRequest.cs │ │ │ │ │ └── PaymentSpecificParameters/ │ │ │ │ │ ├── PayPalMandateRequest.cs │ │ │ │ │ └── SepaDirectDebitMandateRequest.cs │ │ │ │ └── Response/ │ │ │ │ ├── MandateResponse.cs │ │ │ │ ├── MandateResponseLinks.cs │ │ │ │ ├── MandateStatus.cs │ │ │ │ └── PaymentSpecificParameters/ │ │ │ │ ├── CreditCardMandateResponse.cs │ │ │ │ ├── PayPalMandateResponse.cs │ │ │ │ └── SepaDirectDebitMandateResponse.cs │ │ │ ├── Mode.cs │ │ │ ├── Onboarding/ │ │ │ │ ├── Request/ │ │ │ │ │ ├── OnboardingOrganizationRequest.cs │ │ │ │ │ ├── OnboardingProfileRequest.cs │ │ │ │ │ └── SubmitOnboardingDataRequest.cs │ │ │ │ └── Response/ │ │ │ │ ├── OnboardingStatus.cs │ │ │ │ ├── OnboardingStatusResponse.cs │ │ │ │ └── OnboardingStatusResponseLinks.cs │ │ │ ├── Order/ │ │ │ │ ├── OrderAddressDetails.cs │ │ │ │ ├── Request/ │ │ │ │ │ ├── ManageOrderLines/ │ │ │ │ │ │ ├── ManageOrderLinesAddOperation.cs │ │ │ │ │ │ ├── ManageOrderLinesAddOperationData.cs │ │ │ │ │ │ ├── ManageOrderLinesCancelOperation.cs │ │ │ │ │ │ ├── ManageOrderLinesOperation.cs │ │ │ │ │ │ ├── ManageOrderLinesRequest.cs │ │ │ │ │ │ ├── ManageOrderLinesUpdateOperation.cs │ │ │ │ │ │ ├── ManageOrderLinesUpdateOperationData.cs │ │ │ │ │ │ ├── ManagerOrderLinesCancelOperationData.cs │ │ │ │ │ │ └── OrderLineOperation.cs │ │ │ │ │ ├── OrderLineCancellationRequest.cs │ │ │ │ │ ├── OrderLineDetails.cs │ │ │ │ │ ├── OrderLineDetailsType.cs │ │ │ │ │ ├── OrderLineRequest.cs │ │ │ │ │ ├── OrderLineUpdateRequest.cs │ │ │ │ │ ├── OrderPaymentRequest.cs │ │ │ │ │ ├── OrderRefundRequest.cs │ │ │ │ │ ├── OrderRequest.cs │ │ │ │ │ ├── OrderUpdateRequest.cs │ │ │ │ │ └── PaymentSpecificParameters/ │ │ │ │ │ ├── ApplePaySpecificParameters.cs │ │ │ │ │ ├── BillieSpecificParameters.cs │ │ │ │ │ ├── CreditCardSpecificParameters.cs │ │ │ │ │ ├── GiftcardSpecificParameters.cs │ │ │ │ │ ├── IDealSpecificParameters.cs │ │ │ │ │ ├── KbcSpecificParameters.cs │ │ │ │ │ ├── KlarnaSpecificParameters.cs │ │ │ │ │ ├── OrderPaymentParameters.cs │ │ │ │ │ ├── PaySafeCardSpecificParameters.cs │ │ │ │ │ └── SepaDirectDebitSpecificParameters.cs │ │ │ │ └── Response/ │ │ │ │ ├── OrderEmbeddedResponse.cs │ │ │ │ ├── OrderLineResponse.cs │ │ │ │ ├── OrderLineResponseLinks.cs │ │ │ │ ├── OrderLineStatus.cs │ │ │ │ ├── OrderRefundResponse.cs │ │ │ │ ├── OrderResponse.cs │ │ │ │ ├── OrderResponseLinks.cs │ │ │ │ └── OrderStatus.cs │ │ │ ├── Organization/ │ │ │ │ ├── OrganizationResponse.cs │ │ │ │ ├── OrganizationResponseLinks.cs │ │ │ │ ├── PartnerResponse.cs │ │ │ │ ├── PartnerTypes.cs │ │ │ │ └── UserAgentToken.cs │ │ │ ├── Payment/ │ │ │ │ ├── EntryMode.cs │ │ │ │ ├── Locale.cs │ │ │ │ ├── PaymentAddressDetails.cs │ │ │ │ ├── PaymentLine.cs │ │ │ │ ├── PaymentMethod.cs │ │ │ │ ├── PaymentStatus.cs │ │ │ │ ├── Request/ │ │ │ │ │ ├── PaymentRequest.cs │ │ │ │ │ ├── PaymentRoutingRequest.cs │ │ │ │ │ ├── PaymentSpecificParameters/ │ │ │ │ │ │ ├── ApplePayPaymentRequest.cs │ │ │ │ │ │ ├── BankTransferPaymentRequest.cs │ │ │ │ │ │ ├── CreditCardPaymentRequest.cs │ │ │ │ │ │ ├── GiftcardPaymentRequest.cs │ │ │ │ │ │ ├── IDealPaymentRequest.cs │ │ │ │ │ │ ├── KbcIssuer.cs │ │ │ │ │ │ ├── KbcPaymentRequest.cs │ │ │ │ │ │ ├── PayPalPaymentRequest.cs │ │ │ │ │ │ ├── PaySafeCardPaymentRequest.cs │ │ │ │ │ │ ├── PointOfSalePaymentRequest.cs │ │ │ │ │ │ ├── Przelewy24PaymentRequest.cs │ │ │ │ │ │ └── SepaDirectDebitRequest.cs │ │ │ │ │ └── PaymentUpdateRequest.cs │ │ │ │ ├── Resource.cs │ │ │ │ ├── Response/ │ │ │ │ │ ├── PaymentEmbeddedResponse.cs │ │ │ │ │ ├── PaymentResponse.cs │ │ │ │ │ ├── PaymentResponseLinks.cs │ │ │ │ │ ├── PaymentRoutingResponse.cs │ │ │ │ │ ├── PaymentSpecificParameters/ │ │ │ │ │ │ ├── BancontactPaymentResponse.cs │ │ │ │ │ │ ├── BankTransferPaymentResponse.cs │ │ │ │ │ │ ├── BelfiusPaymentResponse.cs │ │ │ │ │ │ ├── CreditCardPaymentResponse.cs │ │ │ │ │ │ ├── EpsPaymentResponse.cs │ │ │ │ │ │ ├── GiftcardPaymentResponse.cs │ │ │ │ │ │ ├── GiropayPaymentResponse.cs │ │ │ │ │ │ ├── IdealPaymentResponse.cs │ │ │ │ │ │ ├── IngHomePayPaymentResponse.cs │ │ │ │ │ │ ├── KbcPaymentResponse.cs │ │ │ │ │ │ ├── PayPalPaymentResponse.cs │ │ │ │ │ │ ├── PaySafeCardPaymentResponse.cs │ │ │ │ │ │ ├── PointOfSalePaymentResponse.cs │ │ │ │ │ │ ├── SepaDirectDebitResponse.cs │ │ │ │ │ │ └── SofortPaymentResponse.cs │ │ │ │ │ └── QrCode.cs │ │ │ │ ├── RoutingDestination.cs │ │ │ │ └── SequenceType.cs │ │ │ ├── PaymentLink/ │ │ │ │ ├── Request/ │ │ │ │ │ ├── PaymentLinkRequest.cs │ │ │ │ │ └── PaymentLinkUpdateRequest.cs │ │ │ │ └── Response/ │ │ │ │ ├── PaymentLinkResponse.cs │ │ │ │ └── PaymentLinkResponseLinks.cs │ │ │ ├── PaymentMethod/ │ │ │ │ └── Response/ │ │ │ │ ├── FixedPricingResponse.cs │ │ │ │ ├── PaymentMethodResponse.cs │ │ │ │ ├── PaymentMethodResponseImage.cs │ │ │ │ ├── PaymentMethodResponseLinks.cs │ │ │ │ ├── PaymentMethodStatus.cs │ │ │ │ └── PricingResponse.cs │ │ │ ├── Permission/ │ │ │ │ └── Response/ │ │ │ │ ├── PermissionResponse.cs │ │ │ │ └── PermissionResponseLInks.cs │ │ │ ├── Profile/ │ │ │ │ ├── ProfileStatus.cs │ │ │ │ ├── Request/ │ │ │ │ │ └── ProfileRequest.cs │ │ │ │ ├── Response/ │ │ │ │ │ ├── ApiKey.cs │ │ │ │ │ ├── EnableGiftCardIssuerResponse.cs │ │ │ │ │ ├── EnableGiftCardIssuerResponseLinks.cs │ │ │ │ │ ├── EnableGiftCardIssuerStatus.cs │ │ │ │ │ ├── ProfileResponse.cs │ │ │ │ │ └── ProfileResponseLinks.cs │ │ │ │ └── ReviewStatus.cs │ │ │ ├── Refund/ │ │ │ │ ├── Request/ │ │ │ │ │ └── RefundRequest.cs │ │ │ │ ├── Response/ │ │ │ │ │ ├── RefundResponse.cs │ │ │ │ │ ├── RefundResponseLinks.cs │ │ │ │ │ └── RefundStatus.cs │ │ │ │ └── RoutingReversal.cs │ │ │ ├── SalesInvoice/ │ │ │ │ ├── EmailDetails.cs │ │ │ │ ├── PaymentDetails.cs │ │ │ │ ├── PaymentDetailsSource.cs │ │ │ │ ├── PaymentTerm.cs │ │ │ │ ├── Recipient.cs │ │ │ │ ├── RecipientType.cs │ │ │ │ ├── Request/ │ │ │ │ │ ├── SalesInvoiceRequest.cs │ │ │ │ │ └── SalesInvoiceUpdateRequest.cs │ │ │ │ ├── Response/ │ │ │ │ │ ├── SalesInvoiceResponse.cs │ │ │ │ │ └── SalesInvoiceResponseLinks.cs │ │ │ │ ├── SalesInvoiceLine.cs │ │ │ │ └── SalesInvoiceStatus.cs │ │ │ ├── Session/ │ │ │ │ ├── Request/ │ │ │ │ │ └── SessionRequest.cs │ │ │ │ ├── Response/ │ │ │ │ │ ├── SessionResponse.cs │ │ │ │ │ ├── SessionResponseLinks.cs │ │ │ │ │ └── SessionStatus.cs │ │ │ │ ├── SessionLine.cs │ │ │ │ ├── SessionLineRecurringDetails.cs │ │ │ │ └── SessionPaymentDetails.cs │ │ │ ├── Settlement/ │ │ │ │ └── Response/ │ │ │ │ ├── SettlementPeriod.cs │ │ │ │ ├── SettlementPeriodCosts.cs │ │ │ │ ├── SettlementPeriodCostsRate.cs │ │ │ │ ├── SettlementPeriodRevenue.cs │ │ │ │ ├── SettlementResponse.cs │ │ │ │ ├── SettlementResponseLinks.cs │ │ │ │ └── SettlementStatus.cs │ │ │ ├── Shipment/ │ │ │ │ ├── Request/ │ │ │ │ │ ├── ShipmentLineRequest.cs │ │ │ │ │ ├── ShipmentRequest.cs │ │ │ │ │ └── ShipmentUpdateRequest.cs │ │ │ │ ├── Response/ │ │ │ │ │ ├── ShipmentResponse.cs │ │ │ │ │ └── ShipmentResponseLinks.cs │ │ │ │ └── TrackingObject.cs │ │ │ ├── SortDirection.cs │ │ │ ├── StatusReason.cs │ │ │ ├── Subscription/ │ │ │ │ ├── Request/ │ │ │ │ │ ├── SubscriptionRequest.cs │ │ │ │ │ └── SubscriptionUpdateRequest.cs │ │ │ │ └── Response/ │ │ │ │ ├── SubscriptionResponse.cs │ │ │ │ ├── SubscriptionResponseLinks.cs │ │ │ │ └── SubscriptionStatus.cs │ │ │ ├── Terminal/ │ │ │ │ └── Response/ │ │ │ │ ├── TerminalResponse.cs │ │ │ │ └── TerminalResponseLinks.cs │ │ │ ├── TestmodeModel.cs │ │ │ ├── Url/ │ │ │ │ ├── UrlLink.cs │ │ │ │ └── UrlObjectLink.cs │ │ │ ├── VatMode.cs │ │ │ ├── VatScheme.cs │ │ │ ├── VoucherCategory.cs │ │ │ ├── Wallet/ │ │ │ │ ├── Request/ │ │ │ │ │ └── ApplePayPaymentSessionRequest.cs │ │ │ │ └── Response/ │ │ │ │ └── ApplePayPaymentSessionResponse.cs │ │ │ ├── Webhook/ │ │ │ │ ├── Request/ │ │ │ │ │ └── WebhookRequest.cs │ │ │ │ ├── Response/ │ │ │ │ │ └── WebhookResponse.cs │ │ │ │ └── WebhookEventTypes.cs │ │ │ └── WebhookEvent/ │ │ │ └── Response/ │ │ │ ├── FullWebhookEventResponse.cs │ │ │ ├── SimpleWebhookEventResponse.cs │ │ │ └── WebhookEventResponseLinks.cs │ │ ├── Mollie.Api.csproj │ │ └── Options/ │ │ ├── MollieClientOptions.cs │ │ └── MollieOptions.cs │ └── Mollie.Api.AspNet/ │ ├── DependencyInjection.cs │ ├── Mollie.Api.AspNet.csproj │ └── Webhooks/ │ ├── Authorization/ │ │ ├── MollieSignatureEndpointFilter.cs │ │ ├── MollieSignatureFilter.cs │ │ └── MollieSignatureValidator.cs │ ├── ModelBinding/ │ │ ├── FromMollieWebhookAttribute.cs │ │ ├── FromMollieWebhookModelBinder.cs │ │ └── MollieModelBinder.cs │ └── Options/ │ └── MollieWebhookOptions.cs └── tests/ ├── Mollie.Tests.Integration/ │ ├── Api/ │ │ ├── ApiExceptionTests.cs │ │ ├── BalanceTests.cs │ │ ├── CaptureTests.cs │ │ ├── ConnectTests.cs │ │ ├── CustomerTests.cs │ │ ├── MandateTests.cs │ │ ├── OrderTests.cs │ │ ├── PaymentLinkTests.cs │ │ ├── PaymentMethodTests.cs │ │ ├── PaymentTests.cs │ │ ├── ProfileTests.cs │ │ ├── RefundTests.cs │ │ ├── SalesInvoiceTests.cs │ │ ├── SessionTests.cs │ │ ├── ShipmentTests.cs │ │ ├── SubscriptionTests.cs │ │ ├── TerminalTests.cs │ │ ├── WebhookEventTests.cs │ │ └── WebhookTests.cs │ ├── Framework/ │ │ ├── BaseMollieApiTestClass.cs │ │ ├── ConfigurationFactory.cs │ │ └── MollieIntegrationTestHttpRetryPolicies.cs │ ├── Mollie.Tests.Integration.csproj │ ├── Startup.cs │ ├── appsettings.json │ └── xunit.runner.json └── Mollie.Tests.Unit/ ├── Client/ │ ├── BalanceClientTests.cs │ ├── BalanceTransferClientTests.cs │ ├── BaseClientTests.cs │ ├── BaseMollieClientTests.cs │ ├── CapabilityClientTests.cs │ ├── CaptureClientTests.cs │ ├── ChargebackClientTests.cs │ ├── ClientClientTests.cs │ ├── ClientLinkClientTests.cs │ ├── ConnectClientTests.cs │ ├── CustomerClientTests.cs │ ├── InvoiceClientTests.cs │ ├── MandateClientTests.cs │ ├── OnboardingClientTests.cs │ ├── OrderClientTests.cs │ ├── OrganizationClientTests.cs │ ├── PaymentClientTests.cs │ ├── PaymentLinkClientTests.cs │ ├── PaymentMethodClientTests.cs │ ├── PermissionClientTests.cs │ ├── ProfileClientTests.cs │ ├── RefundClientTests.cs │ ├── SalesInvoiceClientTests.cs │ ├── SettlementClientTests.cs │ ├── ShipmentClientTests.cs │ ├── SubscriptionClientTests.cs │ ├── TerminalClientTests.cs │ ├── WalletClientTest.cs │ ├── WebhookClientTests.cs │ └── WebhookEventClientTests.cs ├── DependencyInjectionTests.cs ├── Extensions/ │ └── DictionaryExtensionsTests.cs ├── Framework/ │ ├── AmountConversionTests.cs │ ├── Factories/ │ │ ├── BalanceReportResponseFactoryTests.cs │ │ ├── BalanceTransactionFactoryTests.cs │ │ └── PaymentResponseFactoryTests.cs │ └── JsonConverterServiceTests.cs ├── Models/ │ ├── AmountTests.cs │ └── Payment/ │ └── Request/ │ └── PaymentRequestTests.cs └── Mollie.Tests.Unit.csproj ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # editorconfig.org # top-most EditorConfig file root = true # Default settings: # A newline ending every file # Use 4 spaces as indentation [*] insert_final_newline = true indent_style = space indent_size = 4 trim_trailing_whitespace = true [project.json] indent_size = 2 # Generated code [*{_AssemblyInfo.cs,.notsupported.cs}] generated_code = true # C# files [*.cs] # New line preferences csharp_new_line_before_open_brace = none csharp_new_line_before_else = false csharp_new_line_before_catch = false csharp_new_line_before_finally = false csharp_new_line_before_members_in_object_initializers = false csharp_new_line_before_members_in_anonymous_types = false csharp_new_line_between_query_expression_clauses = false # Indentation preferences csharp_indent_block_contents = true csharp_indent_braces = false csharp_indent_case_contents = true csharp_indent_case_contents_when_block = true csharp_indent_switch_labels = true csharp_indent_labels = one_less_than_current # avoid this. unless absolutely necessary dotnet_style_qualification_for_field = false:suggestion dotnet_style_qualification_for_property = false:suggestion dotnet_style_qualification_for_method = false:suggestion dotnet_style_qualification_for_event = false:suggestion # only use var when it's obvious what the variable type is csharp_style_var_for_built_in_types = false:none csharp_style_var_when_type_is_apparent = false:none csharp_style_var_elsewhere = false:suggestion # use language keywords instead of BCL types dotnet_style_predefined_type_for_locals_parameters_members = true:error dotnet_style_predefined_type_for_member_access = true:error # name all constant fields using PascalCase dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style dotnet_naming_symbols.constant_fields.applicable_kinds = field dotnet_naming_symbols.constant_fields.required_modifiers = const dotnet_naming_style.pascal_case_style.capitalization = pascal_case # Code style defaults csharp_using_directive_placement = outside_namespace:warning dotnet_sort_system_directives_first = true csharp_preserve_single_line_blocks = true csharp_preserve_single_line_statements = false # Expression-level preferences dotnet_style_object_initializer = true:suggestion dotnet_style_collection_initializer = true:suggestion dotnet_style_explicit_tuple_names = true:suggestion dotnet_style_coalesce_expression = true:suggestion dotnet_style_null_propagation = true:suggestion # Expression-bodied members csharp_style_expression_bodied_methods = true:none csharp_style_expression_bodied_constructors = false:none csharp_style_expression_bodied_operators = false:none csharp_style_expression_bodied_properties = true:none csharp_style_expression_bodied_indexers = true:none csharp_style_expression_bodied_accessors = true:none # Pattern matching csharp_style_pattern_matching_over_is_with_cast_check = true:error csharp_style_pattern_matching_over_as_with_null_check = true:error csharp_style_inlined_variable_declaration = true:error # Null checking preferences csharp_style_throw_expression = true:suggestion csharp_style_conditional_delegate_call = true:suggestion # Space preferences csharp_space_after_cast = false csharp_space_after_colon_in_inheritance_clause = true csharp_space_after_comma = true csharp_space_after_dot = false csharp_space_after_keywords_in_control_flow_statements = true csharp_space_after_semicolon_in_for_statement = true csharp_space_around_binary_operators = before_and_after csharp_space_around_declaration_statements = do_not_ignore csharp_space_before_colon_in_inheritance_clause = true csharp_space_before_comma = false csharp_space_before_dot = false csharp_space_before_open_square_brackets = false csharp_space_before_semicolon_in_for_statement = false csharp_space_between_empty_square_brackets = false csharp_space_between_method_call_empty_parameter_list_parentheses = false csharp_space_between_method_call_name_and_opening_parenthesis = false csharp_space_between_method_call_parameter_list_parentheses = false csharp_space_between_method_declaration_empty_parameter_list_parentheses = false csharp_space_between_method_declaration_name_and_open_parenthesis = false csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false # C++ Files [*.{cpp,h,in}] curly_bracket_next_line = true indent_brace_style = Allman # Xml project files [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] indent_size = 2 [*.{csproj,vbproj,proj,nativeproj,locproj}] charset = utf-8 # Xml build files [*.builds] indent_size = 2 # Xml files [*.{xml,stylecop,resx,ruleset}] indent_size = 2 # Xml config files [*.{props,targets,config,nuspec}] indent_size = 2 # YAML config files [*.{yml,yaml}] indent_size = 2 # Shell scripts [*.sh] end_of_line = lf [*.{cmd,bat}] end_of_line = crlf ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto # Custom for Visual Studio *.cs diff=csharp # Standard to msysgit *.doc diff=astextplain *.DOC diff=astextplain *.docx diff=astextplain *.DOCX diff=astextplain *.dot diff=astextplain *.DOT diff=astextplain *.pdf diff=astextplain *.PDF diff=astextplain *.rtf diff=astextplain *.RTF diff=astextplain ================================================ FILE: .github/FUNDING.yml ================================================ github: [Viincenttt] ================================================ FILE: .github/workflows/dotnetcore.yml ================================================ name: Run automated tests on: [push] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup .NET Core uses: actions/setup-dotnet@v4 with: dotnet-version: '10.0.x' - name: Install dependencies run: dotnet restore - name: Build unit test project run: dotnet build tests/Mollie.Tests.Unit --configuration Release --no-restore - name: Run unit tests run: dotnet test tests/Mollie.Tests.Unit --no-restore --verbosity normal - name: Build integration test project run: dotnet build tests/Mollie.Tests.Integration --configuration Release --no-restore - name: Run integration tests run: dotnet test tests/Mollie.Tests.Integration --filter "TestCategory!=LocalIntegrationTests" --no-restore --verbosity normal env: Mollie__ApiKey: ${{ secrets.Mollie__ApiKey }} Mollie__AccessKey: ${{ secrets.Mollie__AccessKey }} Mollie__ClientId: ${{ secrets.Mollie__ClientId }} Mollie__ClientSecret: ${{ secrets.Mollie__ClientSecret }} publish: runs-on: ubuntu-latest needs: build steps: - uses: actions/checkout@v4 - name: Setup .NET Core uses: actions/setup-dotnet@v4 with: dotnet-version: '10.0.x' - name: Install dependencies run: dotnet restore - name: Prepare strong-name signing key shell: bash run: | if [ -z "${{ secrets.MOLLIE_STRONG_NAME_KEY_BASE64 }}" ]; then echo "MOLLIE_STRONG_NAME_KEY_BASE64 secret is not set." exit 1 fi mkdir -p "$RUNNER_TEMP/signing" echo "${{ secrets.MOLLIE_STRONG_NAME_KEY_BASE64 }}" | base64 --decode > "$RUNNER_TEMP/signing/mollie.snk" chmod 600 "$RUNNER_TEMP/signing/mollie.snk" echo "MOLLIE_STRONG_NAME_KEY_FILE=$RUNNER_TEMP/signing/mollie.snk" >> "$GITHUB_ENV" - name: Build Mollie.Api run: dotnet build src/Mollie.Api/Mollie.Api.csproj --configuration Release --no-restore /p:MollieStrongNameKeyFile="$MOLLIE_STRONG_NAME_KEY_FILE" - name: Build Mollie.Api.AspNet run: dotnet build src/Mollie.Api.AspNet/Mollie.Api.AspNet.csproj --configuration Release --no-restore /p:MollieStrongNameKeyFile="$MOLLIE_STRONG_NAME_KEY_FILE" - name: Pack Mollie.Api run: dotnet pack src/Mollie.Api/Mollie.Api.csproj --configuration Release --no-restore --no-build --output ./nupkgs /p:MollieStrongNameKeyFile="$MOLLIE_STRONG_NAME_KEY_FILE" - name: Pack Mollie.Api.AspNet run: dotnet pack src/Mollie.Api.AspNet/Mollie.Api.AspNet.csproj --configuration Release --no-restore --no-build --output ./nupkgs /p:MollieStrongNameKeyFile="$MOLLIE_STRONG_NAME_KEY_FILE" - name: Push to NuGet run: dotnet nuget push ./nupkgs/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # User-specific files *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ build/ bld/ [Bb]in/ [Oo]bj/ # Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # DNX project.lock.json artifacts/ *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opensdf *.sdf *.cachefile # Visual Studio profiler *.psess *.vsp *.vspx *.sap # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # NuGet Packages *.nupkg *.snupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # Windows Azure Build Output csx/ *.build.csdef # Windows Azure Emulator efc/ rfc/ # Windows Store app package directory AppPackages/ # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ [Ss]tyle[Cc]op.* ~$* *~ *.dbmdl *.dbproj.schemaview *.pfx *.snk *.publishsettings node_modules/ orleans.codegen.cs # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe # FAKE - F# Make .fake/ # ========================= # Operating System Files # ========================= # OSX # ========================= .DS_Store .AppleDouble .LSOverride # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk # Windows # ========================= # Windows image file caches Thumbs.db ehthumbs.db # Folder config file Desktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msm *.msp # Windows shortcuts *.lnk # Custom **/.idea/ ================================================ FILE: AGENTS.md ================================================ # Agents Guide — Mollie API Client for .NET This document describes the project structure, conventions, and guidelines for AI agents working in this codebase. --- ## Project Overview This is an open-source .NET library that wraps the [Mollie REST API](https://docs.mollie.com/). It is published as two NuGet packages: | Package | Path | Purpose | |---|---|---| | `Mollie.Api` | `src/Mollie.Api` | Core API client library targeting `netstandard2.0` and `net8.0` | | `Mollie.Api.AspNet` | `src/Mollie.Api.AspNet` | ASP.NET-specific webhook helpers | Tests live under `tests/` and samples under `samples/Mollie.WebApplication.Blazor`. --- ## Solution Structure ``` src/ Mollie.Api/ Client/ # One concrete client class per Mollie API resource Client/Abstract/ # One interface per client Models/ # Request and response record types, grouped by resource JsonConverters/ # Custom System.Text.Json converters Framework/ # Auth, idempotency, retry policies, JSON service Extensions/ # Extension methods (IEnumerable, Dictionary helpers) Options/ # MollieOptions, MollieClientOptions DependencyInjection.cs Mollie.Api.AspNet/ Webhooks/ # Model binders and signature filter tests/ Mollie.Tests.Unit/ Client/ # One test class per client Models/ # Serialisation/deserialisation tests Framework/ Mollie.Tests.Integration/ samples/ Mollie.WebApplication.Blazor/ ``` --- ## Key Conventions ### Language & Target - **C# 12** (`LangVersion` is set to `12`). Use modern language features such as `required` members, primary constructors, collection expressions, and `record` types where appropriate. - The library targets **`netstandard2.0`** (via PolySharp for back-fill) and **`net8.0`**. - Nullable reference types are **enabled** (`enable`). Always annotate nullability correctly. ### Naming | Artifact | Convention | Example | |---|---|---| | Client interface | `I{Resource}Client` | `IPaymentClient` | | Client class | `{Resource}Client` | `PaymentClient` | | Request model | `{Resource}Request` | `PaymentRequest` | | Update request | `{Resource}UpdateRequest` | `PaymentUpdateRequest` | | Response model | `{Resource}Response` | `PaymentResponse` | | List response | `ListResponse<{Resource}Response>` | `ListResponse` | | Test class | `{Resource}ClientTests` | `PaymentClientTests` | ### Client Pattern Every API resource follows this pattern: 1. **Interface** in `Client/Abstract/I{Resource}Client.cs` — inherits `IBaseMollieClient`, all public methods documented with XML `` / `` / `` comments. 2. **Implementation** in `Client/{Resource}Client.cs` — inherits `BaseMollieClient`, implements the interface. 3. **Two constructors** on every concrete client: - `(string apiKey, HttpClient? httpClient = null)` — for manual instantiation. - `[ActivatorUtilitiesConstructor] (MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null)` — used by DI. 4. **Register** the new client pair in `DependencyInjection.cs` via `RegisterMollieApiClient`. ```csharp // Typical method signature public async Task GetFooAsync( string fooId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(fooId), fooId); var queryParameters = BuildQueryParameters(testmode: testmode); return await GetAsync( $"foos/{fooId}{queryParameters.ToQueryString()}", cancellationToken: cancellationToken).ConfigureAwait(false); } ``` Key rules: - Always call `ValidateRequiredUrlParameter` for ID path segments. - Always call `ValidateApiKeyIsOauthAccesstoken()` when a parameter requires an OAuth token (e.g., `profileId`, `testmode`, `applicationFee`). - Always pass `cancellationToken` through and call `.ConfigureAwait(false)` on every `await`. - Use the protected helpers `GetAsync`, `PostAsync`, `PatchAsync`, `DeleteAsync`, `GetListAsync` from `BaseMollieClient` — never call `HttpClient` directly. - Build query strings using the `BuildQueryParameters` helper and the `ToQueryString()` extension method. ### Model Pattern - **Request** and **Response** types are `record` types. - Required fields are annotated with the `required` keyword. - Optional fields are nullable (`string?`, `DateTime?`, etc.). - All properties have XML `` documentation comments. - Use `[JsonPropertyName("...")]` only when the JSON key differs from the C# property name. - Use `[JsonConverter(typeof(RawJsonConverter))]` for `Metadata` fields that hold raw JSON. - JSON is handled by `System.Text.Json` — do **not** add a dependency on `Newtonsoft.Json`. - Response models that implement `IEntity` expose an `Id` property. - Embedded resources are placed in a nested `{Resource}EmbeddedResponse` type annotated with `[JsonPropertyName("_embedded")]`. - HAL-style link objects are placed in a nested `{Resource}ResponseLinks` type annotated with `[JsonPropertyName("_links")]` and typed as `UrlObjectLink`. ### Extensions Use the dictionary/list extension helpers in `Mollie.Api.Extensions` for building query parameters: ```csharp result.AddValueIfNotNullOrEmpty("include", someValue); result.AddValueIfTrue("testmode", testmode); includeList.AddValueIfTrue("details.qrCode", includeQrCode); return includeList.ToIncludeParameter(); ``` --- ## Testing ### Framework & Libraries | Library | Purpose | |---|---| | **xUnit** | Test runner | | **Shouldly** | Fluent assertions (`value.ShouldBe(...)`) | | **RichardSzalay.MockHttp** | Mock `HttpClient` responses | | **Moq** | Mocking dependencies when needed | ### Unit Test Conventions - Every client class has a corresponding `{Resource}ClientTests` class in `tests/Mollie.Tests.Unit/Client/`. - All test classes inherit `BaseClientTests` which provides `CreateMockHttpMessageHandler(...)`. - Follow the **Given / When / Then** (or **Arrange / Act / Assert**) comment pattern within each test. - Tests are `async Task` with the `[Fact]` or `[Theory]` attribute. - Never hit real Mollie endpoints in unit tests — use `MockHttpMessageHandler` to return canned JSON. - Store expected JSON response strings as `private const string` fields in the test class. - Use `mockHttp.VerifyNoOutstandingExpectation()` to assert all expected HTTP calls were made. ```csharp [Fact] public async Task GetFooAsync_WithValidId_ResponseIsDeserializedCorrectly() { // Given const string fooId = "foo_123"; const string jsonResponse = "{ ... }"; var mockHttp = CreateMockHttpMessageHandler( HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}foos/{fooId}", jsonResponse); var client = new FooClient("test_api_key", mockHttp.ToHttpClient()); // When FooResponse result = await client.GetFooAsync(fooId); // Then result.Id.ShouldBe(fooId); mockHttp.VerifyNoOutstandingExpectation(); } ``` --- ## Adding a New API Resource Follow these steps in order: 1. **Response model** — `src/Mollie.Api/Models/{Resource}/Response/{Resource}Response.cs` 2. **Request model** (if applicable) — `src/Mollie.Api/Models/{Resource}/Request/{Resource}Request.cs` 3. **Interface** — `src/Mollie.Api/Client/Abstract/I{Resource}Client.cs` 4. **Implementation** — `src/Mollie.Api/Client/{Resource}Client.cs` 5. **Register in DI** — add `RegisterMollieApiClient` to `DependencyInjection.cs` 6. **Unit tests** — `tests/Mollie.Tests.Unit/Client/{Resource}ClientTests.cs` 7. **Sample page** (optional) — add a Blazor page under `samples/Mollie.WebApplication.Blazor/Pages/{Resource}/` --- ## Authentication - **API key** authentication: string starting with `live_` or `test_`. - **OAuth** (access token) authentication: string starting with `access_`. - The `IMollieSecretManager` abstraction allows custom multi-tenant scenarios. - `BaseMollieClient.ValidateApiKeyIsOauthAccesstoken()` enforces that OAuth-only parameters are not used with a plain API key. --- ## Error Handling - HTTP errors are thrown as `MollieApiException` which contains a `MollieErrorMessage` with `Status`, `Title`, and `Detail`. - Callers should catch `MollieApiException` to handle Mollie-specific API errors. --- ## Idempotency - Every HTTP request automatically gets a random `Idempotency-Key` header (UUID). - Callers can supply a custom key via `client.WithIdempotencyKey("my-key")` which returns an `IDisposable` scope. --- ## Style & Formatting - Braces on the **same line** for namespace, class, and method declarations (Allman is **not** used). - Indentation: **tabs** (4-space visual width as configured in the IDE). - Opening braces for single-statement `if`/`foreach` blocks are optional but generally omitted for single-line bodies. - `var` is used where the type is obvious from the right-hand side. - `ConfigureAwait(false)` on every `await` inside library code. - XML documentation on all public members. ================================================ FILE: Directory.Build.props ================================================ 4.19.0.0 4.19.0.0 4.19.0.0 4.19.0.0 Vincent Kok Vincent Kok Mollie Payment API Mollie;Payment;Payments;Webhook https://github.com/Viincenttt/MollieApi https://github.com/Viincenttt/MollieApi.git git README.md LICENSE portable true true snupkg $(NoWarn);CS1591 $(MOLLIE_STRONG_NAME_KEY_FILE) $(MSBuildThisFileDirectory)mollie.publickey.txt $([System.Text.RegularExpressions.Regex]::Replace($([System.IO.File]::ReadAllText('$(MollieStrongNamePublicKeyFile)')), '[^0-9a-fA-F]', '')) $(MOLLIE_STRONG_NAME_PUBLIC_KEY) true $(MollieStrongNameKeyFile) ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 Vincent Kok Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MollieApi.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29920.165 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mollie.Api", "src/Mollie.Api\Mollie.Api.csproj", "{DBA9DC3A-D562-4D15-A7FB-B0A1DC3E517B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mollie.Tests.Integration", "tests/Mollie.Tests.Integration\Mollie.Tests.Integration.csproj", "{27DA8118-1976-4B2D-B578-BC3EB91FC39F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mollie.Tests.Unit", "tests/Mollie.Tests.Unit\Mollie.Tests.Unit.csproj", "{EA502BEF-EA42-45D0-BCC8-F0C28C7A4C41}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mollie.WebApplication.Blazor", "samples\Mollie.WebApplication.Blazor\Mollie.WebApplication.Blazor.csproj", "{ECE7EA04-09CA-44A4-8CD4-622A4631BF85}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{17397A2E-2BA0-418C-928E-D4E8511A3C67}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{6E02EF59-F625-492C-9950-B8AEF7852D65}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mollie.Api.AspNet", "src\Mollie.Api.AspNet\Mollie.Api.AspNet.csproj", "{8982D24F-AC99-43A1-A429-3BCCC96FA110}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {DBA9DC3A-D562-4D15-A7FB-B0A1DC3E517B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DBA9DC3A-D562-4D15-A7FB-B0A1DC3E517B}.Debug|Any CPU.Build.0 = Debug|Any CPU {DBA9DC3A-D562-4D15-A7FB-B0A1DC3E517B}.Release|Any CPU.ActiveCfg = Release|Any CPU {DBA9DC3A-D562-4D15-A7FB-B0A1DC3E517B}.Release|Any CPU.Build.0 = Release|Any CPU {27DA8118-1976-4B2D-B578-BC3EB91FC39F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {27DA8118-1976-4B2D-B578-BC3EB91FC39F}.Debug|Any CPU.Build.0 = Debug|Any CPU {27DA8118-1976-4B2D-B578-BC3EB91FC39F}.Release|Any CPU.ActiveCfg = Release|Any CPU {27DA8118-1976-4B2D-B578-BC3EB91FC39F}.Release|Any CPU.Build.0 = Release|Any CPU {EA502BEF-EA42-45D0-BCC8-F0C28C7A4C41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EA502BEF-EA42-45D0-BCC8-F0C28C7A4C41}.Debug|Any CPU.Build.0 = Debug|Any CPU {EA502BEF-EA42-45D0-BCC8-F0C28C7A4C41}.Release|Any CPU.ActiveCfg = Release|Any CPU {EA502BEF-EA42-45D0-BCC8-F0C28C7A4C41}.Release|Any CPU.Build.0 = Release|Any CPU {ECE7EA04-09CA-44A4-8CD4-622A4631BF85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ECE7EA04-09CA-44A4-8CD4-622A4631BF85}.Debug|Any CPU.Build.0 = Debug|Any CPU {ECE7EA04-09CA-44A4-8CD4-622A4631BF85}.Release|Any CPU.ActiveCfg = Release|Any CPU {ECE7EA04-09CA-44A4-8CD4-622A4631BF85}.Release|Any CPU.Build.0 = Release|Any CPU {8982D24F-AC99-43A1-A429-3BCCC96FA110}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8982D24F-AC99-43A1-A429-3BCCC96FA110}.Debug|Any CPU.Build.0 = Debug|Any CPU {8982D24F-AC99-43A1-A429-3BCCC96FA110}.Release|Any CPU.ActiveCfg = Release|Any CPU {8982D24F-AC99-43A1-A429-3BCCC96FA110}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {EA1EE69B-285E-4518-8936-DB1278A71882} EndGlobalSection GlobalSection(NestedProjects) = preSolution {27DA8118-1976-4B2D-B578-BC3EB91FC39F} = {17397A2E-2BA0-418C-928E-D4E8511A3C67} {EA502BEF-EA42-45D0-BCC8-F0C28C7A4C41} = {17397A2E-2BA0-418C-928E-D4E8511A3C67} {ECE7EA04-09CA-44A4-8CD4-622A4631BF85} = {6E02EF59-F625-492C-9950-B8AEF7852D65} EndGlobalSection EndGlobal ================================================ FILE: README.md ================================================ # Mollie Api Client for .NET [![NuGet](https://img.shields.io/nuget/v/Mollie.Api.svg)](https://www.nuget.org/packages/Mollie.Api) ![Build](https://github.com/Viincenttt/MollieApi/workflows/Run%20automated%20tests/badge.svg) [![GitHub Repo stars](https://img.shields.io/github/stars/Viincenttt/MollieApi)](https://github.com/Viincenttt/MollieApi/stargazers) [![GitHub contributors](https://img.shields.io/github/contributors/Viincenttt/MollieApi)](https://github.com/Viincenttt/MollieApi/graphs/contributors) [![GitHub last commit](https://img.shields.io/github/last-commit/Viincenttt/MollieApi)](https://github.com/Viincenttt/MollieApi) [![GitHub commit activity](https://img.shields.io/github/commit-activity/m/Viincenttt/MollieApi)](https://github.com/Viincenttt/MollieApi/graphs/commit-activity) [![open issues](https://img.shields.io/github/issues/Viincenttt/MollieApi)](https://github.com/Viincenttt/MollieApi/issues) [![Read the Wiki](https://img.shields.io/badge/docs-Wiki-blue)](https://github.com/Viincenttt/MollieApi/wiki) Easily integrate the [Mollie payment provider](https://www.mollie.com) into your .NET application. Full documentation of this library is available on the [Wiki](https://github.com/Viincenttt/MollieApi/wiki) — including usage examples, API references, and integration tips. Mollie offers excellent [API documentation](https://docs.mollie.com/) that we highly recommend reviewing before using this library. If you encounter any issues or have feature requests, feel free to [open an issue](https://github.com/Viincenttt/MollieApi/issues). > 💬 **Need help with integration?** > I’m happy to assist you with your implementation or questions. Feel free to [connect with me on LinkedIn](https://www.linkedin.com/in/vincent-kok-4aa44211/) — I’d love to help! Have feedback or ideas? Join the [official Mollie Developer Discord](https://discord.gg/Pdy49HxCWZ) or [open an issue](https://github.com/Viincenttt/MollieApi/issues). --- ## 📚 Table of Contents - [Sponsor This Project](#-sponsor-this-project) - [Full documentation](#-full-documentation) - [Getting Started](#-getting-started) - [Dependency Injection](#dependency-injection) - [Manual Instantiation](#manual-instantiation) - [Create a Payment in under a minute](#-create-a-payment-in-under-a-minute) - [Example Project (Blazor)](#-blazor-example-project) - [Supported APIs](#-supported-apis) - [Contributions](#-contributions) - [Supported .NET Versions](#-supported-net-versions) --- ## 💖 Sponsor This Project This project is proudly sponsored by Mollie — thank you for supporting open source and developer tooling! If this library has helped you or saved you time, please consider [sponsoring me on GitHub](https://github.com/sponsors/Viincenttt) as well. Your support helps me keep improving the library and providing integration help to the community! --- ## 📖 Full Documentation Looking for the full API docs, usage examples, and advanced guides? 👉 **Check out the full Wiki here:** ➡️ [https://github.com/Viincenttt/MollieApi/wiki](https://github.com/Viincenttt/MollieApi/wiki) You'll find: - Getting started walkthroughs - All supported APIs and code samples - Best practices for integration --- ## 🛠 Getting started Install via NuGet: ```bash Install-Package Mollie.Api ``` ### Dependency Injection You can register all API client interfaces using the built-in DI extension: ```csharp builder.Services.AddMollieApi(options => { options.ApiKey = builder.Configuration["Mollie:ApiKey"]; options.RetryPolicy = MollieHttpRetryPolicies.TransientHttpErrorRetryPolicy(); }); ``` Each API (e.g. payments, customers, mandates) has its own dedicated API client class and interface: * `IPaymentClient`, `PaymentClient` * `ICustomerClient`, `CustomerClient` * `ISubscriptionClient`, `SubscriptionClient` * `IMandateClient`, `MandateClient` * ... and more After registering via DI, inject the interface you need in your services or controllers. ### Manual Instantiation If you prefer not to use DI, you can manually instantiate a client: ``` csharp using IPaymentClient paymentClient = new PaymentClient("{yourApiKey}", new HttpClient()); ``` If you do not provide a HttpClient, one will be created automatically — in that case, remember to dispose the client properly. ### 🚀 Create a Payment in under a minute Here’s a quick example of how to create an **iDEAL** payment for €100: ```csharp using IPaymentClient paymentClient = new PaymentClient("{yourApiKey}", new HttpClient()); var paymentRequest = new PaymentRequest { Amount = new Amount(Currency.EUR, 100.00m), Description = "The .NET library makes creating payments so easy!", RedirectUrl = "https://github.com/Viincenttt/MollieApi", Method = PaymentMethod.Ideal }; PaymentResponse paymentResponse = await paymentClient.CreatePaymentAsync(paymentRequest); // Redirect your user to the checkout URL string checkoutUrl = paymentResponse.Links.Checkout.Href; ``` ### Webhooks Mollie offers two different webhook systems: - [Classic Webhooks](https://docs.mollie.com/reference/webhooks) - [Next-gen Webhooks (beta)](https://docs.mollie.com/reference/webhooks-new) Both systems are supported through the Mollie.Api.AspNet NuGet package included in this library. Install via NuGet: ```bash Install-Package Mollie.Api.AspNet ``` The Mollie.Api.AspNet NuGet package has built in attributes that automatically parse and validate incoming objects in your ASP.NET application. For example: ```C# [HttpPost("full/specific")] [ServiceFilter(typeof(MollieSignatureFilter))] public Task WebhookWithSpecificType([FromMollieWebhook] FullWebhookEventResponse data) { return Task.FromResult(Ok()); } ``` For more information about webhooks, take a look at the [full webhook documentation](https://github.com/Viincenttt/MollieApi/wiki/01.-Getting-started#webhooks) on the Wiki page. ### 🧪 Blazor Example Project Want to see the library in action? Check out the full-featured .NET Blazor example project, which demonstrates real-world usage of several APIs: * Payments * Payment links * Orders * Customers * Mandates * Subscriptions * Payment Methods * Terminals * Webhooks 🔗 [View the Example Project on GitHub](https://github.com/Viincenttt/MollieApi/tree/development/samples/Mollie.WebApplication.Blazor) > It’s a great starting point if you’re new to Mollie or want to explore advanced scenarios like multi-step checkouts or managing recurring payments. --- ## 📦 Supported API's This library currently supports the following API's: - [Payment API](https://github.com/Viincenttt/MollieApi/wiki/02.-Payment-API) - [PaymentMethod API](https://github.com/Viincenttt/MollieApi/wiki/03.-Payment-method-API) - [PaymentLink API](https://github.com/Viincenttt/MollieApi/wiki/14.-Payment-link-Api) - [Customer API](https://github.com/Viincenttt/MollieApi/wiki/05.-Customer-API) - [Mandate API](https://github.com/Viincenttt/MollieApi/wiki/06.-Mandate-API) - [Subscription API](https://github.com/Viincenttt/MollieApi/wiki/07.-Subscription-API) - [Refund API](https://github.com/Viincenttt/MollieApi/wiki/04.-Refund-API) - [Connect API](https://github.com/Viincenttt/MollieApi/wiki/10.-Connect-Api) - Chargeback API (documentation coming soon) - Invoice API (documentation coming soon) - Permissions API (documentation coming soon) - [Profile API](https://github.com/Viincenttt/MollieApi/wiki/11.-Profile-Api) - [Organizations API](https://github.com/Viincenttt/MollieApi/wiki/09.-Organization-API) - [Order API](https://github.com/Viincenttt/MollieApi/wiki/08.-Order-API) - [Capture API](https://github.com/Viincenttt/MollieApi/wiki/12.-Captures-API) - [Onboarding API](https://github.com/Viincenttt/MollieApi/wiki/13.-Onboarding-Api) - [Balances API](https://github.com/Viincenttt/MollieApi/wiki/15.-Balances-Api) - Terminal API (documentation coming soon) - ClientLink API (documentation coming soon) - Wallet API (documentation coming soon) - Client API (documentation coming soon) - Capability API (documentation coming soon) - [Webhooks API](https://github.com/Viincenttt/MollieApi/wiki/19.-Webhook-Api) - [WebhooksEvents API](https://github.com/Viincenttt/MollieApi/wiki/20.-Webhook-Api) - [Balance transfer API](https://github.com/Viincenttt/MollieApi/wiki/21.-Balance-transfer-Api) --- ## 🤝 Contributions Spotted a bug or want to add a new feature? Contributions are welcome! Please target the latest `development` branch and include a clear description of your changes. --- ## ✅ Supported .NET Versions This library targets [.NET Standard 2.0](https://docs.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0), making it compatible with a wide range of platforms: | .NET implementation | Version support | | ------------- | ------------- | | .NET and .NET Core | 2.0, 2.1, 2.2, 3.0, 3.1, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0 | | .NET Framework | 4.6.1, 4.6.2, 4.7, 4.7.1, 4.7.2, 4.8, 4.8.1 | | Mono | 5.4, 6.4 | | Universal Windows Platform | 10.0.16299, TBD | | Xamarin.iOS | 10.14, 12.16 | | Xamarin.Mac | 3.8, 5.16 | | Xamarin.Android | 8.0, 10.0 | | Unity | 2018.1 | > ⚠️ Note: This library uses the required keyword in some model classes. Your project must target **C# 11 or higher**. ================================================ FILE: mollie.publickey.txt ================================================ 002400000480000014020000060200000024000052534131001000000100010015769730260dcfc1911c5d3c54057de5037b99dc84a12ae40ee60b78a7068d46bf356eb20795f859c31dee9603c911573b823aac81d589b4f6bd3f7ad10635d1f7a13c0fb37a679e2ee582408c059fc644c3d49da3023adba8096cfd8921db2c159d0e0ee9b1230cd032ca5366bbb249c0aa9a1843b1769d9732a00e07575c47c525d9a76df3f25c0bd44cdf3a8e98f3618dd9d2d4c332756e7f374cae03e3b1f932e5b2a7cc2042a8b526033b20ffcaa38cbfc29d0c675678d09b2f9edef74767607ebffef5046c3b62e70427754a2fb8790d387dbd0acbba7f587f55716ff50099e9252511eec42260cf39155e1efaea273e5ddcd3051b08c089fbf577cc8c33da86c908d49067cf40f90fbbad68a42dfccc72fec62b43815877c6732e0c7957d044e167bc2848852f8eaa5e1772f735fb9694ab74c9f2ea1fc49de061e3531d6d13dcaad4256f9497cf2e1383d9134d007b058668b1bab9173e146d064a5231eec739a12462a0fbef1a46e2b812a24c6bbc746227bdb390bfed1e0e3d54aecd55ea42249f197664ab20f35a597b53a7f0dc82b03f8ac1be78cab67830a67e30bb514e426355f172d9d56f02bf6d6c4c61cbc650dcbcff74ee129e56ecfdae9bdfd544f55aaee2eb7971b959990f5907ccbeeb14c96817b3d9609e7a529c41560a02a42145bf130be56c047f2bef0eee6f47a6ca8e2c863bda70674c2f43a3 ================================================ FILE: samples/Mollie.WebApplication.Blazor/App.razor ================================================  Not found

Sorry, there's nothing at this address.

================================================ FILE: samples/Mollie.WebApplication.Blazor/Framework/StaticStringListBuilder.cs ================================================ using System.Reflection; namespace Mollie.WebApplication.Blazor.Framework; public static class StaticStringListBuilder { public static IEnumerable GetStaticStringList(Type type) { foreach (FieldInfo fieldInfo in type.GetFields(BindingFlags.Static | BindingFlags.Public)) { string? value = fieldInfo.GetValue(null)?.ToString(); if (value != null) { yield return value; } } } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Framework/Validators/DecimalPlacesAttribute.cs ================================================ using System.ComponentModel.DataAnnotations; using System.Globalization; namespace Mollie.WebApplication.Blazor.Framework.Validators; public class DecimalPlacesAttribute : ValidationAttribute { private int _decimalPlaces { get; } public DecimalPlacesAttribute(int decimalPlaces) { _decimalPlaces = decimalPlaces; } protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { if (value == null) { return new ValidationResult("Value is null"); } decimal amount = (decimal)value; string text = amount.ToString(CultureInfo.InvariantCulture); int dotIndex = text.IndexOf('.'); var decimals = text.Length - dotIndex - 1; var places = _decimalPlaces switch { 0 => "without decimal places", 1 => "with one decimal place", _ => $"with {_decimalPlaces} decimal places" }; return dotIndex < 0 || dotIndex != text.LastIndexOf('.') || decimals != _decimalPlaces ? new ValidationResult(ErrorMessage ?? $"Please enter an amount {places}") : ValidationResult.Success; } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Framework/Validators/StaticStringListAttribute.cs ================================================ using System.ComponentModel.DataAnnotations; using System.Reflection; namespace Mollie.WebApplication.Blazor.Framework.Validators; public class StaticStringListAttribute : ValidationAttribute { private readonly Type _staticClass; public StaticStringListAttribute(Type staticClass) { _staticClass = staticClass; } protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { IEnumerable validValues = _staticClass .GetFields(BindingFlags.Static | BindingFlags.Public) .Select(x => x.GetValue(null)?.ToString()); if (validValues.Contains(value)) { return ValidationResult.Success; } return new ValidationResult($"The value \"{value}\" is invalid"); } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Models/Customer/CreateCustomerModel.cs ================================================ using System.ComponentModel.DataAnnotations; namespace Mollie.WebApplication.Blazor.Models.Customer; public class CreateCustomerModel { [Required] public required string Name { get; set; } [EmailAddress] public required string Email { get; set; } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Models/Mandate/CreateMandateModel.cs ================================================ using System.ComponentModel.DataAnnotations; namespace Mollie.WebApplication.Blazor.Models.Mandate; public class CreateMandateModel { [Required] public required string ConsumerName { get; set; } [Required] public required string ConsumerAccount { get; set; } [Required] public required string ConsumerBic { get; set; } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Models/Order/CreateOrderBillingAddressModel.cs ================================================ using System.ComponentModel.DataAnnotations; namespace Mollie.WebApplication.Blazor.Models.Order; public class CreateOrderBillingAddressModel { [Required] public required string GivenName { get; set; } [Required] public required string FamilyName { get; set; } [Required] [EmailAddress] public required string Email { get; set; } [Required] public required string StreetAndNumber { get; set; } [Required] public required string City { get; set; } [Required] [MaxLength(2)] public required string Country { get; set; } [Required] public required string PostalCode { get; set; } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Models/Order/CreateOrderLineModel.cs ================================================ using System.ComponentModel.DataAnnotations; using Mollie.WebApplication.Blazor.Framework.Validators; namespace Mollie.WebApplication.Blazor.Models.Order; public class CreateOrderLineModel { [Required] public string Name { get; set; } = string.Empty; [Required] [Range(1, 100, ErrorMessage = "Please enter a quantity between 0.01 and 1000")] public int Quantity { get; set; } [Required] [Range(0.01, 10000, ErrorMessage = "Please enter a unit price between 0.01 and 10000")] [DecimalPlaces(2)] public decimal UnitPrice { get; set; } public decimal TotalAmount { get; set; } [Range(0.01, 100, ErrorMessage = "Please enter a vat rate between 0.01 and 100")] [DecimalPlaces(2)] public decimal VatRate { get; set; } public decimal VatAmount { get; set; } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Models/Order/CreateOrderModel.cs ================================================ using System.ComponentModel.DataAnnotations; using Mollie.Api.Models; using Mollie.WebApplication.Blazor.Framework.Validators; namespace Mollie.WebApplication.Blazor.Models.Order; public class CreateOrderModel { [Required] public string? OrderNumber { get; set; } [Required] public string? Locale { get; set; } [Required] public decimal? Amount { get; set; } [Required] [StaticStringList(typeof(Currency))] public required string Currency { get; set; } [Required] [Url] public required string RedirectUrl { get; set; } public List? Lines { get; set; } = new(); public required CreateOrderBillingAddressModel BillingAddress { get; set; } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Models/Payment/CreatePaymentModel.cs ================================================ using System.ComponentModel.DataAnnotations; using Mollie.Api.Models; using Mollie.WebApplication.Blazor.Framework.Validators; namespace Mollie.WebApplication.Blazor.Models.Payment; public class CreatePaymentModel { [Required] [Range(0.01, 1000, ErrorMessage = "Please enter an amount between 0.01 and 1000")] [DecimalPlaces(2)] public required decimal Amount { get; set; } [Required] [StaticStringList(typeof(Currency))] public required string Currency { get; set; } [Required] [Url] public required string RedirectUrl { get; set; } [Url] public string? WebhookUrl { get; set; } [Required] public required string Description { get; set; } [Required] public required string SequenceType { get; set; } public string? CustomerId { get; set; } public string? MandateId { get; set; } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Models/PaymentLink/CreatePaymentModel.cs ================================================ using System.ComponentModel.DataAnnotations; using Mollie.Api.Models; using Mollie.WebApplication.Blazor.Framework.Validators; namespace Mollie.WebApplication.Blazor.Models.PaymentLink; public class CreatePaymentLinkModel { [Required] [Range(0.01, 1000, ErrorMessage = "Please enter an amount between 0.01 and 1000")] [DecimalPlaces(2)] public required decimal Amount { get; set; } [Required] [StaticStringList(typeof(Currency))] public required string Currency { get; set; } [Required] [Url] public required string RedirectUrl { get; set; } [Url] public string? WebhookUrl { get; set; } [Required] public required string Description { get; set; } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Models/Subscription/CreateSubscriptionModel.cs ================================================ using System.ComponentModel.DataAnnotations; using Mollie.Api.Models; using Mollie.WebApplication.Blazor.Framework.Validators; namespace Mollie.WebApplication.Blazor.Models.Subscription; public class CreateSubscriptionModel { [Required] [Range(0.01, 1000, ErrorMessage = "Please enter an amount between 0.01 and 1000")] [DecimalPlaces(2)] public required decimal Amount { get; set; } [Required] [StaticStringList(typeof(Currency))] public required string Currency { get; set; } [Range(1, 10)] public int? Times { get; set; } [Range(1, 20, ErrorMessage = "Please enter a interval number between 1 and 20")] [Required] [Display(Name = "Interval amount")] public int? IntervalAmount { get; set; } [Required] [Display(Name = "Interval period")] public required IntervalPeriod IntervalPeriod { get; set; } [Required] public required string Description { get; set; } public string? MandateId { get; set; } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Models/Subscription/IntervalPeriod.cs ================================================ namespace Mollie.WebApplication.Blazor.Models.Subscription; public enum IntervalPeriod { Months, Weeks, Days } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Models/Webhook/CreateWebhookModel.cs ================================================ using System.ComponentModel.DataAnnotations; namespace Mollie.WebApplication.Blazor.Models.Webhook; public class CreateWebhookModel { [Required] public required string Name { get; set; } [Required] [Url] public required string Url { get; set; } public required List EventTypes { get; set; } = new(); public bool Testmode { get; set; } = true; } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Mollie.WebApplication.Blazor.csproj ================================================ net10.0 enable enable 51758f49-d7ec-4044-8cd9-95cf49d0f3cf ================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/Customer/Create.razor ================================================ @page "/customer/create" @using Mollie.WebApplication.Blazor.Models.Customer @using Mollie.Api.Client @using Mollie.Api.Models.Customer.Request @inject ICustomerClient CustomerClient @inject NavigationManager NavigationManager

Create new customer

@code { private MollieApiException? _apiException; private CreateCustomerModel _customer = new() { Name = "Customer name", Email = "customer@customer.customer" }; private async Task OnSave() { try { _apiException = null; await CustomerClient.CreateCustomerAsync(new CustomerRequest() { Name = _customer.Name, Email = _customer.Email }); NavigationManager.NavigateTo("/customer/overview"); } catch (MollieApiException e) { _apiException = e; } } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/Customer/Overview.razor ================================================ @page "/customer/overview" @using Mollie.Api.Models.Customer.Response @using Mollie.Api.Models.List.Response @inject ICustomerClient CustomerClient

Customers

@if (_customers == null) {

Loading...

} else { @foreach (CustomerResponse customer in _customers.Items) { }
# Date created Name Email Locale Actions
@customer.Id @customer.CreatedAt @customer.Name @customer.Email @customer.Locale View mandates View subscriptions
} @code { [Parameter] [SupplyParameterFromQuery] public string? Url { get; set; } private ListResponse? _customers; protected override async Task OnParametersSetAsync() { await LoadData(); } private async Task LoadData() { if (string.IsNullOrEmpty(Url)) { _customers = await CustomerClient.GetCustomerListAsync(); } else { _customers = await CustomerClient.GetCustomerListAsync(new UrlObjectLink>() { Href = Url, Type = "application/json" }); } } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/Error.cshtml ================================================ @page @model Mollie.WebApplication.Blazor.Pages.ErrorModel Error

Error.

An error occurred while processing your request.

@if (Model.ShowRequestId) {

Request ID: @Model.RequestId

}

Development Mode

Swapping to the Development environment displays detailed information about the error that occurred.

The Development environment shouldn't be enabled for deployed applications. It can result in displaying sensitive information from exceptions to end users. For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development and restarting the app.

================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/Error.cshtml.cs ================================================ using System.Diagnostics; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace Mollie.WebApplication.Blazor.Pages; [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] [IgnoreAntiforgeryToken] public class ErrorModel : PageModel { public string? RequestId { get; set; } public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); public void OnGet() { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/Index.razor ================================================ @page "/" Mollie Example Project

Mollie API Demo application

Welcome to the Mollie API demo application. This is a sample application that demonstrates how to create payments, customers, subscriptions and much more.

Create new payment Create new order

================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/Mandate/Create.razor ================================================ @page "/customer/{customerId}/mandate/create" @using Mollie.WebApplication.Blazor.Models.Mandate @using Mollie.Api.Client @using Mollie.Api.Models.Mandate.Request @using Mollie.Api.Models.Mandate.Request.PaymentSpecificParameters @inject IMandateClient MandateClient @inject NavigationManager NavigationManager

Create new mandate

A mandate is used to get started with recurring payments. A mandate is similar to a regular payment, but the customer is shown information about your organization, and the customer needs to complete the payment with the account or card that will be used for recurring charges in the future.

After the first payment is completed succesfully, the customer’s account or card will immediately be chargeable on-demand, or periodically through subscriptions.

@code { private MollieApiException? _apiException; [Parameter] public required string CustomerId { get; set; } private CreateMandateModel _mandate = new() { ConsumerName = "Consumer name", ConsumerAccount = string.Empty, ConsumerBic = string.Empty }; private async Task OnSave() { try { _apiException = null; await MandateClient.CreateMandateAsync(CustomerId, new SepaDirectDebitMandateRequest { Method = PaymentMethod.DirectDebit, ConsumerName = _mandate.ConsumerName, ConsumerAccount = _mandate.ConsumerAccount, ConsumerBic = _mandate.ConsumerBic }); NavigationManager.NavigateTo($"/customer/{CustomerId}/mandate/overview"); } catch (MollieApiException e) { _apiException = e; } } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/Mandate/Overview.razor ================================================ @page "/customer/{customerId}/mandate/overview" @using Mollie.Api.Models.List.Response @using Mollie.Api.Models.Mandate.Response @inject IMandateClient MandateClient

Mandates

@if (_mandates == null) {

Loading...

} else { @foreach (MandateResponse mandate in _mandates.Items) { }
# Date created Status Actions
@mandate.Id @mandate.CreatedAt @mandate.Status  
} @code { [Parameter] public required string CustomerId { get; set; } [Parameter] [SupplyParameterFromQuery] public string? Url { get; set; } private ListResponse? _mandates; protected override async Task OnParametersSetAsync() { await LoadData(); } private async Task LoadData() { if (string.IsNullOrEmpty(Url)) { _mandates = await MandateClient.GetMandateListAsync(CustomerId); } else { _mandates = await MandateClient.GetMandateListAsync(new UrlObjectLink>() { Href = Url, Type = "application/json" }); } } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/Order/Components/OrderAddressEditor.razor ================================================ @using Mollie.WebApplication.Blazor.Models.Order
@code { [Parameter, EditorRequired] public required CreateOrderBillingAddressModel Address { get; set; } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/Order/Components/OrderLineEditor.razor ================================================ @using Mollie.WebApplication.Blazor.Models.Order @foreach (CreateOrderLineModel orderLine in OrderLines) { }
Name Quantity Unit price Total amount Vat rate Vat amount Actions
@orderLine.Name @orderLine.Quantity € @orderLine.UnitPrice € @orderLine.TotalAmount @orderLine.VatRate % € @orderLine.VatAmount
@code { [Parameter, EditorRequired] public required IList OrderLines { get; set; } private CreateOrderLineModel _newOrderLineModel = new (); private void OnAddOrderLine() { decimal totalAmount = _newOrderLineModel.Quantity * _newOrderLineModel.UnitPrice; decimal vatAmount = (_newOrderLineModel.VatRate / (100 + _newOrderLineModel.VatRate)) * totalAmount; _newOrderLineModel.VatAmount = Math.Round(vatAmount, 2); _newOrderLineModel.TotalAmount = totalAmount; OrderLines.Add(_newOrderLineModel); _newOrderLineModel = new CreateOrderLineModel(); } private void OnRemoveOrderLine(CreateOrderLineModel orderLine) { OrderLines.Remove(orderLine); } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/Order/Create.razor ================================================ @page "/order/create" @using Mollie.WebApplication.Blazor.Pages.Order.Components @using Mollie.WebApplication.Blazor.Models.Order @using System.Globalization @using Mollie.Api.Client @using Mollie.Api.Models.Order.Request @inject IOrderClient OrderClient @inject NavigationManager NavigationManager

Create new order

@foreach (string locale in StaticStringListBuilder.GetStaticStringList(typeof(Locale))) { }
@foreach (string currency in StaticStringListBuilder.GetStaticStringList(typeof(Currency))) { }
Billing address
Order lines
@code { private MollieApiException? _apiException; private readonly CreateOrderModel _order = new() { OrderNumber = "Order number", Locale = Locale.nl_NL, Amount = 100m, Currency = "EUR", RedirectUrl = "https://www.mollie.com/", Lines = new List { new() { Name = "Chocolates", UnitPrice = 100.00m, Quantity = 1, TotalAmount = 100.00m, VatRate = 21.00m, VatAmount = 17.36m } }, BillingAddress = new CreateOrderBillingAddressModel { City = "Amsterdam", Country = "NL", Email = "customer@customer.customer", FamilyName = "Mollie", GivenName = "Mollie", PostalCode = "1015 CW", StreetAndNumber = "Keizersgracht 126" } }; private async Task OnSave() { try { _apiException = null; await OrderClient.CreateOrderAsync(new OrderRequest { OrderNumber = _order.OrderNumber!, Locale = _order.Locale!, Amount = new Amount(_order.Currency, _order.Lines!.Sum(lines => lines.TotalAmount)), RedirectUrl = _order.RedirectUrl, BillingAddress = new OrderAddressDetails { GivenName = _order.BillingAddress.GivenName, FamilyName = _order.BillingAddress.FamilyName, Email = _order.BillingAddress.Email, StreetAndNumber = _order.BillingAddress.StreetAndNumber, PostalCode = _order.BillingAddress.PostalCode, City = _order.BillingAddress.City, Country = _order.BillingAddress.Country }, Lines = _order.Lines!.Select(line => new OrderLineRequest { Name = line.Name, Quantity = line.Quantity, UnitPrice = new Amount(_order.Currency, line.UnitPrice), TotalAmount = new Amount(_order.Currency, line.TotalAmount), VatRate = line.VatRate.ToString(CultureInfo.InvariantCulture), VatAmount = new Amount(_order.Currency, line.VatAmount) }) }); NavigationManager.NavigateTo("/order/overview"); } catch (MollieApiException e) { _apiException = e; } } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/Order/Overview.razor ================================================ @page "/order/overview" @using Mollie.Api.Models.Order.Response @using Mollie.Api.Models.List.Response @inject IOrderClient OrderClient

Orders

@if (_orders == null) {

Loading...

} else { @foreach (OrderResponse order in _orders.Items) { }
# Date created Amount Status Method Metadata Actions
@order.Id @order.CreatedAt @order.Amount.ToString() @order.Status @order.Method @order.Metadata @if (order.Status == OrderStatus.Created && order.Links.Checkout != null) { Pay }
} @code { [Parameter] [SupplyParameterFromQuery] public string? Url { get; set; } private ListResponse? _orders; protected override async Task OnParametersSetAsync() { await LoadData(); } private async Task LoadData() { if (string.IsNullOrEmpty(Url)) { _orders = await OrderClient.GetOrderListAsync(); } else { _orders = await OrderClient.GetOrderListAsync(new UrlObjectLink>() { Href = Url, Type = "application/json" }); } } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/Payment/Create.razor ================================================ @page "/payment/create" @using Mollie.Api.Models.Payment.Request @using Mollie.WebApplication.Blazor.Models.Payment @using Mollie.Api.Client @inject IPaymentClient PaymentClient @inject NavigationManager NavigationManager

Create new payment

@foreach (string currency in StaticStringListBuilder.GetStaticStringList(typeof(Currency))) { }
@foreach (string sequenceType in StaticStringListBuilder.GetStaticStringList(typeof(SequenceType))) { }
@if (_model.SequenceType == SequenceType.First || _model.SequenceType == SequenceType.Recurring) {
@if (_model.SequenceType == SequenceType.Recurring) {
} }
@code { private MollieApiException? _apiException; private CreatePaymentModel _model = new() { Amount = 10.00m, Currency = "EUR", RedirectUrl = "https://www.mollie.com/", WebhookUrl = null, Description = "A payment from the example application", SequenceType = SequenceType.OneOff }; private async Task OnSave() { try { _apiException = null; await PaymentClient.CreatePaymentAsync(new PaymentRequest { Amount = new Amount(_model.Currency, _model.Amount), RedirectUrl = _model.RedirectUrl, WebhookUrl = _model.WebhookUrl, Description = _model.Description, CustomerId = _model.CustomerId, SequenceType = _model.SequenceType, MandateId = _model.MandateId }); NavigationManager.NavigateTo("/payment/overview"); } catch (MollieApiException e) { _apiException = e; } } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/Payment/Overview.razor ================================================ @page "/payment/overview" @using Mollie.Api.Models.List.Response @inject IPaymentClient PaymentClient

Payments

@if (_payments == null) {

Loading...

} else { @foreach (PaymentResponse payment in _payments.Items) { }
# Date created Amount Status Method Metadata Actions
@payment.Id @payment.CreatedAt @payment.Amount.ToString() @payment.Status @payment.Method @payment.Metadata @{ string? checkoutUrl = GetCheckoutUrl(payment); } @if (checkoutUrl != null) { Pay }
} @code { [Parameter] [SupplyParameterFromQuery] public string? Url { get; set; } private ListResponse? _payments; protected override async Task OnParametersSetAsync() { await LoadData(); } private async Task LoadData() { if (string.IsNullOrEmpty(Url)) { _payments = await PaymentClient.GetPaymentListAsync(); } else { _payments = await PaymentClient.GetPaymentListAsync(new UrlObjectLink>() { Href = Url, Type = "application/json" }); } } private string? GetCheckoutUrl(PaymentResponse payment) { if (payment.Status != PaymentStatus.Open && payment.Status != PaymentStatus.Pending) { return null; } return payment.Links.Checkout?.Href ?? payment.Links.ChangePaymentState?.Href; } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/PaymentLink/Create.razor ================================================ @page "/paymentlink/create" @using Mollie.Api.Models.PaymentLink.Request @using Mollie.WebApplication.Blazor.Models.PaymentLink @using Mollie.Api.Client @inject IPaymentLinkClient PaymentLinkClient @inject NavigationManager NavigationManager

Create new payment link

@foreach (string currency in StaticStringListBuilder.GetStaticStringList(typeof(Currency))) { }
@code { private MollieApiException? _apiException; private CreatePaymentLinkModel _model = new() { Amount = 10.00m, Currency = "EUR", RedirectUrl = "https://www.mollie.com/", WebhookUrl = null, Description = "A payment link from the example application" }; private async Task OnSave() { try { _apiException = null; await PaymentLinkClient.CreatePaymentLinkAsync(new PaymentLinkRequest { Amount = new Amount(_model.Currency, _model.Amount), RedirectUrl = _model.RedirectUrl, WebhookUrl = _model.WebhookUrl, Description = _model.Description }); NavigationManager.NavigateTo("/paymentlink/overview"); } catch (MollieApiException e) { _apiException = e; } } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/PaymentLink/Overview.razor ================================================ @page "/paymentlink/overview" @using Mollie.Api.Models.List.Response @using Mollie.Api.Models.PaymentLink.Response @inject IPaymentLinkClient PaymentClient

Payment Links

@if (_paymentLinks == null) {

Loading...

} else { @foreach (PaymentLinkResponse paymentLink in _paymentLinks.Items) { }
# Date created Amount Actions
@paymentLink.Id @paymentLink.CreatedAt @paymentLink.Amount?.ToString() ?? @paymentLink.MinimumAmount?.ToString() Pay
} @code { [Parameter] [SupplyParameterFromQuery] public string? Url { get; set; } private ListResponse? _paymentLinks; protected override async Task OnParametersSetAsync() { await LoadData(); } private async Task LoadData() { if (string.IsNullOrEmpty(Url)) { _paymentLinks = await PaymentClient.GetPaymentLinkListAsync(); } else { _paymentLinks = await PaymentClient.GetPaymentLinkListAsync(new UrlObjectLink>() { Href = Url, Type = "application/json" }); } } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/PaymentMethod/Overview.razor ================================================ @page "/payment-method/overview" @using Mollie.Api.Models.List.Response @using Mollie.Api.Models.PaymentMethod.Response @inject IPaymentMethodClient PaymentMethodClient

Payment methods

@if (_paymentMethods == null) {

Loading...

} else { @foreach (PaymentMethodResponse paymentMethod in _paymentMethods.Items) { }
Description Size1x Size2x Svg
@paymentMethod.Description @paymentMethod.Description @paymentMethod.Description @paymentMethod.Description
} @code { private ListResponse? _paymentMethods; protected override async Task OnInitializedAsync() { _paymentMethods = await PaymentMethodClient.GetPaymentMethodListAsync(); } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/Subscription/Create.razor ================================================ @page "/customer/{customerId}/subscription/create" @using Mollie.WebApplication.Blazor.Models.Subscription @using Mollie.Api.Client @using Mollie.Api.Models.Subscription.Request @inject ISubscriptionClient SubscriptionClient @inject NavigationManager NavigationManager

Create new subscription

@foreach (string currency in StaticStringListBuilder.GetStaticStringList(typeof(Currency))) { }
@foreach (string intervalPeriod in Enum.GetNames(typeof(IntervalPeriod))) { }
@code { private MollieApiException? _apiException; [Parameter] public required string CustomerId { get; set; } private CreateSubscriptionModel _model = new() { Amount = 10.00m, Currency = Currency.EUR, IntervalPeriod = IntervalPeriod.Days, Times = 5, IntervalAmount = 2, Description = "A subscription created by the example application", }; private async Task OnSave() { try { _apiException = null; await SubscriptionClient.CreateSubscriptionAsync(CustomerId, new SubscriptionRequest { Amount = new Amount(_model.Currency, _model.Amount), Interval = $"{_model.IntervalAmount} {_model.IntervalPeriod.ToString().ToLower()}", Times = _model.Times, Description = _model.Description, MandateId = _model.MandateId }); NavigationManager.NavigateTo($"/customer/{CustomerId}/subscription/overview"); } catch (MollieApiException e) { _apiException = e; } } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/Subscription/Overview.razor ================================================ @page "/customer/{customerId}/subscription/overview" @using Mollie.Api.Models.List.Response @using Mollie.Api.Models.Subscription.Response @inject ISubscriptionClient SubscriptionClient

Subscriptions

@if (_subscriptions == null) {

Loading...

} else { @foreach (SubscriptionResponse subscription in _subscriptions.Items) { }
# Description MandateId Date created Amount Mode Status
@subscription.Id @subscription.Description @subscription.MandateId @subscription.CreatedAt @subscription.Amount.ToString() @subscription.Mode @subscription.Status
} @code { [Parameter] public required string CustomerId { get; set; } [Parameter] [SupplyParameterFromQuery] public string? Url { get; set; } private ListResponse? _subscriptions; protected override async Task OnParametersSetAsync() { await LoadData(); } private async Task LoadData() { if (string.IsNullOrEmpty(Url)) { _subscriptions = await SubscriptionClient.GetSubscriptionListAsync(CustomerId); } else { _subscriptions = await SubscriptionClient.GetSubscriptionListAsync(new UrlObjectLink>() { Href = Url, Type = "application/json" }); } } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/Terminal/Overview.razor ================================================ @page "/terminal/overview" @using Mollie.Api.Models.List.Response @using Mollie.Api.Models.Terminal.Response @inject ITerminalClient TerminalClient

Terminals

@if (_terminals == null) {

Loading...

} else { @foreach (TerminalResponse terminal in _terminals.Items) { }
# Date created Status Brand Model Serialnumber Currency
@terminal.Id @terminal.CreatedAt @terminal.Status @terminal.Brand @terminal.Model @terminal.SerialNumber @terminal.Currency
} @code { [Parameter] [SupplyParameterFromQuery] public string? Url { get; set; } private ListResponse? _terminals; protected override async Task OnParametersSetAsync() { await LoadData(); } private async Task LoadData() { if (string.IsNullOrEmpty(Url)) { _terminals = await TerminalClient.GetTerminalListAsync(); } else { _terminals = await TerminalClient.GetTerminalListAsync(new UrlObjectLink>() { Href = Url, Type = "application/json" }); } } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/Webhook/Components/EventTypeEditor.razor ================================================ @using Mollie.Api.Models.Webhook @foreach (string eventType in EventTypes) { }
Event Actions
@eventType
@foreach (string eventType in StaticStringListBuilder.GetStaticStringList(typeof(WebhookEventTypes))) { }
@code { [Parameter, EditorRequired] public required IList EventTypes { get; set; } private string? _selectedEventType = WebhookEventTypes.PaymentLinkPaid; private void OnAddEventType() { if (_selectedEventType == null) { return; } if (EventTypes.Any(x => x == _selectedEventType)) { return; } EventTypes.Add(_selectedEventType); } private void OnRemoveEventType(string eventType) { EventTypes.Remove(eventType); } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/Webhook/Create.razor ================================================ @page "/webhook/create" @using Mollie.WebApplication.Blazor.Models.Webhook @using Mollie.Api.Models.Webhook.Request @using Mollie.Api.Models.Webhook @using Mollie.Api.Client @using Mollie.WebApplication.Blazor.Pages.Webhook.Components @inject IWebhookClient WebhookClient @inject NavigationManager NavigationManager

Create new webhook

@code { private MollieApiException? _apiException; private CreateWebhookModel _model = new() { Name = "Test webhook", Url = "https://example.com/webhook", EventTypes = [WebhookEventTypes.PaymentLinkPaid], Testmode = true }; private async Task OnSave() { try { _apiException = null; await WebhookClient.CreateWebhookAsync(new WebhookRequest { Name = _model.Name, Url = _model.Url, EventTypes = _model.EventTypes, Testmode = _model.Testmode }); NavigationManager.NavigateTo("/webhook/overview"); } catch (MollieApiException e) { _apiException = e; } } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/Webhook/Overview.razor ================================================ @page "/webhook/overview" @using Mollie.Api.Client @using Mollie.Api.Models.List.Response @using Mollie.Api.Models.Webhook.Response @inject IWebhookClient WebhookClient

Webhooks

@if (_webhooks == null) {

Loading...

} else { @foreach (WebhookResponse webhook in _webhooks.Items) { }
# Name Date created Status Event types Url Actions
@webhook.Id @webhook.Name @webhook.CreatedAt @webhook.Status @foreach(string eventType in webhook.EventTypes) { @eventType } @webhook.Url
} @code { [Parameter] [SupplyParameterFromQuery] public string? Url { get; set; } private ListResponse? _webhooks; private MollieApiException? _apiException; protected override async Task OnParametersSetAsync() { await LoadData(); } private async Task LoadData() { if (string.IsNullOrEmpty(Url)) { _webhooks = await WebhookClient.GetWebhookListAsync(testmode: true); } else { _webhooks = await WebhookClient.GetWebhookListAsync(new UrlObjectLink>() { Href = Url, Type = "application/json" }); } } private async Task TestWebhook(string webhookId) { try { await WebhookClient.TestWebhookAsync(webhookId, testmode: true); } catch (MollieApiException e) { _apiException = e; } } private async Task DeleteWebhook(string webhookId) { try { await WebhookClient.DeleteWebhookAsync(webhookId, testmode: true); await LoadData(); } catch (MollieApiException e) { _apiException = e; } } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Pages/_Host.cshtml ================================================ @page "/" @using Microsoft.AspNetCore.Components.Web @namespace Mollie.WebApplication.Blazor.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
An error has occurred. This application may no longer respond until reloaded. An unhandled exception has occurred. See browser dev tools for details. Reload 🗙
================================================ FILE: samples/Mollie.WebApplication.Blazor/Program.cs ================================================ using Mollie.Api; using Mollie.Api.AspNet; using Mollie.Api.Framework; using Mollie.WebApplication.Blazor.Webhooks.Nextgen.MinimalApi; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); builder.Services.AddMollieApi(options => { options.ApiKey = builder.Configuration["Mollie:ApiKey"]!; options.RetryPolicy = MollieHttpRetryPolicies.TransientHttpErrorRetryPolicy(); }); builder.Services.AddMollieWebhook(options => { options.Secret = builder.Configuration["Mollie:WebhookSecret"]!; }); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.MapControllers(); app.MapBlazorHub(); app.MapFallbackToPage("/_Host"); WebhookHandler.RegisterEndpoints(app); app.Run(); ================================================ FILE: samples/Mollie.WebApplication.Blazor/Properties/launchSettings.json ================================================ { "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:46697", "sslPort": 44312 } }, "profiles": { "Watch": { "commandName": "Executable", "workingDirectory": "$(ProjectDir)", "executablePath": "dotnet.exe", "commandLineArgs": "watch run debug --launch-profile https" }, "https": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "applicationUrl": "https://localhost:7212;http://localhost:5088", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Shared/ApiExceptionDisplay.razor ================================================ @using Mollie.Api.Client @if (Exception != null) { } @code { [Parameter, EditorRequired] public MollieApiException? Exception { get; set; } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Shared/MainLayout.razor ================================================ @inherits LayoutComponentBase Mollie.WebApplication.Blazor
@Body
================================================ FILE: samples/Mollie.WebApplication.Blazor/Shared/MainLayout.razor.css ================================================ .page { position: relative; display: flex; flex-direction: column; } main { flex: 1; } .sidebar { background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); } .top-row { background-color: #f7f7f7; border-bottom: 1px solid #d6d5d5; justify-content: flex-end; height: 3.5rem; display: flex; align-items: center; } .top-row ::deep a, .top-row .btn-link { white-space: nowrap; margin-left: 1.5rem; } .top-row a:first-child { overflow: hidden; text-overflow: ellipsis; } @media (max-width: 640.98px) { .top-row:not(.auth) { display: none; } .top-row.auth { justify-content: space-between; } .top-row a, .top-row .btn-link { margin-left: 0; } } @media (min-width: 641px) { .page { flex-direction: row; } .sidebar { width: 250px; height: 100vh; position: sticky; top: 0; } .top-row { position: sticky; top: 0; z-index: 1; } .top-row, article { padding-left: 2rem !important; padding-right: 1.5rem !important; } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Shared/NavMenu.razor ================================================  @code { private bool _collapseNavMenu = true; private string NavMenuCssClass => _collapseNavMenu ? "collapse" : string.Empty; private void ToggleNavMenu() { _collapseNavMenu = !_collapseNavMenu; } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Shared/NavMenu.razor.css ================================================ .navbar-toggler { background-color: rgba(255, 255, 255, 0.1); } .top-row { height: 3.5rem; background-color: rgba(0,0,0,0.4); } .navbar-brand { font-size: 1.1rem; } .oi { width: 2rem; font-size: 1.1rem; vertical-align: text-top; top: -2px; } .nav-item { font-size: 0.9rem; padding-bottom: 0.5rem; } .nav-item:first-of-type { padding-top: 1rem; } .nav-item:last-of-type { padding-bottom: 1rem; } .nav-item ::deep a { color: #d7d7d7; border-radius: 4px; height: 3rem; display: flex; align-items: center; line-height: 3rem; } .nav-item ::deep a.active { background-color: rgba(255,255,255,0.25); color: white; } .nav-item ::deep a:hover { background-color: rgba(255,255,255,0.1); color: white; } @media (min-width: 641px) { .navbar-toggler { display: none; } .collapse { /* Never collapse the sidebar for wide screens */ display: block; } .nav-scrollable { /* Allow sidebar to scroll for tall menus */ height: calc(100vh - 3.5rem); overflow-y: auto; } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Shared/OverviewNavigation.razor ================================================ @using Mollie.Api.Models.Url @inject NavigationManager NavManager @if (Previous != null) { << Previous } @if (Next != null) { Next >> } @code { [Parameter, EditorRequired] public UrlLink? Previous { get; set; } [Parameter, EditorRequired] public UrlLink? Next { get; set; } private string GetCurrentUrlWithoutQueryParameters => NavManager.Uri.Split('?')[0]; } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Webhooks/Classic/PaymentController.cs ================================================ using Microsoft.AspNetCore.Mvc; using Mollie.Api.Client.Abstract; using Mollie.Api.Models.Payment.Response; namespace Mollie.WebApplication.Blazor.Webhooks.Classic; [ApiController] [Route("api/webhook/classic/controllers")] public class PaymentController : ControllerBase { private readonly ILogger _logger; private readonly IPaymentClient _paymentClient; public PaymentController(ILogger logger, IPaymentClient paymentClient) { _logger = logger; _paymentClient = paymentClient; } [HttpPost] public async Task Webhook([FromForm] string id) { PaymentResponse payment = await _paymentClient.GetPaymentAsync(id); _logger.LogInformation("Webhook called for PaymentId={PaymentId}, PaymentStatus={Status}", id, payment.Status); return Ok(); } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Webhooks/Nextgen/Controllers/PaymentLinkController.cs ================================================ using Microsoft.AspNetCore.Mvc; using Mollie.Api.AspNet.Webhooks.Authorization; using Mollie.Api.AspNet.Webhooks.ModelBinding; using Mollie.Api.Models.PaymentLink.Response; using Mollie.Api.Models.WebhookEvent.Response; namespace Mollie.WebApplication.Blazor.Webhooks.Nextgen.Controllers; [ApiController] [Route("api/webhook/nextgen/controllers")] public class PaymentLinkController : ControllerBase { // Example of full webhook event with specific data entity type, such as PaymentLinkResponse [HttpPost("full/specific")] [ServiceFilter(typeof(MollieSignatureFilter))] public Task WebhookWithSpecificType([FromMollieWebhook] FullWebhookEventResponse data) { return Task.FromResult(Ok()); } // Example of full webhook event with generic data entity type [HttpPost("full/generic")] [ServiceFilter(typeof(MollieSignatureFilter))] public Task WebhookWithGenericType([FromMollieWebhook] FullWebhookEventResponse data) { return Task.FromResult(Ok()); } // Example of a simple webhook event response, that does not include the entity data [HttpPost("simple")] [ServiceFilter(typeof(MollieSignatureFilter))] public Task WebhookWithGenericType([FromMollieWebhook] SimpleWebhookEventResponse data) { return Task.FromResult(Ok()); } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/Webhooks/Nextgen/MinimalApi/WebhookHandler.cs ================================================ using Mollie.Api.AspNet.Webhooks.Authorization; using Mollie.Api.AspNet.Webhooks.ModelBinding; using Mollie.Api.Models.PaymentLink.Response; using Mollie.Api.Models.WebhookEvent.Response; namespace Mollie.WebApplication.Blazor.Webhooks.Nextgen.MinimalApi; public static class WebhookHandler { public static void RegisterEndpoints(IEndpointRouteBuilder app) { // Example of full webhook event with specific data entity type, such as PaymentLinkResponse app .MapPost("api/webhook/nextgen/minimalapi/full/specific", (MollieModelBinder> data) => { if (data.Model == null) { return Results.BadRequest(); } return Results.Ok(); }) .AddEndpointFilter(); // Example of full webhook event with generic data entity type app .MapPost("api/webhook/nextgen/minimalapi/full/generic", (MollieModelBinder data) => { if (data.Model == null) { return Results.BadRequest(); } return Results.Ok(); }) .AddEndpointFilter(); // Example of a simple webhook event response, that does not include the entity data app .MapPost("api/webhook/nextgen/minimalapi/simple", (MollieModelBinder data) => Results.Ok()) .AddEndpointFilter(); } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/_Imports.razor ================================================ @using System.Net.Http @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop @using Mollie.WebApplication.Blazor @using Mollie.WebApplication.Blazor.Shared @using Mollie.WebApplication.Blazor.Framework @using Mollie.Api.Client.Abstract @using Mollie.Api.Models.List @using Mollie.Api.Models.Url @using Mollie.Api.Models @using Mollie.Api.Models.Payment @using Mollie.Api.Models.Payment.Response @using Mollie.Api.Models.Customer; @using Mollie.Api.Models.Subscription; @using Mollie.Api.Models.Mandate; @using Mollie.Api.Models.PaymentMethod; @using Mollie.Api.Models.Order; @using Mollie.Api.Models.Terminal; ================================================ FILE: samples/Mollie.WebApplication.Blazor/appsettings.Development.json ================================================ { "DetailedErrors": true, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } } } ================================================ FILE: samples/Mollie.WebApplication.Blazor/appsettings.json ================================================ { "Mollie": { "ApiKey": "", "WebhookSecret": "" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" } ================================================ FILE: samples/Mollie.WebApplication.Blazor/wwwroot/css/open-iconic/FONT-LICENSE ================================================ SIL OPEN FONT LICENSE Version 1.1 Copyright (c) 2014 Waybury PREAMBLE The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. DEFINITIONS "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. "Reserved Font Name" refers to any names specified as such after the copyright statement(s). "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. PERMISSION & CONDITIONS Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. TERMINATION This license becomes null and void if any of the above conditions are not met. DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. ================================================ FILE: samples/Mollie.WebApplication.Blazor/wwwroot/css/open-iconic/ICON-LICENSE ================================================ The MIT License (MIT) Copyright (c) 2014 Waybury Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: samples/Mollie.WebApplication.Blazor/wwwroot/css/open-iconic/README.md ================================================ [Open Iconic v1.1.1](https://github.com/iconic/open-iconic) =========== ### Open Iconic is the open source sibling of [Iconic](https://github.com/iconic/open-iconic). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](https://github.com/iconic/open-iconic) ## What's in Open Iconic? * 223 icons designed to be legible down to 8 pixels * Super-light SVG files - 61.8 for the entire set * SVG sprite—the modern replacement for icon fonts * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. ## Getting Started #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](https://github.com/iconic/open-iconic) and [Reference](https://github.com/iconic/open-iconic) sections. ### General Usage #### Using Open Iconic's SVGs We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). ``` icon name ``` #### Using Open Iconic's SVG Sprite Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* ``` ``` Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. ``` .icon { width: 16px; height: 16px; } ``` Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. ``` .icon-account-login { fill: #f00; } ``` To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). #### Using Open Iconic's Icon Font... ##### …with Bootstrap You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` ``` ``` ``` ``` ##### …with Foundation You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` ``` ``` ``` ``` ##### …on its own You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` ``` ``` ``` ``` ## License ### Icons All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). ### Fonts All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). ================================================ FILE: samples/Mollie.WebApplication.Blazor/wwwroot/css/site.css ================================================ @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); html, body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } h1:focus { outline: none; } a, .btn-link { color: #0071c1; } .btn-primary { color: #fff; background-color: #1b6ec2; border-color: #1861ac; } .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; } .content { padding-top: 1.1rem; } .valid.modified:not([type=checkbox]) { outline: 1px solid #26b050; } .invalid { outline: 1px solid red; } .validation-message { color: red; } #blazor-error-ui { background: lightyellow; bottom: 0; box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); display: none; left: 0; padding: 0.6rem 1.25rem 0.7rem 1.25rem; position: fixed; width: 100%; z-index: 1000; } #blazor-error-ui .dismiss { cursor: pointer; position: absolute; right: 0.75rem; top: 0.5rem; } .blazor-error-boundary { background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; padding: 1rem 1rem 1rem 3.7rem; color: white; } .blazor-error-boundary::after { content: "An error has occurred." } ================================================ FILE: src/Mollie.Api/Client/Abstract/IBalanceClient.cs ================================================ using System; using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models.Balance.Response; using Mollie.Api.Models.Balance.Response.BalanceReport; using Mollie.Api.Models.Balance.Response.BalanceTransaction; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Client.Abstract { public interface IBalanceClient : IBaseMollieClient { /// /// Retrieve a single balance object by its balance identifier. /// /// The balance identifier to retrieve /// Optional cancellation token Task GetBalanceAsync(string balanceId, CancellationToken cancellationToken = default); /// /// Retrieve a single balance object using an URL /// /// The URL of the balance object /// Optional cancellation token Task GetBalanceAsync(UrlObjectLink url, CancellationToken cancellationToken = default); /// /// Retrieve the primary balance. This is the balance of your account’s primary currency, where all payments are /// settled to by default. /// /// Optional cancellation token Task GetPrimaryBalanceAsync(CancellationToken cancellationToken = default); /// /// Retrieve all the organization’s balances, including the primary balance, ordered from newest to oldest. /// /// Offset the result set to the balance with this ID. The balance with this ID is included /// in the result set as well. /// The number of balances to return (with a maximum of 250). /// Currency filter that will make it so only balances in given currency are returned. /// For example EUR. /// Optional cancellation token /// Task> GetBalanceListAsync( string? from = null, int? limit = null, string? currency = null, CancellationToken cancellationToken = default); /// /// Retrieve all the organization’s balances by URL /// /// The URL of the balance objects /// Optional cancellation token Task> GetBalanceListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default); /// /// With the Get balance report endpoint you can retrieve a summarized report for all movements on a given /// balance within a given timeframe. /// /// The balance id for which to retrieve a report /// he start date of the report, in YYYY-MM-DD format. The from date is ‘inclusive’, and in /// Central European Time. This means a report with for example from: 2020-01-01 will include movements of /// 2020-01-01 0:00:00 CET and onwards. /// The end date of the report, in YYYY-MM-DD format. The until date is ‘exclusive’, and /// in Central European Time. This means a report with for example until: 2020-02-01 will include movements up /// until 2020-01-31 23:59:59 CET. /// You can retrieve reports in two different formats: status-balances and /// transaction-categories /// Optional cancellation token Task GetBalanceReportAsync( string balanceId, DateTime from, DateTime until, string? grouping = null, CancellationToken cancellationToken = default); /// /// With the Get primary balance report endpoint you can retrieve a summarized report for all movements on your /// primary balance within a given timeframe. /// /// The start date of the report, in YYYY-MM-DD format. The from date is ‘inclusive’, and in /// Central European Time. This means a report with for example from: 2020-01-01 will include movements of /// 2020-01-01 0:00:00 CET and onwards. /// The end date of the report, in YYYY-MM-DD format. The until date is ‘exclusive’, and in /// Central European Time. This means a report with for example until: 2020-02-01 will include movements up /// until 2020-01-31 23:59:59 CET. /// You can retrieve reports in two different formats: status-balances and /// transaction-categories /// Optional cancellation token Task GetPrimaryBalanceReportAsync( DateTime from, DateTime until, string? grouping = null, CancellationToken cancellationToken = default); /// /// With the List balance transactions endpoint you can retrieve a list of all the movements on your balance. /// This includes payments, refunds, chargebacks, and settlements. /// /// The balance id for which to retrieve a report /// Offset the result set to the balance transactions with this ID. The balance transaction /// with this ID is included in the result set as well. /// The number of balance transactions to return (with a maximum of 250). /// Optional cancellation token Task> GetBalanceTransactionListAsync( string balanceId, string? from = null, int? limit = null, CancellationToken cancellationToken = default); /// /// With the List primary balance transactions endpoint you can retrieve a list of all the movements on your /// primary balance. This includes payments, refunds, chargebacks, and settlements. /// /// Offset the result set to the balance transactions with this ID. The balance transaction /// with this ID is included in the result set as well. /// The number of balance transactions to return (with a maximum of 250). /// Optional cancellation token Task> GetPrimaryBalanceTransactionListAsync( string? from = null, int? limit = null, CancellationToken cancellationToken = default); /// /// Retrieve a list of balance transactions by URL /// /// The URL from which to retrieve the balance transactions /// Optional cancellation token Task> GetBalanceTransactionListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/IBalanceTransferClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models; using Mollie.Api.Models.BalanceTransfer.Request; using Mollie.Api.Models.BalanceTransfer.Response; using Mollie.Api.Models.List.Response; namespace Mollie.Api.Client.Abstract; public interface IBalanceTransferClient { /// /// This API endpoint allows you to create a balance transfer from your organization's balance to a connected /// organization's balance, or vice versa. You can also create a balance transfer between two connected /// organizations. To create a balance transfer, you must be authenticated as the source organization, and the /// destination organization must be a connected organization that has authorized the balance-transfers.write /// scope for your organization. /// Task CreateBalanceTransferAsync( BalanceTransferRequest request, CancellationToken cancellationToken = default); /// /// Returns a paginated list of balance transfers associated with your organization. These may be a balance transfer /// that was received or sent from your balance, or a balance transfer that you initiated on behalf of your clients. /// If no balance transfers are available, the resulting array will be empty. This request should never throw an error. /// Task> GetBalanceTransferListAsync( string? from = null, int? limit = null, SortDirection? sort = null, bool testmode = false, CancellationToken cancellationToken = default); /// /// Retrieve a single Connect balance transfer object by its ID. /// Task GetBalanceTransferAsync( string balanceTransferId, bool testmode = false, CancellationToken cancellationToken = default); } ================================================ FILE: src/Mollie.Api/Client/Abstract/IBaseMollieClient.cs ================================================ using System; namespace Mollie.Api.Client.Abstract { public interface IBaseMollieClient : IDisposable { IDisposable WithIdempotencyKey(string value); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/ICapabilityClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models.Capability.Response; using Mollie.Api.Models.List.Response; namespace Mollie.Api.Client.Abstract; public interface ICapabilityClient { Task> GetCapabilitiesListAsync(CancellationToken cancellationToken = default); } ================================================ FILE: src/Mollie.Api/Client/Abstract/ICaptureClient.cs ================================================ using System.Threading.Tasks; using Mollie.Api.Models.Capture.Request; using Mollie.Api.Models.Capture.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Url; using System.Threading; namespace Mollie.Api.Client.Abstract { public interface ICaptureClient : IBaseMollieClient { Task GetCaptureAsync(string paymentId, string captureId, bool testmode = false, CancellationToken cancellationToken = default); Task GetCaptureAsync(UrlObjectLink url, CancellationToken cancellationToken = default); Task> GetCaptureListAsync(string paymentId, bool testmode = false, CancellationToken cancellationToken = default); Task> GetCaptureListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default); Task CreateCapture(string paymentId, CaptureRequest captureRequest, CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/IChargebackClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models.Chargeback.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Client.Abstract { public interface IChargebackClient : IBaseMollieClient { Task GetChargebackAsync(string paymentId, string chargebackId, bool testmode = false, CancellationToken cancellationToken = default); Task> GetChargebackListAsync(string paymentId, string? from = null, int? limit = null, bool testmode = false, CancellationToken cancellationToken = default); Task> GetChargebackListAsync(string? profileId = null, bool testmode = false, CancellationToken cancellationToken = default); Task> GetChargebackListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/IClientClient.cs ================================================ using System.Threading.Tasks; using Mollie.Api.Models.Client.Response; using Mollie.Api.Models.List.Response; using System.Threading; namespace Mollie.Api.Client.Abstract { public interface IClientClient : IBaseMollieClient { Task GetClientAsync( string clientId, bool embedOrganization = false, bool embedOnboarding = false, bool embedCapabilities = false, CancellationToken cancellationToken = default); Task> GetClientListAsync( string? from = null, int? limit = null, bool embedOrganization = false, bool embedOnboarding = false, bool embedCapabilities = false, CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/IClientLinkClient.cs ================================================ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models.ClientLink.Request; using Mollie.Api.Models.ClientLink.Response; namespace Mollie.Api.Client.Abstract { public interface IClientLinkClient { Task CreateClientLinkAsync(ClientLinkRequest request, CancellationToken cancellationToken = default); string GenerateClientLinkWithParameters( string clientLinkUrl, string state, List scopes, bool forceApprovalPrompt = false); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/IConnectClient.cs ================================================ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models.Connect.Request; using Mollie.Api.Models.Connect.Response; namespace Mollie.Api.Client.Abstract { public interface IConnectClient : IDisposable { /// /// Constructs the Authorize URL for the Authorize endpoint from the parameters /// /// A random string generated by your app to prevent CSRF attacks. /// /// A space separated list of permissions your app requires. Refer to OAuth: Permissions for more /// information about the available scopes. /// /// /// The URL the merchant is sent back to once the request has been authorized. If given, it must /// match the URL you set when registering your app. /// /// /// This parameter can be set to force, to force showing the consent screen to the /// merchant, even when it is not necessary /// /// /// Allows you to preset the language to be used in the login / sign up / authorize flow if the merchant is not known by Mollie. /// When this parameter is omitted, the browser language will be used instead. /// You can provide any ISO 15897 locale, but the authorize flow currently only supports the following languages: /// Possible values: en_US nl_NL nl_BE fr_FR fr_BE de_DE es_ES it_IT /// /// /// Allows you to specify if Mollie should show the login or the signup page, when the merchant is not logged in at Mollie. /// Defaults to the login page. Defaults to login. /// /// The url to the mollie consent screen. string GetAuthorizationUrl( string state, List scopes, string? redirectUri = null, bool forceApprovalPrompt = false, string? locale = null, string? landingPage = null); /// /// Exchange the auth code received at the Authorize endpoint for an actual access token, with which you can /// communicate with the Mollie API. Or Refresh the accestoken /// /// /// /// An token object. Task GetAccessTokenAsync(TokenRequest request, CancellationToken cancellationToken = default); /// /// Revoke an access- or a refresh token. Once revoked the token can not be used anymore. /// /// /// /// Task RevokeTokenAsync(RevokeTokenRequest request, CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/ICustomerClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models.Customer.Request; using Mollie.Api.Models.Customer.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment.Request; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Client.Abstract { public interface ICustomerClient : IBaseMollieClient { Task CreateCustomerAsync(CustomerRequest request, CancellationToken cancellationToken = default); Task UpdateCustomerAsync(string customerId, CustomerRequest request, CancellationToken cancellationToken = default); Task DeleteCustomerAsync(string customerId, bool testmode = false, CancellationToken cancellationToken = default); Task GetCustomerAsync(string customerId, bool testmode = false, CancellationToken cancellationToken = default); Task GetCustomerAsync(UrlObjectLink url, CancellationToken cancellationToken = default); Task> GetCustomerListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default); Task> GetCustomerListAsync(string? from = null, int? limit = null, bool testmode = false, CancellationToken cancellationToken = default); Task> GetCustomerPaymentListAsync(string customerId, string? from = null, int? limit = null, string? profileId = null, bool testmode = false, CancellationToken cancellationToken = default); Task CreateCustomerPayment(string customerId, PaymentRequest paymentRequest, CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/IInvoiceClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models.Invoice.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Client.Abstract { public interface IInvoiceClient : IBaseMollieClient { Task GetInvoiceAsync(string invoiceId, CancellationToken cancellationToken = default); Task GetInvoiceAsync(UrlObjectLink url, CancellationToken cancellationToken = default); Task> GetInvoiceListAsync( string? reference = null, int? year = null, string? from = null, int? limit = null, CancellationToken cancellationToken = default); Task> GetInvoiceListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/IMandateClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Mandate.Request; using Mollie.Api.Models.Mandate.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Client.Abstract { public interface IMandateClient : IBaseMollieClient { Task GetMandateAsync(string customerId, string mandateId, bool testmode = false, CancellationToken cancellationToken = default); Task> GetMandateListAsync(string customerId, string? from = null, int? limit = null, bool testmode = false, CancellationToken cancellationToken = default); Task CreateMandateAsync(string customerId, MandateRequest request, CancellationToken cancellationToken = default); Task> GetMandateListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default); Task GetMandateAsync(UrlObjectLink url, CancellationToken cancellationToken = default); Task RevokeMandate(string customerId, string mandateId, bool testmode = false, CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/IOnboardingClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models.Onboarding.Request; using Mollie.Api.Models.Onboarding.Response; namespace Mollie.Api.Client.Abstract { public interface IOnboardingClient : IBaseMollieClient { Task GetOnboardingStatusAsync(CancellationToken cancellationToken = default); Task SubmitOnboardingDataAsync(SubmitOnboardingDataRequest request, CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/IOrderClient.cs ================================================ using System; using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Order.Request; using Mollie.Api.Models.Order.Request.ManageOrderLines; using Mollie.Api.Models.Order.Response; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Client.Abstract { [Obsolete("Mollie no longer recommends using the Orders API. Please refer to the Payments API instead.")] public interface IOrderClient : IBaseMollieClient { Task CreateOrderAsync(OrderRequest orderRequest, CancellationToken cancellationToken = default); Task GetOrderAsync( string orderId, bool embedPayments = false, bool embedRefunds = false, bool embedShipments = false, bool testmode = false, CancellationToken cancellationToken = default); Task GetOrderAsync(UrlObjectLink url, CancellationToken cancellationToken = default); Task UpdateOrderAsync(string orderId, OrderUpdateRequest orderUpdateRequest, CancellationToken cancellationToken = default); Task UpdateOrderLinesAsync(string orderId, string orderLineId, OrderLineUpdateRequest orderLineUpdateRequest, CancellationToken cancellationToken = default); Task ManageOrderLinesAsync(string orderId, ManageOrderLinesRequest manageOrderLinesRequest, CancellationToken cancellationToken = default); Task CancelOrderAsync(string orderId, bool testmode = false, CancellationToken cancellationToken = default); Task> GetOrderListAsync( string? from = null, int? limit = null, string? profileId = null, bool testmode = false, SortDirection? sort = null, CancellationToken cancellationToken = default); Task> GetOrderListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default); Task CancelOrderLinesAsync(string orderId, OrderLineCancellationRequest cancelationRequest, CancellationToken cancellationToken = default); Task CreateOrderPaymentAsync(string orderId, OrderPaymentRequest createOrderPaymentRequest, CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/IOrganizationClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Organization; using Mollie.Api.Models.Url; namespace Mollie.Api.Client.Abstract { public interface IOrganizationClient : IBaseMollieClient { Task GetCurrentOrganizationAsync(CancellationToken cancellationToken = default); Task GetOrganizationAsync(string organizationId, CancellationToken cancellationToken = default); Task> GetOrganizationListAsync(string? from = null, int? limit = null, CancellationToken cancellationToken = default); Task> GetOrganizationListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default); Task GetOrganizationAsync(UrlObjectLink url, CancellationToken cancellationToken = default); Task GetPartnerStatusAsync(CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/IPaymentClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment.Request; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Client.Abstract { public interface IPaymentClient : IBaseMollieClient { /// /// Create a payment object. /// /// The payment request object containing the payment details /// Include a QR code object for the payment. Only available for iDEAL, Bancontact and bank transfer payments. /// Token to cancel the request /// The payment object created by Mollie. Once the payment is created, redirect the user to Links.Checkout.Redirect Task CreatePaymentAsync( PaymentRequest paymentRequest, bool includeQrCode = false, CancellationToken cancellationToken = default); /// /// Retrieve a single payment object by its payment identifier. /// /// The payment's ID, for example tr_7UhSN1zuXS. /// Oauth - Optional – Set this to true to get a payment made in test mode. If you omit /// this parameter, you can only retrieve live mode payments. /// Include a QR code object. Only available for iDEAL, Bancontact and bank transfer /// payments. /// Include the Payment method-specific response parameters of the /// ‘remainder payment’ as well. This applies to gift card and voucher payments where only part of the payment /// was completed with gift cards or vouchers, and the remainder was completed with a regular payment method. /// /// Include all refunds created for the payment. /// Include all chargebacks issued for the payment. /// A cancellation token that can be used to cancel the request. /// Task GetPaymentAsync( string paymentId, bool testmode = false, bool includeQrCode = false, bool includeRemainderDetails = false, bool embedRefunds = false, bool embedChargebacks = false, CancellationToken cancellationToken = default); /// /// Some payment methods are cancellable for an amount of time, usually until the next day. Or as long as the /// payment status is open. Payments may be cancelled manually from the Dashboard, or automatically by using /// this endpoint. /// /// /// Oauth - Optional – Set this to true to cancel a test mode payment. /// A cancellation token that can be used to cancel the request. /// Task CancelPaymentAsync( string paymentId, bool testmode = false, CancellationToken cancellationToken = default); /// /// Releases the full remaining authorized amount. Call this endpoint when you will not be making any additional /// captures. Payment authorizations may also be released manually from the Mollie Dashboard. /// /// Provide the ID of the related payment. /// Oauth - Optional – Set this to true to release the authorization of a test mode payment. /// /// Task ReleasePaymentAuthorization( string paymentId, bool testmode = false, CancellationToken cancellationToken = default); /// /// Retrieve all payments created with the current payment profile, ordered from newest to oldest. /// /// Used for pagination. Offset the result set to the payment with this ID. The payment with /// this ID is included in the result set as well. /// The number of payments to return (with a maximum of 250). /// The website profile’s unique identifier, for example pfl_3RkSN1zuPE. Omit this /// parameter to retrieve all payments across all profiles. /// Set this to true to only retrieve payments made in test mode. By default, only live /// payments are returned. /// Include a QR code object for each payment that supports it. Only available for /// iDEAL, Bancontact and bank transfer payments. /// Include any refunds created for the payments. /// Include any chargebacks issued for the payments. /// Used for setting the direction of the results based on the from parameter. Can be set /// to desc or asc. Default is desc. /// A cancellation token that can be used to cancel the request. /// Task> GetPaymentListAsync( string? from = null, int? limit = null, string? profileId = null, bool testmode = false, bool includeQrCode = false, bool embedRefunds = false, bool embedChargebacks = false, SortDirection? sort = null, CancellationToken cancellationToken = default); /// /// Retrieve a list of payments by URL /// /// The URL from which to retrieve the payments /// A cancellation token that can be used to cancel the request. /// A list of paginated payments Task> GetPaymentListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default); /// /// Retrieve a single payment by URL /// /// The URL from which to retrieve the payment /// A cancellation token that can be used to cancel the request. /// The found payment Task GetPaymentAsync( UrlObjectLink url, CancellationToken cancellationToken = default); /// /// This endpoint can be used to update some details of a created payment. /// /// The payment id to update /// The payment parameters to update /// A cancellation token that can be used to cancel the request. /// The changed payment /// Updating the payment details will not result in a webhook call Task UpdatePaymentAsync( string paymentId, PaymentUpdateRequest paymentUpdateRequest, CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/IPaymentLinkClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.PaymentLink.Request; using Mollie.Api.Models.PaymentLink.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Client.Abstract { public interface IPaymentLinkClient : IBaseMollieClient { /// /// Create a new payment link /// /// The payment link request /// Token to cancel the operation /// Task CreatePaymentLinkAsync( PaymentLinkRequest paymentLinkRequest, CancellationToken cancellationToken = default); /// /// Update a payment link /// /// Provide the ID of the item you want to perform this operation on. /// The request body /// Token to cancel the operation /// The updated payment link response Task UpdatePaymentLinkAsync( string paymentLinkId, PaymentLinkUpdateRequest paymentLinkUpdateRequest, CancellationToken cancellationToken = default); /// /// Payment links for which no payments have been made yet can be deleted entirely. This can be useful for /// removing payment links that have been incorrectly configured or that are no longer relevant. /// /// Provide the ID of the item you want to perform this operation on. /// The website profile’s unique identifier, for example pfl_3RkSN1zuPE. /// Most API credentials are specifically created for either live mode or test mode. /// In those cases the testmode query parameter can be omitted. For organization-level credentials such as /// OAuth access tokens, you can enable test mode by setting the testmode query parameter to true. /// Token to cancel the operation /// Task DeletePaymentLinkAsync( string paymentLinkId, string? profileId = null, bool testmode = false, CancellationToken cancellationToken = default); /// /// Retrieve a single payment link object by its token. /// /// The payment link to retrieve /// Oauth - Optional – Set this to true to get a payment links made in test mode. If you omit /// this parameter, you can only retrieve live mode payments. /// Token to cancel the operation Task GetPaymentLinkAsync( string paymentLinkId, bool testmode = false, CancellationToken cancellationToken = default); /// /// Retrieve all payment links created with the current payment link profile, ordered from newest to oldest. /// /// Used for pagination. Offset the result set to the payment link with this ID. The payment /// link with this ID is included in the result set as well. /// The number of payment links to return (with a maximum of 250). /// The website profile’s unique identifier, for example pfl_3RkSN1zuPE. Omit this /// parameter to retrieve the payment links of all profiles of the current organization. /// Set this to true to only retrieve payment links made in test mode. By default, only /// live payment links are returned. /// Token to cancel the operation /// Task> GetPaymentLinkListAsync( string? from = null, int? limit = null, string? profileId = null, bool testmode = false, CancellationToken cancellationToken = default); /// /// Retrieve a list of payment links by URL /// /// The URL from which to retrieve the payment links /// Token to cancel the operation /// Task> GetPaymentLinkListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default); /// /// Retrieve a single payment link by URL /// /// The URL from which to retrieve the payment link /// Token to cancel the operation /// Task GetPaymentLinkAsync( UrlObjectLink url, CancellationToken cancellationToken = default); /// /// Retrieve the list of payments for a specific payment link. /// /// Provide the ID of the item you want to perform this operation on. /// Provide an ID to start the result set from the item with the given ID and onwards. This /// allows you to paginate the result set. /// The maximum number of items to return. Defaults to 50 items. /// Most API credentials are specifically created for either live mode or test mode. In /// those cases the testmode query parameter can be omitted. For organization-level credentials such as OAuth access /// tokens, you can enable test mode by setting the testmode query parameter to true. Test entities cannot be /// retrieved when the endpoint is set to live mode, and vice versa. /// Used for setting the direction of the result set. Defaults to descending order, meaning /// the results are ordered from newest to oldest. /// Token to cancel the operation /// Task> GetPaymentLinkPaymentListAsync( string paymentLinkId, string? from = null, int? limit = null, bool testmode = false, SortDirection? sort = null, CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/IPaymentMethodClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment; using Mollie.Api.Models.PaymentMethod.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Client.Abstract { public interface IPaymentMethodClient : IBaseMollieClient { Task GetPaymentMethodAsync( string paymentMethod, bool includeIssuers = false, string? locale = null, string? profileId = null, bool testmode = false, string? currency = null, CancellationToken cancellationToken = default); Task> GetAllPaymentMethodListAsync( string? locale = null, Amount? amount = null, bool includeIssuers = false, bool includePricing = false, string? profileId = null, bool testmode = false, CancellationToken cancellationToken = default); Task> GetPaymentMethodListAsync( string? sequenceType = null, string? locale = null, Amount? amount = null, bool includeIssuers = false, string? profileId = null, bool testmode = false, Resource? resource = null, string? billingCountry = null, string? includeWallets = null, CancellationToken cancellationToken = default); Task GetPaymentMethodAsync(UrlObjectLink url, CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/IPermissionClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Permission.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Client.Abstract { public interface IPermissionClient : IBaseMollieClient { Task GetPermissionAsync(string permissionId, CancellationToken cancellationToken = default); Task GetPermissionAsync(UrlObjectLink url, CancellationToken cancellationToken = default); Task> GetPermissionListAsync( CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/IProfileClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.PaymentMethod.Response; using Mollie.Api.Models.Profile.Request; using Mollie.Api.Models.Profile.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Client.Abstract { public interface IProfileClient : IBaseMollieClient { Task CreateProfileAsync(ProfileRequest request, CancellationToken cancellationToken = default); Task GetProfileAsync(string profileId, CancellationToken cancellationToken = default); Task GetProfileAsync(UrlObjectLink url, CancellationToken cancellationToken = default); Task> GetProfileListAsync(string? from = null, int? limit = null, CancellationToken cancellationToken = default); Task> GetProfileListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default); Task UpdateProfileAsync(string profileId, ProfileRequest request, CancellationToken cancellationToken = default); Task DeleteProfileAsync(string profileId, CancellationToken cancellationToken = default); Task GetCurrentProfileAsync(CancellationToken cancellationToken = default); Task EnablePaymentMethodAsync(string profileId, string paymentMethod, CancellationToken cancellationToken = default); Task EnablePaymentMethodAsync(string paymentMethod, CancellationToken cancellationToken = default); Task DisablePaymentMethodAsync(string profileId, string paymentMethod, CancellationToken cancellationToken = default); Task DisablePaymentMethodAsync(string paymentMethod, CancellationToken cancellationToken = default); Task EnableGiftCardIssuerAsync(string profileId, string issuer, CancellationToken cancellationToken = default); Task EnableGiftCardIssuerAsync(string issuer, CancellationToken cancellationToken = default); Task DisableGiftCardIssuerAsync(string profileId, string issuer, CancellationToken cancellationToken = default); Task DisableGiftCardIssuerAsync(string issuer, CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/IRefundClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Order.Request; using Mollie.Api.Models.Order.Response; using Mollie.Api.Models.Refund.Request; using Mollie.Api.Models.Refund.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Client.Abstract { public interface IRefundClient : IBaseMollieClient { Task CreatePaymentRefundAsync(string paymentId, RefundRequest refundRequest, CancellationToken cancellationToken = default); Task GetPaymentRefundAsync(string paymentId, string refundId, bool testmode = false, CancellationToken cancellationToken = default); Task CancelPaymentRefundAsync(string paymentId, string refundId, bool testmode = false, CancellationToken cancellationToken = default); Task> GetPaymentRefundListAsync(string paymentId, string? from = null, int? limit = null, bool testmode = false, CancellationToken cancellationToken = default); Task CreateOrderRefundAsync(string orderId, OrderRefundRequest createOrderRefundRequest, CancellationToken cancellationToken = default); Task> GetOrderRefundListAsync(string orderId, string? from = null, int? limit = null, bool testmode = false, CancellationToken cancellationToken = default); Task> GetRefundListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default); Task GetRefundAsync(UrlObjectLink url, CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/ISalesInvoiceClient.cs ================================================ using System; using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.SalesInvoice.Request; using Mollie.Api.Models.SalesInvoice.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Client.Abstract; public interface ISalesInvoiceClient : IDisposable { Task CreateSalesInvoiceAsync( SalesInvoiceRequest salesInvoiceRequest, CancellationToken cancellationToken = default); Task> GetSalesInvoiceListAsync( string? from = null, int? limit = null, bool testmode = false, CancellationToken cancellationToken = default); Task> GetSalesInvoiceListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default); Task GetSalesInvoiceAsync( string salesInvoiceId, bool testmode = false, CancellationToken cancellationToken = default); Task GetSalesInvoiceAsync( UrlObjectLink url, CancellationToken cancellationToken = default); Task UpdateSalesInvoiceAsync( string salesInvoiceId, SalesInvoiceUpdateRequest salesInvoiceRequest, CancellationToken cancellationToken = default); Task DeleteSalesInvoiceAsync(string salesInvoiceId, bool testmode = false, CancellationToken cancellationToken = default); } ================================================ FILE: src/Mollie.Api/Client/Abstract/ISessionClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Session.Request; using Mollie.Api.Models.Session.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Client.Abstract { public interface ISessionClient : IBaseMollieClient { /// /// Create a new Session. /// /// The Session request object containing the Session details /// Token to cancel the request /// The Session object created by Mollie Task CreateSessionAsync(SessionRequest request, CancellationToken cancellationToken = default); /// /// Retrieve a single Session by its ID. /// /// The Session ID of the Session to retrieve /// Indicates whether the Session is in test mode or not /// Token to cancel the request /// The Session object retrieved by Mollie Task GetSessionAsync(string sessionId, bool testmode = false, CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/ISettlementClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models.Capture.Response; using Mollie.Api.Models.Chargeback.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Refund.Response; using Mollie.Api.Models.Settlement.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Client.Abstract { public interface ISettlementClient : IBaseMollieClient { Task GetSettlementAsync(string settlementId, CancellationToken cancellationToken = default); Task GetNextSettlement(CancellationToken cancellationToken = default); Task GetOpenSettlement(CancellationToken cancellationToken = default); Task> GetSettlementListAsync(string? reference = null, string? from = null, int? limit = null, CancellationToken cancellationToken = default); Task> GetSettlementListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default); Task> GetSettlementPaymentListAsync(string settlementId, string? from = null, int? limit = null, CancellationToken cancellationToken = default); Task> GetSettlementPaymentListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default); Task> GetSettlementRefundListAsync(string settlementId, string? from = null, int? limit = null, CancellationToken cancellationToken = default); Task> GetSettlementRefundListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default); Task> GetSettlementChargebackListAsync(string settlementId, string? from = null, int? limit = null, CancellationToken cancellationToken = default); Task> GetSettlementChargebackListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default); Task> GetSettlementCaptureListAsync(string settlementId, string? offset = null, int? count = null, CancellationToken cancellationToken = default); Task> GetSettlementCaptureListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default); Task GetSettlementAsync(UrlObjectLink url, CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/IShipmentClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Shipment.Request; using Mollie.Api.Models.Shipment.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Client.Abstract { public interface IShipmentClient : IBaseMollieClient { Task CreateShipmentAsync(string orderId, ShipmentRequest shipmentRequest, CancellationToken cancellationToken = default); Task GetShipmentAsync(string orderId, string shipmentId, bool testmode = false, CancellationToken cancellationToken = default); Task GetShipmentAsync(UrlObjectLink url, CancellationToken cancellationToken = default); Task> GetShipmentListAsync(string orderId, bool testmode = false, CancellationToken cancellationToken = default); Task> GetShipmentListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default); Task UpdateShipmentAsync(string orderId, string shipmentId, ShipmentUpdateRequest shipmentUpdateRequest, CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/ISubscriptionClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Subscription.Request; using Mollie.Api.Models.Subscription.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Client.Abstract { public interface ISubscriptionClient : IBaseMollieClient { /// /// Cancel an existing subscription. Canceling a subscription has no effect on the mandates of the customer. /// /// The customer ID of the customer to which the subscription belongs /// The subscription ID of the subscription to cancel /// Indicates whether the subscription is in test mode or not /// Token to cancel the request. Cancelling the token might not cancel the subscription Task CancelSubscriptionAsync(string customerId, string subscriptionId, bool testmode = false, CancellationToken cancellationToken = default); /// /// Create a new subscription for a customer. /// /// The customer ID of the customer to which the subscription belongs /// The subscription request object containing the subscription details /// Token to cancel the request /// The subscription object created by Mollie Task CreateSubscriptionAsync(string customerId, SubscriptionRequest request, CancellationToken cancellationToken = default); /// /// Retrieve a single subscription by its ID and the ID of its parent customer. /// /// The customer ID of the customer to which the subscription belongs /// The subscription ID of the subscription to retrieve /// Indicates whether the subscription is in test mode or not /// Token to cancel the request /// The subscription object retrieved by Mollie Task GetSubscriptionAsync(string customerId, string subscriptionId, bool testmode = false, CancellationToken cancellationToken = default); /// /// Retrieve all subscriptions of a customer. /// /// The customer ID of the customer to which the subscription belongs /// The cursor to start pagination from /// The maximum number of subscriptions to return /// The profile ID to filter subscriptions by /// Indicates whether the subscription is in test mode or not /// Token to cancel the request /// A list of paginated subscriptions Task> GetSubscriptionListAsync(string customerId, string? from = null, int? limit = null, string? profileId = null, bool testmode = false, CancellationToken cancellationToken = default); Task> GetSubscriptionListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default); /// /// Get all subscriptions /// /// The cursor to start pagination from /// The maximum number of subscriptions to return /// The profile ID to filter subscriptions by /// Indicates whether the subscription is in test mode or not /// Token to cancel the request /// A list of paginated subscriptions Task> GetAllSubscriptionList(string? from = null, int? limit = null, string? profileId = null, bool testmode = false, CancellationToken cancellationToken = default); Task GetSubscriptionAsync(UrlObjectLink url, CancellationToken cancellationToken = default); /// /// Update an existing subscription. /// /// The customer ID of the customer to which the subscription belongs /// The subscription ID of the subscription to update /// The subscription update request /// Token to cancel the request /// The updated subscription object /// Canceled subscriptions cannot be updated Task UpdateSubscriptionAsync(string customerId, string subscriptionId, SubscriptionUpdateRequest request, CancellationToken cancellationToken = default); /// /// Retrieve all payments of a specific subscription. /// /// The customer ID of the customer to which the subscription belongs /// The subscription ID of the subscription to retrieve payments for /// The cursor to start pagination from /// The maximum number of payments to return /// Indicates whether the subscription is in test mode or not /// Token to cancel the request /// A list of paginated payments Task> GetSubscriptionPaymentListAsync(string customerId, string subscriptionId, string? from = null, int? limit = null, bool testmode = false, CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/ITerminalClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Terminal.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Client.Abstract { /// /// Calls in this class are documented in https://docs.mollie.com/reference/v2/terminals-api/overview /// public interface ITerminalClient : IBaseMollieClient { Task GetTerminalAsync(string terminalId, bool testmode = false, CancellationToken cancellationToken = default); Task GetTerminalAsync(UrlObjectLink url, CancellationToken cancellationToken = default); Task> GetTerminalListAsync(string? from = null, int? limit = null, string? profileId = null, bool testmode = false, CancellationToken cancellationToken = default); Task> GetTerminalListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/IWalletClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models.Wallet.Request; using Mollie.Api.Models.Wallet.Response; namespace Mollie.Api.Client.Abstract { public interface IWalletClient : IBaseMollieClient { Task RequestApplePayPaymentSessionAsync(ApplePayPaymentSessionRequest request, CancellationToken cancellationToken = default); } } ================================================ FILE: src/Mollie.Api/Client/Abstract/IWebhookClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Url; using Mollie.Api.Models.Webhook.Request; using Mollie.Api.Models.Webhook.Response; namespace Mollie.Api.Client.Abstract; public interface IWebhookClient : IBaseMollieClient { Task CreateWebhookAsync(WebhookRequest request, CancellationToken cancellationToken = default); Task> GetWebhookListAsync(string? from = null, int? limit = null, bool testmode = false, CancellationToken cancellationToken = default); Task> GetWebhookListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default); Task GetWebhookAsync(string webhookId, bool testmode = false, CancellationToken cancellationToken = default); Task UpdateWebhookAsync(string webhookId, WebhookRequest request, CancellationToken cancellationToken = default); Task DeleteWebhookAsync(string webhookId, bool testmode = false, CancellationToken cancellationToken = default); Task TestWebhookAsync(string webhookId, bool testmode = false, CancellationToken cancellationToken = default); } ================================================ FILE: src/Mollie.Api/Client/Abstract/IWebhookEventClient.cs ================================================ using System.Threading; using System.Threading.Tasks; using Mollie.Api.Models; using Mollie.Api.Models.WebhookEvent.Response; namespace Mollie.Api.Client.Abstract; public interface IWebhookEventClient : IBaseMollieClient { Task> GetWebhookEventAsync(string webhookEventId, bool testmode = false, CancellationToken cancellationToken = default) where T : IEntity; Task GetWebhookEventAsync(string webhookEventId, bool testmode = false, CancellationToken cancellationToken = default); } ================================================ FILE: src/Mollie.Api/Client/BalanceClient.cs ================================================ using System; using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models.Balance.Response; using Mollie.Api.Models.Balance.Response.BalanceReport; using Mollie.Api.Models.Balance.Response.BalanceTransaction; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Url; using Mollie.Api.Options; namespace Mollie.Api.Client { public class BalanceClient : BaseMollieClient, IBalanceClient { public BalanceClient(string apiKey, HttpClient? httpClient = null) : base(apiKey, httpClient) { } [ActivatorUtilitiesConstructor] public BalanceClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task GetBalanceAsync(string balanceId, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(balanceId), balanceId); return await GetAsync($"balances/{balanceId}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetBalanceAsync(UrlObjectLink url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task GetPrimaryBalanceAsync(CancellationToken cancellationToken = default) { return await GetAsync("balances/primary", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetBalanceListAsync( string? from = null, int? limit = null, string? currency = null, CancellationToken cancellationToken = default) { var queryParameters = BuildListBalanceQueryParameters(currency); return await GetListAsync>( $"balances", from, limit, queryParameters, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetBalanceListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task GetBalanceReportAsync( string balanceId, DateTime from, DateTime until, string? grouping = null, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(balanceId), balanceId); var queryParameters = BuildGetBalanceReportQueryParameters(from, until, grouping); return await GetAsync( $"balances/{balanceId}/report{queryParameters.ToQueryString()}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetPrimaryBalanceReportAsync( DateTime from, DateTime until, string? grouping = null, CancellationToken cancellationToken = default) { var queryParameters = BuildGetBalanceReportQueryParameters(from, until, grouping); return await GetAsync( $"balances/primary/report{queryParameters.ToQueryString()}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetBalanceTransactionListAsync( string balanceId, string? from = null, int? limit = null, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(balanceId), balanceId); return await GetListAsync>( $"balances/{balanceId}/transactions", from, limit, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetPrimaryBalanceTransactionListAsync( string? from = null, int? limit = null, CancellationToken cancellationToken = default) { return await GetListAsync>( $"balances/primary/transactions", from, limit, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetBalanceTransactionListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken).ConfigureAwait(false); } private Dictionary BuildGetBalanceReportQueryParameters(DateTime from, DateTime until, string? grouping = null) { var result = new Dictionary(); result.AddValueIfNotNullOrEmpty("from", from.ToString("yyyy-MM-dd")); result.AddValueIfNotNullOrEmpty("until", until.ToString("yyyy-MM-dd")); result.AddValueIfNotNullOrEmpty("grouping", grouping); return result; } private Dictionary BuildListBalanceQueryParameters(string? currency) { var result = new Dictionary(); result.AddValueIfNotNullOrEmpty("currency", currency); return result; } } } ================================================ FILE: src/Mollie.Api/Client/BalanceTransferClient.cs ================================================ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.BalanceTransfer.Request; using Mollie.Api.Models.BalanceTransfer.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Options; namespace Mollie.Api.Client; public class BalanceTransferClient : BaseMollieClient, IBalanceTransferClient { public BalanceTransferClient(string oauthAccessToken, HttpClient? httpClient = null) : base(oauthAccessToken, httpClient) { } [ActivatorUtilitiesConstructor] public BalanceTransferClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task CreateBalanceTransferAsync( BalanceTransferRequest request, CancellationToken cancellationToken = default) { return await PostAsync( "connect/balance-transfers", request, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetBalanceTransferListAsync( string? from = null, int? limit = null, SortDirection? sort = null, bool testmode = false, CancellationToken cancellationToken = default) { var queryParameters = BuildQueryParameters(testmode: testmode, sort: sort); return await GetListAsync>( "connect/balance-transfers", from, limit, queryParameters, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetBalanceTransferAsync( string balanceTransferId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(balanceTransferId), balanceTransferId); var queryParameters = BuildQueryParameters(testmode: testmode); return await GetAsync( $"connect/balance-transfers/{balanceTransferId}{queryParameters.ToQueryString()}", cancellationToken: cancellationToken) .ConfigureAwait(false); } } ================================================ FILE: src/Mollie.Api/Client/BaseMollieClient.cs ================================================ using System; using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Mollie.Api.Extensions; using Mollie.Api.Framework; using Mollie.Api.Framework.Authentication; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Framework.Idempotency; using Mollie.Api.Models.Error; using Mollie.Api.Models.Url; using Mollie.Api.Options; using Mollie.Api.Client.Abstract; using Mollie.Api.Models; namespace Mollie.Api.Client { public abstract class BaseMollieClient : IBaseMollieClient { public const string DefaultBaseApiEndPoint = "https://api.mollie.com/v2/"; private readonly string _apiEndpoint = DefaultBaseApiEndPoint; private readonly IMollieSecretManager _mollieSecretManager; private readonly MollieClientOptions _options; private readonly HttpClient _httpClient; private readonly JsonConverterService _jsonConverterService; private readonly AsyncLocalVariable _idempotencyKey = new (null); private readonly bool _createdHttpClient; protected BaseMollieClient(string apiKey, HttpClient? httpClient = null) { if (string.IsNullOrWhiteSpace(apiKey)) { throw new ArgumentNullException(nameof(apiKey), "Mollie API key cannot be empty"); } _jsonConverterService = new JsonConverterService(); _createdHttpClient = httpClient == null; _httpClient = httpClient ?? new HttpClient(); _mollieSecretManager = new DefaultMollieSecretManager(apiKey); _options = new MollieClientOptions { ApiKey = apiKey }; } protected BaseMollieClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) { _jsonConverterService = new JsonConverterService(); _createdHttpClient = httpClient == null; _httpClient = httpClient ?? new HttpClient(); _mollieSecretManager = mollieSecretManager; _options = options; _apiEndpoint = options.ApiBaseUrl; } protected BaseMollieClient(HttpClient? httpClient = null, string apiEndpoint = DefaultBaseApiEndPoint) { _apiEndpoint = apiEndpoint; _jsonConverterService = new JsonConverterService(); _createdHttpClient = httpClient == null; _httpClient = httpClient ?? new HttpClient(); _mollieSecretManager = new DefaultMollieSecretManager(string.Empty); _options = new() { ApiKey = string.Empty }; } public IDisposable WithIdempotencyKey(string value) { _idempotencyKey.Value = value; return _idempotencyKey; } private async Task SendHttpRequest( HttpMethod httpMethod, string relativeUri, object? data = null, CancellationToken cancellationToken = default) { HttpRequestMessage httpRequest = CreateHttpRequest(httpMethod, relativeUri); if (data != null) { if (data is ITestModeRequest testModeRequest) { testModeRequest.Testmode ??= _options.Testmode; } if (data is IProfileRequest profileRequest) { profileRequest.ProfileId ??= _options.ProfileId; } var jsonData = _jsonConverterService.Serialize(data); var content = new StringContent(jsonData, Encoding.UTF8, "application/json"); httpRequest.Content = content; } var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false); return await ProcessHttpResponseMessage(response).ConfigureAwait(false); } protected async Task GetListAsync( string relativeUri, string? from, int? limit, IDictionary? otherParameters = null, CancellationToken cancellationToken = default) { string url = relativeUri + BuildListQueryString(from, limit, otherParameters); return await SendHttpRequest(HttpMethod.Get, url, cancellationToken: cancellationToken) .ConfigureAwait(false); } protected async Task GetAsync(string relativeUri, CancellationToken cancellationToken = default) { return await SendHttpRequest(HttpMethod.Get, relativeUri, cancellationToken: cancellationToken) .ConfigureAwait(false); } protected async Task GetAsync(UrlObjectLink urlObject, CancellationToken cancellationToken = default) { ValidateUrlLink(urlObject); return await GetAsync(urlObject.Href, cancellationToken: cancellationToken) .ConfigureAwait(false); } protected async Task PostAsync( string relativeUri, object? data, CancellationToken cancellationToken = default) { return await SendHttpRequest(HttpMethod.Post, relativeUri, data, cancellationToken) .ConfigureAwait(false); } protected async Task PatchAsync(string relativeUri, object? data, CancellationToken cancellationToken = default) { return await SendHttpRequest(new HttpMethod("PATCH"), relativeUri, data, cancellationToken) .ConfigureAwait(false); } protected async Task DeleteAsync(string relativeUri, object? data = null, CancellationToken cancellationToken = default) { await SendHttpRequest(HttpMethod.Delete, relativeUri, data, cancellationToken) .ConfigureAwait(false); } private async Task ProcessHttpResponseMessage(HttpResponseMessage response) { var resultContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); if (response.IsSuccessStatusCode) { resultContent = string.IsNullOrEmpty(resultContent) ? "{}" : resultContent; return _jsonConverterService.Deserialize(resultContent)!; } MollieErrorMessage errorDetails = ParseMollieErrorMessage(response.StatusCode, resultContent); throw new MollieApiException(errorDetails); } protected void ValidateApiKeyIsOauthAccesstoken(bool isConstructor = false) { string apiKey = _mollieSecretManager.GetBearerToken(); if (!apiKey.StartsWith("access_")) { if (isConstructor) { throw new InvalidOperationException( "The provided token isn't an oauth token. Are you trying to use oauth specific clients using an API key?"); } throw new InvalidOperationException( "The provided token isn't an oauth token. Are you trying to use oauth specific parameters such as ProfileId or TestMode using an API key?"); } } private void ValidateUrlLink(UrlLink urlObject) { // Make sure the URL is not empty if (string.IsNullOrEmpty(urlObject.Href)) { throw new ArgumentException($"Url object is null or href is empty: {urlObject}"); } // Don't execute any requests that don't point to the Mollie API URL for security reasons if (!urlObject.Href.Contains(_apiEndpoint)) { throw new ArgumentException($"Url does not point to the Mollie API: {urlObject.Href}"); } } protected virtual HttpRequestMessage CreateHttpRequest(HttpMethod method, string relativeUri, HttpContent? content = null) { var httpRequest = new HttpRequestMessage(method, new Uri(new Uri(_apiEndpoint), relativeUri)); httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _mollieSecretManager.GetBearerToken()); httpRequest.Headers.Add("User-Agent", GetUserAgent()); var idemPotencyKey = _idempotencyKey.Value ?? Guid.NewGuid().ToString(); httpRequest.Headers.Add("Idempotency-Key", idemPotencyKey); httpRequest.Content = content; return httpRequest; } private string BuildListQueryString(string? from, int? limit, IDictionary? otherParameters = null) { var queryParameters = new Dictionary(); queryParameters.AddValueIfNotNullOrEmpty(nameof(from), from); queryParameters.AddValueIfNotNullOrEmpty(nameof(limit), Convert.ToString(limit)); if (otherParameters != null) { foreach (var parameter in otherParameters) { queryParameters.AddValueIfNotNullOrEmpty(parameter.Key, parameter.Value); } } return queryParameters.ToQueryString(); } private string GetUserAgent() { const string packageName = "Mollie.Api.NET"; string versionNumber = typeof(BaseMollieClient).GetTypeInfo().Assembly.GetName().Version?.ToString() ?? "unknown"; string userAgent = $"{packageName}/{versionNumber}"; if (!string.IsNullOrEmpty(_options.CustomUserAgent)) { userAgent = $"{userAgent} {_options.CustomUserAgent}"; } return userAgent; } private MollieErrorMessage ParseMollieErrorMessage(HttpStatusCode responseStatusCode, string responseBody) { try { return _jsonConverterService.Deserialize(responseBody)!; } catch (JsonException) { return new MollieErrorMessage { Title = "Unknown error", Status = (int)responseStatusCode, Detail = responseBody }; } } protected void ValidateRequiredUrlParameter(string parameterName, string parameterValue) { if (string.IsNullOrWhiteSpace(parameterValue)) { throw new ArgumentException($"Required URL argument '{parameterName}' is null or empty"); } } protected Dictionary BuildQueryParameters( string? profileId = null, bool testmode = false, SortDirection? sort = null) { var result = new Dictionary(); result.AddValueIfTrue(nameof(testmode), testmode || _options.Testmode == true); result.AddValueIfNotNullOrEmpty(nameof(profileId), profileId ?? _options.ProfileId); result.AddValueIfNotNullOrEmpty(nameof(sort), sort?.ToString()?.ToLowerInvariant()); return result; } protected TestmodeModel? CreateTestmodeModel(bool testmode) { return TestmodeModel.Create(testmode || _options.Testmode == true); } public void Dispose() { if (_createdHttpClient) { _httpClient.Dispose(); } } } } ================================================ FILE: src/Mollie.Api/Client/CapabilityClient.cs ================================================ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models.Capability.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Options; namespace Mollie.Api.Client; public class CapabilityClient : BaseMollieClient, ICapabilityClient { public CapabilityClient(string oauthAccessToken, HttpClient? httpClient = null) : base(oauthAccessToken, httpClient) { } [ActivatorUtilitiesConstructor] public CapabilityClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task> GetCapabilitiesListAsync(CancellationToken cancellationToken = default) { return await GetListAsync>( "capabilities", from: null, limit: null, cancellationToken: cancellationToken) .ConfigureAwait(false); } } ================================================ FILE: src/Mollie.Api/Client/CaptureClient.cs ================================================ using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models.Capture.Request; using Mollie.Api.Models.Capture.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Url; using Mollie.Api.Options; namespace Mollie.Api.Client { public class CaptureClient : BaseMollieClient, ICaptureClient { public CaptureClient(string apiKey, HttpClient? httpClient = null) : base(apiKey, httpClient) { } [ActivatorUtilitiesConstructor] public CaptureClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task GetCaptureAsync( string paymentId, string captureId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(paymentId), paymentId); ValidateRequiredUrlParameter(nameof(captureId), captureId); var queryParameters = BuildQueryParameters(testmode: testmode); return await GetAsync( $"payments/{paymentId}/captures/{captureId}{queryParameters.ToQueryString()}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetCaptureAsync( UrlObjectLink url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetCaptureListAsync( string paymentId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(paymentId), paymentId); var queryParameters = BuildQueryParameters(testmode: testmode); return await GetAsync>( $"payments/{paymentId}/captures{queryParameters.ToQueryString()}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetCaptureListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task CreateCapture(string paymentId, CaptureRequest captureRequest, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(paymentId), paymentId); return await PostAsync( $"payments/{paymentId}/captures", captureRequest, cancellationToken: cancellationToken) .ConfigureAwait(false); } } } ================================================ FILE: src/Mollie.Api/Client/ChargebackClient.cs ================================================ using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models.Chargeback.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Url; using Mollie.Api.Options; namespace Mollie.Api.Client { public class ChargebackClient : BaseMollieClient, IChargebackClient { public ChargebackClient(string apiKey, HttpClient? httpClient = null) : base(apiKey, httpClient) { } [ActivatorUtilitiesConstructor] public ChargebackClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task GetChargebackAsync(string paymentId, string chargebackId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(paymentId), paymentId); ValidateRequiredUrlParameter(nameof(chargebackId), chargebackId); var queryParameters = BuildQueryParameters(testmode: testmode); return await GetAsync( $"payments/{paymentId}/chargebacks/{chargebackId}{queryParameters.ToQueryString()}", cancellationToken) .ConfigureAwait(false); } public async Task> GetChargebackListAsync(string paymentId, string? from = null, int? limit = null, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(paymentId), paymentId); var queryParameters = BuildQueryParameters(testmode: testmode); return await GetListAsync>( $"payments/{paymentId}/chargebacks", from, limit, queryParameters, cancellationToken) .ConfigureAwait(false); } public async Task> GetChargebackListAsync(string? profileId = null, bool testmode = false, CancellationToken cancellationToken = default) { var queryParameters = BuildQueryParameters(profileId, testmode); return await GetListAsync>( "chargebacks", null, null, queryParameters, cancellationToken) .ConfigureAwait(false); } public async Task> GetChargebackListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken) .ConfigureAwait(false); } } } ================================================ FILE: src/Mollie.Api/Client/ClientClient.cs ================================================ using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models.Client.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Options; namespace Mollie.Api.Client { public class ClientClient : OauthBaseMollieClient, IClientClient { public ClientClient(string oauthAccessToken, HttpClient? httpClient = null) : base(oauthAccessToken, httpClient) { } [ActivatorUtilitiesConstructor] public ClientClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task GetClientAsync( string clientId, bool embedOrganization = false, bool embedOnboarding = false, bool embedCapabilities = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(clientId), clientId); var queryParameters = BuildQueryParameters(embedOrganization, embedOnboarding, embedCapabilities); return await GetAsync( $"clients/{clientId}{queryParameters.ToQueryString()}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetClientListAsync( string? from = null, int? limit = null, bool embedOrganization = false, bool embedOnboarding = false, bool embedCapabilities = false, CancellationToken cancellationToken = default) { var queryParameters = BuildQueryParameters(embedOrganization, embedOnboarding, embedCapabilities); return await GetListAsync>( "clients", from, limit, queryParameters, cancellationToken: cancellationToken) .ConfigureAwait(false); } private Dictionary BuildQueryParameters( bool embedOrganization = false, bool embedOnboarding = false, bool embedCapabilities = false) { var result = new Dictionary(); result.AddValueIfNotNullOrEmpty("embed", BuildEmbedParameter(embedOrganization, embedOnboarding, embedCapabilities)); return result; } private string BuildEmbedParameter( bool embedOrganization = false, bool embedOnboarding = false, bool embedCapabilities = false) { var includeList = new List(); includeList.AddValueIfTrue("organization", embedOrganization); includeList.AddValueIfTrue("onboarding", embedOnboarding); includeList.AddValueIfTrue("capabilities", embedCapabilities); return includeList.ToIncludeParameter(); } } } ================================================ FILE: src/Mollie.Api/Client/ClientLinkClient.cs ================================================ using System; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models.ClientLink.Request; using Mollie.Api.Models.ClientLink.Response; using System.Threading; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Options; namespace Mollie.Api.Client { public class ClientLinkClient : OauthBaseMollieClient, IClientLinkClient { private readonly string _clientId; public ClientLinkClient(string clientId, string oauthAccessToken, HttpClient? httpClient = null) : base(oauthAccessToken, httpClient) { _clientId = clientId; } [ActivatorUtilitiesConstructor] public ClientLinkClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { if (string.IsNullOrWhiteSpace(options.ClientId)) { throw new ArgumentNullException(nameof(options.ClientId)); } _clientId = options.ClientId!; } public async Task CreateClientLinkAsync(ClientLinkRequest request, CancellationToken cancellationToken = default) { return await PostAsync( "client-links", request, cancellationToken: cancellationToken) .ConfigureAwait(false); } public string GenerateClientLinkWithParameters( string clientLinkUrl, string state, List scopes, bool forceApprovalPrompt = false) { var parameters = new Dictionary { {"client_id", _clientId}, {"state", state}, {"scope", string.Join(" ", scopes)}, {"approval_prompt", forceApprovalPrompt ? "force" : "auto"} }; return clientLinkUrl + parameters.ToQueryString(); } } } ================================================ FILE: src/Mollie.Api/Client/ConnectClient.cs ================================================ using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Models.Connect.Request; using Mollie.Api.Models.Connect.Response; using Mollie.Api.Options; namespace Mollie.Api.Client { public class ConnectClient : BaseMollieClient, IConnectClient { public const string DefaultAuthorizeEndpoint = "https://my.mollie.com/oauth2/authorize"; public const string DefaultTokenEndpoint = "https://api.mollie.com/oauth2/"; private readonly string _authorizeEndPoint; private readonly string _tokenEndPoint; private readonly string _clientId; private readonly string _clientSecret; public ConnectClient(string? clientId, string? clientSecret, HttpClient? httpClient = null) : base(httpClient, DefaultTokenEndpoint) { if (string.IsNullOrWhiteSpace(clientId)) { throw new ArgumentNullException(nameof(clientId)); } if (string.IsNullOrWhiteSpace(clientSecret)) { throw new ArgumentNullException(nameof(clientSecret)); } _clientSecret = clientSecret!; _clientId = clientId!; _authorizeEndPoint = DefaultAuthorizeEndpoint; _tokenEndPoint = DefaultTokenEndpoint; } [ActivatorUtilitiesConstructor] public ConnectClient(MollieClientOptions options, HttpClient? httpClient = null) : base(httpClient, options.ConnectTokenEndPoint) { if (string.IsNullOrWhiteSpace(options.ClientId)) { throw new ArgumentNullException(nameof(options.ClientId)); } if (string.IsNullOrWhiteSpace(options.ClientSecret)) { throw new ArgumentNullException(nameof(options.ClientSecret)); } _clientSecret = options.ClientSecret!; _clientId = options.ClientId!; _authorizeEndPoint = options.ConnectOAuthAuthorizeEndPoint; _tokenEndPoint = options.ConnectTokenEndPoint; } public string GetAuthorizationUrl( string state, List scopes, string? redirectUri = null, bool forceApprovalPrompt = false, string? locale = null, string? landingPage = null) { var parameters = new Dictionary { {"client_id", _clientId}, {"state", state}, {"scope", string.Join(" ", scopes)}, {"response_type", "code"}, {"approval_prompt", forceApprovalPrompt ? "force" : "auto"} }; parameters.AddValueIfNotNullOrEmpty("redirect_uri", redirectUri); parameters.AddValueIfNotNullOrEmpty("locale", locale); parameters.AddValueIfNotNullOrEmpty("landing_page", landingPage); return _authorizeEndPoint + parameters.ToQueryString(); } public async Task GetAccessTokenAsync( TokenRequest request, CancellationToken cancellationToken = default) { return await PostAsync( "tokens", request, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task RevokeTokenAsync( RevokeTokenRequest request, CancellationToken cancellationToken = default) { await DeleteAsync( "tokens", request, cancellationToken: cancellationToken) .ConfigureAwait(false); } protected override HttpRequestMessage CreateHttpRequest( HttpMethod method, string relativeUri, HttpContent? content = null) { var httpRequest = new HttpRequestMessage(method, new Uri(new Uri(_tokenEndPoint), relativeUri)); httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic", Base64Encode($"{_clientId}:{_clientSecret}")); httpRequest.Content = content; return httpRequest; } private string Base64Encode(string value) { var bytes = Encoding.UTF8.GetBytes(value); return Convert.ToBase64String(bytes); } } } ================================================ FILE: src/Mollie.Api/Client/CustomerClient.cs ================================================ using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.Customer.Request; using Mollie.Api.Models.Customer.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment.Request; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Url; using Mollie.Api.Options; namespace Mollie.Api.Client { public class CustomerClient : BaseMollieClient, ICustomerClient { public CustomerClient(string apiKey, HttpClient? httpClient = null) : base(apiKey, httpClient) { } [ActivatorUtilitiesConstructor] public CustomerClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task CreateCustomerAsync( CustomerRequest request, CancellationToken cancellationToken = default) { return await PostAsync( "customers", request, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task UpdateCustomerAsync( string customerId, CustomerRequest request, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(customerId), customerId); return await PostAsync( $"customers/{customerId}", request, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task DeleteCustomerAsync( string customerId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(customerId), customerId); var data = CreateTestmodeModel(testmode); await DeleteAsync( $"customers/{customerId}", data, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetCustomerAsync( string customerId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(customerId), customerId); var queryParameters = BuildQueryParameters(testmode: testmode); return await GetAsync( $"customers/{customerId}{queryParameters.ToQueryString()}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetCustomerAsync(UrlObjectLink url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task> GetCustomerListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task> GetCustomerListAsync( string? from = null, int? limit = null, bool testmode = false, CancellationToken cancellationToken = default) { var queryParameters = BuildQueryParameters(testmode: testmode); return await GetListAsync>( "customers", from, limit, queryParameters, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetCustomerPaymentListAsync( string customerId, string? from = null, int? limit = null, string? profileId = null, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(customerId), customerId); var queryParameters = BuildQueryParameters(profileId, testmode); return await GetListAsync>( $"customers/{customerId}/payments", from, limit, queryParameters, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task CreateCustomerPayment( string customerId, PaymentRequest paymentRequest, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(customerId), customerId); return await PostAsync( $"customers/{customerId}/payments", paymentRequest, cancellationToken: cancellationToken) .ConfigureAwait(false); } } } ================================================ FILE: src/Mollie.Api/Client/InvoiceClient.cs ================================================ using System; using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models.Invoice.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Url; using Mollie.Api.Options; namespace Mollie.Api.Client { public class InvoiceClient : OauthBaseMollieClient, IInvoiceClient { public InvoiceClient(string oauthAccessToken, HttpClient? httpClient = null) : base(oauthAccessToken, httpClient) { } [ActivatorUtilitiesConstructor] public InvoiceClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task GetInvoiceAsync( string invoiceId, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(invoiceId), invoiceId); return await GetAsync( $"invoices/{invoiceId}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetInvoiceAsync( UrlObjectLink url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetInvoiceListAsync( string? reference = null, int? year = null, string? from = null, int? limit = null, CancellationToken cancellationToken = default) { var parameters = new Dictionary(); parameters.AddValueIfNotNullOrEmpty(nameof(reference), reference); parameters.AddValueIfNotNullOrEmpty(nameof(year), Convert.ToString(year)); return await GetListAsync>( "invoices", from, limit, parameters, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetInvoiceListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken).ConfigureAwait(false); } } } ================================================ FILE: src/Mollie.Api/Client/MandateClient.cs ================================================ using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Mandate.Request; using Mollie.Api.Models.Mandate.Response; using Mollie.Api.Models.Url; using Mollie.Api.Options; namespace Mollie.Api.Client { public class MandateClient : BaseMollieClient, IMandateClient { public MandateClient(string apiKey, HttpClient? httpClient = null) : base(apiKey, httpClient) { } [ActivatorUtilitiesConstructor] public MandateClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task GetMandateAsync( string customerId, string mandateId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(customerId), customerId); ValidateRequiredUrlParameter(nameof(mandateId), mandateId); var queryParameters = BuildQueryParameters(testmode: testmode); return await GetAsync( $"customers/{customerId}/mandates/{mandateId}{queryParameters.ToQueryString()}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetMandateListAsync( string customerId, string? from = null, int? limit = null, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(customerId), customerId); var queryParameters = BuildQueryParameters(testmode: testmode); return await GetListAsync>( $"customers/{customerId}/mandates", from, limit, queryParameters, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task CreateMandateAsync( string customerId, MandateRequest request, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(customerId), customerId); return await PostAsync( $"customers/{customerId}/mandates", request, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetMandateListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetMandateAsync( UrlObjectLink url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task RevokeMandate( string customerId, string mandateId, bool testmode = false, CancellationToken cancellationToken = default) { var data = CreateTestmodeModel(testmode); await DeleteAsync($"customers/{customerId}/mandates/{mandateId}", data, cancellationToken: cancellationToken) .ConfigureAwait(false); } } } ================================================ FILE: src/Mollie.Api/Client/MollieApiException.cs ================================================ using System; using Mollie.Api.Models.Error; namespace Mollie.Api.Client { public class MollieApiException : Exception { public MollieErrorMessage Details { get; set; } public MollieApiException(MollieErrorMessage details) : base(details.ToString()) { Details = details; } } } ================================================ FILE: src/Mollie.Api/Client/OauthBaseMollieClient.cs ================================================ using System; using System.Net.Http; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Options; namespace Mollie.Api.Client { public class OauthBaseMollieClient : BaseMollieClient { protected OauthBaseMollieClient(string oauthAccessToken, HttpClient? httpClient = null) : base(oauthAccessToken, httpClient) { ValidateApiKeyIsOauthAccesstoken(oauthAccessToken); } protected OauthBaseMollieClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { ValidateApiKeyIsOauthAccesstoken(mollieSecretManager.GetBearerToken()); } private void ValidateApiKeyIsOauthAccesstoken(string oauthAccessToken) { if (string.IsNullOrWhiteSpace(oauthAccessToken)) { throw new ArgumentNullException(nameof(oauthAccessToken), "Mollie API key cannot be empty"); } ValidateApiKeyIsOauthAccesstoken(true); } } } ================================================ FILE: src/Mollie.Api/Client/OnboardingClient.cs ================================================ using Mollie.Api.Client.Abstract; using Mollie.Api.Models.Onboarding.Request; using Mollie.Api.Models.Onboarding.Response; using System.Net.Http; using System.Threading.Tasks; using Mollie.Api.Framework.Authentication.Abstract; using System.Threading; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Options; namespace Mollie.Api.Client { public class OnboardingClient : BaseMollieClient, IOnboardingClient { public OnboardingClient(string apiKey, HttpClient? httpClient = null) : base(apiKey, httpClient) { } [ActivatorUtilitiesConstructor] public OnboardingClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task GetOnboardingStatusAsync( CancellationToken cancellationToken = default) { return await GetAsync( "onboarding/me", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task SubmitOnboardingDataAsync( SubmitOnboardingDataRequest request, CancellationToken cancellationToken = default) { await PostAsync( "onboarding/me", request, cancellationToken: cancellationToken) .ConfigureAwait(false); } } } ================================================ FILE: src/Mollie.Api/Client/OrderClient.cs ================================================ using System; using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Order.Request; using Mollie.Api.Models.Order.Request.ManageOrderLines; using Mollie.Api.Models.Order.Response; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Url; using Mollie.Api.Options; namespace Mollie.Api.Client { [Obsolete("Mollie no longer recommends using the Orders API. Please refer to the Payments API instead.")] public class OrderClient : BaseMollieClient, IOrderClient { public OrderClient(string apiKey, HttpClient? httpClient = null) : base(apiKey, httpClient) { } [ActivatorUtilitiesConstructor] public OrderClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task CreateOrderAsync( OrderRequest orderRequest, CancellationToken cancellationToken = default) { return await PostAsync("orders", orderRequest, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetOrderAsync( string orderId, bool embedPayments = false, bool embedRefunds = false, bool embedShipments = false, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(orderId), orderId); var queryParameters = BuildQueryParameters(testmode: testmode); queryParameters.AddValueIfNotNullOrEmpty("embed", BuildEmbedParameter(embedPayments, embedRefunds, embedShipments)); return await GetAsync( $"orders/{orderId}{queryParameters.ToQueryString()}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetOrderAsync( UrlObjectLink url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task UpdateOrderAsync( string orderId, OrderUpdateRequest orderUpdateRequest, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(orderId), orderId); return await PatchAsync( $"orders/{orderId}", orderUpdateRequest, cancellationToken: cancellationToken ).ConfigureAwait(false); } public async Task UpdateOrderLinesAsync( string orderId, string orderLineId, OrderLineUpdateRequest orderLineUpdateRequest, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(orderId), orderId); ValidateRequiredUrlParameter(nameof(orderLineId), orderLineId); return await PatchAsync( $"orders/{orderId}/lines/{orderLineId}", orderLineUpdateRequest, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task ManageOrderLinesAsync( string orderId, ManageOrderLinesRequest manageOrderLinesRequest, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(orderId), orderId); return await PatchAsync( $"orders/{orderId}/lines", manageOrderLinesRequest, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task CancelOrderAsync( string orderId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(orderId), orderId); var data = CreateTestmodeModel(testmode); await DeleteAsync( $"orders/{orderId}", data, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetOrderListAsync( string? from = null, int? limit = null, string? profileId = null, bool testmode = false, SortDirection? sort = null, CancellationToken cancellationToken = default) { var queryParameters = BuildQueryParameters(profileId, testmode, sort); return await GetListAsync>( "orders", from, limit, queryParameters, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetOrderListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task CancelOrderLinesAsync( string orderId, OrderLineCancellationRequest cancelationRequest, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(orderId), orderId); await DeleteAsync($"orders/{orderId}/lines", cancelationRequest, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task CreateOrderPaymentAsync( string orderId, OrderPaymentRequest createOrderPaymentRequest, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(orderId), orderId); return await PostAsync( $"orders/{orderId}/payments", createOrderPaymentRequest, cancellationToken: cancellationToken) .ConfigureAwait(false); } private string BuildEmbedParameter(bool embedPayments = false, bool embedRefunds = false, bool embedShipments = false) { var includeList = new List(); includeList.AddValueIfTrue("payments", embedPayments); includeList.AddValueIfTrue("refunds", embedRefunds); includeList.AddValueIfTrue("shipments", embedShipments); return includeList.ToIncludeParameter(); } } } ================================================ FILE: src/Mollie.Api/Client/OrganizationClient.cs ================================================ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Organization; using Mollie.Api.Models.Url; using Mollie.Api.Options; namespace Mollie.Api.Client { public class OrganizationClient : OauthBaseMollieClient, IOrganizationClient { public OrganizationClient(string oauthAccessToken, HttpClient? httpClient = null) : base(oauthAccessToken, httpClient) { } [ActivatorUtilitiesConstructor] public OrganizationClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task GetCurrentOrganizationAsync(CancellationToken cancellationToken = default) { return await GetAsync($"organizations/me", cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task GetOrganizationAsync(string organizationId, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(organizationId), organizationId); return await GetAsync($"organizations/{organizationId}", cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task GetOrganizationAsync(UrlObjectLink url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task> GetOrganizationListAsync(string? from = null, int? limit = null, CancellationToken cancellationToken = default) { return await GetListAsync>("organizations", from, limit, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task> GetOrganizationListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task GetPartnerStatusAsync(CancellationToken cancellationToken = default) { return await GetAsync($"organizations/me/partner", cancellationToken: cancellationToken) .ConfigureAwait(false); } } } ================================================ FILE: src/Mollie.Api/Client/PaymentClient.cs ================================================ using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment.Request; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Url; using Mollie.Api.Options; namespace Mollie.Api.Client { public class PaymentClient : BaseMollieClient, IPaymentClient { public PaymentClient(string apiKey, HttpClient? httpClient = null) : base(apiKey, httpClient) { } [ActivatorUtilitiesConstructor] public PaymentClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task CreatePaymentAsync( PaymentRequest paymentRequest, bool includeQrCode = false, CancellationToken cancellationToken = default) { if (!string.IsNullOrWhiteSpace(paymentRequest.ProfileId) || paymentRequest.Testmode.HasValue || paymentRequest.ApplicationFee != null) { ValidateApiKeyIsOauthAccesstoken(); } var queryParameters = BuildQueryParameters( includeQrCode: includeQrCode); return await PostAsync( $"payments{queryParameters.ToQueryString()}", paymentRequest, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task GetPaymentAsync( string paymentId, bool testmode = false, bool includeQrCode = false, bool includeRemainderDetails = false, bool embedRefunds = false, bool embedChargebacks = false, CancellationToken cancellationToken = default) { if (testmode) { ValidateApiKeyIsOauthAccesstoken(); } ValidateRequiredUrlParameter(nameof(paymentId), paymentId); var queryParameters = BuildQueryParameters( testmode: testmode, includeQrCode: includeQrCode, includeRemainderDetails: includeRemainderDetails, embedRefunds: embedRefunds, embedChargebacks: embedChargebacks ); return await GetAsync( $"payments/{paymentId}{queryParameters.ToQueryString()}", cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task CancelPaymentAsync( string paymentId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(paymentId), paymentId); var data = CreateTestmodeModel(testmode); await DeleteAsync( $"payments/{paymentId}", data, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task ReleasePaymentAuthorization( string paymentId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(paymentId), paymentId); var queryParameters = BuildQueryParameters(testmode: testmode); await PostAsync( $"payments/{paymentId}/release-authorization{queryParameters.ToQueryString()}", null, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task GetPaymentAsync( UrlObjectLink url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task> GetPaymentListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task> GetPaymentListAsync( string? from = null, int? limit = null, string? profileId = null, bool testmode = false, bool includeQrCode = false, bool embedRefunds = false, bool embedChargebacks = false, SortDirection? sort = null, CancellationToken cancellationToken = default) { if (!string.IsNullOrWhiteSpace(profileId) || testmode) { ValidateApiKeyIsOauthAccesstoken(); } var queryParameters = BuildQueryParameters( profileId: profileId, testmode: testmode, includeQrCode: includeQrCode, embedRefunds: embedRefunds, embedChargebacks: embedChargebacks, sort: sort); return await GetListAsync>( $"payments", from, limit, queryParameters, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task UpdatePaymentAsync( string paymentId, PaymentUpdateRequest paymentUpdateRequest, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(paymentId), paymentId); return await PatchAsync( $"payments/{paymentId}", paymentUpdateRequest, cancellationToken: cancellationToken).ConfigureAwait(false); } private Dictionary BuildQueryParameters( string? profileId = null, bool testmode = false, bool includeQrCode = false, bool includeRemainderDetails = false, bool embedRefunds = false, bool embedChargebacks = false, SortDirection? sort = null) { var result = base.BuildQueryParameters(profileId, testmode); result.AddValueIfNotNullOrEmpty("include", BuildIncludeParameter(includeQrCode, includeRemainderDetails)); result.AddValueIfNotNullOrEmpty("embed", BuildEmbedParameter(embedRefunds, embedChargebacks)); result.AddValueIfNotNullOrEmpty(nameof(sort), sort?.ToString()?.ToLowerInvariant()); return result; } private string BuildIncludeParameter(bool includeQrCode = false, bool includeRemainderDetails = false) { var includeList = new List(); includeList.AddValueIfTrue("details.qrCode", includeQrCode); includeList.AddValueIfTrue("details.remainderDetails", includeRemainderDetails); return includeList.ToIncludeParameter(); } private string BuildEmbedParameter(bool embedRefunds = false, bool embedChargebacks = false) { var embedList = new List(); embedList.AddValueIfTrue("refunds", embedRefunds); embedList.AddValueIfTrue("chargebacks", embedChargebacks); return embedList.ToIncludeParameter(); } } } ================================================ FILE: src/Mollie.Api/Client/PaymentLinkClient.cs ================================================ using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.PaymentLink.Request; using Mollie.Api.Models.PaymentLink.Response; using Mollie.Api.Models.Url; using Mollie.Api.Options; namespace Mollie.Api.Client { public class PaymentLinkClient : BaseMollieClient, IPaymentLinkClient { public PaymentLinkClient(string apiKey, HttpClient? httpClient = null) : base(apiKey, httpClient) { } [ActivatorUtilitiesConstructor] public PaymentLinkClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task CreatePaymentLinkAsync( PaymentLinkRequest paymentLinkRequest, CancellationToken cancellationToken = default) { if (!string.IsNullOrWhiteSpace(paymentLinkRequest.ProfileId) || paymentLinkRequest.Testmode.HasValue) { ValidateApiKeyIsOauthAccesstoken(); } return await PostAsync( $"payment-links", paymentLinkRequest, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task UpdatePaymentLinkAsync( string paymentLinkId, PaymentLinkUpdateRequest paymentLinkUpdateRequest, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(paymentLinkId), paymentLinkId); if (paymentLinkUpdateRequest.Testmode.HasValue) { ValidateApiKeyIsOauthAccesstoken(); } string relativeUri = $"payment-links/{paymentLinkId}"; return await PatchAsync( relativeUri, paymentLinkUpdateRequest, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task DeletePaymentLinkAsync( string paymentLinkId, string? profileId = null, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(paymentLinkId), paymentLinkId); var queryParameters = BuildQueryParameters(profileId); var data = CreateTestmodeModel(testmode); string relativeUri = $"payment-links/{paymentLinkId}{queryParameters.ToQueryString()}"; await DeleteAsync( relativeUri, data, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task GetPaymentLinkAsync( string paymentLinkId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(paymentLinkId), paymentLinkId); if (testmode) { ValidateApiKeyIsOauthAccesstoken(); } var queryParameters = BuildQueryParameters( testmode: testmode); return await GetAsync( $"payment-links/{paymentLinkId}{queryParameters.ToQueryString()}", cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task GetPaymentLinkAsync( UrlObjectLink url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task> GetPaymentLinkListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task> GetPaymentLinkListAsync( string? from = null, int? limit = null, string? profileId = null, bool testmode = false, CancellationToken cancellationToken = default) { if (!string.IsNullOrWhiteSpace(profileId) || testmode) { ValidateApiKeyIsOauthAccesstoken(); } var queryParameters = BuildQueryParameters( profileId: profileId, testmode: testmode); return await GetListAsync>( "payment-links", from, limit, queryParameters, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task> GetPaymentLinkPaymentListAsync( string paymentLinkId, string? from = null, int? limit = null, bool testmode = false, SortDirection? sort = null, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(paymentLinkId), paymentLinkId); if (testmode) { ValidateApiKeyIsOauthAccesstoken(); } var queryParameters = BuildQueryParameters( testmode: testmode, sort: sort); return await GetListAsync>( $"payment-links/{paymentLinkId}/payments", from, limit, queryParameters, cancellationToken: cancellationToken).ConfigureAwait(false); } } } ================================================ FILE: src/Mollie.Api/Client/PaymentMethodClient.cs ================================================ using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment; using Mollie.Api.Models.PaymentMethod.Response; using Mollie.Api.Models.Url; using Mollie.Api.Options; namespace Mollie.Api.Client { public class PaymentMethodClient : BaseMollieClient, IPaymentMethodClient { public PaymentMethodClient(string apiKey, HttpClient? httpClient = null) : base(apiKey, httpClient) { } [ActivatorUtilitiesConstructor] public PaymentMethodClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task GetPaymentMethodAsync( string paymentMethod, bool includeIssuers = false, string? locale = null, string? profileId = null, bool testmode = false, string? currency = null, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(paymentMethod), paymentMethod); Dictionary queryParameters = BuildQueryParameters( locale: locale, currency: currency, profileId: profileId, testmode: testmode, includeIssuers: includeIssuers); return await GetAsync( $"methods/{paymentMethod.ToLower()}{queryParameters.ToQueryString()}", cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task> GetAllPaymentMethodListAsync( string? locale = null, Amount? amount = null, bool includeIssuers = false, bool includePricing = false, string? profileId = null, bool testmode = false, CancellationToken cancellationToken = default) { Dictionary queryParameters = BuildQueryParameters( locale: locale, amount: amount, includeIssuers: includeIssuers, includePricing: includePricing, profileId: profileId, testmode: testmode); return await GetListAsync>( "methods/all", null, null, queryParameters, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task> GetPaymentMethodListAsync( string? sequenceType = null, string? locale = null, Amount? amount = null, bool includeIssuers = false, string? profileId = null, bool testmode = false, Resource? resource = null, string? billingCountry = null, string? includeWallets = null, CancellationToken cancellationToken = default) { Dictionary queryParameters = BuildQueryParameters( sequenceType: sequenceType, locale: locale, amount: amount, includeIssuers: includeIssuers, resource: resource, profileId: profileId, testmode: testmode, billingCountry: billingCountry, includeWallets: includeWallets); return await GetListAsync>( "methods", null, null, queryParameters, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task GetPaymentMethodAsync( UrlObjectLink url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken).ConfigureAwait(false); } private Dictionary BuildQueryParameters( string? sequenceType = null, string? locale = null, Amount? amount = null, bool includeIssuers = false, bool includePricing = false, string? profileId = null, bool testmode = false, Resource? resource = null, string? currency = null, string? billingCountry = null, string? includeWallets = null) { var result = base.BuildQueryParameters(profileId, testmode); result.AddValueIfNotNullOrEmpty(nameof(sequenceType), sequenceType?.ToLower()); result.AddValueIfNotNullOrEmpty(nameof(locale), locale); result.AddValueIfNotNullOrEmpty("amount[currency]", amount?.Currency); result.AddValueIfNotNullOrEmpty("amount[value]", amount?.Value); result.AddValueIfNotNullOrEmpty("include", BuildIncludeParameter(includeIssuers, includePricing)); result.AddValueIfNotNullOrEmpty(nameof(resource), resource?.ToString()?.ToLower()); result.AddValueIfNotNullOrEmpty(nameof(currency), currency); result.AddValueIfNotNullOrEmpty(nameof(billingCountry), billingCountry); result.AddValueIfNotNullOrEmpty(nameof(includeWallets), includeWallets); return result; } private string BuildIncludeParameter(bool includeIssuers = false, bool includePricing = false) { var includeList = new List(); includeList.AddValueIfTrue("issuers", includeIssuers); includeList.AddValueIfTrue("pricing", includePricing); return includeList.ToIncludeParameter(); } } } ================================================ FILE: src/Mollie.Api/Client/PermissionClient.cs ================================================ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Permission.Response; using Mollie.Api.Models.Url; using Mollie.Api.Options; namespace Mollie.Api.Client { public class PermissionClient : OauthBaseMollieClient, IPermissionClient { public PermissionClient(string oauthAccessToken, HttpClient? httpClient = null) : base(oauthAccessToken, httpClient) { } [ActivatorUtilitiesConstructor] public PermissionClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task GetPermissionAsync( string permissionId, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(permissionId), permissionId); return await GetAsync( $"permissions/{permissionId}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetPermissionAsync( UrlObjectLink url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetPermissionListAsync( CancellationToken cancellationToken = default) { return await GetListAsync>( "permissions", null, null, cancellationToken: cancellationToken) .ConfigureAwait(false); } } } ================================================ FILE: src/Mollie.Api/Client/ProfileClient.cs ================================================ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.PaymentMethod.Response; using Mollie.Api.Models.Profile.Request; using Mollie.Api.Models.Profile.Response; using Mollie.Api.Models.Url; using Mollie.Api.Options; namespace Mollie.Api.Client { public class ProfileClient : BaseMollieClient, IProfileClient { public ProfileClient(string apiKey, HttpClient? httpClient = null) : base(apiKey, httpClient) { } [ActivatorUtilitiesConstructor] public ProfileClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task CreateProfileAsync( ProfileRequest request, CancellationToken cancellationToken = default) { return await PostAsync( "profiles", request, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetProfileAsync( string profileId, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(profileId), profileId); return await GetAsync( $"profiles/{profileId}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetProfileAsync( UrlObjectLink url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetCurrentProfileAsync(CancellationToken cancellationToken = default) { return await GetAsync( "profiles/me", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetProfileListAsync( string? from = null, int? limit = null, CancellationToken cancellationToken = default) { return await GetListAsync>( "profiles", from, limit, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetProfileListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task UpdateProfileAsync( string profileId, ProfileRequest request, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(profileId), profileId); return await PatchAsync( $"profiles/{profileId}", request, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task EnablePaymentMethodAsync( string profileId, string paymentMethod, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(profileId), profileId); ValidateRequiredUrlParameter(nameof(paymentMethod), paymentMethod); return await PostAsync( $"profiles/{profileId}/methods/{paymentMethod}", null, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task EnablePaymentMethodAsync( string paymentMethod, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(paymentMethod), paymentMethod); return await PostAsync( $"profiles/me/methods/{paymentMethod}", null, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task DisablePaymentMethodAsync( string profileId, string paymentMethod, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(profileId), profileId); ValidateRequiredUrlParameter(nameof(paymentMethod), paymentMethod); await DeleteAsync($"profiles/{profileId}/methods/{paymentMethod}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task DisablePaymentMethodAsync(string paymentMethod, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(paymentMethod), paymentMethod); await DeleteAsync($"profiles/me/methods/{paymentMethod}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task DeleteProfileAsync(string profileId, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(profileId), profileId); await DeleteAsync($"profiles/{profileId}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task EnableGiftCardIssuerAsync( string profileId, string issuer, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(profileId), profileId); ValidateRequiredUrlParameter(nameof(issuer), issuer); return await PostAsync( $"profiles/{profileId}/methods/giftcard/issuers/{issuer}", null, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task EnableGiftCardIssuerAsync( string issuer, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(issuer), issuer); return await PostAsync( $"profiles/me/methods/giftcard/issuers/{issuer}", null, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task DisableGiftCardIssuerAsync( string profileId, string issuer, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(profileId), profileId); ValidateRequiredUrlParameter(nameof(issuer), issuer); await DeleteAsync( $"profiles/{profileId}/methods/giftcard/issuers/{issuer}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task DisableGiftCardIssuerAsync( string issuer, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(issuer), issuer); await DeleteAsync( $"profiles/me/methods/giftcard/issuers/{issuer}", cancellationToken: cancellationToken) .ConfigureAwait(false); } } } ================================================ FILE: src/Mollie.Api/Client/RefundClient.cs ================================================ using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using System.Threading; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Order.Request; using Mollie.Api.Models.Order.Response; using Mollie.Api.Models.Refund.Request; using Mollie.Api.Models.Refund.Response; using Mollie.Api.Models.Url; using Mollie.Api.Options; namespace Mollie.Api.Client { public class RefundClient : BaseMollieClient, IRefundClient { public RefundClient(string apiKey, HttpClient? httpClient = null) : base(apiKey, httpClient) { } [ActivatorUtilitiesConstructor] public RefundClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task CreatePaymentRefundAsync( string paymentId, RefundRequest refundRequest, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(paymentId), paymentId); if (refundRequest.Testmode.HasValue) { ValidateApiKeyIsOauthAccesstoken(); } return await PostAsync( $"payments/{paymentId}/refunds", refundRequest, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetRefundListAsync( string? from = null, int? limit = null, bool testmode = false, CancellationToken cancellationToken = default) { var queryParameters = BuildQueryParameters(testmode: testmode); return await GetListAsync>( "refunds", from, limit, queryParameters, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetPaymentRefundListAsync( string paymentId, string? from = null, int? limit = null, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(paymentId), paymentId); var queryParameters = BuildQueryParameters(testmode: testmode); return await GetListAsync>( $"payments/{paymentId}/refunds", from, limit, queryParameters, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetRefundListAsync(UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task GetRefundAsync(UrlObjectLink url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task GetPaymentRefundAsync( string paymentId, string refundId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(paymentId), paymentId); ValidateRequiredUrlParameter(nameof(refundId), refundId); var queryParameters = BuildQueryParameters(testmode: testmode); return await GetAsync( $"payments/{paymentId}/refunds/{refundId}{queryParameters.ToQueryString()}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task CancelPaymentRefundAsync( string paymentId, string refundId, bool testmode = default, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(paymentId), paymentId); ValidateRequiredUrlParameter(nameof(refundId), refundId); var queryParameters = BuildQueryParameters(testmode: testmode); await DeleteAsync($"payments/{paymentId}/refunds/{refundId}{queryParameters.ToQueryString()}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task CreateOrderRefundAsync( string orderId, OrderRefundRequest createOrderRefundRequest, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(orderId), orderId); return await PostAsync( $"orders/{orderId}/refunds", createOrderRefundRequest, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetOrderRefundListAsync( string orderId, string? from = null, int? limit = null, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(orderId), orderId); var queryParameters = BuildQueryParameters(testmode: testmode); return await GetListAsync>( $"orders/{orderId}/refunds", from, limit, queryParameters, cancellationToken: cancellationToken) .ConfigureAwait(false); } } } ================================================ FILE: src/Mollie.Api/Client/SalesInvoiceClient.cs ================================================ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.SalesInvoice.Request; using Mollie.Api.Models.SalesInvoice.Response; using Mollie.Api.Options; using Mollie.Api.Models.Url; namespace Mollie.Api.Client; public class SalesInvoiceClient : BaseMollieClient, ISalesInvoiceClient { public SalesInvoiceClient(string apiKey, HttpClient? httpClient = null) : base(apiKey, httpClient) { } [ActivatorUtilitiesConstructor] public SalesInvoiceClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task CreateSalesInvoiceAsync( SalesInvoiceRequest salesInvoiceRequest, CancellationToken cancellationToken = default) { return await PostAsync($"sales-invoices", salesInvoiceRequest, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetSalesInvoiceListAsync( string? from = null, int? limit = null, bool testmode = false, CancellationToken cancellationToken = default) { var queryParameters = BuildQueryParameters(testmode: testmode); return await GetListAsync>("sales-invoices", from, limit, queryParameters, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetSalesInvoiceListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task GetSalesInvoiceAsync( string salesInvoiceId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(salesInvoiceId), salesInvoiceId); var queryParameters = BuildQueryParameters(testmode: testmode); return await GetAsync($"sales-invoices/{salesInvoiceId}{queryParameters.ToQueryString()}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetSalesInvoiceAsync( UrlObjectLink url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task UpdateSalesInvoiceAsync( string salesInvoiceId, SalesInvoiceUpdateRequest salesInvoiceRequest, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(salesInvoiceId), salesInvoiceId); return await PatchAsync($"sales-invoices/{salesInvoiceId}", salesInvoiceRequest, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task DeleteSalesInvoiceAsync(string salesInvoiceId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(salesInvoiceId), salesInvoiceId); var data = CreateTestmodeModel(testmode); await DeleteAsync($"sales-invoices/{salesInvoiceId}", data, cancellationToken: cancellationToken).ConfigureAwait(false); } } ================================================ FILE: src/Mollie.Api/Client/SessionClient.cs ================================================ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models.Session.Request; using Mollie.Api.Models.Session.Response; using Mollie.Api.Options; namespace Mollie.Api.Client { public class SessionClient : BaseMollieClient, ISessionClient { public SessionClient(string apiKey, HttpClient? httpClient = null) : base(apiKey, httpClient) { } [ActivatorUtilitiesConstructor] public SessionClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task GetSessionAsync( string sessionId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(sessionId), sessionId); var queryParameters = BuildQueryParameters(testmode: testmode); return await GetAsync( $"sessions/{sessionId}{queryParameters.ToQueryString()}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task CreateSessionAsync( SessionRequest request, CancellationToken cancellationToken = default) { return await PostAsync( $"sessions", request, cancellationToken: cancellationToken) .ConfigureAwait(false); } } } ================================================ FILE: src/Mollie.Api/Client/SettlementClient.cs ================================================ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models.Capture.Response; using Mollie.Api.Models.Chargeback.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Refund.Response; using Mollie.Api.Models.Settlement.Response; using Mollie.Api.Models.Url; using Mollie.Api.Options; namespace Mollie.Api.Client { public class SettlementClient : BaseMollieClient, ISettlementClient { public SettlementClient(string oauthAccessToken, HttpClient? httpClient = null) : base(oauthAccessToken, httpClient) { } [ActivatorUtilitiesConstructor] public SettlementClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task GetSettlementAsync( string settlementId, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(settlementId), settlementId); return await GetAsync( $"settlements/{settlementId}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetNextSettlement(CancellationToken cancellationToken = default) { return await GetAsync( "settlements/next", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetOpenSettlement(CancellationToken cancellationToken = default) { return await GetAsync( $"settlements/open", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetSettlementListAsync( string? reference = null, string? offset = null, int? count = null, CancellationToken cancellationToken = default) { var queryString = !string.IsNullOrWhiteSpace(reference) ? $"?reference={reference}" : string.Empty; return await GetListAsync>( $"settlements{queryString}", offset, count, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetSettlementListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetSettlementPaymentListAsync( string settlementId, string? offset = null, int? count = null, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(settlementId), settlementId); return await GetListAsync>( $"settlements/{settlementId}/payments", offset, count, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetSettlementPaymentListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task> GetSettlementRefundListAsync( string settlementId, string? offset = null, int? count = null, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(settlementId), settlementId); return await GetListAsync>( $"settlements/{settlementId}/refunds", offset, count, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetSettlementRefundListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetSettlementChargebackListAsync( string settlementId, string? offset = null, int? count = null, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(settlementId), settlementId); return await GetListAsync>( $"settlements/{settlementId}/chargebacks", offset, count, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetSettlementChargebackListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetSettlementCaptureListAsync( string settlementId, string? offset = null, int? count = null, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(settlementId), settlementId); return await GetListAsync>( $"settlements/{settlementId}/captures", offset, count, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetSettlementCaptureListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetSettlementAsync( UrlObjectLink url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken) .ConfigureAwait(false); } } } ================================================ FILE: src/Mollie.Api/Client/ShipmentClient.cs ================================================ using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Shipment.Request; using Mollie.Api.Models.Shipment.Response; using Mollie.Api.Models.Url; using Mollie.Api.Options; namespace Mollie.Api.Client { public class ShipmentClient : BaseMollieClient, IShipmentClient { public ShipmentClient(string apiKey, HttpClient? httpClient = null) : base(apiKey, httpClient) { } [ActivatorUtilitiesConstructor] public ShipmentClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task CreateShipmentAsync( string orderId, ShipmentRequest shipmentRequest, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(orderId), orderId); return await PostAsync( $"orders/{orderId}/shipments", shipmentRequest, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetShipmentAsync( string orderId, string shipmentId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(orderId), orderId); ValidateRequiredUrlParameter(nameof(shipmentId), shipmentId); var queryParameters = BuildQueryParameters(testmode: testmode); return await GetAsync( $"orders/{orderId}/shipments/{shipmentId}{queryParameters.ToQueryString()}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetShipmentAsync( UrlObjectLink url, CancellationToken cancellationToken = default) { return await GetAsync(url).ConfigureAwait(false); } public async Task> GetShipmentListAsync( string orderId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(orderId), orderId); var queryParameters = BuildQueryParameters(testmode: testmode); return await GetAsync>( $"orders/{orderId}/shipments{queryParameters.ToQueryString()}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetShipmentListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task UpdateShipmentAsync( string orderId, string shipmentId, ShipmentUpdateRequest shipmentUpdateRequest, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(orderId), orderId); ValidateRequiredUrlParameter(nameof(shipmentId), shipmentId); return await PatchAsync( $"orders/{orderId}/shipments/{shipmentId}", shipmentUpdateRequest, cancellationToken: cancellationToken) .ConfigureAwait(false); } } } ================================================ FILE: src/Mollie.Api/Client/SubscriptionClient.cs ================================================ using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Subscription.Request; using Mollie.Api.Models.Subscription.Response; using Mollie.Api.Models.Url; using Mollie.Api.Options; namespace Mollie.Api.Client { public class SubscriptionClient : BaseMollieClient, ISubscriptionClient { public SubscriptionClient(string apiKey, HttpClient? httpClient = null) : base(apiKey, httpClient) { } [ActivatorUtilitiesConstructor] public SubscriptionClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task> GetSubscriptionListAsync( string customerId, string? from = null, int? limit = null, string? profileId = null, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(customerId), customerId); var queryParameters = BuildQueryParameters(profileId, testmode); return await GetListAsync>( $"customers/{customerId}/subscriptions", from, limit, queryParameters, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetAllSubscriptionList( string? from = null, int? limit = null, string? profileId = null, bool testmode = false, CancellationToken cancellationToken = default) { var queryParameters = BuildQueryParameters(profileId, testmode); return await GetListAsync>( "subscriptions", from, limit, queryParameters, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetSubscriptionListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetSubscriptionAsync( UrlObjectLink url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetSubscriptionAsync( string customerId, string subscriptionId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(customerId), customerId); ValidateRequiredUrlParameter(nameof(subscriptionId), subscriptionId); var queryParameters = BuildQueryParameters(testmode: testmode); return await GetAsync( $"customers/{customerId}/subscriptions/{subscriptionId}{queryParameters.ToQueryString()}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task CreateSubscriptionAsync( string customerId, SubscriptionRequest request, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(customerId), customerId); return await PostAsync( $"customers/{customerId}/subscriptions", request, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task CancelSubscriptionAsync( string customerId, string subscriptionId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(customerId), customerId); ValidateRequiredUrlParameter(nameof(subscriptionId), subscriptionId); var data = CreateTestmodeModel(testmode); await DeleteAsync( $"customers/{customerId}/subscriptions/{subscriptionId}", data, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task UpdateSubscriptionAsync( string customerId, string subscriptionId, SubscriptionUpdateRequest request, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(customerId), customerId); ValidateRequiredUrlParameter(nameof(subscriptionId), subscriptionId); return await PatchAsync( $"customers/{customerId}/subscriptions/{subscriptionId}", request, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetSubscriptionPaymentListAsync( string customerId, string subscriptionId, string? from = null, int? limit = null, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(customerId), customerId); ValidateRequiredUrlParameter(nameof(subscriptionId), subscriptionId); var queryParameters = BuildQueryParameters(testmode: testmode); return await GetListAsync>( $"customers/{customerId}/subscriptions/{subscriptionId}/payments", from, limit, queryParameters, cancellationToken: cancellationToken) .ConfigureAwait(false); } } } ================================================ FILE: src/Mollie.Api/Client/TerminalClient.cs ================================================ using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Terminal.Response; using Mollie.Api.Models.Url; using Mollie.Api.Options; namespace Mollie.Api.Client { public class TerminalClient : BaseMollieClient, ITerminalClient { public TerminalClient(string apiKey, HttpClient? httpClient = null) : base(apiKey, httpClient) { } [ActivatorUtilitiesConstructor] public TerminalClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task GetTerminalAsync( string terminalId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(terminalId), terminalId); var queryParameters = BuildQueryParameters(testmode: testmode); return await GetAsync( $"terminals/{terminalId}{queryParameters.ToQueryString()}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetTerminalAsync( UrlObjectLink url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken).ConfigureAwait(false); } public async Task> GetTerminalListAsync( string? from = null, int? limit = null, string? profileId = null, bool testmode = false, CancellationToken cancellationToken = default) { var queryParameters = BuildQueryParameters(profileId, testmode); return await GetListAsync>( "terminals", from, limit, queryParameters, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetTerminalListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken) .ConfigureAwait(false); } } } ================================================ FILE: src/Mollie.Api/Client/WalletClient.cs ================================================ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models.Wallet.Request; using Mollie.Api.Models.Wallet.Response; using Mollie.Api.Options; namespace Mollie.Api.Client { public class WalletClient : BaseMollieClient, IWalletClient { public WalletClient(string apiKey, HttpClient? httpClient = null) : base(apiKey, httpClient) { } [ActivatorUtilitiesConstructor] public WalletClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task RequestApplePayPaymentSessionAsync( ApplePayPaymentSessionRequest request, CancellationToken cancellationToken = default) { return await PostAsync( "wallets/applepay/sessions", request, cancellationToken: cancellationToken) .ConfigureAwait(false); } } } ================================================ FILE: src/Mollie.Api/Client/WebhookClient.cs ================================================ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Url; using Mollie.Api.Models.Webhook.Request; using Mollie.Api.Models.Webhook.Response; using Mollie.Api.Options; namespace Mollie.Api.Client; public class WebhookClient : BaseMollieClient, IWebhookClient { public WebhookClient(string apiKey, HttpClient? httpClient = null) : base(apiKey, httpClient) { } [ActivatorUtilitiesConstructor] public WebhookClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task CreateWebhookAsync(WebhookRequest request, CancellationToken cancellationToken = default) { return await PostAsync("webhooks", request, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetWebhookListAsync(string? from = null, int? limit = null, bool testmode = false, CancellationToken cancellationToken = default) { var queryParameters = BuildQueryParameters(testmode: testmode); return await GetListAsync>( "webhooks", from, limit, queryParameters, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetWebhookListAsync( UrlObjectLink> url, CancellationToken cancellationToken = default) { return await GetAsync(url, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task GetWebhookAsync(string webhookId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(webhookId), webhookId); var queryParameters = BuildQueryParameters(testmode: testmode); return await GetAsync($"webhooks/{webhookId}{queryParameters.ToQueryString()}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task UpdateWebhookAsync(string webhookId, WebhookRequest request, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(webhookId), webhookId); return await PatchAsync($"webhooks/{webhookId}", request, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task DeleteWebhookAsync(string webhookId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(webhookId), webhookId); var data = CreateTestmodeModel(testmode); await DeleteAsync($"webhooks/{webhookId}", data, cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task TestWebhookAsync(string webhookId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(webhookId), webhookId); var queryParameters = BuildQueryParameters(testmode: testmode); await PostAsync($"webhooks/{webhookId}/ping{queryParameters.ToQueryString()}", null, cancellationToken: cancellationToken); } } ================================================ FILE: src/Mollie.Api/Client/WebhookEventClient.cs ================================================ using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.WebhookEvent.Response; using Mollie.Api.Options; namespace Mollie.Api.Client; public class WebhookEventClient : BaseMollieClient, IWebhookEventClient { public WebhookEventClient(string apiKey, HttpClient? httpClient = null) : base(apiKey, httpClient) { } [ActivatorUtilitiesConstructor] public WebhookEventClient(MollieClientOptions options, IMollieSecretManager mollieSecretManager, HttpClient? httpClient = null) : base(options, mollieSecretManager, httpClient) { } public async Task GetWebhookEventAsync(string webhookEventId, bool testmode = false, CancellationToken cancellationToken = default) { ValidateRequiredUrlParameter(nameof(webhookEventId), webhookEventId); var queryParameters = BuildQueryParameters(testmode); return await GetAsync($"events/{webhookEventId}{queryParameters.ToQueryString()}", cancellationToken: cancellationToken) .ConfigureAwait(false); } public async Task> GetWebhookEventAsync(string webhookEventId, bool testmode = false, CancellationToken cancellationToken = default) where T : IEntity { ValidateRequiredUrlParameter(nameof(webhookEventId), webhookEventId); var queryParameters = BuildQueryParameters(testmode); return await GetAsync>($"events/{webhookEventId}{queryParameters.ToQueryString()}", cancellationToken: cancellationToken) .ConfigureAwait(false); } private Dictionary BuildQueryParameters(bool testmode = false) { var result = new Dictionary(); result.AddValueIfTrue("testmode", testmode); return result; } } ================================================ FILE: src/Mollie.Api/DependencyInjection.cs ================================================ using System; using System.Net.Http; using Microsoft.Extensions.DependencyInjection; using Mollie.Api.Client; using Mollie.Api.Client.Abstract; using Mollie.Api.Framework.Authentication; using Mollie.Api.Framework.Authentication.Abstract; using Mollie.Api.Options; using Polly; namespace Mollie.Api { public static class DependencyInjection { public static IServiceCollection AddMollieApi( this IServiceCollection services, Action mollieOptionsDelegate) { MollieOptions mollieOptions = new(); mollieOptionsDelegate.Invoke(mollieOptions); if (mollieOptions.CustomMollieSecretManager != null) { services.AddScoped(typeof(IMollieSecretManager), mollieOptions.CustomMollieSecretManager); } else { services.AddScoped(_ => new DefaultMollieSecretManager(mollieOptions.ApiKey)); } MollieClientOptions mollieClientOptions = new() { ApiKey = mollieOptions.ApiKey, ClientId = mollieOptions.ClientId, ClientSecret = mollieOptions.ClientSecret, CustomUserAgent = mollieOptions.CustomUserAgent, Testmode = mollieOptions.Testmode, ProfileId = mollieOptions.ProfileId, ApiBaseUrl = mollieOptions.ApiBaseUrl, ConnectTokenEndPoint = mollieOptions.ConnectTokenEndPoint, ConnectOAuthAuthorizeEndPoint = mollieOptions.ConnectOAuthAuthorizeEndPoint }; services.AddSingleton(mollieClientOptions); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); #pragma warning disable CS0618 RegisterMollieApiClient(services, mollieOptions.RetryPolicy); #pragma warning restore CS0618 RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); RegisterMollieApiClient(services, mollieOptions.RetryPolicy); return services; } static void RegisterMollieApiClient( IServiceCollection services, IAsyncPolicy? retryPolicy = null) where TInterface : class where TImplementation : class, TInterface { IHttpClientBuilder clientBuilder = services.AddHttpClient(); if (retryPolicy != null) { clientBuilder.AddPolicyHandler(retryPolicy); } } } } ================================================ FILE: src/Mollie.Api/Extensions/DateTimeExtensions.cs ================================================ using System; namespace Mollie.Api.Extensions { internal static class DateTimeExtensions { public static DateTime Truncate(this DateTime dateTime, TimeSpan timeSpan) { if (timeSpan == TimeSpan.Zero) { return dateTime; } if (dateTime == DateTime.MinValue || dateTime == DateTime.MaxValue) { return dateTime; } return dateTime.AddTicks(-(dateTime.Ticks % timeSpan.Ticks)); } } } ================================================ FILE: src/Mollie.Api/Extensions/DictionaryExtensions.cs ================================================ using System.Collections.Generic; using System.Linq; using System.Net; namespace Mollie.Api.Extensions { internal static class DictionaryExtensions { public static string ToQueryString(this IDictionary parameters) { if (!parameters.Any()) { return string.Empty; } return "?" + string.Join("&", parameters.Select(x => $"{WebUtility.UrlEncode(x.Key)}={WebUtility.UrlEncode(x.Value)}")); } public static void AddValueIfNotNullOrEmpty(this IDictionary dictionary, string key, string? value) { if (!string.IsNullOrEmpty(value)) { dictionary.Add(key, value!); } } public static void AddValueIfTrue(this IDictionary dictionary, string key, bool value) { if (value) { dictionary.Add(key, bool.TrueString.ToLower()); } } } } ================================================ FILE: src/Mollie.Api/Extensions/HttpClientExtensions.cs ================================================ using System; using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace Mollie.Api.Extensions { internal static class HttpClientExtensions { public static Task PatchAsync(this HttpClient client, string requestUri, HttpContent content) { return client.PatchAsync(CreateUri(requestUri), content); } public static Task PatchAsync(this HttpClient client, Uri requestUri, HttpContent content) { return client.PatchAsync(requestUri, content, CancellationToken.None); } public static Task PatchAsync(this HttpClient client, string requestUri, HttpContent content, CancellationToken cancellationToken) { return client.PatchAsync(CreateUri(requestUri), content, cancellationToken); } public static Task PatchAsync(this HttpClient client, Uri requestUri, HttpContent content, CancellationToken cancellationToken) { return client.SendAsync(new HttpRequestMessage(new HttpMethod("PATCH"), requestUri) { Content = content }, cancellationToken); } private static Uri CreateUri(string uri) { return new Uri(uri, UriKind.RelativeOrAbsolute); } } } ================================================ FILE: src/Mollie.Api/Extensions/ListExtensions.cs ================================================ using System.Collections.Generic; namespace Mollie.Api.Extensions { internal static class ListExtensions { public static string ToIncludeParameter(this List includeList) { return string.Join(",", includeList); } public static void AddValueIfTrue(this List list, string valueToAdd, bool valueToCheck) { if (valueToCheck) { list.Add(valueToAdd); } } } } ================================================ FILE: src/Mollie.Api/Framework/Authentication/Abstract/IMollieSecretManager.cs ================================================ namespace Mollie.Api.Framework.Authentication.Abstract; public interface IMollieSecretManager { public string GetBearerToken(); } ================================================ FILE: src/Mollie.Api/Framework/Authentication/DefaultMollieSecretManager.cs ================================================ using Mollie.Api.Framework.Authentication.Abstract; namespace Mollie.Api.Framework.Authentication; public class DefaultMollieSecretManager(string apiKey) : IMollieSecretManager { public string GetBearerToken() => apiKey; } ================================================ FILE: src/Mollie.Api/Framework/Factories/BalanceReportResponseFactory.cs ================================================ using System; using Mollie.Api.Models.Balance.Response.BalanceReport; using Mollie.Api.Models.Balance.Response.BalanceReport.Specific.StatusBalance; using Mollie.Api.Models.Balance.Response.BalanceReport.Specific.TransactionCategories; namespace Mollie.Api.Framework.Factories { internal class BalanceReportResponseFactory : ITypeFactory { public BalanceReportResponse Create(string? reportGrouping) { switch (reportGrouping) { case ReportGrouping.StatusBalances: return Activator.CreateInstance(); case ReportGrouping.TransactionCategories: return Activator.CreateInstance(); default: return Activator.CreateInstance(); } } } } ================================================ FILE: src/Mollie.Api/Framework/Factories/BalanceTransactionFactory.cs ================================================ using System; using Mollie.Api.Models.Balance.Response.BalanceTransaction; using Mollie.Api.Models.Balance.Response.BalanceTransaction.Specific; namespace Mollie.Api.Framework.Factories { internal class BalanceTransactionFactory : ITypeFactory { public BalanceTransactionResponse Create(string? type) { if (string.IsNullOrEmpty(type)) { return Activator.CreateInstance(); } switch (type) { case BalanceTransactionContextType.Payment: case BalanceTransactionContextType.UnauthorizedDirectDebit: case BalanceTransactionContextType.FailedPayment: case BalanceTransactionContextType.ChargebackReversal: case BalanceTransactionContextType.ApplicationFee: case BalanceTransactionContextType.SplitPayment: return Activator.CreateInstance(); case BalanceTransactionContextType.Capture: return Activator.CreateInstance(); case BalanceTransactionContextType.Refund: case BalanceTransactionContextType.ReturnedRefund: case BalanceTransactionContextType.PlatformPaymentRefund: return Activator.CreateInstance(); case BalanceTransactionContextType.Chargeback: case BalanceTransactionContextType.PlatformPaymentChargeback: return Activator.CreateInstance(); case BalanceTransactionContextType.OutgoingTransfer: case BalanceTransactionContextType.CanceledOutgoingTransfer: case BalanceTransactionContextType.ReturnedTransfer: return Activator.CreateInstance(); case BalanceTransactionContextType.InvoiceCompensation: return Activator.CreateInstance(); default: return Activator.CreateInstance(); } } } } ================================================ FILE: src/Mollie.Api/Framework/Factories/ITypeFactory.cs ================================================ namespace Mollie.Api.Framework.Factories; internal interface ITypeFactory { T Create(string? type); } ================================================ FILE: src/Mollie.Api/Framework/Factories/MandateResponseFactory.cs ================================================ using System; using Mollie.Api.Models.Mandate.Response; using Mollie.Api.Models.Mandate.Response.PaymentSpecificParameters; using Mollie.Api.Models.Payment; namespace Mollie.Api.Framework.Factories; internal class MandateResponseFactory : ITypeFactory { public MandateResponse Create(string? paymentMethod) { if (string.IsNullOrEmpty(paymentMethod)) { return Activator.CreateInstance(); } switch (paymentMethod) { case PaymentMethod.PayPal: return Activator.CreateInstance(); case PaymentMethod.DirectDebit: return Activator.CreateInstance(); case PaymentMethod.CreditCard: return Activator.CreateInstance(); default: return Activator.CreateInstance(); } } } ================================================ FILE: src/Mollie.Api/Framework/Factories/PaymentResponseFactory.cs ================================================ using System; using Mollie.Api.Models.Payment; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Payment.Response.PaymentSpecificParameters; namespace Mollie.Api.Framework.Factories { internal class PaymentResponseFactory : ITypeFactory { public PaymentResponse Create(string? paymentMethod) { if (string.IsNullOrEmpty(paymentMethod)) { return Activator.CreateInstance(); } switch (paymentMethod) { case PaymentMethod.BankTransfer: return Activator.CreateInstance(); case PaymentMethod.CreditCard: return Activator.CreateInstance(); case PaymentMethod.Ideal: return Activator.CreateInstance(); case PaymentMethod.Giropay: return Activator.CreateInstance(); case PaymentMethod.Eps: return Activator.CreateInstance(); case PaymentMethod.Bancontact: return Activator.CreateInstance(); case PaymentMethod.PayPal: return Activator.CreateInstance(); case PaymentMethod.PaySafeCard: return Activator.CreateInstance(); case PaymentMethod.Sofort: return Activator.CreateInstance(); case PaymentMethod.Belfius: return Activator.CreateInstance(); case PaymentMethod.DirectDebit: return Activator.CreateInstance(); case PaymentMethod.Kbc: return Activator.CreateInstance(); case PaymentMethod.GiftCard: return Activator.CreateInstance(); case PaymentMethod.IngHomePay: return Activator.CreateInstance(); case PaymentMethod.PointOfSale: return Activator.CreateInstance(); default: return Activator.CreateInstance(); } } } } ================================================ FILE: src/Mollie.Api/Framework/Idempotency/AsyncLocalVariable.cs ================================================ using System; using System.Threading; namespace Mollie.Api.Framework.Idempotency { internal class AsyncLocalVariable : IDisposable where T : class { private readonly AsyncLocal _asyncLocal = new(); public T? Value { get => _asyncLocal.Value; set => _asyncLocal.Value = value; } public AsyncLocalVariable(T? value) { _asyncLocal.Value = value; } public void Dispose() { _asyncLocal.Value = null; } } } ================================================ FILE: src/Mollie.Api/Framework/JsonConverterService.cs ================================================ using System; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using Mollie.Api.Framework.Factories; using Mollie.Api.JsonConverters; using Mollie.Api.Models.Balance.Response.BalanceReport; using Mollie.Api.Models.Balance.Response.BalanceTransaction; using Mollie.Api.Models.Mandate.Response; using Mollie.Api.Models.Payment.Response; namespace Mollie.Api.Framework; internal class JsonConverterService { private readonly JsonSerializerOptions _defaultJsonDeserializerOptions; public JsonConverterService() { _defaultJsonDeserializerOptions = CreateDefaultJsonDeserializerOptions(); } public string Serialize(object objectToSerialize) { var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = false, AllowTrailingCommas = true, }; options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); return JsonSerializer.Serialize(objectToSerialize, options); } public T? Deserialize(string json) { return JsonSerializer.Deserialize(json, _defaultJsonDeserializerOptions); } public object? Deserialize(string json, Type type) { return JsonSerializer.Deserialize(json, type, _defaultJsonDeserializerOptions); } /// /// Creates the default Json serializer options for System.Text.Json parsing. /// private JsonSerializerOptions CreateDefaultJsonDeserializerOptions() { var options = new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, AllowTrailingCommas = true, TypeInfoResolver = new DefaultJsonTypeInfoResolver { Modifiers = { static typeInfo => { if (typeInfo.Kind != JsonTypeInfoKind.Object) { return; } foreach (JsonPropertyInfo propertyInfo in typeInfo.Properties) { // Strip IsRequired constraint from every property. propertyInfo.IsRequired = false; } } } } }; options.Converters.Add(new JsonStringEnumConverter()); options.Converters.Add(new PolymorphicConverter(new PaymentResponseFactory(), "method")); options.Converters.Add(new PolymorphicConverter(new MandateResponseFactory(), "method")); options.Converters.Add(new PolymorphicConverter(new BalanceReportResponseFactory(), "grouping")); options.Converters.Add(new PolymorphicConverter(new BalanceTransactionFactory(), "type")); return options; } } ================================================ FILE: src/Mollie.Api/Framework/MollieHttpRetryPolicies.cs ================================================ using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; using Mollie.Api.Client; using Polly; using Polly.Extensions.Http; using Polly.Retry; namespace Mollie.Api.Framework { public static class MollieHttpRetryPolicies { /// /// Retry policy to automatically retry transient errors. /// Transient errors are network failures, http 5xx and http 408 errors. /// public static IAsyncPolicy TransientHttpErrorRetryPolicy(int numberOfRetries = 3) { return HttpPolicyExtensions .HandleTransientHttpError() .WaitAndRetryAsync(numberOfRetries, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); } } } ================================================ FILE: src/Mollie.Api/JsonConverters/CollectionToCommaSeparatedListConverter.cs ================================================ using System; using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; namespace Mollie.Api.JsonConverters; internal class CollectionToCommaSeparatedListConverter : JsonConverter> { public override IList Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { throw new NotImplementedException(); } public override void Write(Utf8JsonWriter writer, IList value, JsonSerializerOptions options) { if (value.Count == 0) { writer.WriteStringValue(string.Empty); return; } string commaSeparated = string.Join(",", value); writer.WriteStringValue(commaSeparated); } } ================================================ FILE: src/Mollie.Api/JsonConverters/DateJsonConverter.cs ================================================ using System; using System.Text.Json; using System.Text.Json.Serialization; namespace Mollie.Api.JsonConverters; /// /// Custom converter to handle date format yyyy-MM-dd in System.Text.Json. /// internal class DateJsonConverter : JsonConverter { private const string Format = "yyyy-MM-dd"; public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.Null) { return null; } var value = reader.GetString(); if (value == null) { throw new JsonException("Expected date string value."); } return DateTime.ParseExact(value, Format, null); } public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options) { if (value == null) { writer.WriteNullValue(); } else { writer.WriteStringValue(value.Value.ToString(Format)); } } } ================================================ FILE: src/Mollie.Api/JsonConverters/Iso8601DateTimeConverter.cs ================================================ using System; using System.Text.Json; using System.Text.Json.Serialization; namespace Mollie.Api.JsonConverters { internal class Iso8601DateTimeConverter : JsonConverter { public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return DateTime.Parse(reader.GetString()!); } public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ssK")); } } } ================================================ FILE: src/Mollie.Api/JsonConverters/ListResponseJsonConverter.cs ================================================ using System; using System.Collections; using System.Text.Json; using System.Text.Json.Serialization; namespace Mollie.Api.JsonConverters; internal class ListResponseConverter : JsonConverter { public override bool CanConvert(Type typeToConvert) { // Check if the target type is assignable from IList (List implements IList) return typeof(IList).IsAssignableFrom(typeToConvert); } public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.Null) { return null; } using JsonDocument document = JsonDocument.ParseValue(ref reader); JsonElement root = document.RootElement; if (root.ValueKind == JsonValueKind.Object) { var enumerator = root.EnumerateObject(); if (enumerator.MoveNext()) { var firstProperty = enumerator.Current; if (firstProperty.Value.ValueKind == JsonValueKind.Array) { var arrayJson = firstProperty.Value.GetRawText(); return JsonSerializer.Deserialize(arrayJson, typeToConvert, options); } } } return null; } public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { throw new NotImplementedException("Not implemented"); } } ================================================ FILE: src/Mollie.Api/JsonConverters/MicrosecondEpochConverter.cs ================================================ using System; using System.Text.Json; using System.Text.Json.Serialization; namespace Mollie.Api.JsonConverters { internal class MicrosecondEpochConverter : JsonConverter { public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var longValue = reader.GetInt64(); return DateTimeOffset.FromUnixTimeMilliseconds(longValue).UtcDateTime; } public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) { throw new NotImplementedException(); } } } ================================================ FILE: src/Mollie.Api/JsonConverters/PolymorphicConverter.cs ================================================ using System; using System.Text.Json; using System.Text.Json.Serialization; using Mollie.Api.Framework.Factories; namespace Mollie.Api.JsonConverters; internal class PolymorphicConverter : JsonConverter { private readonly ITypeFactory _typeFactory; private readonly string _typeFieldName; public PolymorphicConverter(ITypeFactory typeFactory, string typeFieldName) { _typeFactory = typeFactory; _typeFieldName = typeFieldName; } public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // Parse the current JSON object into a JsonDocument for easy querying using JsonDocument document = JsonDocument.ParseValue(ref reader); JsonElement root = document.RootElement; // Try to get the "method" property value string? type = null; if (root.TryGetProperty(_typeFieldName, out JsonElement typeProperty)) { type = typeProperty.GetString(); } // Use the factory to create the right PaymentResponse instance T response = _typeFactory.Create(type); // Deserialize the JSON into the created instance, using the root element JSON // Since System.Text.Json cannot deserialize into existing objects directly, // we re-serialize the root JSON and deserialize again into the correct type. var json = root.GetRawText(); // Create new options without this converter to avoid stack overflow JsonSerializerOptions newOptions = new(options); newOptions.Converters.Remove(this); var result = (T)JsonSerializer.Deserialize(json, response!.GetType(), newOptions)!; return result; } public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { JsonSerializer.Serialize(writer, value, value!.GetType(), options); } } ================================================ FILE: src/Mollie.Api/JsonConverters/RawJsonConverter.cs ================================================ using System; using System.Text.Json; using System.Text.Json.Serialization; namespace Mollie.Api.JsonConverters; internal class RawJsonConverter : JsonConverter { public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { using JsonDocument document = JsonDocument.ParseValue(ref reader); if (document.RootElement.ValueKind == JsonValueKind.Null) { return null; } if (document.RootElement.ValueKind == JsonValueKind.String) { return document.RootElement.GetString(); } return document.RootElement.GetRawText(); } public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) { if (string.IsNullOrEmpty(value)) { writer.WriteNullValue(); return; } if (IsValidJson(value!)) { writer.WriteRawValue(value!); } else { writer.WriteStringValue(value); } } private bool IsValidJson(string strInput) { strInput = strInput.Trim(); if ((strInput.StartsWith("{") && strInput.EndsWith("}")) || (strInput.StartsWith("[") && strInput.EndsWith("]"))) { try { JsonDocument.Parse(strInput); return true; } catch (JsonException) { return false; } catch (Exception) { return false; } } else { return false; } } } ================================================ FILE: src/Mollie.Api/JsonConverters/SettlementPeriodConverter.cs ================================================ using System; using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; using Mollie.Api.Models.Settlement.Response; namespace Mollie.Api.JsonConverters; internal class SettlementPeriodConverter : JsonConverter>> { public override Dictionary>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // If we have no periods, Mollie returns a empty array instead of an object if (reader.TokenType == JsonTokenType.StartArray) { reader.Skip(); return new Dictionary>(); } var result = new Dictionary>(); using JsonDocument doc = JsonDocument.ParseValue(ref reader); foreach (var property in doc.RootElement.EnumerateObject()) { int month = int.Parse(property.Name); var monthPeriods = new Dictionary(); foreach (var period in property.Value.EnumerateObject()) { int day = int.Parse(period.Name); var settlementPeriod = JsonSerializer.Deserialize(period.Value.GetRawText(), options)!; monthPeriods[day] = settlementPeriod; } result[month] = monthPeriods; } return result; } public override void Write(Utf8JsonWriter writer, Dictionary> value, JsonSerializerOptions options) { throw new NotImplementedException(); } } ================================================ FILE: src/Mollie.Api/JsonConverters/StringToDecimalConverter.cs ================================================ using System; using System.Text.Json; using System.Text.Json.Serialization; namespace Mollie.Api.JsonConverters; internal class StringToDecimalConverter : JsonConverter { public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.Null) { throw new JsonException("Cannot convert null to decimal."); } if (reader.TokenType == JsonTokenType.String) { string value = reader.GetString() ?? "0"; if (decimal.TryParse(value, out decimal result)) { return result; } } else if (reader.TokenType == JsonTokenType.Number) { return reader.GetDecimal(); } throw new JsonException($"Unable to convert \"{reader.GetString()}\" to {typeToConvert.Name}."); } public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString("G", System.Globalization.CultureInfo.InvariantCulture)); } } ================================================ FILE: src/Mollie.Api/JsonConverters/WebhookEventEntityJsonConverter.cs ================================================ using System; using System.Text.Json; using System.Text.Json.Serialization; using Mollie.Api.Models; using Mollie.Api.Models.Balance.Response; using Mollie.Api.Models.Capture.Response; using Mollie.Api.Models.Chargeback.Response; using Mollie.Api.Models.Customer.Response; using Mollie.Api.Models.Invoice.Response; using Mollie.Api.Models.Issuer.Response; using Mollie.Api.Models.Mandate.Response; using Mollie.Api.Models.Order.Response; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.PaymentLink.Response; using Mollie.Api.Models.Refund.Response; using Mollie.Api.Models.SalesInvoice.Response; using Mollie.Api.Models.Settlement.Response; using Mollie.Api.Models.Shipment.Response; using Mollie.Api.Models.Subscription.Response; using Mollie.Api.Models.Terminal.Response; namespace Mollie.Api.JsonConverters; internal class WebhookEventEntityJsonConverter : JsonConverter { public override bool CanConvert(Type typeToConvert) { return typeof(IEntity).IsAssignableFrom(typeToConvert); } public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.Null) { return null; } using JsonDocument document = JsonDocument.ParseValue(ref reader); JsonElement root = document.RootElement; if (root.ValueKind == JsonValueKind.Object) { var enumerator = root.EnumerateObject(); if (enumerator.MoveNext()) { var firstProperty = enumerator.Current; if (firstProperty.Value.ValueKind == JsonValueKind.Object) { typeToConvert = GetEntityTypeFromJson(firstProperty, typeToConvert); var arrayJson = firstProperty.Value.GetRawText(); return JsonSerializer.Deserialize(arrayJson, typeToConvert, options); } } } return null; } public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { throw new NotImplementedException("Not implemented"); } private Type GetEntityTypeFromJson(JsonProperty entityRoot, Type typeToConvert) { if (typeToConvert != typeof(IEntity)) { return typeToConvert; } if (entityRoot.Value.TryGetProperty("resource", out JsonElement resourceProperty)) { string? resource = resourceProperty.GetString(); switch (resource) { case "balance": return typeof(BalanceResponse); case "capture": return typeof(CaptureResponse); case "chargeback": return typeof(ChargebackResponse); case "customer": return typeof(CustomerResponse); case "invoice": return typeof(InvoiceResponse); case "issuer": return typeof(IssuerResponse); case "mandate": return typeof(MandateResponse); case "order": return typeof(OrderResponse); case "refund": return typeof(RefundResponse); case "settlement": return typeof(SettlementResponse); case "shipment": return typeof(ShipmentResponse); case "subscription": return typeof(SubscriptionResponse); case "terminal": return typeof(TerminalResponse); case "payment": return typeof(PaymentResponse); case "payment-link": return typeof(PaymentLinkResponse); case "sales-invoice": return typeof(SalesInvoiceResponse); default: throw new JsonException( $"Unable to convert embedded JSON to entity type. Resource '{resource}' is not supported or recognized."); } } throw new JsonException("Unable to convert embedded JSON to entity type. No resource property found"); } } ================================================ FILE: src/Mollie.Api/Models/AddressObject.cs ================================================ namespace Mollie.Api.Models { public record AddressObject { /// /// The card holder’s street and street number. /// public string? StreetAndNumber { get; set; } /// /// Any additional addressing details, for example an apartment number. /// public string? StreetAdditional { get; set; } /// /// The card holder’s postal code. /// public string? PostalCode { get; set; } /// /// The card holder’s city. /// public string? City { get; set; } /// /// The card holder’s region. /// public string? Region { get; set; } /// /// The card holder’s country in ISO 3166-1 alpha-2 format. /// public string? Country { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Amount.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text.Json.Serialization; namespace Mollie.Api.Models { /// /// The amount of a payment, refund, or chargeback. /// public record Amount { private static readonly IDictionary CurrenciesWithAlternativeDecimalPrecision = new Dictionary() { { "JPY", "0" }, { "ISK", "0" }, }; /// /// An ISO 4217 currency code. The currencies supported depend on the payment methods that are enabled on your account. /// public required string Currency { get; set; } /// /// An ISO 4217 currency code. The currencies supported depend on the payment methods that are enabled on your account. /// public required string Value { get; set; } /// /// Constructor for constructing based on a string value /// /// An ISO 4217 currency code. The currencies supported depend on the payment methods that are enabled on your account. /// A string containing an exact monetary amount in the given currency. [JsonConstructor] [SetsRequiredMembers] public Amount(string currency, string value) { Currency = currency; Value = value; } /// /// Constructor for constructing based on a decimal value /// /// An ISO 4217 currency code. The currencies supported depend on the payment methods that are enabled on your account. /// The amount in the specified currency. [SetsRequiredMembers] public Amount(string currency, decimal value) { Currency = currency; Value = ConvertDecimalAmountToStringAmount(currency, value); } /// /// Implicit cast operator from Amount to decimal. /// /// public static implicit operator decimal(Amount amount) => decimal.TryParse(amount.Value, NumberStyles.Number, CultureInfo.InvariantCulture, out var a) ? a : throw new InvalidCastException($"Cannot convert {amount.Value} to decimal"); /// /// Implicit cast operator from Amount? to decimal?. /// /// public static implicit operator decimal?(Amount? amount) => amount == null ? null : (decimal)amount; private static string ConvertDecimalAmountToStringAmount(string currency, decimal value) { if (CurrenciesWithAlternativeDecimalPrecision.TryGetValue(currency, out string? format)) { return value.ToString(format, CultureInfo.InvariantCulture); } return value.ToString("0.00", CultureInfo.InvariantCulture); } public override string ToString() { return $"{Value} {Currency}"; } public override int GetHashCode() { unchecked { return (Currency.GetHashCode() * 397) ^ Value.GetHashCode(); } } } } ================================================ FILE: src/Mollie.Api/Models/ApplicationFee.cs ================================================ namespace Mollie.Api.Models { public record ApplicationFee { /// /// The amount in EURO that the app wants to charge, e.g. 10.00 if the app would want to charge €10.00. /// public required Amount Amount { get; set; } /// /// The description of the application fee. This will appear on settlement reports to the merchant and to you. /// public required string Description { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceReport/BalanceReportAmount.cs ================================================ namespace Mollie.Api.Models.Balance.Response.BalanceReport { public record BalanceReportAmount { public required Amount Amount { get; set; } public override string ToString() { return Amount.ToString(); } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceReport/BalanceReportAmountWithSubtotals.cs ================================================ using System.Collections.Generic; namespace Mollie.Api.Models.Balance.Response.BalanceReport { public record BalanceReportAmountWithSubtotals : BalanceReportAmount { public required IEnumerable Subtotals { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceReport/BalanceReportLinks.cs ================================================ using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Balance.Response.BalanceReport { public record BalanceReportLinks { /// /// The API resource URL of the balance report itself. /// public required UrlObjectLink Self { get; set; } /// /// The URL to the order retrieval endpoint documentation. /// public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceReport/BalanceReportResponse.cs ================================================ using System; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Balance.Response.BalanceReport { public record BalanceReportResponse { /// /// Indicates the response contains a balance report object. Will always contain balance-report for this endpoint. /// public required string Resource { get; set; } /// /// The ID of a Balance this report is generated for. /// public required string BalanceId { get; set; } /// /// The time zone used for the from and until parameters. Currently only time zone Europe/Amsterdam is supported. /// public required string TimeZone { get; set; } /// /// The start date of the report, in YYYY-MM-DD format. The from date is ‘inclusive’, and in Central European Time. /// This means a report with for example from: 2020-01-01 will include movements of 2020-01-01 0:00:00 CET and onwards. /// public required DateTime From { get; set; } /// /// The end date of the report, in YYYY-MM-DD format. The until date is ‘exclusive’, and in Central European Time. /// This means a report with for example until: 2020-02-01 will include movements up until 2020-01-31 23:59:59 CET. /// public required DateTime Until { get; set; } /// /// You can retrieve reports in two different formats. With the status-balances format, transactions are grouped by status /// (e.g. pending, available), then by direction of movement (e.g. moved from pending to available), then by transaction type, /// and then by other sub-groupings where available (e.g. payment method). /// With the transaction-categories format, transactions are grouped by transaction type, then by direction of movement, and /// then again by other sub-groupings where available. /// Both reporting formats will always contain opening and closing amounts that correspond to the start and end dates of the report. /// Possible values: status-balances transaction-categories /// public required string Grouping { get; set; } /// /// An object with several URL objects relevant to the balance report. /// [JsonPropertyName("_links")] public required BalanceReportLinks Links { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceReport/BalanceReportSubtotals.cs ================================================ using System.Collections.Generic; namespace Mollie.Api.Models.Balance.Response.BalanceReport { public record BalanceReportSubtotals { public string? TransactionType { get; set; } public string? Method { get; set; } public string? PrepaymentPartType { get; set; } public required string FeeType { get; set; } public required int Count { get; set; } public required Amount Amount { get; set; } public IEnumerable? Subtotals { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceReport/ReportGrouping.cs ================================================ namespace Mollie.Api.Models.Balance.Response.BalanceReport { public static class ReportGrouping { public const string StatusBalances = "status-balances"; public const string TransactionCategories = "transaction-categories"; } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceReport/Specific/StatusBalance/StatusBalanceAvailableBalance.cs ================================================ namespace Mollie.Api.Models.Balance.Response.BalanceReport.Specific.StatusBalance { public record StatusBalanceAvailableBalance { public required BalanceReportAmount Open { get; set; } public required BalanceReportAmount Close { get; set; } public required BalanceReportAmountWithSubtotals MovedFromPending { get; set; } public required BalanceReportAmountWithSubtotals ImmediatelyAvailable { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceReport/Specific/StatusBalance/StatusBalanceReportResponse.cs ================================================ namespace Mollie.Api.Models.Balance.Response.BalanceReport.Specific.StatusBalance { public record StatusBalanceReportResponse : BalanceReportResponse { public required StatusBalancesTotal Totals { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceReport/Specific/StatusBalance/StatusBalancesPendingBalance.cs ================================================ namespace Mollie.Api.Models.Balance.Response.BalanceReport.Specific.StatusBalance { public record StatusBalancesPendingBalance { public required BalanceReportAmount Open { get; set; } public required BalanceReportAmount Close { get; set; } public required BalanceReportAmountWithSubtotals Pending { get; set; } public required BalanceReportAmountWithSubtotals MovedToAvailable { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceReport/Specific/StatusBalance/StatusBalancesTotal.cs ================================================ namespace Mollie.Api.Models.Balance.Response.BalanceReport.Specific.StatusBalance { public record StatusBalancesTotal { public required StatusBalancesPendingBalance PendingBalance { get; set; } public required StatusBalanceAvailableBalance AvailableBalance { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceReport/Specific/TransactionCategories/TransactionCategoriesReportResponse.cs ================================================ namespace Mollie.Api.Models.Balance.Response.BalanceReport.Specific.TransactionCategories { public record TransactionCategoriesReportResponse : BalanceReportResponse { public required TransactionCategoriesTotal Totals { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceReport/Specific/TransactionCategories/TransactionCategoriesSummaryBalances.cs ================================================ namespace Mollie.Api.Models.Balance.Response.BalanceReport.Specific.TransactionCategories { public record TransactionCategoriesSummaryBalances { public required BalanceReportAmount Pending { get; set; } public required BalanceReportAmount Available { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceReport/Specific/TransactionCategories/TransactionCategoriesTotal.cs ================================================ using System.Text.Json.Serialization; namespace Mollie.Api.Models.Balance.Response.BalanceReport.Specific.TransactionCategories { public record TransactionCategoriesTotal { public required TransactionCategoriesSummaryBalances Open { get; set; } public required TransactionCategoriesSummaryBalances Close { get; set; } public required TransactionCategoriesTransaction Payments { get; set; } public required TransactionCategoriesTransaction Refunds { get; set; } public required TransactionCategoriesTransaction Chargebacks { get; set; } public required TransactionCategoriesTransaction Capital { get; set; } public required TransactionCategoriesTransaction Transfers { get; set; } [JsonPropertyName("fee-prepayments")] public required TransactionCategoriesTransaction FeePrepayments { get; set; } public required TransactionCategoriesTransaction Corrections { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceReport/Specific/TransactionCategories/TransactionCategoriesTransaction.cs ================================================ namespace Mollie.Api.Models.Balance.Response.BalanceReport.Specific.TransactionCategories { public record TransactionCategoriesTransaction { public required BalanceReportAmountWithSubtotals Pending { get; set; } public required BalanceReportAmountWithSubtotals MovedToAvailable { get; set; } public required BalanceReportAmountWithSubtotals ImmediatelyAvailable { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceResponse.cs ================================================ using System; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Balance.Response { public class BalanceResponse : IEntity { /// /// Indicates the response contains a balance object. Will always contain balance for this endpoint. /// public required string Resource { get; set; } /// /// The identifier uniquely referring to this balance. Mollie assigns this identifier at balance creation time. For example bal_gVMhHKqSSRYJyPsuoPNFH. /// public required string Id { get; set; } /// /// The balance’s date and time of creation, in ISO 8601 format. /// public required DateTime CreatedAt { get; set; } /// /// The balance’s ISO 4217 currency code. /// public required string Currency { get; set; } /// /// The status of the balance. /// public required BalanceResponseStatus Status { get; set; } /// /// The frequency at which the available amount on the balance will be settled to the configured transfer destination. /// See transferDestination. /// public required string TransferFrequency { get; set; } /// /// The minimum amount configured for scheduled automatic settlements. As soon as the amount on the balance exceeds this threshold, /// the complete balance will be paid out to the transferDestination according to the configured transferFrequency. /// public required Amount TransferThreshold { get; set; } /// /// The transfer reference set to be included in all the transfers for this balance. Either a string or null. /// public string? TransferReference { get; set; } /// /// The destination where the available amount will be automatically transferred to according to the configured transferFrequency. /// public required BalanceTransferDestination TransferDestination { get; set; } /// /// The amount directly available on the balance, e.g. {"currency":"EUR", "value":"100.00"}. /// public required Amount AvailableAmount { get; set; } /// /// The total amount that is queued to be transferred to your balance. For example, a credit card payment can take a few days to clear. /// public required Amount PendingAmount { get; set; } /// /// An object with several URL objects relevant to the balance. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required BalanceResponseLinks Links { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceResponseLinks.cs ================================================ using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Balance.Response { public class BalanceResponseLinks { /// /// The API resource URL of the balance itself. /// public required UrlObjectLink Self { get; set; } /// /// The URL to the order retrieval endpoint documentation. /// public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceResponseStatus.cs ================================================ using System.Runtime.Serialization; namespace Mollie.Api.Models.Balance.Response { public enum BalanceResponseStatus { [EnumMember(Value = "active")] Active, [EnumMember(Value = "inactive")] Inactive } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceTransaction/BalanceTransactionContextType.cs ================================================ namespace Mollie.Api.Models.Balance.Response.BalanceTransaction { public static class BalanceTransactionContextType { public const string Payment = "payment"; public const string Capture = "capture"; public const string UnauthorizedDirectDebit = "unauthorized-direct-debit"; public const string FailedPayment = "failed-payment"; public const string Refund = "refund"; public const string ReturnedRefund = "returned-refund"; public const string Chargeback = "chargeback"; public const string ChargebackReversal = "chargeback-reversal"; public const string OutgoingTransfer = "outgoing-transfer"; public const string CanceledOutgoingTransfer = "canceled-outgoing-transfer"; public const string ReturnedTransfer = "returned-transfer"; public const string InvoiceCompensation = "invoice-compensation"; public const string BalanceCorrection = "balance-correction"; public const string ApplicationFee = "application-fee"; public const string SplitPayment = "split-payment"; public const string PlatformPaymentRefund = "platform-payment-refund"; public const string PlatformPaymentChargeback = "platform-payment-chargeback"; } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceTransaction/BalanceTransactionResponse.cs ================================================ using System; namespace Mollie.Api.Models.Balance.Response.BalanceTransaction { public class BalanceTransactionResponse { /// /// Indicates the response contains a balance transaction object. Will always contain balance_transaction /// for this endpoint. /// public required string Resource { get; set; } /// /// The identifier uniquely referring to this balance transaction. Mollie assigns this identifier at /// transaction creation time. For example baltr_QM24QwzUWR4ev4Xfgyt29d. /// public required string Id { get; set; } /// /// The type of movement, for example payment or refund. See Mollie docs for a full list of values /// public required string Type { get; set; } /// /// The final amount that was moved to or from the balance, e.g. {"currency":"EUR", "value":"100.00"}. /// If the transaction moves funds away from the balance, for example when it concerns a refund, the /// amount will be negative. /// public required Amount ResultAmount { get; set; } /// /// The amount that was to be moved to or from the balance, excluding deductions. If the transaction /// moves funds away from the balance, for example when it concerns a refund, the amount will be negative. /// public required Amount InitialAmount { get; set; } /// /// The total amount of deductions withheld from the movement. For example, if a €10,00 payment comes in /// with a €0,29 fee, the deductions amount will be {"currency":"EUR", "value":"-0.29"}. When moving funds /// to a balance, we always round the deduction to a ‘real’ amount. Any differences between these realtime /// rounded amounts and the final invoice will be compensated when the invoice is generated. /// public required Amount Deductions { get; set; } /// /// The date and time of the movement, in ISO 8601 format. /// public required DateTime CreatedAt { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceTransaction/Specific/CaptureBalanceTransactionResponse.cs ================================================ namespace Mollie.Api.Models.Balance.Response.BalanceTransaction.Specific { public class CaptureBalanceTransactionResponse : BalanceTransactionResponse { public required CaptureTransactionContext Context { get; set; } } public class CaptureTransactionContext { public required string PaymentId { get; set; } public required string CaptureId { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceTransaction/Specific/ChargebackBalanceTransactionResponse.cs ================================================ namespace Mollie.Api.Models.Balance.Response.BalanceTransaction.Specific { public class ChargebackBalanceTransactionResponse : BalanceTransactionResponse { public required ChargebackTransactionContext Context { get; set; } } public class ChargebackTransactionContext { public required string PaymentId { get; set; } public required string ChargebackId { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceTransaction/Specific/InvoiceBalanceTransactionResponse.cs ================================================ namespace Mollie.Api.Models.Balance.Response.BalanceTransaction.Specific { public class InvoiceBalanceTransactionResponse : BalanceTransactionResponse { public required InvoiceTransactionContext Context { get; set; } } public class InvoiceTransactionContext { public required string InvoiceId { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceTransaction/Specific/PaymentBalanceTransactionResponse.cs ================================================ namespace Mollie.Api.Models.Balance.Response.BalanceTransaction.Specific { public class PaymentBalanceTransactionResponse : BalanceTransactionResponse { public required PaymentTransactionContext Context { get; set; } } public class PaymentTransactionContext { public required string PaymentId { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceTransaction/Specific/RefundBalanceTransactionResponse.cs ================================================ namespace Mollie.Api.Models.Balance.Response.BalanceTransaction.Specific { public class RefundBalanceTransactionResponse : BalanceTransactionResponse { public required RefundTransactionContext Context { get; set; } } public class RefundTransactionContext { public required string PaymentId { get; set; } public required string RefundId { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceTransaction/Specific/SettlementBalanceTransactionResponse.cs ================================================ namespace Mollie.Api.Models.Balance.Response.BalanceTransaction.Specific { public class SettlementBalanceTransactionResponse : BalanceTransactionResponse { public required SettlementTransactionContext Context { get; set; } } public class SettlementTransactionContext { public required string TransferId { get; set; } public required string SettlementId { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Balance/Response/BalanceTransferDestination.cs ================================================ namespace Mollie.Api.Models.Balance.Response { public class BalanceTransferDestination { /// /// The default destination of automatic scheduled transfers. Currently only bank-account is supported. /// public required string Type { get; set; } /// /// The configured bank account number of the beneficiary the balance amount is to be transferred to. /// public required string BankAccount { get; set; } /// /// The full name of the beneficiary the balance amount is to be transferred to. /// public required string BeneficiaryName { get; set; } public override string ToString() { return $"{Type} - {BankAccount} - {BeneficiaryName}"; } } } ================================================ FILE: src/Mollie.Api/Models/BalanceTransfer/BalanceTransferParty.cs ================================================ namespace Mollie.Api.Models.BalanceTransfer; public record BalanceTransferParty { /// /// Defines the type of the party. At the moment, only organization is supported. /// public required string Type { get; set; } /// /// Identifier of the party. For example, this contains the organization token if the type is organization. /// public required string Id { get; set; } /// /// The transfer description for the transfer party. This is the description that will appear in the financial reports of the party. /// public required string Description { get; set; } } ================================================ FILE: src/Mollie.Api/Models/BalanceTransfer/Request/BalanceTransferRequest.cs ================================================ using System.Text.Json; using System.Text.Json.Serialization; using Mollie.Api.JsonConverters; namespace Mollie.Api.Models.BalanceTransfer.Request; public record BalanceTransferRequest : ITestModeRequest { /// /// The amount to be transferred, e.g. {"currency":"EUR", "value":"1000.00"} if you would like to transfer €1000.00. /// public required Amount Amount { get; set; } /// /// A party involved in the balance transfer, either the sender or the receiver. /// public required BalanceTransferParty Source { get; set; } /// /// A party involved in the balance transfer, either the sender or the receiver. /// public required BalanceTransferParty Destination { get; set; } /// /// The transfer description for initiating party. /// public required string Description { get; set; } /// /// The type of the transfer. Different fees may apply to different types of transfers. /// public string? Category { get; set; } /// /// A JSON object that you can attach to a balance transfer. This can be useful for storing additional /// information about the transfer in a structured format. Maximum size is approximately 1KB. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// Whether to create the entity in test mode or live mode. You can enable test mode by setting testmode to true. /// public bool? Testmode { get; set; } public void SetMetadata(object metadataObj, JsonSerializerOptions? jsonSerializerOptions = null) { Metadata = JsonSerializer.Serialize(metadataObj, jsonSerializerOptions); } } ================================================ FILE: src/Mollie.Api/Models/BalanceTransfer/Response/BalanceTransferResponse.cs ================================================ using System; using System.Text.Json; using System.Text.Json.Serialization; using Mollie.Api.JsonConverters; using Mollie.Api.Models.Payment.Response; namespace Mollie.Api.Models.BalanceTransfer.Response; public record BalanceTransferResponse { /// /// Indicates the response contains a balance transfer object. Will always contain the string /// connect-balance-transfer for this endpoint. /// public required string Resource { get; set; } /// /// The identifier uniquely referring to this balance transfer. Mollie assigns this identifier at balance transfer /// creation time. Mollie will always refer to the balance transfer by this ID. Example: cbtr_j8NvRAM2WNZtsykpLEX8J. /// public required string Id { get; set; } /// /// The amount to be transferred, e.g. {"currency":"EUR", "value":"1000.00"} if you would like to transfer €1000.00. /// public required Amount Amount { get; set; } /// /// A party involved in the balance transfer, either the sender or the receiver. /// public required BalanceTransferParty Source { get; set; } /// /// A party involved in the balance transfer, either the sender or the receiver. /// public required BalanceTransferParty Destination { get; set; } /// /// The transfer description for initiating party. /// public required string Description { get; set; } /// /// The status of the transfer. /// public required string Status { get; set; } /// /// The reason for the current status of the transfer, if applicable /// public required StatusReason StatusReason { get; set; } /// /// The entity's date and time of creation, in ISO 8601 format. /// public required DateTime CreatedAt { get; set; } /// /// The date and time when the transfer was completed, in ISO 8601 format. /// This parameter is omitted if the transfer is not executed (yet). /// public DateTime? ExecutedAt { get; set; } /// /// Whether this entity was created in live mode or in test mode. /// public required Mode Mode { get; set; } /// /// A JSON object that you can attach to a balance transfer. This can be useful for storing additional /// information about the transfer in a structured format. Maximum size is approximately 1KB. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } public void SetMetadata(object metadataObj, JsonSerializerOptions? jsonSerializerOptions = null) { Metadata = JsonSerializer.Serialize(metadataObj, jsonSerializerOptions); } } ================================================ FILE: src/Mollie.Api/Models/BalanceTransfer/Response/BalanceTransferStatusReason.cs ================================================ namespace Mollie.Api.Models.BalanceTransfer.Response; public class BalanceTransferStatusReason { /// /// A machine-readable code that indicates the reason for the transfer's status. /// public required string Code { get; set; } /// /// A description of the status reason, localized according to the transfer. /// public required string Message { get; set; } } ================================================ FILE: src/Mollie.Api/Models/Capability/CapabilityRequirementStatus.cs ================================================ namespace Mollie.Api.Models.Capability; public static class CapabilityRequirementStatus { public const string CurrentlyDue = "currently-due"; public const string PastDue = "past-due"; public const string Requested = "requested"; } ================================================ FILE: src/Mollie.Api/Models/Capability/CapabilityStatus.cs ================================================ namespace Mollie.Api.Models.Capability; public static class CapabilityStatus { public const string Unrequested = "unrequested"; public const string Enabled = "enabled"; public const string Disabled = "disabled"; public const string Pending = "pending"; } ================================================ FILE: src/Mollie.Api/Models/Capability/CapabilityStatusReason.cs ================================================ namespace Mollie.Api.Models.Capability; public static class CapabilityStatusReason { public const string RequirementPastDue = "requirement-past-due"; public const string OnboardingInformtionNeeded = "onboarding-information-needed"; } ================================================ FILE: src/Mollie.Api/Models/Capability/Response/CapabilityRequirement.cs ================================================ using System; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Capability.Response; public record CapabilityRequirement { /// /// The name of this requirement, referring to the task to be fulfilled by the organization to enable or re-enable /// the capability. The name is unique among other requirements of the same capability. /// public required string Id { get; set; } /// /// The status of the requirement depends on its due date. If no due date is given, the status will be requested. /// A list of possible values can be found in the Mollie.Api.Models.Capability.CapabilityRequirementStatus class. /// public required string Status { get; set; } /// /// Due date until the requirement must be fulfilled, if any /// public DateTime? DueDate { get; set; } /// /// Related links /// [JsonPropertyName("_links")] public required CapabilityRequirementLinks Links { get; set; } } ================================================ FILE: src/Mollie.Api/Models/Capability/Response/CapabilityRequirementLinks.cs ================================================ using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Capability.Response; public record CapabilityRequirementLinks { /// /// If known, a deep link to the Mollie dashboard of the client, where the requirement can be fulfilled. /// For example, where necessary documents are to be uploaded. /// public required UrlLink Dashboard { get; set; } } ================================================ FILE: src/Mollie.Api/Models/Capability/Response/CapabilityResponse.cs ================================================ using System.Collections.Generic; namespace Mollie.Api.Models.Capability.Response; public record CapabilityResponse { /// /// Always the word capability for this resource type. /// public required string Resource { get; set; } /// /// A unique name for this capability like payments / settlements. /// public required string Name { get; set; } /// /// The status of the capability. A list of possible values can be found in the /// Mollie.Api.Models.Capability.CapabilityStatus class. /// public required string Status { get; set; } /// /// The reason the capability is in this status. A list of possible values can be found in the /// Mollie.Api.Models.Capability.CapabilityStatusReason class. /// public required string StatusReason { get; set; } /// /// The requirements that need to be fulfilled before the capability can be enabled. /// public required IEnumerable Requirements { get; set; } /// /// Related links /// public required CapabilityResponseLinks Links { get; set; } } ================================================ FILE: src/Mollie.Api/Models/Capability/Response/CapabilityResponseLinks.cs ================================================ using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Capability.Response; public record CapabilityResponseLinks { public required UrlLink Documentation { get; set; } } ================================================ FILE: src/Mollie.Api/Models/Capture/CaptureMode.cs ================================================ namespace Mollie.Api.Models.Capture { public static class CaptureMode { public const string Automatic = "automatic"; public const string Manual = "manual"; } } ================================================ FILE: src/Mollie.Api/Models/Capture/Request/CaptureRequest.cs ================================================ using System.Text.Json; using System.Text.Json.Serialization; using Mollie.Api.JsonConverters; namespace Mollie.Api.Models.Capture.Request { public record CaptureRequest : ITestModeRequest { /// /// The amount to capture. /// public Amount? Amount { get; set; } /// /// The description of the capture you are creating. /// public string? Description { get; set; } /// /// Provide any data you like, for example a string or a JSON object. We will save the data alongside the capture. /// Whenever you fetch the capture with our API, we will also include the metadata. You can use up to /// approximately 1kB. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// Oauth only - Optional – Set this to true to make this capture for a test payment /// public bool? Testmode { get; set; } public void SetMetadata(object metadataObj, JsonSerializerOptions? jsonSerializerOptions = null) { Metadata = JsonSerializer.Serialize(metadataObj, jsonSerializerOptions); } public override string ToString() { return $"Amount: {Amount} Description: {Description}"; } } } ================================================ FILE: src/Mollie.Api/Models/Capture/Response/CaptureResponse.cs ================================================ using System; using Mollie.Api.JsonConverters; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Capture.Response { public record CaptureResponse : IEntity { /// /// Indicates the response contains a capture object. Will always contain capture for this endpoint. /// public required string Resource { get; set; } /// /// The capture’s unique identifier, for example cpt_4qqhO89gsT. /// public required string Id { get; set; } /// /// The mode used to create this capture. /// Possible values: live test /// public required string Mode { get; set; } /// /// The amount captured. /// public required Amount Amount { get; set; } /// /// The capture’s status. /// public required string Status { get; set; } /// /// This optional field will contain the amount that will be settled to your account, converted to the currency your account is settled in. It follows the same syntax as the amount property. /// public required Amount SettlementAmount { get; set; } /// /// The unique identifier of the payment this capture was created for, for example: tr_7UhSN1zuXS /// public required string PaymentId { get; set; } /// /// The unique identifier of the shipment that triggered the creation of this capture, for example: shp_3wmsgCJN4U /// public required string ShipmentId { get; set; } /// /// The unique identifier of the settlement this capture was settled with, for example: stl_jDk30akdN /// public required string SettlementId { get; set; } /// /// The capture’s date and time of creation, in ISO 8601 format. /// public required DateTime CreatedAt { get; set; } /// /// The optional metadata you provided upon payment creation. Metadata can be used to link an order to a payment. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// The optional metadata you provided upon capture creation. Metadata can for example be used to link an bookkeeping ID to a capture. /// [JsonPropertyName("_links")] public required CaptureResponseLinks Links { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Capture/Response/CaptureResponseLinks.cs ================================================ using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Settlement.Response; using Mollie.Api.Models.Shipment.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Capture.Response { public record CaptureResponseLinks { /// /// The API resource URL of the capture itself. /// public required UrlObjectLink Self { get; set; } /// /// The API resource URL of the payment the capture belongs to. /// public required UrlObjectLink Payment { get; set; } /// /// The API resource URL of the shipment that triggered the capture to be created. /// public required UrlObjectLink Shipment { get; set; } /// /// The API resource URL of the settlement this capture has been settled with. Not present if not yet settled. /// public required UrlObjectLink Settlement { get; set; } /// /// The URL to the order retrieval endpoint documentation. /// public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Chargeback/Response/ChargebackResponse.cs ================================================ using System; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Chargeback.Response { public record ChargebackResponse : IEntity { /// /// Indicates the response contains a chargeback object. Will always contain the string chargeback for this endpoint. /// public required string Resource { get; set; } /// /// The chargeback's unique identifier, for example chb_n9z0tp. /// public required string Id { get; set; } /// /// The amount charged back. /// public required Amount Amount { get; set; } /// /// This optional field will contain the amount that will be deducted from your account, converted to the currency /// your account is settled in. It follows the same syntax as the amount property. /// public Amount? SettlementAmount { get; set; } /// /// The date and time the chargeback was issued, in ISO 8601 format. /// public required DateTime CreatedAt { get; set; } /// /// The date and time the chargeback was reversed, in ISO 8601 format. /// public DateTime? ReversedAt { get; set; } /// /// The id of the payment this chargeback belongs to. /// public required string PaymentId { get; set; } /// /// The reason given for a Chargeback, this can help determine the cost for the chargeback /// public ChargebackResponseReason? Reason { get; set; } /// /// An object with several URL objects relevant to the chargeback. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required ChargebackResponseLinks Links { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Chargeback/Response/ChargebackResponseLinks.cs ================================================ using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Settlement.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Chargeback.Response { public record ChargebackResponseLinks { /// /// The API resource URL of the chargeback itself. /// public required UrlObjectLink Self { get; set; } /// /// The API resource URL of the payment this chargeback belongs to. /// public required UrlObjectLink Payment { get; set; } /// /// The API resource URL of the settlement this payment has been settled with. Not present if not yet settled. /// public UrlObjectLink? Settlement { get; set; } /// /// The URL to the chargeback retrieval endpoint documentation. /// public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Chargeback/Response/ChargebackResponseReason.cs ================================================ namespace Mollie.Api.Models.Chargeback.Response { public record ChargebackResponseReason { /// /// The reason for the chargeback, these are documented here on Mollie's website https://help.mollie.com/hc/en-us/articles/115000309865-Why-did-my-direct-debit-payment-fail- /// public required string Code { get; set; } /// /// an accompanying note to the code /// public required string Description { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Client/Response/ClientCommissionResponse.cs ================================================ namespace Mollie.Api.Models.Client.Response { public record ClientCommissionResponse { /// /// The commission count. /// public int Count { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Client/Response/ClientEmbeddedResponse.cs ================================================ using Mollie.Api.Models.Capability.Response; using Mollie.Api.Models.Onboarding.Response; using Mollie.Api.Models.Organization; namespace Mollie.Api.Models.Client.Response { public record ClientEmbeddedResponse { public OrganizationResponse? Organization { get; set; } public OnboardingStatusResponse? Onboarding { get; set; } public CapabilityResponse? Capabilities { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Client/Response/ClientResponse.cs ================================================ using System; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Client.Response { public record ClientResponse : IEntity { /// /// Indicates the response contains a client object. Will always contain the string client for this resource type. /// public required string Resource { get; set; } /// /// The identifier uniquely referring to this client. Example: org_12345678. /// public required string Id { get; set; } /// /// The commission object. /// public ClientCommissionResponse? Commission { get; set; } /// /// The date and time the client organization was created. /// public required DateTime OrganizationCreatedAt { get; set; } [JsonPropertyName("_embedded")] public ClientEmbeddedResponse? Embedded { get; set; } /// /// An object with several URL objects relevant to the order line. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required ClientResponseLinks Links { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Client/Response/ClientResponseLinks.cs ================================================ using Mollie.Api.Models.Onboarding.Response; using Mollie.Api.Models.Organization; using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Client.Response { public record ClientResponseLinks { /// /// The API resource URL of the client itself. /// public required UrlObjectLink Self { get; set; } /// /// A link pointing to the product page in your web shop of the product sold. /// public required UrlObjectLink Organization { get; set; } /// /// The API resource URL of the client's onboarding status. /// public required UrlObjectLink Onboarding { get; set; } /// /// The URL to the client retrieval endpoint documentation. /// public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/ClientLink/Request/ClientLinkOwner.cs ================================================ namespace Mollie.Api.Models.ClientLink.Request { public record ClientLinkOwner { /// /// The email address of your customer. /// public required string Email { get; set; } /// /// The given name (first name) of your customer. /// public required string GivenName { get; set; } /// /// The family name (surname) of your customer. /// public required string FamilyName { get; set; } /// /// Allows you to preset the language to be used in the login / authorize flow. When this parameter is /// omitted, the browser language will be used instead. You can provide any xx_XX format ISO 15897 locale, /// but the authorize flow currently only supports the following languages: /// en_US nl_NL nl_BE fr_FR fr_BE de_DE es_ES it_IT /// public string? Locale { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/ClientLink/Request/ClientLinkRequest.cs ================================================ namespace Mollie.Api.Models.ClientLink.Request { public record ClientLinkRequest { /// /// Personal data of your customer which is required for this endpoint. /// public required ClientLinkOwner Owner { get; set; } /// /// Name of the organization. /// public required string Name { get; set; } /// /// Address of the organization. Note that the country parameter /// must always be provided. /// public required AddressObject Address { get; set; } /// /// The Chamber of Commerce (or local equivalent) registration number /// of the organization. /// public string? RegistrationNumber { get; set; } /// /// The VAT number of the organization, if based in the European Union /// or the United Kingdom. /// public string? VatNumber { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/ClientLink/Response/ClientLinkResponse.cs ================================================ using System.Text.Json.Serialization; namespace Mollie.Api.Models.ClientLink.Response { public record ClientLinkResponse : IEntity { public required string Id { get; set; } public required string Resource { get; set; } [JsonPropertyName("_links")] public required ClientLinkResponseLinks Links { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/ClientLink/Response/ClientLinkResponseLinks.cs ================================================ using Mollie.Api.Models.Url; namespace Mollie.Api.Models.ClientLink.Response { public record ClientLinkResponseLinks { public required UrlLink ClientLink { get; set; } public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/CompanyEntityType.cs ================================================ namespace Mollie.Api.Models { public static class CompanyEntityType { public const string LimitedCompany = "limited-company"; public const string PublicLimitedCompany = "public-limited-company"; public const string EntrepreneurialCompany = "entrepreneurial-company"; public const string LimitedPartnershipLimitedCompany = "limited-partnership-limited-company"; public const string LimitedPartnership = "limited-partnership"; public const string GeneralPartnership = "general-partnership"; public const string RegisteredSoleTrader = "registered-sole-trader"; public const string SoleTrader = "sole-trader"; public const string CivilLawPartnership = "civil-law-partnership"; public const string PublicInstitution = "public-institution"; } } ================================================ FILE: src/Mollie.Api/Models/CompanyObject.cs ================================================ namespace Mollie.Api.Models { public record CompanyObject { /// /// Organization’s registration number. /// public string? RegistrationNumber { get; set; } /// /// Organization’s VAT number. /// public string? VatNumber { get; set; } /// /// Organization’s entity type. /// The Mollie.Api.Models.CompanyEntityType class contains a full list of possible values /// public string? EntityType { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Connect/Request/AppPermissions.cs ================================================ namespace Mollie.Api.Models.Connect.Request { public static class AppPermissions { public const string BalancesRead = "balances.read"; public const string BalancesTransfersRead = "balance-transfers.read"; public const string BalancesTransfersWrite = "balance-transfers.write"; public const string CustomersRead = "customers.read"; public const string CustomersWrite = "customers.write"; public const string ExternalAccountsRead = "external-accounts.read"; public const string ExternalAccountsWrite = "external-accounts.write"; public const string InvoicesRead = "invoices.read"; public const string MandatesRead = "mandates.read"; public const string MandatesWrite = "mandates.write"; public const string OnboardingRead = "onboarding.read"; public const string OnboardingWrite = "onboarding.write"; public const string OrdersRead = "orders.read"; public const string OrdersWrite = "orders.write"; public const string OrganizationRead = "organizations.read"; public const string OrganizationWrite = "organizations.write"; public const string PaymentLinksRead = "payment-links.read"; public const string PaymentLinksWrite = "payment-links.write"; public const string PaymentsRead = "payments.read"; public const string PaymentsWrite = "payments.write"; public const string PersonsRead = "persons.read"; public const string PersonsWrite = "persons.write"; public const string ProfilesRead = "profiles.read"; public const string ProfilesWrite = "profiles.write"; public const string RefundsRead = "refunds.read"; public const string RefundsWrite = "refunds.write"; public const string SettlementsRead = "settlements.read"; public const string ShipmentsRead = "shipments.read"; public const string ShipmentsWrite = "shipments.write"; public const string SubscriptionsRead = "subscriptions.read"; public const string SubscriptionsWrite = "subscriptions.write"; public const string TerminalsRead = "terminals.read"; public const string TerminalsWrite = "terminals.write"; } } ================================================ FILE: src/Mollie.Api/Models/Connect/Request/RevokeTokenRequest.cs ================================================ using System.Text.Json.Serialization; namespace Mollie.Api.Models.Connect.Request { public record RevokeTokenRequest { /// /// Type of the token you want to revoke. /// [JsonPropertyName("token_type_hint")] public required string TokenTypeHint { get; set; } /// /// The token you want to revoke /// public required string Token { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Connect/Request/TokenRequest.cs ================================================ using System.Text.Json.Serialization; namespace Mollie.Api.Models.Connect.Request { public record TokenRequest { /// This can be an authorization code or a refresh token. The correct grant type will be automatically selected /// The URL the merchant is sent back to once the request has been authorized. It must match the URL you set when registering your app. public TokenRequest(string code, string redirectUri) { if (code.StartsWith("refresh_")) { GrantType = "refresh_token"; RefreshToken = code; } else { GrantType = "authorization_code"; Code = code; } RedirectUri = redirectUri; } /// /// If you wish to exchange your auth code for an access token, use grant type authorization_code. If you wish to renew /// your access token with your refresh token, use grant type refresh_token. /// Possible values: authorization_code refresh_token /// [JsonPropertyName("grant_type")] public string GrantType { get; } /// /// Optional – The auth code you've received when creating the authorization. Only use this field when using grant type /// authorization_code. /// public string? Code { get; set; } /// /// Optional – The refresh token you've received when creating the authorization. Only use this field when using grant /// type refresh_token. /// [JsonPropertyName("refresh_token")] public string? RefreshToken { get; set; } /// /// The URL the merchant is sent back to once the request has been authorized. It must match the URL you set when /// registering your app. /// [JsonPropertyName("redirect_uri")] public string? RedirectUri { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Connect/Request/TokenType.cs ================================================ namespace Mollie.Api.Models.Connect.Request { public static class TokenType { public const string AccessToken = "access_token"; public const string RefreshToken = "refresh_token"; } } ================================================ FILE: src/Mollie.Api/Models/Connect/Response/TokenResponse.cs ================================================ using System.Text.Json.Serialization; namespace Mollie.Api.Models.Connect.Response { public record TokenResponse { /// /// The access token, with which you will be able to access the Mollie API on the merchant's behalf. /// [JsonPropertyName("access_token")] public required string AccessToken { get; set; } /// /// The refresh token, with which you will be able to retrieve new access tokens on this endpoint. Please note that the /// refresh token does not expire. /// [JsonPropertyName("refresh_token")] public required string RefreshToken { get; set; } /// /// The number of seconds left before the access token expires. Be sure to renew your access token before this reaches /// zero. /// [JsonPropertyName("expires_in")] public required int ExpiresIn { get; set; } /// /// As per OAuth standards, the provided access token can only be used with bearer authentication. /// Possible values: bearer /// [JsonPropertyName("token_type")] public required string TokenType { get; set; } /// /// A space separated list of permissions. Please refer to OAuth: Permissions for the full permission list. /// public required string Scope { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Currency.cs ================================================ namespace Mollie.Api.Models { public static class Currency { public const string AUD = nameof(AUD); public const string BGN = nameof(BGN); public const string CAD = nameof(CAD); public const string HRK = nameof(HRK); public const string CZK = nameof(CZK); public const string DKK = nameof(DKK); public const string HKD = nameof(HKD); public const string HUF = nameof(HUF); public const string ISK = nameof(ISK); public const string ILS = nameof(ILS); public const string JPY = nameof(JPY); public const string NOK = nameof(NOK); public const string PLN = nameof(PLN); public const string GBP = nameof(GBP); public const string RON = nameof(RON); public const string SEK = nameof(SEK); public const string CHF = nameof(CHF); public const string USD = nameof(USD); public const string EUR = nameof(EUR); } } ================================================ FILE: src/Mollie.Api/Models/Customer/Request/CustomerRequest.cs ================================================ using System.Text.Json; using System.Text.Json.Serialization; using Mollie.Api.JsonConverters; namespace Mollie.Api.Models.Customer.Request { public record CustomerRequest : ITestModeRequest { /// /// The full name of the customer. /// public string? Name { get; set; } /// /// The email address of the customer. /// public string? Email { get; set; } /// /// Allows you to preset the language to be used in the payment screens shown to the consumer. When this parameter is not /// provided, the browser language will be used instead in the payment flow (which is usually more accurate). /// public string? Locale { get; set; } /// /// Optional - Provide any data you like in JSON notation, and we will save the data alongside the customer. Whenever /// you fetch the customer with our API, we'll also include the metadata. You can use up to 1kB of JSON. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// Oauth only - Optional – Set this to true to make this customer a test customer. /// public bool? Testmode { get; set; } public void SetMetadata(object metadataObj, JsonSerializerOptions? jsonSerializerOptions = null) { Metadata = JsonSerializer.Serialize(metadataObj, jsonSerializerOptions); } } } ================================================ FILE: src/Mollie.Api/Models/Customer/Response/CustomerResponse.cs ================================================ using System; using System.Text.Json; using Mollie.Api.JsonConverters; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Customer.Response { public record CustomerResponse : IEntity { /// /// Indicates the response contains a customer object. Will always contain customer for this endpoint. /// public required string Resource { get; set; } /// /// The customer's unique identifier, for example cst_4pmbK7CqtT. /// Store this identifier for later recurring payments. /// public required string Id { get; set; } /// /// The mode used to create this payment. Mode determines whether a payment is real or a test payment. /// public Mode Mode { get; set; } /// /// Name of your customer. /// public string? Name { get; set; } /// /// E-mailaddress of your customer. /// public string? Email { get; set; } /// /// Allows you to preset the language to be used in the payment screens shown to the consumer. If this parameter was not /// provided when the customer was created, the browser language will be used instead in the payment flow (which is usually /// more accurate). /// public string? Locale { get; set; } /// /// Optional metadata. Use this if you want Mollie to store additional info. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// DateTime when user was created. /// public required DateTime CreatedAt { get; set; } /// /// An object with several URL objects relevant to the customer. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required CustomerResponseLinks Links { get; set; } public T? GetMetadata(JsonSerializerOptions? jsonSerializerOptions = null) { return Metadata != null ? JsonSerializer.Deserialize(Metadata, jsonSerializerOptions) : default; } } } ================================================ FILE: src/Mollie.Api/Models/Customer/Response/CustomerResponseLinks.cs ================================================ using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Mandate.Response; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Subscription.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Customer.Response { public record CustomerResponseLinks { /// /// The API resource URL of the customer itself. /// public required UrlObjectLink Self { get; set; } /// /// The URL of the customer's dashboard in the Mollie administration panel. /// public required UrlLink Dashboard { get; set; } /// /// The API resource URL of the subscriptions belonging to the Customer, if there are no subscriptions this parameter is omitted. /// public UrlObjectLink>? Subscriptions { get; set; } /// /// The API resource URL of the payments belonging to the Customer, if there are no payments this parameter is omitted. /// public UrlObjectLink>? Payments { get; set; } /// /// The API resource URL of the mandates belonging to the Customer, if there are no mandates this parameter is omitted. /// public UrlObjectLink>? Mandates { get; set; } /// /// The URL to the customer retrieval endpoint documentation. /// public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Error/MollieErrorMessage.cs ================================================ using System.Text.Json.Serialization; namespace Mollie.Api.Models.Error { public record MollieErrorMessage { public int Status { get; set; } public required string Title { get; set; } public required string Detail { get; set; } /// /// The errors that are returned by the Connect client have a different format for some reason /// In order to use the same object, we just map private properties to the public properties /// that are used by the public api /// [JsonPropertyName("error")] [JsonInclude] private string Error { init { Title = value; } } [JsonPropertyName("error_description")] [JsonInclude] private string ErrorDescription { init { Detail = value; } } public override string ToString() { return $"{Title} - {Detail}"; } } } ================================================ FILE: src/Mollie.Api/Models/IEntity.cs ================================================ namespace Mollie.Api.Models; public interface IEntity { public string Resource { get; set; } public string Id { get; set; } } ================================================ FILE: src/Mollie.Api/Models/IProfileRequest.cs ================================================ namespace Mollie.Api.Models; public interface IProfileRequest { string? ProfileId { get; set; } } ================================================ FILE: src/Mollie.Api/Models/ITestModeRequest.cs ================================================ namespace Mollie.Api.Models; public interface ITestModeRequest { bool? Testmode { get; set; } } ================================================ FILE: src/Mollie.Api/Models/Invoice/Response/InvoiceLine.cs ================================================ namespace Mollie.Api.Models.Invoice.Response { public record InvoiceLine { /// /// The administrative period (YYYY) on which the line should be booked. /// public required string Period { get; set; } /// /// Description of the product. /// public required string Description { get; set; } /// /// Number of products invoiced (usually number of payments). /// public required int Count { get; set; } /// /// Optional – VAT percentage rate that applies to this product. /// public required decimal VatPercentage { get; set; } /// /// Amount excluding VAT. /// public required Amount Amount { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Invoice/Response/InvoiceResponse.cs ================================================ using System.Collections.Generic; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Invoice.Response { public record InvoiceResponse : IEntity { /// /// Indicates the response contains an invoice object. Will always contain invoice for this endpoint. /// public required string Resource { get; set; } /// /// The invoice's unique identifier, for example inv_FrvewDA3Pr. /// public required string Id { get; set; } /// /// The reference number of the invoice. An example value would be: 2016.10000. /// public required string Reference { get; set; } /// /// Optional – The VAT number to which the invoice was issued to (if applicable). /// public string? VatNumber { get; set; } /// /// Status of the invoices - See the Mollie.Api.Models.Invoice.InvoiceStatus class for /// a full list of known values /// public required string Status { get; set; } /// /// The invoice date (in YYYY-MM-DD format). /// public required string IssuedAt { get; set; } /// /// Optional – The date on which the invoice was paid (in YYYY-MM-DD format). Only for paid invoices. /// public string? PaidAt { get; set; } /// /// Optional – The date on which the invoice is due (in YYYY-MM-DD format). Only for due invoices. /// public string? DueAt { get; set; } /// /// Total amount of the invoice excluding VAT, e.g. {"currency":Currency.EUR, "value":"100.00"}. /// public required Amount NetAmount { get; set; } /// /// VAT amount of the invoice. Only for merchants registered in the Netherlands. For EU merchants, VAT /// will be shifted to recipient; article 44 and 196 EU VAT Directive 2006/112. For merchants outside the /// EU, no VAT will be charged. /// public required Amount VatAmount { get; set; } /// /// Total amount of the invoice including VAT. /// public required Amount GrossAmount { get; set; } /// /// The collection of products which make up the invoice. /// public required List Lines { get; set; } /// /// Useful URLs to related resources. /// [JsonPropertyName("_links")] public required InvoiceResponseLinks Links { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Invoice/Response/InvoiceResponseLinks.cs ================================================ using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Invoice.Response { public record InvoiceResponseLinks { /// /// The API resource URL of the invoice itself. /// public required UrlObjectLink Self { get; set; } /// /// The URL to the PDF version of the invoice. The URL will expire after 60 minutes. /// public UrlLink? Pdf { get; set; } /// /// The URL to the invoice retrieval endpoint documentation. /// public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Invoice/Response/InvoiceStatus.cs ================================================ namespace Mollie.Api.Models.Invoice.Response { public static class InvoiceStatus { public const string Open = "open"; public const string Paid = "paid"; public const string Overdue = "overdue"; } } ================================================ FILE: src/Mollie.Api/Models/Issuer/Response/IssuerResponse.cs ================================================ namespace Mollie.Api.Models.Issuer.Response { public record IssuerResponse : IEntity { /// /// Contains "issuer" /// public required string Resource { get; set; } /// /// The issuer's unique identifier, for example ideal_ABNANL2A. When creating a payment, specify this ID as the issuer /// parameter to forward /// the consumer to their banking environment directly. /// public required string Id { get; set; } /// /// The issuer's full name, for example 'ABN AMRO'. /// public required string Name { get; set; } /// /// Different Issuer Image icons (iDEAL). /// public required IssuerResponseImage Image { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Issuer/Response/IssuerResponseImage.cs ================================================ namespace Mollie.Api.Models.Issuer.Response { /// /// URLs of images representing the issuer. /// public record IssuerResponseImage { public required string Size1x { get; set; } public required string Size2x { get; set; } public required string Svg { get; set; } public override string ToString() { return Size1x; } } } ================================================ FILE: src/Mollie.Api/Models/List/Response/ListResponse.cs ================================================ using System.Collections.Generic; using Mollie.Api.JsonConverters; using System.Text.Json.Serialization; namespace Mollie.Api.Models.List.Response { public record ListResponse where T : class { public int Count { get; set; } [JsonConverter(typeof(ListResponseConverter))] [JsonPropertyName("_embedded")] public required List Items { get; set; } [JsonPropertyName("_links")] public required ListResponseLinks Links { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/List/Response/ListResponseLinks.cs ================================================ using Mollie.Api.Models.Url; namespace Mollie.Api.Models.List.Response { /// /// Links to help navigate through the lists of objects, based on the given offset. /// public record ListResponseLinks where T : class { /// /// The URL to the current set of payments. /// public required UrlObjectLink> Self { get; set; } /// /// The previous set of objects, if available. /// public UrlObjectLink>? Previous { get; set; } /// /// The next set of objects, if available. /// public UrlObjectLink>? Next { get; set; } /// /// The URL to the payments list endpoint documentation. /// public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Mandate/Request/MandateRequest.cs ================================================ using System; using System.Text.Json.Serialization; using Mollie.Api.Framework; using Mollie.Api.JsonConverters; namespace Mollie.Api.Models.Mandate.Request { public record MandateRequest : ITestModeRequest { /// /// Payment method of the mandate - Possible values: `directdebit` `paypal` /// public required string Method { get; set; } /// /// Required - Name of consumer you add to the mandate /// public required string ConsumerName { get; set; } /// /// Optional - The date when the mandate was signed. /// [JsonConverter(typeof(DateJsonConverter))] public DateTime? SignatureDate { get; set; } /// /// Optional - A custom reference /// public string? MandateReference { get; set; } /// /// Oauth only - Optional – Set this to true to make this mandate a test mandate. /// public bool? Testmode { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Mandate/Request/PaymentSpecificParameters/PayPalMandateRequest.cs ================================================ namespace Mollie.Api.Models.Mandate.Request.PaymentSpecificParameters { public record PayPalMandateRequest : MandateRequest { public PayPalMandateRequest() { Method = Payment.PaymentMethod.PayPal; } /// /// Required For Paypal - The consumer's email address. /// public required string ConsumerEmail { get; set; } /// /// Required for `paypal` mandates - The billing agreement ID given by PayPal. /// public required string PaypalBillingAgreementId { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Mandate/Request/PaymentSpecificParameters/SepaDirectDebitMandateRequest.cs ================================================ namespace Mollie.Api.Models.Mandate.Request.PaymentSpecificParameters { public record SepaDirectDebitMandateRequest : MandateRequest { public SepaDirectDebitMandateRequest() { Method = Payment.PaymentMethod.DirectDebit; } /// /// Required for `directdebit` mandates - Consumer's IBAN account /// public required string ConsumerAccount { get; set; } /// /// Optional - The consumer's bank's BIC / SWIFT code. /// public string? ConsumerBic { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Mandate/Response/MandateResponse.cs ================================================ using System; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Mandate.Response { public record MandateResponse : IEntity { /// /// Indicates the response contains a mandate object. Will always contain mandate for this endpoint. /// public required string Resource { get; set; } /// /// Unique identifier of you mandate. /// public required string Id { get; set; } /// /// Whether this entity was created in live mode or in test mode. /// public required Mode Mode { get; set; } /// /// Current status of mandate - See the Mollie.Api.Models.Mandate.MandateStatus class for a full /// list of known values. /// public required string Status { get; set; } /// /// Payment method of the mandate - See the Mollie.Api.Models.Payment.PaymentMethod class for a full list of known values. /// public required string Method { get; set; } /// /// The mandate’s custom reference, if this was provided when creating the mandate. /// public string? MandateReference { get; set; } /// /// The signature date of the mandate in YYYY-MM-DD format. /// public string? SignatureDate { get; set; } /// /// The identifier referring to the customer this mandate was linked to. /// public required string CustomerId { get; set; } /// /// DateTime when mandate was created. /// public DateTime CreatedAt { get; set; } /// /// An object with several URL objects relevant to the mandate. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required MandateResponseLinks Links { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Mandate/Response/MandateResponseLinks.cs ================================================ using Mollie.Api.Models.Customer.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Mandate.Response { public record MandateResponseLinks { /// /// The API resource URL of the mandate itself. /// public required UrlObjectLink Self { get; set; } /// /// The API resource URL of the customer the mandate is for. /// public required UrlObjectLink Customer { get; set; } /// /// The URL to the mandate retrieval endpoint documentation. /// public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Mandate/Response/MandateStatus.cs ================================================ namespace Mollie.Api.Models.Mandate.Response { public static class MandateStatus { public const string Valid = "valid"; public const string Invalid = "invalid"; public const string Pending = "pending"; } } ================================================ FILE: src/Mollie.Api/Models/Mandate/Response/PaymentSpecificParameters/CreditCardMandateResponse.cs ================================================ namespace Mollie.Api.Models.Mandate.Response.PaymentSpecificParameters; public record CreditCardMandateResponse : MandateResponse { public required CreditCardMandateResponseDetails Details { get; set; } } public record CreditCardMandateResponseDetails { /// /// The credit card holder's name. /// public string? CardHolder { get; set; } /// /// The last four digits of the credit card number. /// public string? CardNumber { get; set; } /// /// The credit card's label. Note that not all labels can be acquired through Mollie. /// public string? CardLabel { get; set; } /// /// Unique alphanumeric representation of credit card, usable for identifying returning customers. /// public string? CardFingerprint { get; set; } /// /// Expiry date of the credit card card in YYYY-MM-DD format. /// public string? CardExpiryDate { get; set; } } ================================================ FILE: src/Mollie.Api/Models/Mandate/Response/PaymentSpecificParameters/PayPalMandateResponse.cs ================================================ namespace Mollie.Api.Models.Mandate.Response.PaymentSpecificParameters; public record PayPalMandateResponse : MandateResponse { public required PayPalMandateResponseDetails Details { get; set; } } public record PayPalMandateResponseDetails { /// /// Only available if the payment has been completed – The consumer's name. /// public string? ConsumerName { get; set; } /// /// Only available if the payment has been completed – The consumer's IBAN. /// public string? ConsumerAccount { get; set; } } ================================================ FILE: src/Mollie.Api/Models/Mandate/Response/PaymentSpecificParameters/SepaDirectDebitMandateResponse.cs ================================================ namespace Mollie.Api.Models.Mandate.Response.PaymentSpecificParameters; public record SepaDirectDebitMandateResponse : MandateResponse { public required SepaDirectDebitMandateResponseDetails Details { get; set; } } public record SepaDirectDebitMandateResponseDetails { /// /// Only available if the payment has been completed – The consumer's name. /// public string? ConsumerName { get; set; } /// /// Only available if the payment has been completed – The consumer's IBAN. /// public string? ConsumerAccount { get; set; } /// /// Only available if the payment has been completed – The consumer's bank's BIC. /// public string? ConsumerBic { get; set; } } ================================================ FILE: src/Mollie.Api/Models/Mode.cs ================================================ using System.Runtime.Serialization; namespace Mollie.Api.Models { public enum Mode { [EnumMember(Value = "live")] Live, [EnumMember(Value = "test")] Test } } ================================================ FILE: src/Mollie.Api/Models/Onboarding/Request/OnboardingOrganizationRequest.cs ================================================ namespace Mollie.Api.Models.Onboarding.Request { /// /// Data of the organization you want to provide. /// public record OnboardingOrganizationRequest { /// /// Name of the organization. /// public string? Name { get; set; } /// /// Address of the organization. /// public AddressObject? Address { get; set; } /// /// The Chamber of Commerce (or local equivalent) registration number of the organization. /// public string? RegistrationNumber { get; set; } /// /// The VAT number of the organization, if based in the European Union or the United Kingdom. /// public string? VatNumber { get; set; } /// /// The organization’s VAT regulation, if based in the European Union. Either shifted (VAT is shifted) /// or dutch (Dutch VAT rate) is accepted. /// public string? VatRegulation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Onboarding/Request/OnboardingProfileRequest.cs ================================================ namespace Mollie.Api.Models.Onboarding.Request { /// /// Data of the payment profile you want to provide. /// public record OnboardingProfileRequest { /// /// The profile’s name should reflect the tradename or brand name of the profile’s website or application. /// public string? Name { get; set; } /// /// The URL to the profile’s website or application. The URL must be compliant to RFC3986 with the exception /// that we only accept URLs with http:// or https:// schemes and domains that contain a TLD. URLs containing /// an @ are not allowed. /// public string? Url { get; set; } /// /// The email address associated with the profile’s tradename or brand. /// public string? Email { get; set; } /// /// A description of what kind of goods and/or products will be offered via the payment profile. /// public string? Description { get; set; } /// /// The phone number associated with the profile’s trade name or brand. Must be in the E.164 format. For example /// +31208202070. /// public string? Phone { get; set; } /// /// The industry associated with the profile’s trade name or brand. Please refer to the documentation of the business category /// for more information on which values are accepted. /// public string? BusinessCategory { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Onboarding/Request/SubmitOnboardingDataRequest.cs ================================================ namespace Mollie.Api.Models.Onboarding.Request { public record SubmitOnboardingDataRequest { /// /// Data of the organization you want to provide. /// public OnboardingOrganizationRequest? Organization { get; set; } /// /// Data of the payment profile you want to provide. /// public OnboardingProfileRequest? Profile { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Onboarding/Response/OnboardingStatus.cs ================================================ namespace Mollie.Api.Models.Onboarding.Response { /// /// The current status of the organization’s onboarding process /// public static class OnboardingStatus { /// /// The onboarding is not completed and the merchant needs to provide (more) information /// public const string NeedsData = "needs-data"; /// /// The merchant provided all information and Mollie needs to check this /// public const string InReview = "in-review"; /// /// The onboarding is completed /// public const string Completed = "completed"; } } ================================================ FILE: src/Mollie.Api/Models/Onboarding/Response/OnboardingStatusResponse.cs ================================================ using System.Text.Json.Serialization; using System; namespace Mollie.Api.Models.Onboarding.Response { public record OnboardingStatusResponse { /// /// Indicates the response contains an onboarding object. Will always contain onboarding for this endpoint. /// public required string Resource { get; set; } /// /// The name of the organization. /// public string? Name { get; set; } /// /// The sign up date and time of the organization. /// public DateTime SignedUpAt { get; set; } /// /// The current status of the organization’s onboarding process. See the Mollie.Api.Models.Onboarding.Response.OnboardingStatus /// class for a full list of known values. /// public required string Status { get; set; } /// /// Whether or not the organization can receive payments. /// public bool CanReceivePayments { get; set; } /// /// Whether or not the organization can receive settlements. /// public bool CanReceiveSettlements { get; set; } /// /// An object with several URL objects relevant to the organization. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required OnboardingStatusResponseLinks Links { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Onboarding/Response/OnboardingStatusResponseLinks.cs ================================================ using Mollie.Api.Models.Organization; using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Onboarding.Response { /// /// An object with several URL objects relevant to the onboarding status. Every URL object will contain an href and a type field. /// public record OnboardingStatusResponseLinks { /// /// The API resource URL of this endpoint itself. /// public required UrlLink Self { get; set; } /// /// The URL of the onboarding process in Mollie Dashboard. You can redirect your customer to here for e.g. completing the onboarding process. /// public required UrlLink Dashboard { get; set; } /// /// The API resource URL of the organization. /// public UrlObjectLink? Organization { get; set; } /// /// The URL to the onboarding status retrieval endpoint documentation. /// public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/OrderAddressDetails.cs ================================================ namespace Mollie.Api.Models.Order { public record OrderAddressDetails : AddressObject { /// /// The person’s organization, if applicable. /// public string? OrganizationName { get; set; } /// /// The title of the person, for example Mr. or Mrs.. /// public string? Title { get; set; } /// /// The given name (first name) of the person. /// public string? GivenName { get; set; } /// /// The family name (surname) of the person. /// public string? FamilyName { get; set; } /// /// The email address of the person. /// public string? Email { get; set; } /// /// The phone number of the person. Some payment methods require this information. If you have it, you /// should pass it so that your customer does not have to enter it again in the checkout. Must be in /// the E.164 format. For example +31208202070. /// public string? Phone { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/ManageOrderLines/ManageOrderLinesAddOperation.cs ================================================ namespace Mollie.Api.Models.Order.Request.ManageOrderLines { public record ManageOrderLinesAddOperation : ManageOrderLinesOperation { public required ManageOrderLinesAddOperationData Data { get; set; } public ManageOrderLinesAddOperation() { Operation = OrderLineOperation.Add; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/ManageOrderLines/ManageOrderLinesAddOperationData.cs ================================================ namespace Mollie.Api.Models.Order.Request.ManageOrderLines { public record ManageOrderLinesAddOperationData : OrderLineRequest; } ================================================ FILE: src/Mollie.Api/Models/Order/Request/ManageOrderLines/ManageOrderLinesCancelOperation.cs ================================================ namespace Mollie.Api.Models.Order.Request.ManageOrderLines { public record ManageOrderLinesCancelOperation : ManageOrderLinesOperation { public required ManagerOrderLinesCancelOperationData Data { get; set; } public ManageOrderLinesCancelOperation() { Operation = OrderLineOperation.Cancel; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/ManageOrderLines/ManageOrderLinesOperation.cs ================================================ using System.Text.Json.Serialization; namespace Mollie.Api.Models.Order.Request.ManageOrderLines { [JsonPolymorphic(TypeDiscriminatorPropertyName = "operation")] [JsonDerivedType(typeof(ManageOrderLinesAddOperation), OrderLineOperation.Add)] [JsonDerivedType(typeof(ManageOrderLinesCancelOperation), OrderLineOperation.Cancel)] [JsonDerivedType(typeof(ManageOrderLinesUpdateOperation), OrderLineOperation.Update)] public abstract record ManageOrderLinesOperation { /// /// Operation type. Either `add`, `update`, or `cancel`. /// [JsonIgnore] public string Operation { get; protected set; } = string.Empty; } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/ManageOrderLines/ManageOrderLinesRequest.cs ================================================ using System.Collections.Generic; namespace Mollie.Api.Models.Order.Request.ManageOrderLines { public record ManageOrderLinesRequest : ITestModeRequest { /// /// List of operations to be processed. /// public required IList Operations { get; set; } /// /// Oauth only - Optional /// public bool? Testmode { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/ManageOrderLines/ManageOrderLinesUpdateOperation.cs ================================================ namespace Mollie.Api.Models.Order.Request.ManageOrderLines { public record ManageOrderLinesUpdateOperation : ManageOrderLinesOperation { public required ManageOrderLinesUpdateOperationData Data { get; set; } public ManageOrderLinesUpdateOperation() { Operation = OrderLineOperation.Update; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/ManageOrderLines/ManageOrderLinesUpdateOperationData.cs ================================================ namespace Mollie.Api.Models.Order.Request.ManageOrderLines { public record ManageOrderLinesUpdateOperationData : OrderLineUpdateRequest { public required string Id { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/ManageOrderLines/ManagerOrderLinesCancelOperationData.cs ================================================ namespace Mollie.Api.Models.Order.Request.ManageOrderLines { public record ManagerOrderLinesCancelOperationData : OrderLineDetails; } ================================================ FILE: src/Mollie.Api/Models/Order/Request/ManageOrderLines/OrderLineOperation.cs ================================================ namespace Mollie.Api.Models.Order.Request.ManageOrderLines { public static class OrderLineOperation { public const string Add = "add"; public const string Update = "update"; public const string Cancel = "cancel"; } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/OrderLineCancellationRequest.cs ================================================ using System.Collections.Generic; namespace Mollie.Api.Models.Order.Request { public record OrderLineCancellationRequest : ITestModeRequest { public required IEnumerable Lines { get; set; } /// /// Oauth only - Optional /// public bool? Testmode { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/OrderLineDetails.cs ================================================ namespace Mollie.Api.Models.Order.Request { public record OrderLineDetails { public required string Id { get; set; } public int? Quantity { get; set; } public Amount? Amount { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/OrderLineDetailsType.cs ================================================ namespace Mollie.Api.Models.Order.Request { public static class OrderLineDetailsType { public const string Physical = "physical"; public const string Discount = "discount"; public const string Digital = "digital"; public const string ShippingFee = "shipping_fee"; public const string StoreCredit = "gift_card"; public const string GiftCard = "gift_card"; public const string Surcharge = "surcharge"; public const string Tip = "tip"; } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/OrderLineRequest.cs ================================================ using Mollie.Api.JsonConverters; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Order.Request { public record OrderLineRequest { /// /// The type of product bought, for example, a physical or a digital product. See the /// Mollie.Api.Models.Order.OrderLineDetailsType class for a full list of known values. /// public string? Type { get; set; } /// /// The category of product bought. See the Mollie.Api.Models.Order.OrderLineDetailsCategory class /// for a full list of known values /// public string? Category { get; set; } /// /// A description of the order line, for example LEGO 4440 Forest Police Station. /// public required string Name { get; set; } /// /// The number of items in the order line. /// public required int Quantity { get; set; } /// /// The price of a single item in the order line. /// public required Amount UnitPrice { get; set; } /// /// Any discounts applied to the order line. For example, if you have a two-for-one sale, you should pass the /// amount discounted as a positive amount. /// public Amount? DiscountAmount { get; set; } /// /// The total amount of the line, including VAT and discounts. Adding all totalAmount values together should /// result in the same amount as the amount top level property. /// public required Amount TotalAmount { get; set; } /// /// The VAT rate applied to the order line, for example "21.00" for 21%. The vatRate should be passed as a /// string and not as a float to ensure the correct number of decimals are passed. /// public required string VatRate { get; set; } /// /// The amount of value-added tax on the line. The totalAmount field includes VAT, so the vatAmount can be /// calculated with the formula totalAmount × (vatRate / (100 + vatRate)). Any deviations from this will /// result in an error. /// public required Amount VatAmount { get; set; } /// /// The SKU, EAN, ISBN or UPC of the product sold. The maximum character length is 64. /// public string? Sku { get; set; } /// /// A link pointing to an image of the product sold. /// public string? ImageUrl { get; set; } /// /// A link pointing to the product page in your web shop of the product sold. /// public string? ProductUrl { get; set; } /// /// The optional metadata you provided upon line creation. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/OrderLineUpdateRequest.cs ================================================ using System.Text.Json; using System.Text.Json.Serialization; using Mollie.Api.JsonConverters; namespace Mollie.Api.Models.Order.Request { public record OrderLineUpdateRequest : ITestModeRequest { /// /// A description of the order line, for example LEGO 4440 Forest Police Station. /// public string? Name { get; set; } /// /// A link pointing to an image of the product sold. /// public string? ImageUrl { get; set; } /// /// A link pointing to the product page in your web shop of the product sold. /// public string? ProductUrl { get; set; } /// /// The SKU, EAN, ISBN or UPC of the product sold. The maximum character length is 64. /// public string? Sku { get; set; } /// /// Provide any data you like, for example a string or a JSON object. We will /// save the data alongside the order line. Whenever you fetch the order with our API, /// we'll also include the metadata. You can use up to approximately 1kB. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// The number of items in the order line. /// public int? Quantity { get; set; } /// /// The price of a single item including VAT in the order line. /// public Amount? UnitPrice { get; set; } /// /// Any discounts applied to the order line. For example, if you have a two-for-one sale, you should pass the /// amount discounted as a positive amount. /// public Amount? DiscountAmount { get; set; } /// /// The total amount of the line, including VAT and discounts. Adding all totalAmount values together should /// result in the same amount as the amount top level property. /// public Amount? TotalAmount { get; set; } /// /// The amount of value-added tax on the line. The totalAmount field includes VAT, so the vatAmount can be /// calculated with the formula totalAmount × (vatRate / (100 + vatRate)). /// public Amount? VatAmount { get; set; } /// /// The VAT rate applied to the order line, for example "21.00" for 21%. The vatRate should be passed as a /// string and not as a float to ensure the correct number of decimals are passed. /// public string? VatRate { get; set; } /// /// Oauth only - Optional /// public bool? Testmode { get; set; } public void SetMetadata(object metadataObj, JsonSerializerOptions? jsonSerializerOptions = null) { Metadata = JsonSerializer.Serialize(metadataObj, jsonSerializerOptions); } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/OrderPaymentRequest.cs ================================================ using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Order.Request { public record OrderPaymentRequest : ITestModeRequest { /// /// Normally, a payment method selection screen is shown. However, when using this parameter, your customer will skip the /// selection screen and will be sent directly to the chosen payment method. The parameter enables you to fully integrate /// the payment method selection into your website, however note Mollie’s country based conversion optimization is lost. /// See the Mollie.Api.Models.Payment.PaymentMethod class for a full list of known values. /// [JsonIgnore] public string? Method { get => Methods?.FirstOrDefault(); set { if (value == null) { Methods = null; } else { Methods = new List(); Methods.Add(value); } } } /// /// Normally, a payment method screen is shown. However, when using this parameter, you can choose a specific payment method /// and your customer will skip the selection screen and is sent directly to the chosen payment method. The parameter /// enables you to fully integrate the payment method selection into your website. /// You can also specify the methods in an array.By doing so we will still show the payment method selection screen but will /// only show the methods specified in the array. For example, you can use this functionality to only show payment methods /// from a specific country to your customer. /// [JsonPropertyName("method")] public IList? Methods { get; set; } /// /// The ID of the Customer for whom the payment is being created. This is used for recurring payments /// and single click payments. /// public string? CustomerId { get; set; } /// /// When creating recurring payments, the ID of a specific Mandate may be supplied to indicate which /// of the consumer’s accounts should be credited. /// public string? MandateId { get; set; } /// /// Oauth only - Optional – Set this to true to make this payment a test payment. /// public bool? Testmode { get; set; } /// /// Oauth only - Optional – Adding an Application Fee allows you to charge the merchant a small sum for the payment and transfer /// this to your own account. /// public ApplicationFee? ApplicationFee { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/OrderRefundRequest.cs ================================================ using System.Collections.Generic; using Mollie.Api.JsonConverters; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Order.Request { public record OrderRefundRequest : ITestModeRequest { /// /// An array of objects containing the order line details you want to create a refund for. If you send /// an empty array, the entire order will be refunded. /// public required IEnumerable Lines { get; set; } /// /// The description of the refund you are creating. This will be shown to the consumer on their card or /// bank statement when possible. Max. 140 characters. /// public string? Description { get; set; } /// /// The optional metadata you provided upon line creation. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// Oauth only - Optional /// public bool? Testmode { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/OrderRequest.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using Mollie.Api.JsonConverters; using Mollie.Api.Models.Order.Request.PaymentSpecificParameters; using System.Text.Json.Serialization; using Mollie.Api.Framework; namespace Mollie.Api.Models.Order.Request { public record OrderRequest : ITestModeRequest, IProfileRequest { /// /// The total amount of the order, including VAT and discounts. This is the amount that will be charged /// to your customer. /// public required Amount Amount { get; set; } /// /// The order number. For example, 16738. We recommend that each order should have a unique order number. /// public required string OrderNumber { get; set; } /// /// The lines in the order. Each line contains details such as a description of the item ordered, its /// price et cetera. /// public required IEnumerable Lines { get; set; } /// /// The billing person and address for the order. /// public OrderAddressDetails? BillingAddress { get; set; } /// /// The shipping address for the order. See Order address details for the exact fields needed. If omitted, /// it is assumed to be identical to the billingAddress. /// public OrderAddressDetails? ShippingAddress { get; set; } /// /// The date of birth of your customer. Some payment methods need this value and if you have it, you should /// send it so that your customer does not have to enter it again later in the checkout process. /// [JsonConverter(typeof(DateJsonConverter))] public DateTime? ConsumerDateOfBirth { get; set; } /// /// The URL your customer will be redirected to after the payment process. /// public string? RedirectUrl { get; set; } /// /// The URL your consumer will be redirected to when the consumer explicitly cancels the payment. If this URL /// is not provided, the consumer will be redirected to the redirectUrl instead — see above. /// /// Mollie will always give you status updates via webhooks, including for the canceled status. This parameter /// is therefore entirely optional, but can be useful when implementing a dedicated consumer-facing flow to handle /// payment cancellations. /// /// The parameter can be omitted for orders with payment.sequenceType set to recurring. /// public string? CancelUrl { get; set; } /// /// Set the webhook URL, where we will send order status changes to. /// public string? WebhookUrl { get; set; } /// /// Allows you to preset the language to be used in the hosted payment pages shown to the consumer. /// public required string Locale { get; set; } /// /// Normally, a payment method selection screen is shown. However, when using this parameter, your customer /// will skip the selection screen and will be sent directly to the chosen payment method. The parameter enables /// you to fully integrate the payment method selection into your website. See the /// Mollie.Api.Models.Payment.PaymentMethod class for a full list of known values. /// [JsonIgnore] public string? Method { get => Methods?.FirstOrDefault(); set { if (value == null) { Methods = null; } else { Methods = new List(); Methods.Add(value); } } } /// /// Normally, a payment method screen is shown. However, when using this parameter, you can choose a specific payment method /// and your customer will skip the selection screen and is sent directly to the chosen payment method. The parameter /// enables you to fully integrate the payment method selection into your website. /// You can also specify the methods in an array.By doing so we will still show the payment method selection screen but will /// only show the methods specified in the array. For example, you can use this functionality to only show payment methods /// from a specific country to your customer. /// [JsonPropertyName("method")] public IList? Methods { get; set; } /// /// Optional - Any payment specific properties can be passed here. /// public OrderPaymentParameters? Payment { get; set; } /// /// Provide any data you like, and we will save the data alongside the subscription. Whenever you fetch the subscription /// with our API, we’ll also include the metadata. You can use up to 1kB of JSON. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// The date the order should expire in YYYY-MM-DD format. The minimum date is tomorrow and the maximum date is 100 days /// after tomorrow. /// [JsonConverter(typeof(DateJsonConverter))] public DateTime? ExpiresAt { get; set; } /// /// For digital goods, you must make sure to apply the VAT rate from your customer’s country in most jurisdictions. Use /// this parameter to restrict the payment methods available to your customer to methods from the billing country only. /// public bool? ShopperCountryMustMatchBillingCountry { get; set; } /// /// Oauth only - The payment profile's unique identifier, for example pfl_3RkSN1zuPE. /// public string? ProfileId { get; set; } /// /// Oauth only - Optional – Set this to true to make this payment a test payment. /// public bool? Testmode { get; set; } public void SetMetadata(object metadataObj, JsonSerializerOptions? jsonSerializerOptions = null) { Metadata = JsonSerializer.Serialize(metadataObj, jsonSerializerOptions); } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/OrderUpdateRequest.cs ================================================ namespace Mollie.Api.Models.Order.Request { public record OrderUpdateRequest : ITestModeRequest { /// /// The billing person and address for the order. See Order address details for the /// exact fields needed. /// public OrderAddressDetails? BillingAddress { get; set; } /// /// The shipping address for the order. See Order address details for the exact /// fields needed. /// public OrderAddressDetails? ShippingAddress { get; set; } /// /// The order number. For example, 16738. We recommend that each order should have a unique order number. /// public string? OrderNumber { get; set; } /// /// The URL your customer will be redirected to after the payment process. Updating this field is only possible /// when the payment is not yet finalized. /// public string? RedirectUrl { get; set; } /// /// The URL your consumer will be redirected to when the consumer explicitly cancels the order. If this URL /// is not provided, the consumer will be redirected to the redirectUrl instead — see above. Updating this /// field is only possible when the payment is not yet finalized. /// public string? CancelUrl { get; set; } /// /// Set the webhook URL, where we will send order status changes to. The webhookUrl must be reachable from /// Mollie’s point of view, so you cannot use localhost. If you want to use webhook during development on /// localhost, you should use a tool like ngrok to have the webhooks delivered to your local machine. /// public string? WebhookUrl { get; set; } /// /// Oauth only - Optional /// public bool? Testmode { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/PaymentSpecificParameters/ApplePaySpecificParameters.cs ================================================ namespace Mollie.Api.Models.Order.Request.PaymentSpecificParameters { public record ApplePaySpecificParameters : OrderPaymentParameters { /// /// Optional - The Apple Pay Payment Token object (encoded as JSON) that is part of the result of authorizing a payment /// request. The token contains the payment information needed to authorize the payment. /// public string? ApplePayPaymentToken { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/PaymentSpecificParameters/BillieSpecificParameters.cs ================================================ namespace Mollie.Api.Models.Order.Request.PaymentSpecificParameters { public record BillieSpecificParameters : OrderPaymentParameters { /// /// Billie is a B2B payment method, thus it requires some extra information to identify the business /// that is creating the order. It is recommended to include these parameters as part of the create /// order request for a seamless flow, otherwise the customer will be asked to fill the missing fields /// at the Billie’s checkout page. /// public CompanyObject? Company { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/PaymentSpecificParameters/CreditCardSpecificParameters.cs ================================================ namespace Mollie.Api.Models.Order.Request.PaymentSpecificParameters { public record CreditCardSpecificParameters : OrderPaymentParameters { /// /// The card token you get from Mollie Components. The token contains the card information /// (such as card holder, card number and expiry date) needed to complete the payment. /// public string? CardToken { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/PaymentSpecificParameters/GiftcardSpecificParameters.cs ================================================ namespace Mollie.Api.Models.Order.Request.PaymentSpecificParameters { public record GiftcardSpecificParameters : OrderPaymentParameters { /// /// The gift card brand to use for the payment. These issuers are not dynamically available through the Issuers API, /// but can be retrieved by using the issuers include in the Methods API. If you need a brand not in the list, contact /// our support department. If only one issuer is activated on your account, you can omit this parameter. /// public string? Issuer { get; set; } /// /// The card number on the gift card. /// public string? VoucherNumber { get; set; } /// /// The PIN code on the gift card. Only required if there is a PIN code printed on the gift card. /// public string? VoucherPin { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/PaymentSpecificParameters/IDealSpecificParameters.cs ================================================ namespace Mollie.Api.Models.Order.Request.PaymentSpecificParameters { public record IDealSpecificParameters : OrderPaymentParameters { /// /// Optional - iDEAL issuer id. The id could for example be ideal_INGBNL2A. The returned paymentUrl will then directly /// point to the ING web site. /// public string? Issuer { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/PaymentSpecificParameters/KbcSpecificParameters.cs ================================================ namespace Mollie.Api.Models.Order.Request.PaymentSpecificParameters { public record KbcSpecificParameters : OrderPaymentParameters { /// /// The issuer to use for the KBC/CBC payment. These issuers are not dynamically available through the Issuers API, /// but can be retrieved by using the issuers include in the Methods API. See the Mollie.Api.Models.Payment.Request.KbcIssuer /// class for a full list of known values. /// public string? Issuer { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/PaymentSpecificParameters/KlarnaSpecificParameters.cs ================================================ namespace Mollie.Api.Models.Order.Request.PaymentSpecificParameters { public record KlarnaSpecificParameters : OrderPaymentParameters where T : class { public T? ExtraMerchantData { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/PaymentSpecificParameters/OrderPaymentParameters.cs ================================================ namespace Mollie.Api.Models.Order.Request.PaymentSpecificParameters { public record OrderPaymentParameters { public string? CustomerId { get; set; } /// /// See the Mollie.Api.Models.Payment.SequenceType class for a full list of known values. /// public string? SequenceType { get; set; } public string? WebhookUrl { get; set; } /// /// Adding an application fee allows you to charge the merchant for the payment and transfer this to your own account. /// public ApplicationFee? ApplicationFee { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/PaymentSpecificParameters/PaySafeCardSpecificParameters.cs ================================================ namespace Mollie.Api.Models.Order.Request.PaymentSpecificParameters { public record PaySafeCardSpecificParameters : OrderPaymentParameters { /// /// Used for consumer identification. For example, you could use the consumer’s IP address. /// public string? CustomerReference { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Request/PaymentSpecificParameters/SepaDirectDebitSpecificParameters.cs ================================================ namespace Mollie.Api.Models.Order.Request.PaymentSpecificParameters { public record SepaDirectDebitSpecificParameters : OrderPaymentParameters { /// /// Optional - IBAN of the account holder. /// public string? ConsumerAccount { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Response/OrderEmbeddedResponse.cs ================================================ using System.Collections.Generic; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Refund.Response; using Mollie.Api.Models.Shipment.Response; namespace Mollie.Api.Models.Order.Response { public record OrderEmbeddedResponse { public IEnumerable? Payments { get; set; } public IEnumerable? Refunds { get; set; } public IEnumerable? Shipments { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Response/OrderLineResponse.cs ================================================ using System; using Mollie.Api.JsonConverters; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Order.Response { public record OrderLineResponse { /// /// The order line’s unique identifier, for example odl_dgtxyl. /// public required string Id { get; set; } /// /// Indicates the response contains an order line object. Will always contain orderline for this endpoint. /// public required string Resource { get; set; } /// /// The ID of the order the line belongs too, for example ord_kEn1PlbGa. /// public required string OrderId { get; set; } /// /// The type of product bought, for example, a physical or a digital product. See the /// Mollie.Api.Models.Order.OrderLineDetailsType class for a full list of known values. /// public string? Type { get; set; } /// /// A description of the order line, for example LEGO 4440 Forest Police Station. /// public required string Name { get; set; } /// /// Status of the order line - See the Mollie.Api.Models.Order.OrderLineStatus class for /// a full list of known values. /// public required string Status { get; set; } /// /// Whether or not the order line can be (partially) canceled. /// public required bool IsCancelable { get; set; } /// /// The number of items in the order line. /// public required int Quantity { get; set; } /// /// The number of items that are shipped for this order line. /// public required int QuantityShipped { get; set; } /// /// The total amount that is shipped for this order line. /// public required Amount AmountShipped { get; set; } /// /// The number of items that are refunded for this order line. /// public required int QuantityRefunded { get; set; } /// /// The total amount that is refunded for this order line. /// public required Amount AmountRefunded { get; set; } /// /// The number of items that are canceled in this order line. /// public required int QuantityCanceled { get; set; } /// /// The total amount that is canceled in this order line. /// public required Amount AmountCanceled { get; set; } /// /// The number of items that can still be shipped for this order line. /// public required int ShippableQuantity { get; set; } /// /// The number of items that can still be refunded for this order line. /// public required int RefundableQuantity { get; set; } /// /// The number of items that can still be canceled for this order line. /// public required int CancelableQuantity { get; set; } /// /// The price of a single item in the order line. /// public required Amount UnitPrice { get; set; } /// /// Any discounts applied to the order line. For example, if you have a two-for-one sale, you should pass the /// amount discounted as a positive amount. /// public Amount? DiscountAmount { get; set; } /// /// The total amount of the line, including VAT and discounts. Adding all totalAmount values together should /// result in the same amount as the amount top level property. /// public required Amount TotalAmount { get; set; } /// /// The VAT rate applied to the order line, for example "21.00" for 21%. The vatRate should be passed as a /// string and not as a float to ensure the correct number of decimals are passed. /// public required string VatRate { get; set; } /// /// The amount of value-added tax on the line. The totalAmount field includes VAT, so the vatAmount can be /// calculated with the formula totalAmount × (vatRate / (100 + vatRate)). Any deviations from this will /// result in an error. /// public required Amount VatAmount { get; set; } /// /// The SKU, EAN, ISBN or UPC of the product sold. The maximum character length is 64. /// public string? Sku { get; set; } /// /// The order line’s date and time of creation /// public required DateTime CreatedAt { get; set; } /// /// An object with several URL objects relevant to the order line. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required OrderLineResponseLinks Links { get; set; } /// /// The optional metadata you provided upon line creation. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Response/OrderLineResponseLinks.cs ================================================ using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Order.Response { public record OrderLineResponseLinks { /// /// A link pointing to the product page in your web shop of the product sold. /// public UrlLink? ProductUrl { get; set; } /// /// A link pointing to an image of the product sold. /// public UrlLink? ImageUrl { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Response/OrderLineStatus.cs ================================================ namespace Mollie.Api.Models.Order.Response { public static class OrderLineStatus { public const string Created = "created"; public const string Paid = "paid"; public const string Authorized = "authorized"; public const string Canceled = "canceled"; public const string Shipping = "shipping"; public const string Completed = "completed"; } } ================================================ FILE: src/Mollie.Api/Models/Order/Response/OrderRefundResponse.cs ================================================ using System.Collections.Generic; using Mollie.Api.Models.Refund.Response; namespace Mollie.Api.Models.Order.Response { public record OrderRefundResponse : RefundResponse { /// /// The unique identifier of the order this refund was created for. For example: ord_stTC2WHAuS. /// public required string OrderId { get; set; } /// /// An array of order line objects as described in Get order. Only available if the refund was created via the /// Create Order Refund API. /// public required IEnumerable Lines { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Response/OrderResponse.cs ================================================ using System; using System.Collections.Generic; using System.Text.Json; using Mollie.Api.JsonConverters; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Order.Response { public record OrderResponse : IEntity { /// /// Indicates the response contains an order object. Will always contain order for this endpoint. /// public required string Resource { get; set; } /// /// The order’s unique identifier, for example ord_vsKJpSsabw. /// public required string Id { get; set; } /// /// The profile the order was created on, for example pfl_v9hTwCvYqw. /// public required string ProfileId { get; set; } /// /// The payment method last used when paying for the order - See the /// Mollie.Api.Models.Payment.PaymentMethod class for a full list of known values. /// public string? Method { get; set; } /// /// For digital goods, you must make sure to apply the VAT rate from your customer's country in most jurisdictions. /// You can use this parameter to restrict the payment methods available to your customer to methods from the billing /// country only. /// public bool ShopperCountryMustMatchBillingCountry { get; set; } /// /// The mode used to create this order. /// public Mode Mode { get; set; } /// /// The total amount of the order, including VAT and discounts. /// public required Amount Amount { get; set; } /// /// The amount captured, thus far. The captured amount will be settled to your account. /// public Amount? AmountCaptured { get; set; } /// /// The total amount refunded, thus far. /// public Amount? AmountRefunded { get; set; } /// /// The status of the order - See the Mollie.Api.Models.Order.OrderStatus class for a full list of known values. /// public required string Status { get; set; } /// /// Whether or not the order can be (partially) canceled. /// public required bool IsCancelable { get; set; } /// /// The person and the address the order is billed to. See below. /// public OrderAddressDetails? BillingAddress { get; set; } /// /// The date of birth of your customer, if available. /// public DateTime? ConsumerDateOfBirth { get; set; } /// /// Your order number that was used when creating the order. /// public required string OrderNumber { get; set; } /// /// The person and the address the order is billed to. See below. /// public OrderAddressDetails? ShippingAddress { get; set; } /// /// The locale used during checkout. /// public required string Locale { get; set; } /// /// Provide any data you like, for example a string or a JSON object. We will save the data /// alongside the order. Whenever you fetch the order with our API, we’ll also include the /// metadata. You can use up to approximately 1kB. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// The URL your customer will be redirected to after completing or canceling the payment process. /// public string? RedirectUrl { get; set; } /// /// The optional redirect URL you provided during payment creation. Consumer that explicitly cancel the /// order will be redirected to this URL if provided, or otherwise to the redirectUrl instead — see above. /// /// Mollie will always give you status updates via webhooks, including for the canceled status. This parameter /// is therefore entirely optional, but can be useful when implementing a dedicated consumer-facing flow to /// handle order cancellations. /// /// The URL will be null for recurring orders. /// public string? CancelUrl { get; set; } /// /// The URL Mollie will call as soon an important status change on the order takes place. /// public string? WebhookUrl { get; set; } /// /// The order’s date and time of creation, in ISO 8601 format. /// public required DateTime CreatedAt { get; set; } /// /// The date and time the order will expire, in ISO 8601 format. Note that you have until this date to fully ship the /// order. /// public DateTime? ExpiresAt { get; set; } /// /// If the order is expired, the time of expiration will be present in ISO 8601 format. /// public DateTime? ExpiredAt { get; set; } /// /// If the order has been paid, the time of payment will be present in ISO 8601 format. /// public DateTime? PaidAt { get; set; } /// /// If the order has been authorized, the time of authorization will be present in ISO 8601 format. /// public DateTime? AuthorizedAt { get; set; } /// /// If the order has been canceled, the time of cancellation will be present in ISO 8601 format. /// public DateTime? CanceledAt { get; set; } /// /// If the order is completed, the time of completion will be present in ISO 8601 format. /// public DateTime? CompletedAt { get; set; } public required IEnumerable Lines { get; set; } [JsonPropertyName("_embedded")] public OrderEmbeddedResponse? Embedded { get; set; } /// /// An object with several URL objects relevant to the order. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required OrderResponseLinks Links { get; set; } public T? GetMetadata(JsonSerializerOptions? jsonSerializerOptions = null) { return Metadata != null ? JsonSerializer.Deserialize(Metadata, jsonSerializerOptions) : default; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Response/OrderResponseLinks.cs ================================================ using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Order.Response { public record OrderResponseLinks { /// /// The API resource URL of the order itself. /// public required UrlObjectLink Self { get; set; } /// /// A link to the Mollie dashboard page. /// public required UrlLink Dashboard { get; set; } /// /// The URL your customer should visit to make the payment for the order. /// This is where you should redirect the customer to after creating the order. /// public UrlLink? Checkout { get; set; } /// /// The URL to the order retrieval endpoint documentation. /// public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Order/Response/OrderStatus.cs ================================================ namespace Mollie.Api.Models.Order.Response { public static class OrderStatus { public const string Created = "created"; public const string Paid = "paid"; public const string Authorized = "authorized"; public const string Canceled = "canceled"; public const string Shipping = "shipping"; public const string Completed = "completed"; public const string Expired = "expired"; } } ================================================ FILE: src/Mollie.Api/Models/Organization/OrganizationResponse.cs ================================================ using System.Text.Json.Serialization; namespace Mollie.Api.Models.Organization { public record OrganizationResponse : IEntity { /// /// Indicates the response contains a organization object. /// public required string Resource { get; set; } /// /// The organization's identifier, for example org_1234567. /// public required string Id { get; set; } /// /// The organization's official name. /// public required string Name { get; set; } /// /// The organization's email. /// public required string Email { get; set; } /// /// The preferred locale of the merchant which has been set in Mollie Dashboard. /// public required string Locale { get; set; } /// /// The address of the organization. /// public required AddressObject Address { get; set; } /// /// The registration number of the organization at the (local) chamber of commerce. /// public required string RegistrationNumber { get; set; } /// /// The VAT number of the organization, if based in the European Union. The VAT number has been checked with the VIES by Mollie. /// public string? VatNumber { get; set; } /// /// The organization’s VAT regulation, if based in the European Union. Either shifted (VAT is shifted) or dutch (Dutch VAT rate). /// public string? VatRegulation { get; set; } /// /// An object with several URL objects relevant to the organization. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required OrganizationResponseLinks Links { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Organization/OrganizationResponseLinks.cs ================================================ using Mollie.Api.Models.Chargeback.Response; using Mollie.Api.Models.Customer.Response; using Mollie.Api.Models.Invoice.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Profile.Response; using Mollie.Api.Models.Refund.Response; using Mollie.Api.Models.Settlement.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Organization { public record OrganizationResponseLinks { /// /// The API resource URL of the organization itself. /// public required UrlObjectLink Self { get; set; } /// /// The API resource URL where the organization’s chargebacks can be retrieved. /// public UrlObjectLink>? Chargebacks { get; set; } /// /// The API resource URL where the organization’s customers can be retrieved. /// public UrlObjectLink>? Customers { get; set; } /// /// The API resource URL where the organization’s invoices can be retrieved. /// public UrlObjectLink>? Invoices { get; set; } /// /// The API resource URL where the organization’s payments can be retrieved. /// public UrlObjectLink>? Payments { get; set; } /// /// The API resource URL where the organization’s profiles can be retrieved. /// public UrlObjectLink>? Profiles { get; set; } /// /// The API resource URL where the organization’s refunds can be retrieved. /// public UrlObjectLink>? Refunds { get; set; } /// /// The API resource URL where the organization’s settlements can be retrieved. /// public UrlObjectLink>? Settlements { get; set; } /// /// The URL to the organization dashboard /// public required UrlLink Dashboard { get; set; } /// /// The URL to the payment method retrieval endpoint documentation. /// public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Organization/PartnerResponse.cs ================================================ using System; using System.Collections.Generic; using System.Text.Json.Serialization; using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Organization; public record PartnerResponse { /// /// Indicates the response contains a partner status object. Will always contain the string partner for this endpoint. /// public required string Resource { get; set; } /// /// Indicates the type of partner. Will be null if the currently authenticated organization is not enrolled as a partner. /// Use the Mollie.Api.Models.Organization.PartnerTypes class for a full list of known values. /// public string? PartnerType { get; set; } /// /// Whether the current organization is receiving commissions. /// public required bool IsCommissionPartner { get; set; } /// /// Array of User-Agent token objects. Present if the organization is a partner of type useragent, or if they were in the past. /// public IEnumerable UserAgentTokens { get; set; } = new List(); /// /// The date the partner contract was signed, in ISO 8601 format. Omitted if no contract has been signed (yet). /// public DateTimeOffset? PartnerContractSignedAt { get; set; } /// /// Whether an update to the partner contract is available and requiring the organization's agreement. /// public required bool PartnerContractUpdateAvailable { get; set; } /// /// The expiration date of the signed partner contract, in ISO 8601 format. Omitted if contract has no expiration date (yet). /// public DateTimeOffset? PartnerContractExpiresAt { get; set; } /// /// An object with several relevant URLs. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required PartnerResponseLinks Links { get; set; } } public record PartnerResponseLinks { /// /// In v2 endpoints, URLs are commonly represented as objects with an href and type field. /// public required UrlLink Self { get; set; } /// /// The URL that can be used to have new organizations sign up and be automatically linked to this partner. /// Will be omitted if the partner is not of type signuplink. /// public UrlLink? Signuplink { get; set; } /// /// In v2 endpoints, URLs are commonly represented as objects with an href and type field. /// public required UrlLink Documentation { get; set; } } ================================================ FILE: src/Mollie.Api/Models/Organization/PartnerTypes.cs ================================================ namespace Mollie.Api.Models.Organization; public static class PartnerTypes { public const string OAuth = "oauth"; public const string SignupLink = "signuplink"; public const string Useragent = "useragent"; } ================================================ FILE: src/Mollie.Api/Models/Organization/UserAgentToken.cs ================================================ using System; namespace Mollie.Api.Models.Organization; public record UserAgentToken { /// /// The unique User-Agent token. /// public required string Token { get; set; } /// /// The date from which the token is active, in ISO 8601 format. /// public required DateTimeOffset StartsAt { get; set; } /// /// The date until when the token will be active, in ISO 8601 format. Will be null if the token does not /// have an end date (yet). /// public DateTimeOffset? EndsAt { get; set; } } ================================================ FILE: src/Mollie.Api/Models/Payment/EntryMode.cs ================================================ namespace Mollie.Api.Models.Payment; public static class EntryMode { public const string Moto = "moto"; } ================================================ FILE: src/Mollie.Api/Models/Payment/Locale.cs ================================================ namespace Mollie.Api.Models.Payment { public static class Locale { public static string de_DE = "de_DE"; public static string en_US = "en_US"; public static string es_ES = "es_ES"; public static string fr_FR = "fr_FR"; public static string nl_BE = "nl_BE"; public static string fr_BE = "fr_BE"; public static string nl_NL = "nl_NL"; public static string en_GB = "en_GB"; } } ================================================ FILE: src/Mollie.Api/Models/Payment/PaymentAddressDetails.cs ================================================ namespace Mollie.Api.Models.Payment; public record PaymentAddressDetails : AddressObject { /// /// The person’s organization, if applicable. /// public string? OrganizationName { get; set; } /// /// The title of the person, for example Mr. or Mrs.. /// public string? Title { get; set; } /// /// The given name (first name) of the person. /// public string? GivenName { get; set; } /// /// The family name (surname) of the person. /// public string? FamilyName { get; set; } /// /// The email address of the person. /// public string? Email { get; set; } /// /// The phone number of the person. Some payment methods require this information. If you have it, you /// should pass it so that your customer does not have to enter it again in the checkout. Must be in /// the E.164 format. For example +31208202070. /// public string? Phone { get; set; } } ================================================ FILE: src/Mollie.Api/Models/Payment/PaymentLine.cs ================================================ using System.Collections.Generic; namespace Mollie.Api.Models.Payment; public record PaymentLine { /// /// The type of product purchased. For example, a physical or a digital product. /// Use the Mollie.Api.Models.Order.Request.OrderLineDetailsType class for a full list of known values. /// public required string Type { get; set; } /// /// A description of the line item. For example LEGO 4440 Forest Police Station. /// public required string Description { get; set; } /// /// The number of items. /// public required int Quantity { get; set; } /// /// The unit for the quantity. For example pcs, kg, or cm. /// public string? QuantityUnit { get; set; } /// /// The price of a single item including VAT. The unit price can be zero in case of free items. /// public required Amount UnitPrice { get; set; } /// /// Any line-specific discounts, as a positive amount. Not relevant if the line itself is already a discount type. /// public Amount? DiscountAmount { get; set; } /// /// The total amount of the line, including VAT and discounts. /// public required Amount TotalAmount { get; set; } /// /// The VAT rate applied to the line, for example 21.00 for 21%. The vatRate should be passed as a string and not /// as a float, to ensure the correct number of decimals are passed. /// public string? VatRate { get; set; } // TODO: make it decimal? /// /// The amount of value-added tax on the line. The totalAmount field includes VAT, so the vatAmount can be /// calculated with the formula totalAmount × (vatRate / (100 + vatRate)). /// public Amount? VatAmount { get; set; } /// /// An array with the voucher categories, in case of a line eligible for a voucher. See the Integrating /// Vouchers guide for more information. Use the Mollie.Api.Models.VoucherCategory class for a full list of known values. /// public IEnumerable? Categories { get; set; } /// /// The SKU, EAN, ISBN or UPC of the product sold. /// public string? Sku { get; set; } /// /// A link pointing to an image of the product sold. /// public string? ImageUrl { get; set; } /// /// A link pointing to the product page in your web shop of the product sold. /// public string? ProductUrl { get; set; } } ================================================ FILE: src/Mollie.Api/Models/Payment/PaymentMethod.cs ================================================ namespace Mollie.Api.Models.Payment { public static class PaymentMethod { public const string Bancontact = "bancontact"; public const string BankTransfer = "banktransfer"; public const string Belfius = "belfius"; public const string CreditCard = "creditcard"; public const string DirectDebit = "directdebit"; public const string Eps = "eps"; public const string GiftCard = "giftcard"; public const string Giropay = "giropay"; public const string Ideal = "ideal"; public const string IngHomePay = "inghomepay"; public const string Kbc = "kbc"; public const string PayPal = "paypal"; public const string PaySafeCard = "paysafecard"; public const string Sofort = "sofort"; public const string Refund = "refund"; public const string KlarnaPayLater = "klarnapaylater"; public const string KlarnaPayNow = "klarnapaynow "; public const string KlarnaSliceIt = "klarnasliceit"; public const string KlarnaOne = "klarna"; public const string Przelewy24 = "przelewy24"; public const string ApplePay = "applepay"; public const string MealVoucher = "mealvoucher"; public const string In3 = "in3"; public const string PointOfSale = "pointofsale"; public const string Billie = "billie"; public const string Trustly = "trustly"; public const string Twint = "twint"; public const string Satispay = "satispay"; public const string Riverty = "riverty"; public const string Blik = "blik"; public const string BancomatPay = "bancomat_pay"; public const string BacsDirectDebit = "bacs"; public const string Alma = "alma"; public const string GooglePay = "googlepay"; public const string Voucher = "voucher"; public const string MbWay = "mbway"; public const string Multibanco = "multibanco"; public const string Swish = "swish"; public const string MobilePay = "mobilepay"; public const string MyBank = "mybank"; public const string PayByBank = "paybybank"; } } ================================================ FILE: src/Mollie.Api/Models/Payment/PaymentStatus.cs ================================================ namespace Mollie.Api.Models.Payment { public static class PaymentStatus { public const string Open = "open"; public const string Canceled = "canceled"; public const string Pending = "pending"; public const string Authorized = "authorized"; public const string Expired = "expired"; public const string Failed = "failed"; public const string Paid = "paid"; } } ================================================ FILE: src/Mollie.Api/Models/Payment/Request/PaymentRequest.cs ================================================ using System; using Mollie.Api.JsonConverters; using System.Text.Json.Serialization; using System.Collections.Generic; using System.Linq; using System.Text.Json; namespace Mollie.Api.Models.Payment.Request { public record PaymentRequest : ITestModeRequest, IProfileRequest { /// /// The amount that you want to charge, e.g. {"currency":"EUR", "value":"100.00"} if you would want to charge €100.00. /// public required Amount Amount { get; set; } /// /// The description of the payment you’re creating. This will be shown to the consumer on their card or bank statement /// when possible. We truncate the description automatically according to the limits of the used payment method. The /// description is also visible in any exports you generate. We recommend you use a unique identifier so that you can /// always link the payment to the order.This is particularly useful for bookkeeping. /// public required string Description { get; set; } /// /// Required - The URL the consumer will be redirected to after the payment process. It could make sense for the /// redirectURL to contain a unique /// identifier – like your order ID – so you can show the right page referencing the order when the consumer returns. /// public string? RedirectUrl { get; set; } /// /// The URL your consumer will be redirected to when the consumer explicitly cancels the payment. If this URL is not /// provided, the consumer will be redirected to the redirectUrl instead — see above. /// Mollie will always give you status updates via webhooks, including for the canceled status. This parameter is /// therefore entirely optional, but can be useful when implementing a dedicated consumer-facing flow to handle payment /// cancellations. /// public string? CancelUrl { get; set; } /// /// Set the webhook URL, where we will send payment status updates to. /// public string? WebhookUrl { get; set; } /// /// Optionally provide the order lines for the payment. Each line contains details such as a description of the item ordered and its price. /// All lines must have the same currency as the payment. /// public List? Lines { get; set; } /// /// The customer's billing address details. We advise to provide these details to improve fraud protection and conversion. This is /// particularly relevant for card payments. /// public PaymentAddressDetails? BillingAddress { get; set; } /// /// The customer's shipping address details. We advise to provide these details to improve fraud protection and conversion. This is /// particularly relevant for card payments. /// public PaymentAddressDetails? ShippingAddress { get; set; } /// /// Allows you to preset the language to be used in the payment screens shown to the consumer. Setting a locale is highly /// recommended and will greatly improve your conversion rate. When this parameter is omitted, the browser language will /// be used instead if supported by the payment method. You can provide any ISO 15897 locale, but our payment screen currently /// only supports the following languages: en_US nl_NL nl_BE fr_FR fr_BE de_DE de_AT de_CH es_ES ca_ES pt_PT it_IT nb_NO /// sv_SE fi_FI da_DK is_IS hu_HU pl_PL lv_LV lt_LT /// public string? Locale { get; set; } /// /// Normally, a payment method selection screen is shown. However, when using this parameter, your customer will skip the /// selection screen and will be sent directly to the chosen payment method. The parameter enables you to fully integrate /// the payment method selection into your website, however note Mollie’s country based conversion optimization is lost. /// See the Mollie.Api.Models.Payment.PaymentMethod class for a full list of known values. /// [JsonIgnore] public string? Method { get => Methods?.FirstOrDefault(); set { if (value == null) { Methods = null; } else { Methods = new List(); Methods.Add(value); } } } /// /// Only relevant for iDEAL, KBC/CBC, gift card, and voucher payments. /// Some payment methods are a network of connected banks or card issuers. In these cases, after selecting the payment method, /// the customer may still need to select the appropriate issuer before the payment can proceed. /// public string? Issuer { get; set; } /// /// Normally, a payment method screen is shown. However, when using this parameter, you can choose a specific payment method /// and your customer will skip the selection screen and is sent directly to the chosen payment method. The parameter /// enables you to fully integrate the payment method selection into your website. /// You can also specify the methods in an array.By doing so we will still show the payment method selection screen but will /// only show the methods specified in the array. For example, you can use this functionality to only show payment methods /// from a specific country to your customer. /// [JsonPropertyName("method")] public IList? Methods { get; set; } /// /// Provide any data you like, for example a string or a JSON object. We will save the data alongside the payment. Whenever /// you fetch the payment with our API, we’ll also include the metadata. You can use up to approximately 1kB. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// Indicate which type of payment this is in a recurring sequence. If set to first, a first payment is created for the /// customer, allowing the customer to agree to automatic recurring charges taking place on their account in the future. /// If set to recurring, the customer’s card is charged automatically. Defaults to oneoff, which is a regular non-recurring /// payment(see also: Recurring). See the Mollie.Api.Models.Payment.SequenceType class for a full list of known values. /// public string? SequenceType { get; set; } /// /// The ID of the Customer for whom the payment is being created. This is used for recurring payments and single click payments. /// public string? CustomerId { get; set; } /// /// When creating recurring payments, the ID of a specific Mandate may be supplied to indicate which of the consumer’s accounts /// should be credited. /// public string? MandateId { get; set; } /// /// Oauth only - The payment profile's unique identifier, for example pfl_3RkSN1zuPE. This field is mandatory. /// public string? ProfileId { get; set; } /// /// The date by which the payment should be completed in YYYY-MM-DD format /// [JsonConverter(typeof(DateJsonConverter))] public DateTime? DueDate { get; set; } /// /// Oauth only - Optional – Set this to true to make this payment a test payment. /// public bool? Testmode { get; set; } /// /// Oauth only - Optional – Adding an Application Fee allows you to charge the merchant a small sum for the payment and transfer /// this to your own account. /// public ApplicationFee? ApplicationFee { get; set; } /// /// Oauth only - Optional - An optional routing configuration which enables you to route a successful payment, or part of the payment, to one or more connected accounts. /// Additionally, you can schedule (parts of) the payment to become available on the connected account on a future date. /// [JsonPropertyName("routing")] public IList? Routings { get; set; } /// /// For digital goods in most jurisdictions, you must apply the VAT rate from your customer’s country. Choose the VAT rates /// you have used for the order to ensure your customer’s country matches the VAT country. Use this parameter to restrict the /// payment methods available to your customer to those from a single country. If available, the credit card method will still /// be offered, but only cards from the allowed country are accepted. /// public string? RestrictPaymentMethodsToCountry { get; set; } /// /// Indicates whether the capture will be scheduled automatically or not. Set to manual to capture the payment manually using the /// Create capture endpoint. Set to automatic by default, which indicates the payment will be captured automatically, without /// having to separately request it. Setting automatic without a captureDelay will result in a regular payment. /// See the Mollie.Api.Models.Capture.CaptureMode class for a full list of known values. /// public string? CaptureMode { get; set; } /// /// Interval to wait before the payment is captured, for example 8 hours or 2 days. In order to schedule an automatic capture, /// the captureMode must be set to either automatic or be omitted. /// Possible values: ... hours ... days /// public string? CaptureDelay { get; set; } public void SetMetadata(object metadataObj, JsonSerializerOptions? jsonSerializerOptions = null) { Metadata = JsonSerializer.Serialize(metadataObj, jsonSerializerOptions); } public override string ToString() { return $"Method: {Method} - Amount: {Amount}"; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Request/PaymentRoutingRequest.cs ================================================ using System; using System.Text.Json.Serialization; using Mollie.Api.JsonConverters; namespace Mollie.Api.Models.Payment.Request; public record PaymentRoutingRequest { /// /// If more than one routing object is given, the routing objects must indicate what portion of the total payment amount is being routed. /// public Amount? Amount { get; set; } /// /// The destination of this portion of the payment. /// public required RoutingDestination Destination { get; set; } /// /// Optionally, schedule this portion of the payment to be transferred to its destination on a later date. If no date is given, the funds become available to the balance as soon as the payment succeeds. /// [JsonConverter(typeof(DateJsonConverter))] public DateTime? ReleaseDate { get; set; } } ================================================ FILE: src/Mollie.Api/Models/Payment/Request/PaymentSpecificParameters/ApplePayPaymentRequest.cs ================================================ using System.Diagnostics.CodeAnalysis; namespace Mollie.Api.Models.Payment.Request.PaymentSpecificParameters { public record ApplePayPaymentRequest : PaymentRequest { public ApplePayPaymentRequest() { Method = PaymentMethod.ApplePay; } [SetsRequiredMembers] public ApplePayPaymentRequest(PaymentRequest paymentRequest) : base(paymentRequest) { Method = PaymentMethod.ApplePay; } /// /// Optional - The Apple Pay Payment Token object (encoded as JSON) that is part of the result of authorizing a payment /// request. The token contains the payment information needed to authorize the payment. /// public string? ApplePayPaymentToken { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Request/PaymentSpecificParameters/BankTransferPaymentRequest.cs ================================================ using System.Diagnostics.CodeAnalysis; namespace Mollie.Api.Models.Payment.Request.PaymentSpecificParameters { public record BankTransferPaymentRequest : PaymentRequest { public BankTransferPaymentRequest() { Method = PaymentMethod.BankTransfer; } [SetsRequiredMembers] public BankTransferPaymentRequest(PaymentRequest paymentRequest) : base(paymentRequest) { Method = PaymentMethod.BankTransfer; } /// /// Optional - Consumer's e-mail address, to automatically send the bank transfer details to. Please note: the payment /// instructions will be sent immediately when creating the payment. if you don't specify the locale parameter, the /// email will be sent in English, as we haven't yet been able to detect the consumer's browser language. /// public string? BillingEmail { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Request/PaymentSpecificParameters/CreditCardPaymentRequest.cs ================================================ using System.Diagnostics.CodeAnalysis; namespace Mollie.Api.Models.Payment.Request.PaymentSpecificParameters { public record CreditCardPaymentRequest : PaymentRequest { public CreditCardPaymentRequest() { Method = PaymentMethod.CreditCard; } [SetsRequiredMembers] public CreditCardPaymentRequest(PaymentRequest paymentRequest) : base(paymentRequest) { Method = PaymentMethod.CreditCard; } /// /// The card token you get from Mollie Components. The token contains the card information /// (such as card holder, card number and expiry date) needed to complete the payment. /// public string? CardToken { get; set; } /// /// Beta feature: The entrymode of the payment. See the Mollie.Api.Models.Payment.EntryMode class for a full /// list of known values /// public string? EntryMode { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Request/PaymentSpecificParameters/GiftcardPaymentRequest.cs ================================================ using System.Diagnostics.CodeAnalysis; namespace Mollie.Api.Models.Payment.Request.PaymentSpecificParameters { public record GiftcardPaymentRequest : PaymentRequest { public GiftcardPaymentRequest() { Method = PaymentMethod.GiftCard; } [SetsRequiredMembers] public GiftcardPaymentRequest(PaymentRequest paymentRequest) : base(paymentRequest) { Method = PaymentMethod.GiftCard; } /// /// The card number on the gift card. /// public string? VoucherNumber { get; set; } /// /// The PIN code on the gift card. Only required if there is a PIN code printed on the gift card. /// public string? VoucherPin { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Request/PaymentSpecificParameters/IDealPaymentRequest.cs ================================================ using System; using System.Diagnostics.CodeAnalysis; namespace Mollie.Api.Models.Payment.Request.PaymentSpecificParameters { [Obsolete("Ideal no longer has specific parameters, so this class is identical to PaymentRequest")] public record IdealPaymentRequest : PaymentRequest { public IdealPaymentRequest() { Method = PaymentMethod.Ideal; } [SetsRequiredMembers] public IdealPaymentRequest(PaymentRequest paymentRequest) : base(paymentRequest) { Method = PaymentMethod.Ideal; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Request/PaymentSpecificParameters/KbcIssuer.cs ================================================ namespace Mollie.Api.Models.Payment.Request.PaymentSpecificParameters { public static class KbcIssuer { public const string Kbc = "kbc"; public const string Cbc = "cbc"; } } ================================================ FILE: src/Mollie.Api/Models/Payment/Request/PaymentSpecificParameters/KbcPaymentRequest.cs ================================================ using System; using System.Diagnostics.CodeAnalysis; namespace Mollie.Api.Models.Payment.Request.PaymentSpecificParameters { [Obsolete("Kbc no longer has specific parameters, so this class is identical to PaymentRequest")] public record KbcPaymentRequest : PaymentRequest { public KbcPaymentRequest() { Method = PaymentMethod.Kbc; } [SetsRequiredMembers] public KbcPaymentRequest(PaymentRequest paymentRequest) : base(paymentRequest) { Method = PaymentMethod.Kbc; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Request/PaymentSpecificParameters/PayPalPaymentRequest.cs ================================================ using System.Diagnostics.CodeAnalysis; namespace Mollie.Api.Models.Payment.Request.PaymentSpecificParameters { public record PayPalPaymentRequest : PaymentRequest { public PayPalPaymentRequest() { Method = PaymentMethod.PayPal; } [SetsRequiredMembers] public PayPalPaymentRequest(PaymentRequest paymentRequest) : base(paymentRequest) { Method = PaymentMethod.PayPal; } /// /// The unique ID you have used for the PayPal fraud library. You should include this if you use PayPal /// for an on-demand payment. The maximum character length is 32. /// Please refer to the Recurring payments guide for more information on how to implement the fraud library. /// public string? SessionId { get; set; } /// /// Indicate if you�re about to deliver digital goods, like for example a license. Setting this parameter can /// have consequences for your Seller Protection by PayPal. Please see PayPal�s help article about Seller /// Protection for more information. /// public bool? DigitalGoods { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Request/PaymentSpecificParameters/PaySafeCardPaymentRequest.cs ================================================ using System.Diagnostics.CodeAnalysis; namespace Mollie.Api.Models.Payment.Request.PaymentSpecificParameters { public record PaySafeCardPaymentRequest : PaymentRequest { public PaySafeCardPaymentRequest() { Method = PaymentMethod.PaySafeCard; } [SetsRequiredMembers] public PaySafeCardPaymentRequest(PaymentRequest paymentRequest) : base(paymentRequest) { Method = PaymentMethod.PaySafeCard; } /// /// Used for consumer identification. For example, you could use the consumer’s IP address. /// public string? CustomerReference { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Request/PaymentSpecificParameters/PointOfSalePaymentRequest.cs ================================================ using System.Diagnostics.CodeAnalysis; namespace Mollie.Api.Models.Payment.Request.PaymentSpecificParameters; public record PointOfSalePaymentRequest : PaymentRequest { public PointOfSalePaymentRequest() { Method = PaymentMethod.PointOfSale; } [SetsRequiredMembers] public PointOfSalePaymentRequest(PaymentRequest paymentRequest, string terminalId) : base(paymentRequest) { Method = PaymentMethod.PointOfSale; TerminalId = terminalId; } /// /// The ID of the terminal device where you want to initiate the payment on /// public required string TerminalId { get; set; } } ================================================ FILE: src/Mollie.Api/Models/Payment/Request/PaymentSpecificParameters/Przelewy24PaymentRequest.cs ================================================ using System.Diagnostics.CodeAnalysis; namespace Mollie.Api.Models.Payment.Request.PaymentSpecificParameters { public record Przelewy24PaymentRequest : PaymentRequest { public Przelewy24PaymentRequest() { Method = PaymentMethod.Przelewy24; } [SetsRequiredMembers] public Przelewy24PaymentRequest(PaymentRequest paymentRequest) : base(paymentRequest) { Method = PaymentMethod.Przelewy24; } /// /// Consumer�s email address, this is required for Przelewy24 payments. /// public string? BillingEmail { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Request/PaymentSpecificParameters/SepaDirectDebitRequest.cs ================================================ using System.Diagnostics.CodeAnalysis; namespace Mollie.Api.Models.Payment.Request.PaymentSpecificParameters { public record SepaDirectDebitRequest : PaymentRequest { public SepaDirectDebitRequest() { Method = PaymentMethod.DirectDebit; } [SetsRequiredMembers] public SepaDirectDebitRequest(PaymentRequest paymentRequest) : base(paymentRequest) { Method = PaymentMethod.DirectDebit; } /// /// Optional - Beneficiary name of the account holder. /// public string? ConsumerName { get; set; } /// /// Optional - IBAN of the account holder. /// public string? ConsumerAccount { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Request/PaymentUpdateRequest.cs ================================================ using System.Text.Json; using System.Text.Json.Serialization; using Mollie.Api.JsonConverters; namespace Mollie.Api.Models.Payment.Request { public record PaymentUpdateRequest : ITestModeRequest { /// /// The description of the payment. This will be shown to your customer on their card or bank statement /// when possible. We truncate the description automatically according to the limits of the used payment /// method. The description is also visible in any exports you generate. We recommend you use a unique /// identifier so that you can always link the payment to the order in your back office.This is particularly /// useful for bookkeeping. /// public string? Description { get; set; } /// /// The URL your customer will be redirected to after the payment process. It could make sense for the redirectUrl /// to contain a unique identifier – like your order ID – so you can show the right page referencing the order /// when your customer returns. Updating this field is only possible when the payment is not yet finalized. /// public string? RedirectUrl { get; set; } /// /// Can be updated while the payment is in an open state. /// public string? CancelUrl { get; set; } /// /// Set the webhook URL, where we will send payment status updates to. /// public string? WebhookUrl { get; set; } /// /// Provide any data you like, for example a string or a JSON object. We will save the data alongside the payment. Whenever /// you fetch the payment with our API, we’ll also include the metadata. You can use up to approximately 1kB. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// Can be updated while no payment method has been chosen yet. /// See the Mollie.Api.Models.Payment.PaymentMethod class for a full list of known values. /// public string? Method { get; set; } /// /// Allows you to preset the language to be used in the payment screens shown to the consumer. Setting a locale is highly /// recommended and will greatly improve your conversion rate. When this parameter is omitted, the browser language will /// be used instead if supported by the payment method. You can provide any ISO 15897 locale, but our payment screen currently /// only supports the following languages: en_US nl_NL nl_BE fr_FR fr_BE de_DE de_AT de_CH es_ES ca_ES pt_PT it_IT nb_NO /// sv_SE fi_FI da_DK is_IS hu_HU pl_PL lv_LV lt_LT /// public string? Locale { get; set; } /// /// For digital goods in most jurisdictions, you must apply the VAT rate from your customer’s country. Choose the VAT rates /// you have used for the order to ensure your customer’s country matches the VAT country. Use this parameter to restrict the /// payment methods available to your customer to those from a single country. If available, the credit card method will still /// be offered, but only cards from the allowed country are accepted. /// public string? RestrictPaymentMethodsToCountry { get; set; } /// /// Oauth only - Optional – Set this to true to make this payment a test payment. /// public bool? Testmode { get; set; } public void SetMetadata(object metadataObj, JsonSerializerOptions? jsonSerializerOptions = null) { Metadata = JsonSerializer.Serialize(metadataObj, jsonSerializerOptions); } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Resource.cs ================================================ using System.Runtime.Serialization; namespace Mollie.Api.Models.Payment { public enum Resource { [EnumMember(Value = "orders")] Orders, [EnumMember(Value = "payments")] Payments } } ================================================ FILE: src/Mollie.Api/Models/Payment/Response/PaymentEmbeddedResponse.cs ================================================ using System.Collections.Generic; using Mollie.Api.Models.Chargeback.Response; using Mollie.Api.Models.Refund.Response; namespace Mollie.Api.Models.Payment.Response { public record PaymentEmbeddedResponse { public IEnumerable? Refunds { get; set; } public IEnumerable? Chargebacks { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Response/PaymentResponse.cs ================================================ using System; using System.Collections.Generic; using System.Text.Json; using Mollie.Api.JsonConverters; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Payment.Response { public record PaymentResponse : IEntity { /// /// Indicates the response contains a payment object. Will always contain payment for this endpoint. /// public required string Resource { get; set; } /// /// The identifier uniquely referring to this payment. Mollie assigns this identifier randomly at payment creation /// time. For example tr_7UhSN1zuXS. Its ID will always be used by Mollie to refer to a certain payment. /// public required string Id { get; set; } /// /// The mode used to create this payment. Mode determines whether a payment is real or a test payment. /// public required Mode Mode { get; set; } /// /// The payment's date and time of creation, in ISO 8601 format. /// public required DateTime CreatedAt { get; set; } /// /// The payment's status. Please refer to the page about statuses for more info about which statuses occur at what /// point. See the Mollie.Api.Models.Payment.PaymentStatus class for a full list of known values. /// public required string Status { get; set; } /// /// This object offers details about the status of a payment. Currently it is only available for point-of-sale payments. /// public StatusReason? StatusReason { get; set; } /// /// Whether or not the payment can be canceled. /// public bool? IsCancelable { get; set; } /// /// The date and time the payment became authorized, in ISO 8601 format. This parameter is omitted if the payment is not authorized (yet). /// public DateTime? AuthorizedAt { get; set; } /// /// The date and time the payment became paid, in ISO 8601 format. Null is returned if the payment isn't completed /// (yet). /// public DateTime? PaidAt { get; set; } /// /// The date and time the payment was cancelled, in ISO 8601 format. Null is returned if the payment isn't cancelled /// (yet). /// public DateTime? CanceledAt { get; set; } /// /// The date and time the payment was expired, in ISO 8601 format. Null is returned if the payment did not expire /// (yet). /// public DateTime? ExpiresAt { get; set; } /// /// The time until the payment will expire in ISO 8601 duration format. /// public DateTime? ExpiredAt { get; set; } /// /// The date and time the payment failed, in ISO 8601 format. This parameter is omitted if the payment did not fail (yet). /// public DateTime? FailedAt { get; set; } /// /// The amount of the payment, e.g. {"currency":"EUR", "value":"100.00"} for a €100.00 payment. /// public required Amount Amount { get; set; } /// /// Only available when refunds are available for this payment – The total amount in EURO that is already refunded. For /// some payment methods, this /// amount may be higher than the payment amount, for example to allow reimbursement of the costs for a return shipment /// to the consumer. /// public Amount? AmountRefunded { get; set; } /// /// Only available when refunds are available for this payment – The remaining amount in EURO that can be refunded. /// public Amount? AmountRemaining { get; set; } /// /// The total amount that is already captured for this payment. Only available when this payment supports captures. /// public Amount? AmountCaptured { get; set; } /// /// The total amount that was charged back for this payment. Only available when the total charged back amount is not zero. /// public Amount? AmountChargedBack { get; set; } /// /// A short description of the payment. The description will be shown on the consumer's bank or card statement when /// possible. /// public string? Description { get; set; } /// /// The URL the consumer will be redirected to after completing or cancelling the payment process. /// public string? RedirectUrl { get; set; } /// /// The optional redirect URL you provided during payment creation. Consumer that explicitly cancel the payment /// will be redirected to this URL if provided, or otherwise to the redirectUrl instead — see above. /// /// Mollie will always give you status updates via webhooks, including for the canceled status. This parameter /// is therefore entirely optional, but can be useful when implementing a dedicated consumer-facing flow to /// handle payment cancellations. /// /// The URL will be null for recurring payments. /// public string? CancelUrl { get; set; } /// /// The URL Mollie will call as soon an important status change takes place. /// public string? WebhookUrl { get; set; } /// /// Optionally provide the order lines for the payment. Each line contains details such as a description of the item ordered and its price. /// All lines must have the same currency as the payment. /// public List? Lines { get; set; } /// /// The customer's billing address details. We advise to provide these details to improve fraud protection and conversion. This is /// particularly relevant for card payments. /// public PaymentAddressDetails? BillingAddress { get; set; } /// /// The customer's shipping address details. We advise to provide these details to improve fraud protection and conversion. This is /// particularly relevant for card payments. /// public PaymentAddressDetails? ShippingAddress { get; set; } /// /// An optional routing configuration that you provided, which enables you to route a successful payment, or part of the payment, to one or more connected accounts. /// Additionally, you can schedule (parts of) the payment to become available on the connected account on a future date. /// [JsonPropertyName("routing")] public IList? Routings { get; set; } /// /// The payment method used for this payment, either forced on creation by specifying the method parameter, or chosen /// by the consumer our payment method selection screen. See the Mollie.Api.Models.Payment.PaymentMethod class for a /// full list of known values. /// public string? Method { get; set; } /// /// The optional metadata you provided upon payment creation. Metadata can be used to link an order to a payment. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// The consumer's locale, either forced on creation by specifying the locale parameter, or detected by us during /// checkout. /// public string? Locale { get; set; } /// /// The customer’s ISO 3166-1 alpha-2 country code, detected by us during checkout. For example: BE. /// public string? CountryCode { get; set; } /// /// The identifier referring to the profile this payment was created on. For example, pfl_QkEhN94Ba. /// public required string ProfileId { get; set; } /// /// This optional field will contain the amount that will be settled to your account, converted to the currency your /// account is settled in. It follows the same syntax as the amount property. /// public Amount? SettlementAmount { get; set; } /// /// The identifier referring to the settlement this payment belongs to. For example, stl_BkEjN2eBb. /// public string? SettlementId { get; set; } /// /// The customerid of this payment /// public string? CustomerId { get; set; } /// /// Indicates which type of payment this is in a recurring sequence. Set to first for first payments that allow the customer to agree /// to automatic recurring charges taking place on their account in the future. Set to recurring for payments where the customer’s card /// is charged automatically. See the Mollie.Api.Models.Payment.SequenceType class for a full list of known values. /// public required string SequenceType { get; set; } /// /// Only available for recurring payments – If the payment is a recurring payment, this field will hold the ID of the /// mandate used to authorize the recurring payment. /// public string? MandateId { get; set; } /// /// Only available for recurring payments – When implementing the Subscriptions API, any recurring charges resulting /// from the subscription will hold the ID of the subscription that triggered the payment. /// public string? SubscriptionId { get; set; } /// /// If the payment was created for an order, the ID of that order will be part of the response. /// public string? OrderId { get; set; } /// /// The application fee, if the payment was created with one. /// public ApplicationFee? ApplicationFee { get; set; } /// /// Indicates whether the capture will be scheduled automatically or not. Set to manual for payments that can be captured /// manually using the Create capture endpoint. Set to automatic by default, which indicates the payment will be captured /// automatically, without having to separately request it. /// public string? CaptureMode { get; set; } /// /// Indicates the interval to wait before the payment is captured, for example 8 hours or 2 days. The capture delay will be /// added to the date and time the payment became authorized. /// public string? CaptureDelay { get; set; } /// /// Indicates the datetime on which the merchant has to have captured the payment, before we can no longer guarantee a /// successful capture, in ISO 8601 format. This parameter is omitted if the payment is not authorized (yet). /// public DateTime? CaptureBefore { get; set; } [JsonPropertyName("_embedded")] public PaymentEmbeddedResponse? Embedded { get; set; } /// /// An object with several URL objects relevant to the payment. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public PaymentResponseLinks Links { get; set; } = null!; public T? GetMetadata(JsonSerializerOptions? jsonSerializerOptions = null) { return Metadata != null ? JsonSerializer.Deserialize(Metadata, jsonSerializerOptions) : default; } public override string ToString() { return $"Id: {Id} - Status: {Status} - Method: {Method} - Amount: {Amount}"; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Response/PaymentResponseLinks.cs ================================================ using Mollie.Api.Models.Chargeback.Response; using Mollie.Api.Models.Customer.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Mandate.Response; using Mollie.Api.Models.Order.Response; using Mollie.Api.Models.Settlement.Response; using Mollie.Api.Models.Subscription.Response; using Mollie.Api.Models.Terminal.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Payment.Response { public record PaymentResponseLinks { /// /// The API resource URL of the payment itself. /// public required UrlObjectLink Self { get; set; } /// /// The URL your customer should visit to make the payment. This is where you should redirect the consumer to. /// public UrlLink? Checkout { get; set; } /// /// The deeplink URL to the app of the payment method. Currently only available for bancontact. /// public UrlLink? MobileAppCheckout { get; set; } /// /// Direct link to the payment in the Mollie Dashboard. /// public required UrlLink Dashboard { get; set; } /// /// The API resource URL of the refunds that belong to this payment. /// public UrlLink? Refunds { get; set; } /// /// The API resource URL of the chargebacks that belong to this payment. /// public UrlObjectLink>? Chargebacks { get; set; } /// /// The API resource URL of the captures that belong to this payment. /// public UrlLink? Captures { get; set; } /// /// The API resource URL of the settlement this payment has been settled with. Not present if not yet settled. /// public UrlObjectLink? Settlement { get; set; } /// /// The URL to the payment retrieval endpoint documentation. /// public required UrlLink Documentation { get; set; } /// /// The API resource URL of the mandate linked to this payment. Not present if a one-off payment. /// public UrlObjectLink? Mandate { get; set; } /// /// The API resource URL of the subscription this payment is part of. Not present if not a subscription payment. /// public UrlObjectLink? Subscription { get; set; } /// /// The API resource URL of the customer this payment belongs to. Not present if not linked to a customer. /// public UrlObjectLink? Customer { get; set; } /// /// The API resource URL of the order this payment was created for. Not present if not created for an order. /// public UrlObjectLink? Order { get; set; } /// /// The API resource URL of the terminal this payment was created for. Not present if not created for a terminal. /// public UrlObjectLink? Terminal { get; set; } /// /// Recurring payments do not have a checkout URL, because these payments are executed without any user interaction. /// This link is included for test mode recurring payments, and allows you to set the final payment state for such payments. /// public UrlLink? ChangePaymentState { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Response/PaymentRoutingResponse.cs ================================================ using System; namespace Mollie.Api.Models.Payment.Response { public record PaymentRoutingResponse { /// /// Indicates the response contains a routing object. Will always contain route for this endpoint. /// public required string Resource { get; set; } /// /// The identifier uniquely referring to this route. Mollie assigns this identifier randomly at payment creation /// time. For example rt_k6cjd01h. Its ID will always be used by Mollie to refer to a certain route. /// public required string Id { get; set; } /// /// If more than one routing object is given, the routing objects must indicate what portion of the total payment amount is being routed. /// public required Amount Amount { get; set; } /// /// The destination of this portion of the payment. /// public required RoutingDestination Destination { get; set; } /// /// Optional property you provided to schedule this portion of the payment to be transferred to its destination on a later date. /// If no date is given, the funds become available to the balance as soon as the payment succeeds. /// public DateTime? ReleaseDate { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Response/PaymentSpecificParameters/BancontactPaymentResponse.cs ================================================ using System; namespace Mollie.Api.Models.Payment.Response.PaymentSpecificParameters { public record BancontactPaymentResponse : PaymentResponse { public required BancontactPaymentResponseDetails? Details { get; set; } } public record BancontactPaymentResponseDetails { /// /// Only available if the payment is completed - The last four digits of the card number. /// public string? CardNumber { get; set; } /// /// Only available if the payment is completed - Unique alphanumeric representation of card, usable for /// identifying returning customers. This field is deprecated as of November 28th, 2019. The fingerprint /// is now unique per transaction what makes it not usefull anymore for identifying returning customers. /// Use the consumerAccount field instead. /// [Obsolete(@"This field is deprecated as of November 28th, 2019. The fingerprint is now unique per transaction what makes it not usefull anymore for identifying returning customers. Use the consumerAccount field instead.")] public string? CardFingerprint { get; set; } /// /// Only available if requested during payment creation - The QR code that can be scanned by the mobile /// Bancontact application. This enables the desktop to mobile feature. /// public QrCode? QrCode { get; set; } /// /// Only available if the payment is completed – The consumer’s bank account. This may be an IBAN, or it may be a domestic account number. /// public string? ConsumerAccount { get; set; } /// /// Only available if the payment is completed – The consumer’s name. /// public string? ConsumerName { get; set; } /// /// Only available if the payment is completed – The consumer’s bank’s BIC / SWIFT code. /// public string? ConsumerBic { get; set; } /// /// The reason why the payment did not succeed. Only available when there's a reason known. /// public string? FailureReason { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Response/PaymentSpecificParameters/BankTransferPaymentResponse.cs ================================================ using Mollie.Api.Models.Url; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Payment.Response.PaymentSpecificParameters { public record BankTransferPaymentResponse : PaymentResponse, IJsonOnDeserialized { public required BankTransferPaymentResponseDetails? Details { get; set; } /// /// For bank transfer payments, the _links object will contain some additional URL objects relevant to the payment. /// [JsonPropertyName("_links")] public new required BankTransferPaymentResponseLinks Links { get; set; } public void OnDeserialized() { base.Links = Links; } } public record BankTransferPaymentResponseDetails { /// /// The name of the bank the consumer should wire the amount to. /// public required string BankName { get; set; } /// /// The IBAN the consumer should wire the amount to. /// public required string BankAccount { get; set; } /// /// The BIC of the bank the consumer should wire the amount to. /// public required string BankBic { get; set; } /// /// The reference the consumer should use when wiring the amount. Note you should not apply any formatting here; show /// it to the consumer as-is. /// public required string TransferReference { get; set; } /// /// Only available if the payment has been completed � The consumer's name. /// public string? ConsumerName { get; set; } /// /// Only available if the payment has been completed � The consumer's bank account. This may be an IBAN, or it may be a /// domestic account number. /// public string? ConsumerAccount { get; set; } /// /// Only available if the payment has been completed � The consumer's bank's BIC / SWIFT code. /// public string? ConsumerBic { get; set; } /// /// Only available if filled out in the API or by the consumer � The email address which the consumer asked the payment /// instructions to be sent to. /// public string? BillingEmail { get; set; } /// /// Include a QR code object. Only available for iDEAL, Bancontact and bank transfer payments. /// public QrCode? QrCode { get; set; } } public record BankTransferPaymentResponseLinks : PaymentResponseLinks { /// /// A link to a hosted payment page where your customer can check the status of their payment. /// public required UrlLink Status { get; set; } /// /// A link to a hosted payment page where your customer can finish the payment using an alternative payment method also /// activated on your website profile. /// public required UrlLink PayOnline { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Response/PaymentSpecificParameters/BelfiusPaymentResponse.cs ================================================ namespace Mollie.Api.Models.Payment.Response.PaymentSpecificParameters { public record BelfiusPaymentResponse : PaymentResponse { public required BelfiusPaymentResponseDetails? Details { get; set; } } public record BelfiusPaymentResponseDetails { /// /// Only available if the payment has been completed – The consumer's name. /// public string? ConsumerName { get; set; } /// /// Only available if the payment has been completed – The consumer's IBAN. /// public string? ConsumerAccount { get; set; } /// /// Only available if the payment has been completed – The consumer's bank's BIC. /// public string? ConsumerBic { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Response/PaymentSpecificParameters/CreditCardPaymentResponse.cs ================================================ namespace Mollie.Api.Models.Payment.Response.PaymentSpecificParameters { public record CreditCardPaymentResponse : PaymentResponse { /// /// An object with credit card details. /// public CreditCardPaymentResponseDetails? Details { get; set; } } public record CreditCardPaymentResponseDetails { /// /// Only available if the payment has been completed - Unique alphanumeric representation of card, usable for identifying /// returning customers. /// public string? CardFingerprint { get; set; } /// /// The last four digits of the card number. /// public string? CardNumber { get; set; } /// /// The card holder's name. /// public string? CardHolder { get; set; } /// /// Not always available. – The card's target audience. See the Mollie.Api.Models.Payment.Response.CreditCardAudience /// class for a full list of known values /// public string? CardAudience { get; set; } /// /// The card's label. Note that not all labels can be acquired through Mollie. See the /// Mollie.Api.Models.Payment.Response.CreditCardLabel class for a full list of known values /// public string? CardLabel { get; set; } /// /// The ISO 3166-1 alpha-2 country code of the country the card was issued in. For example: BE. /// public string? CardCountryCode { get; set; } /// /// The expiry date (MM/YY) of the card as displayed on the card. /// public string? CardExpiryDate { get; set; } /// /// The card type. See the Mollie.Api.Models.Payment.Response.CreditCardFunding class for a full list of known /// values. /// public string? CardFunding { get; set; } /// /// Only available if the payment succeeded. – The payment's security type. See the /// Mollie.Api.Models.Payment.Response.CreditCardSecurity class for a full list of known values /// public string? CardSecurity { get; set; } /// /// Only available if the payment succeeded. – The fee region for the payment. See your credit card addendum for /// details. intra-eu for consumer cards from the EU, and other for all other cards. See the /// Mollie.Api.Models.Payment.Response.CreditCardFeeRegion class for a full list of known values /// public string? FeeRegion { get; set; } /// /// The first6 and last4 digits of the card number. /// public string? CardMaskedNumber { get; set; } /// /// The outcome of authentication attempted on transactions enforced by 3DS (ie valid only for oneoff and /// first). /// public string? Card3dsEci { get; set; } /// /// The first6 digit of the card bank identification number. /// public string? CardBin { get; set; } /// /// The issuer of the Card. /// public string? CardIssuer { get; set; } /// /// Only available for failed payments. Contains a failure reason code. See the /// Mollie.Api.Models.Payment.Response.CreditCardFailureReason class for a full list of known values /// public string? FailureReason { get; set; } /// /// A localized message that can be shown to your customer, depending on the failureReason /// public string? FailureMessage { get; set; } /// /// The wallet used when creating the payment. /// public string? Wallet { get; set; } /// /// Beta feature: The entrymode of the payment. See the Mollie.Api.Models.Payment.EntryMode class for a full /// list of known values /// public string? EntryMode { get; set; } } /// /// The card's target audience. /// public static class CreditCardAudience { public const string Consumer = "consumer"; public const string Business = "business"; } /// /// Only available if the payment has been completed – The type of security used during payment processing. /// public static class CreditCardSecurity { public const string Normal = "normal"; public const string Secure3D = "3dsecure"; } /// /// Only available if the payment has been completed – The fee region for the payment: intra-eu for consumer cards from the EU, and /// other for all other cards. /// public static class CreditCardFeeRegion { public const string AmericanExpress = "american-express"; public const string AmexIntraEea = "amex-intra-eea"; public const string CarteBancaire = "carte-bancaire"; public const string IntraEu = "intra-eu"; public const string IntraEuCorporate = "intra-eu-corporate"; public const string Domestic = "domestic"; public const string Maestro = "maestro"; public const string Other = "other"; } /// /// Only available for failed payments. Contains a failure reason code. /// public static class CreditCardFailureReason { public const string InvalidCardNumber = "invalid_card_number"; public const string InvalidCvv = "invalid_cvv"; public const string InvalidCardHolderName = "invalid_card_holder_name"; public const string CardExpired = "card_expired"; public const string CardDeclined = "card_declined"; public const string InvalidCardType = "invalid_card_type"; public const string RefusedByIssuer = "refused_by_issuer"; public const string InsufficientFunds = "insufficient_funds"; public const string InactiveCard = "inactive_card"; public const string UnknownReason = "unknown_reason"; public const string PossibleFraud = "possible_fraud"; public const string AuthenticationFailed = "authentication_failed"; public const string AuthenticationRequired = "authentication_required"; public const string AuthenticationAbandoned = "authentication_abandoned"; public const string AuthenticationUnavailableAcs = "authentication_unavailable_acs"; } /// /// The card's label. Note that not all labels can be acquired through Mollie. /// public static class CreditCardLabel { public const string AmericanExpress = "American Express"; public const string CartaSi = "Carta si"; public const string CarteBleue = "Carte Bleue"; public const string Dankort = nameof(Dankort); public const string DinersClub = "Diners Club"; public const string Discover = nameof(Discover); public const string Jcb = "JCB"; public const string Laser = nameof(Laser); public const string Maestro = nameof(Maestro); public const string Mastercard = nameof(Mastercard); public const string Unionpay = nameof(Unionpay); public const string Visa = nameof(Visa); public const string Vpay = nameof(Vpay); } /// /// The card type. /// public static class CreditCardFunding { public const string Debit = "debit"; public const string Credit = "credit"; public const string Prepaid = "prepaid"; public const string DeferredDebit = "deferred-debit"; } } ================================================ FILE: src/Mollie.Api/Models/Payment/Response/PaymentSpecificParameters/EpsPaymentResponse.cs ================================================ namespace Mollie.Api.Models.Payment.Response.PaymentSpecificParameters { public record EpsPaymentResponse : PaymentResponse { /// /// An object with the consumer bank account details. /// public required EpsPaymentResponseDetails? Details { get; set; } } public record EpsPaymentResponseDetails { /// /// Only available if the payment has been completed – The consumer's name. /// public string? ConsumerName { get; set; } /// /// Only available if the payment has been completed – The consumer's IBAN. /// public string? ConsumerAccount { get; set; } /// /// Only available if the payment has been completed – The consumer's bank's BIC. /// public string? ConsumerBic { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Response/PaymentSpecificParameters/GiftcardPaymentResponse.cs ================================================ using System.Collections.Generic; namespace Mollie.Api.Models.Payment.Response.PaymentSpecificParameters { public record GiftcardPaymentResponse : PaymentResponse { /// /// An object with payment details. /// public required GiftcardPaymentResponseDetails? Details { get; set; } } public record GiftcardPaymentResponseDetails { /// /// The voucher number, with the last four digits masked. When multiple gift cards are used, this is the first voucher /// number. Example: 606436353088147****. /// public string? VoucherNumber { get; set; } /// /// A list of details of all giftcards that are used for this payment. Each object will contain the following properties. /// public List? Giftcards { get; set; } /// /// Only available if another payment method was used to pay the remainder amount – The amount that was paid with /// another payment method for the remainder amount. /// public Amount? RemainderAmount { get; set; } /// /// Only available if another payment method was used to pay the remainder amount – The payment method that was used to /// pay the remainder amount. /// public string? RemainderMethod { get; set; } } public record Giftcard { /// /// The ID of the gift card brand that was used during the payment. /// public required string Issuer { get; set; } /// /// The amount in EUR that was paid with this gift card. /// public required Amount Amount { get; set; } /// /// The voucher number, with the last four digits masked. Example: 606436353088147**** /// public required string VoucherNumber { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Response/PaymentSpecificParameters/GiropayPaymentResponse.cs ================================================ namespace Mollie.Api.Models.Payment.Response.PaymentSpecificParameters { public record GiropayPaymentResponse : PaymentResponse { /// /// An object with the consumer bank account details. /// public required GiropayPaymentResponseDetails? Details { get; set; } } public record GiropayPaymentResponseDetails { /// /// Only available if the payment has been completed – The consumer's name. /// public string? ConsumerName { get; set; } /// /// Only available if the payment has been completed – The consumer's IBAN. /// public string? ConsumerAccount { get; set; } /// /// Only available if the payment has been completed – The consumer's bank's BIC. /// public string? ConsumerBic { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Response/PaymentSpecificParameters/IdealPaymentResponse.cs ================================================ namespace Mollie.Api.Models.Payment.Response.PaymentSpecificParameters { public record IdealPaymentResponse : PaymentResponse { /// /// An object with the consumer bank account details. /// public required IdealPaymentResponseDetails? Details { get; set; } } public record IdealPaymentResponseDetails { /// /// Only available if the payment has been completed – The consumer's name. /// public string? ConsumerName { get; set; } /// /// Only available if the payment has been completed – The consumer's IBAN. /// public string? ConsumerAccount { get; set; } /// /// Only available if the payment has been completed – The consumer's bank's BIC. /// public string? ConsumerBic { get; set; } /// /// Include a QR code object. Only available for iDEAL, Bancontact and bank transfer payments. /// public QrCode? QrCode { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Response/PaymentSpecificParameters/IngHomePayPaymentResponse.cs ================================================ namespace Mollie.Api.Models.Payment.Response.PaymentSpecificParameters { public record IngHomePayPaymentResponse : PaymentResponse { /// /// An object with payment details. /// public required IngHomePayPaymentResponseDetails? Details { get; set; } } public record IngHomePayPaymentResponseDetails { /// /// Only available one banking day after the payment has been completed – The consumer’s name. /// public string? ConsumerName { get; set; } /// /// Only available one banking day after the payment has been completed – The consumer’s IBAN. /// public string? ConsumerAccount { get; set; } /// /// Only available one banking day after the payment has been completed – BBRUBEBB. /// public string? ConsumerBic { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Response/PaymentSpecificParameters/KbcPaymentResponse.cs ================================================ namespace Mollie.Api.Models.Payment.Response.PaymentSpecificParameters { public record KbcPaymentResponse : PaymentResponse { public required KbcPaymentResponseDetails? Details { get; set; } } public record KbcPaymentResponseDetails { /// /// Only available if the payment has been completed – The consumer's name. /// public string? ConsumerName { get; set; } /// /// Only available if the payment has been completed – The consumer's IBAN. /// public string? ConsumerAccount { get; set; } /// /// Only available if the payment has been completed – The consumer's bank's BIC. /// public string? ConsumerBic { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Response/PaymentSpecificParameters/PayPalPaymentResponse.cs ================================================ using System.Text.Json.Serialization; namespace Mollie.Api.Models.Payment.Response.PaymentSpecificParameters { public record PayPalPaymentResponse : PaymentResponse { public PayPalPaymentResponseDetails? Details { get; set; } } public record PayPalPaymentResponseDetails { /// /// Only available if the payment has been completed – The consumer’s first and last name. /// public string? ConsumerName { get; set; } /// /// Only available if the payment has been completed – The consumer’s email address. /// public string? ConsumerAccount { get; set; } /// /// PayPal’s reference for the transaction, for instance 9AL35361CF606152E. /// [JsonPropertyName("paypalReference")] public string? PayPalReference { get; set; } /// /// ID for the consumer's PayPal account, for instance WDJJHEBZ4X2LY. /// public string? PaypalPayerId { get; set; } /// /// Indicates if the payment is eligible for PayPal's Seller Protection. This parameter is omitted if we did not /// received the information from PayPal. See the Mollie.Api.Models.Payment.Response.PayPalSellerProtection class /// for a full list of known values. /// public string? SellerProtection { get; set; } /// /// The shipping address details. /// public AddressObject? ShippingAddress { get; set; } /// /// The amount of fee PayPal will charge for this transaction. This field is omitted if PayPal will not charge a fee /// for this transaction. /// public Amount? PaypalFee { get; set; } } public static class PayPalSellerProtection { public const string Eligible = nameof(Eligible); public const string Ineligible = nameof(Ineligible); public const string PartiallyEligibleInrOnly = "Partially Eligible - INR Only"; public const string PartiallyEligibleUnauthOnly = "Partially Eligible - Unauth Only"; public const string PartiallyEligible = nameof(PartiallyEligible); public const string None = nameof(None); public const string ActiveFraudControlUnauthPremiumEligible = "Active Fraud Control - Unauth Premium Eligible"; } } ================================================ FILE: src/Mollie.Api/Models/Payment/Response/PaymentSpecificParameters/PaySafeCardPaymentResponse.cs ================================================ namespace Mollie.Api.Models.Payment.Response.PaymentSpecificParameters { public record PaySafeCardPaymentResponse : PaymentResponse { public required PaySafeCardPaymentResponseDetails? Details { get; set; } } public record PaySafeCardPaymentResponseDetails { /// /// The consumer identification supplied when the payment was created. /// public string? CustomerReference { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Response/PaymentSpecificParameters/PointOfSalePaymentResponse.cs ================================================ namespace Mollie.Api.Models.Payment.Response.PaymentSpecificParameters; public record PointOfSalePaymentResponse : PaymentResponse { /// /// An object with payment details. /// public required PointOfSalePaymentResponseDetails? Details { get; set; } } public record PointOfSalePaymentResponseDetails { /// /// The identifier referring to the terminal this payment was created for. For example, term_utGtYu756h. /// public required string TerminalId { get; set; } /// /// Only available if the payment has been completed - The last four digits of the card number. /// public string? CardNumber { get; set; } /// /// The first 6 digits & last 4 digits of the customer's masked card number. /// public string? MaskedNumber { get; set; } /// /// Only available if the payment has been completed - Unique alphanumeric representation of card, usable for /// identifying returning customers. /// public string? CardFingerprint { get; set; } /// /// Only available if the payment has been completed and if the data is available - The card’s target audience. /// /// Check the Mollie.Api.Models.Payment.Response.CreditCardAudience class for a full list of known values. /// public string? CardAudience { get; set; } /// /// Only available if the payment has been completed - The card’s label. Note that not all labels can be /// processed through Mollie. /// /// Check the Mollie.Api.Models.Payment.Response.CreditCardLabel class for a full list of known values. /// public string? CardLabel { get; set; } /// /// The card funding type, if known. Possible values: credit, debit /// public string? CardFunding { get; set; } /// /// Only available if the payment has been completed - The ISO 3166-1 alpha-2 country code of the country /// the card was issued in. For example: BE. /// public string? CardCountryCode { get; set; } /// /// The applicable card fee region. For example, intra-eea applies to consumer cards from the European Economic /// Area (EEA). See the Mollie.Api.Models.Payment.Response.CreditCardFeeRegion class for a full list of known values /// public string? FeeRegion { get; set; } /// /// The Point of sale receipt object. /// public PointOfSaleReceipt? Receipt { get; set; } /// /// Beta feature: The entrymode of the payment. See the Mollie.Api.Models.Payment.EntryMode class for a full /// list of known values /// public string? EntryMode { get; set; } } public record PointOfSaleReceipt { /// /// A unique code provided by the cardholder’s bank to confirm that the transaction was successfully approved. /// public string? AuthorizationCode { get; set; } /// /// The unique number that identifies a specific payment application on a chip card. /// public string? ApplicationIdentifier { get; set; } /// /// The method by which the card was read by the terminal. /// See the Mollie.Api.Models.Payment.Response.PaymentSpecificParameters.CardReadMethod class for a full list of known values /// public string? CardReadMethod { get; set; } /// /// The method used to verify the cardholder's identity. /// See the Mollie.Api.Models.Payment.Response.PaymentSpecificParameters.CardVerificationMethod class for a full list of known values /// public string? CardVerificationMethod { get; set; } } /// /// The method by which the card was read by the terminal. /// public static class CardReadMethod { public const string IntegratedCircuitCard = "integrated-circuit-card"; public const string MagneticStripeReader = "magnetic-stripe-reader"; public const string NearFieldCommunication = "near-field-communication"; public const string Contactless = "contactless "; public const string Manual = "manual "; } /// /// the method used to verify the cardholder's identity. /// public static class CardVerificationMethod { public const string NoCvmRequired = "no-cvm-required"; public const string OnlinePin = "online-pin"; public const string Signature = "signature"; public const string SignatureAndOnlinePin = "signature-and-online-pin"; public const string OnlinePinAndSignature = "online-pin-and-signature"; public const string None = "none"; public const string Failed = "failed"; } ================================================ FILE: src/Mollie.Api/Models/Payment/Response/PaymentSpecificParameters/SepaDirectDebitResponse.cs ================================================ namespace Mollie.Api.Models.Payment.Response.PaymentSpecificParameters { public record SepaDirectDebitResponse : PaymentResponse { public required SepaDirectDebitResponseDetails? Details { get; set; } } public record SepaDirectDebitResponseDetails { /// /// Transfer reference used by Mollie to identify this payment. /// public string? TransferReference { get; set; } /// /// The creditor identifier indicates who is authorized to execute the payment. In this case, it is a reference to /// Mollie. /// public string? CreditorIdentifier { get; set; } /// /// Optional – The consumer's name. /// public string? ConsumerName { get; set; } /// /// Optional – The consumer's IBAN. /// public string? ConsumerAccount { get; set; } /// /// Optional – The consumer's bank's BIC. /// public string? ConsumerBic { get; set; } /// /// Estimated date the payment is debited from the consumer's bank account, in YYYY-MM-DD format. /// public string? DueDate { get; set; } /// /// Only available if the payment has been verified – Date the payment has been signed by the consumer, in YYYY-MM-DD format. /// format. /// public string? SignatureDate { get; set; } /// /// Only available if the payment has failed – The official reason why this payment has failed. A detailed description /// of each reason is available on the website of the European Payments Council. /// public string? BankReasonCode { get; set; } /// /// Only available if the payment has failed – A textual desciption of the failure reason. /// public string? BankReason { get; set; } /// /// Only available for batch transactions – The original end-to-end identifier that you've specified in your batch. /// public string? EndToEndIdentifier { get; set; } /// /// Only available for batch transactions – The original mandate reference that you've specified in your batch. /// public string? MandateReference { get; set; } /// /// Only available for batch transactions – The original batch reference that you've specified in your batch. /// public string? BatchReference { get; set; } /// /// Only available for batch transactions – The original file reference that you've specified in your batch. /// public string? FileReference { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Response/PaymentSpecificParameters/SofortPaymentResponse.cs ================================================ namespace Mollie.Api.Models.Payment.Response.PaymentSpecificParameters { public record SofortPaymentResponse : PaymentResponse { public required SofortPaymentResponseDetails? Details { get; set; } } public record SofortPaymentResponseDetails { /// /// Only available if the payment has been completed - The consumer's name. /// public string? ConsumerName { get; set; } /// /// Only available if the payment has been completed - The consumer's IBAN. /// public string? ConsumerAccount { get; set; } /// /// Only available if the payment has been completed - The consumer's bank's BIC. /// public string? ConsumerBic { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/Response/QrCode.cs ================================================ namespace Mollie.Api.Models.Payment.Response { public record QrCode { /// /// Height of the image in pixels. /// public required int Height { get; set; } /// /// Width of the image in pixels. /// public required int Width { get; set; } /// /// The URI you can use to display the QR code. Note that we can send both data URIs as well as links to HTTPS images. /// You should support both. /// public required string Src { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/RoutingDestination.cs ================================================ namespace Mollie.Api.Models.Payment { public record RoutingDestination { /// /// The type of destination. Currently only the destination type organization is supported. /// public required string Type { get; set; } /// /// Required for destination type organization.The ID of the connected organization the funds should be routed /// to, for example org_12345. Please note that me or the ID of the current organization are not accepted as an /// organizationId. After all portions of the total payment amount have been routed, the amount left will be /// routed to the current organization automatically. /// public string? OrganizationId { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Payment/SequenceType.cs ================================================ namespace Mollie.Api.Models.Payment { public static class SequenceType { public const string OneOff = "oneoff"; public const string First = "first"; public const string Recurring = "recurring"; } } ================================================ FILE: src/Mollie.Api/Models/PaymentLink/Request/PaymentLinkRequest.cs ================================================ using Mollie.Api.JsonConverters; using System.Text.Json.Serialization; using System; using System.Collections.Generic; using Mollie.Api.Models.Payment; namespace Mollie.Api.Models.PaymentLink.Request { public record PaymentLinkRequest : ITestModeRequest, IProfileRequest { /// /// This description will also be used as the payment description and will be shown to your customer on their card or bank /// statement when possible. /// public required string Description { get; set; } /// /// The amount that you want to charge, e.g. {"currency":"EUR", "value":"1000.00"} if you would want to charge €1000.00. /// public Amount? Amount { get; set; } /// /// The minimum amount of the payment link. This property is only allowed when there is no amount provided. /// The customer will be prompted to enter a value greater than or equal to the minimum amount. /// public Amount? MinimumAmount { get; set; } /// /// Optional - The URL your customer will be redirected to after completing the payment process. /// public string? RedirectUrl { get; set; } /// /// Set the webhook URL, where we will send payment status updates to. /// public string? WebhookUrl { get; set; } /// /// Optionally provide the order lines for the payment. Each line contains details such as a description of the item ordered and its price. /// All lines must have the same currency as the payment. /// public List? Lines { get; set; } /// /// The customer's billing address details. We advise to provide these details to improve fraud protection and conversion. /// public PaymentAddressDetails? BillingAddress { get; set; } /// /// The customer's shipping address details. We advise to provide these details to improve fraud protection and conversion. /// public PaymentAddressDetails? ShippingAddress { get; set; } /// /// Oauth only - The website profile’s unique identifier, for example pfl_3RkSN1zuPE. This field is mandatory. /// public string? ProfileId { get; set; } /// /// Indicates whether the payment link is reusable. If this field is set to true, customers can make multiple /// payments using the same link. If no value is specified, the field defaults to false, allowing only a single /// payment per link. /// public bool? Reusable { get; set; } /// /// The expiry date of the payment link in ISO 8601 format. For example: 2021-12-24T12:00:16+01:00. /// [JsonConverter(typeof(Iso8601DateTimeConverter))] public DateTime? ExpiresAt { get; set; } /// /// An array of payment methods that are allowed to be used for this payment link. When this parameter is not /// provided or is an empty array, all enabled payment methods will be available. /// See the Mollie.Api.Models.Payment.PaymentMethod class for a full list of known values. /// public IEnumerable? AllowedMethods { get; set; } /// /// With Mollie Connect you can charge fees on payment links that your app is processing on behalf of other /// Mollie merchants. /// /// If you use OAuth to create payment links on a connected merchant's account, you can charge /// a fee using this applicationFee parameter. If a payment on the payment link succeeds, the fee will be /// deducted from the merchant's balance and sent to your own account balance. /// public ApplicationFee? ApplicationFee { get; set; } /// /// If set to first, a payment mandate is established right after a payment is made by the customer. /// Defaults to oneoff, which is a regular payment link and will not establish a mandate after payment. /// The mandate ID can be retrieved by making a call to the Payment Link Payments Endpoint. /// See the Mollie.Api.Models.Payment.SequenceType class for a full list of known values. /// public string? SequenceType { get; set; } /// /// The ID of the customer the payment link is being created for. If a value is not provided, the customer /// will be required to input relevant information which will be used to establish a mandate after the payment /// is made. /// public string? CustomerId { get; set; } /// /// Oauth only - Optional – Set this to true to only retrieve payment links made in test mode. By default, only /// live payment links are returned. /// public bool? Testmode { get; set; } public override string ToString() { return $"Amount: {Amount} - Description: {Description}"; } } } ================================================ FILE: src/Mollie.Api/Models/PaymentLink/Request/PaymentLinkUpdateRequest.cs ================================================ using System.Collections.Generic; using Mollie.Api.Models.Payment; namespace Mollie.Api.Models.PaymentLink.Request; public record PaymentLinkUpdateRequest : ITestModeRequest { /// /// A short description of the payment link. The description is visible in the Dashboard and will be shown on the /// customer's bank or card statement when possible. /// /// Updating the description does not affect any previously existing payments created for this payment link. /// public required string Description { get; set; } /// /// The minimum amount of the payment link. This property is only allowed when there is no amount provided. /// The customer will be prompted to enter a value greater than or equal to the minimum amount. /// public Amount? MinimumAmount { get; set; } /// /// Whether the payment link is archived. Customers will not be able to complete payments on archived payment links. /// public required bool Archived { get; set; } /// /// An array of payment methods that are allowed to be used for this payment link. When this parameter is not /// provided or is an empty array, all enabled payment methods will be available. /// See the Mollie.Api.Models.Payment.PaymentMethod class for a full list of known values. /// public IEnumerable? AllowedMethods { get; set; } /// /// Optionally provide the order lines for the payment. Each line contains details such as a description of the item ordered and its price. /// All lines must have the same currency as the payment. /// public List? Lines { get; set; } /// /// The customer's billing address details. We advise to provide these details to improve fraud protection and conversion. /// public PaymentAddressDetails? BillingAddress { get; set; } /// /// The customer's shipping address details. We advise to provide these details to improve fraud protection and conversion. /// public PaymentAddressDetails? ShippingAddress { get; set; } /// /// Oauth only - Optional – Most API credentials are specifically created for either live mode or test mode. /// For organization-level credentials such as OAuth access tokens, you can enable test mode by setting testmode /// to true. /// /// Test entities cannot be retrieved when the endpoint is set to live mode, and vice versa. /// public bool? Testmode { get; set; } } ================================================ FILE: src/Mollie.Api/Models/PaymentLink/Response/PaymentLinkResponse.cs ================================================ using System; using System.Collections.Generic; using Mollie.Api.Models.Payment; using System.Text.Json.Serialization; namespace Mollie.Api.Models.PaymentLink.Response { public record PaymentLinkResponse : IEntity { /// /// Indicates the response contains a payment object. Will always contain payment-link for this endpoint. /// public required string Resource { get; set; } /// /// The identifier uniquely referring to this payment link. Mollie assigns this identifier at creation time. /// For example pl_4Y0eZitmBnQ6IDoMqZQKh. Its ID will always be used by Mollie to refer to a certain payment link. /// public required string Id { get; set; } /// /// The mode used to create this payment link. Mode determines whether a payment link is real (live mode) or a test payment link. /// public Mode Mode { get; set; } /// /// A short description of the payment link. The description is visible in the Dashboard and will be shown on the /// customer’s bank or card statement when possible. This description will eventual been used as payment description. /// public required string Description { get; set; } /// /// The amount of the payment link, e.g. {"currency":"EUR", "value":"100.00"} for a €100.00 payment link. /// public Amount? Amount { get; set; } /// /// The minimum amount of the payment link. This property is only allowed when there is no amount provided. /// The customer will be prompted to enter a value greater than or equal to the minimum amount. /// public Amount? MinimumAmount { get; set; } /// /// Whether the payment link is archived. Customers will not be able to complete payments on archived payment links. /// public required bool Archived { get; set; } /// /// The URL your customer will be redirected to after completing the payment process. /// public string? RedirectUrl { get; set; } /// /// The URL Mollie will call as soon an important status change takes place. /// public string? WebhookUrl { get; set; } /// /// Optionally provide the order lines for the payment. Each line contains details such as a description of the item ordered and its price. /// All lines must have the same currency as the payment. /// public List? Lines { get; set; } /// /// The customer's billing address details. We advise to provide these details to improve fraud protection and conversion. /// public PaymentAddressDetails? BillingAddress { get; set; } /// /// The customer's shipping address details. We advise to provide these details to improve fraud protection and conversion. /// public PaymentAddressDetails? ShippingAddress { get; set; } /// /// The identifier referring to the profile this payment link was created on. For example, pfl_QkEhN94Ba. /// public string? ProfileId { get; set; } /// /// Indicates whether the payment link is reusable. If this field is set to true, customers can make multiple /// payments using the same link. If no value is specified, the field defaults to false, allowing only a single /// payment per link. /// public bool? Reusable { get; set; } /// /// The payment link’s date and time of creation, in ISO 8601 format. /// public DateTime? CreatedAt { get; set; } /// /// The date and time the payment link became paid, in ISO 8601 format. /// public DateTime? PaidAt { get; set; } /// /// The date and time the payment link last status change, in ISO 8601 format. /// public DateTime? UpdatedAt { get; set; } /// /// The expiry date and time of the payment link, in ISO 8601 format. /// public DateTime? ExpiresAt { get; set; } /// /// An array of payment methods that are allowed to be used for this payment link. When this parameter is not /// provided or is an empty array, all enabled payment methods will be available. /// See the Mollie.Api.Models.Payment.PaymentMethod class for a full list of known values. /// public IEnumerable? AllowedMethods { get; set; } /// /// With Mollie Connect you can charge fees on payment links that your app is processing on behalf of other /// Mollie merchants. /// /// If you use OAuth to create payment links on a connected merchant's account, you can charge /// a fee using this applicationFee parameter. If a payment on the payment link succeeds, the fee will be /// deducted from the merchant's balance and sent to your own account balance. /// public ApplicationFee? ApplicationFee { get; set; } /// /// If set to first, a payment mandate is established right after a payment is made by the customer. /// Defaults to oneoff, which is a regular payment link and will not establish a mandate after payment. /// The mandate ID can be retrieved by making a call to the Payment Link Payments Endpoint. /// See the Mollie.Api.Models.Payment.SequenceType class for a full list of known values. /// public string? SequenceType { get; set; } /// /// Only relevant when sequenceType is set to first. The ID of the customer the payment link is being created /// for. If a value is not provided, the customer will be required to input relevant information which will /// be used to establish a mandate after the payment /// is made. /// public string? CustomerId { get; set; } /// /// An object with several URL objects relevant to the payment. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required PaymentLinkResponseLinks Links { get; set; } public override string ToString() { return $"Id: {Id} - Description: {Description} - Amount: {Amount}"; } } } ================================================ FILE: src/Mollie.Api/Models/PaymentLink/Response/PaymentLinkResponseLinks.cs ================================================ using Mollie.Api.Models.Url; namespace Mollie.Api.Models.PaymentLink.Response { public record PaymentLinkResponseLinks { /// /// The API resource URL of the payment link itself. /// public required UrlObjectLink Self { get; set; } /// /// Direct link to the payment link. /// public required UrlLink PaymentLink { get; set; } /// ///The URL to the payment link retrieval endpoint documentation. /// public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/PaymentMethod/Response/FixedPricingResponse.cs ================================================ using System.Text.Json.Serialization; using Mollie.Api.JsonConverters; namespace Mollie.Api.Models.PaymentMethod.Response { public record FixedPricingResponse { /// /// The ISO 4217 currency code. /// public required string Currency { get; set; } /// /// A string containing the exact amount in the given currency. /// [JsonConverter(typeof(StringToDecimalConverter))] public required decimal Value { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/PaymentMethod/Response/PaymentMethodResponse.cs ================================================ using System.Collections.Generic; using Mollie.Api.Models.Issuer.Response; using System.Text.Json.Serialization; namespace Mollie.Api.Models.PaymentMethod.Response { public record PaymentMethodResponse : IEntity { /// /// Indicates the response contains a method object. Will always contain method for this endpoint. /// public required string Resource { get; set; } /// /// The unique identifier of the payment method. When used during payment creation, the payment method selection screen will be skipped. /// public required string Id { get; set; } /// /// The full name of the payment method. /// public required string Description { get; set; } /// /// Minimum payment amount required to use this payment method. /// public required Amount MinimumAmount { get; set; } /// /// Maximum payment amount allowed when using this payment method. (Could be null) /// public required Amount MaximumAmount { get; set; } /// /// URLs of images representing the payment method. /// public required PaymentMethodResponseImage Image { get; set; } /// /// The payment method's activation status for this profile. /// See the class for possible values. /// public string? Status { get; set; } /// /// List of Issuers /// public List? Issuers { get; set; } /// /// Pricing set of the payment method what will be include if you add the parameter. /// public List? Pricing { get; set; } /// /// An object with several URL objects relevant to the payment method. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required PaymentMethodResponseLinks Links { get; set; } public override string ToString() { return Description; } } } ================================================ FILE: src/Mollie.Api/Models/PaymentMethod/Response/PaymentMethodResponseImage.cs ================================================ namespace Mollie.Api.Models.PaymentMethod.Response { /// /// URLs of images representing the payment method. /// public record PaymentMethodResponseImage { /// /// The URL for a payment method icon of 55x37 pixels. /// public required string Size1x { get; set; } /// /// The URL for a payment method icon of 110x74 pixels. Use this for high resolution screens. /// public required string Size2x { get; set; } /// /// The URL for a payment method icon in vector format. Usage of this format is preferred since it can scale to any desired size. /// public required string Svg { get; set; } public override string ToString() { return Size1x; } } } ================================================ FILE: src/Mollie.Api/Models/PaymentMethod/Response/PaymentMethodResponseLinks.cs ================================================ using Mollie.Api.Models.Url; namespace Mollie.Api.Models.PaymentMethod.Response { public record PaymentMethodResponseLinks { /// /// The API resource URL of the payment method itself. /// public required UrlObjectLink Self { get; set; } /// /// The URL to the payment method retrieval endpoint documentation. /// public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/PaymentMethod/Response/PaymentMethodStatus.cs ================================================ namespace Mollie.Api.Models.PaymentMethod.Response; /// /// The payment method's activation status for this profile. /// public static class PaymentMethodStatus { public const string Activated = "activated"; public const string PendingBoarding = "pending-boarding"; public const string PendingReview = "pending-review"; public const string PendingExternal = "pending-external"; public const string Rejected = "rejected"; } ================================================ FILE: src/Mollie.Api/Models/PaymentMethod/Response/PricingResponse.cs ================================================ using System.Text.Json.Serialization; using Mollie.Api.JsonConverters; namespace Mollie.Api.Models.PaymentMethod.Response { public record PricingResponse { /// /// The area or product-type where the pricing is applied for, translated in the optional locale passed. /// public required string Description { get; set; } /// /// The fixed price per transaction /// public required FixedPricingResponse Fixed { get; set; } /// /// A string containing the percentage what will be charged over the payment amount besides the fixed price. /// [JsonConverter(typeof(StringToDecimalConverter))] public required decimal Variable { get; set; } /// /// This value is only available for credit card rates. It will correspond with the regions as documented in /// the Payments API. See the Mollie.Api.Models.Payment.Response.CreditCardFeeRegion class for a full list of /// known values. /// public required string? FeeRegion { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Permission/Response/PermissionResponse.cs ================================================ using System.Text.Json.Serialization; namespace Mollie.Api.Models.Permission.Response { public record PermissionResponse : IEntity { /// /// Indicates the response contains a permission object. /// Possible values: permission /// public required string Resource { get; set; } /// /// The permission's identifier. See OAuth: Permissions for more details about the available permissions. /// public required string Id { get; set; } /// /// A short description of what the permission allows. /// public required string Description { get; set; } /// /// Whether this permission is granted to the app by the organization or not. /// public bool Granted { get; set; } /// /// An object with several URL objects relevant to the permission. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required PermissionResponseLinks Links { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Permission/Response/PermissionResponseLInks.cs ================================================ using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Permission.Response { public record PermissionResponseLinks { /// /// The API resource URL of the permission itself. /// public required UrlObjectLink Self { get; set; } /// /// The URL to the permission retrieval endpoint documentation. /// public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Profile/ProfileStatus.cs ================================================ namespace Mollie.Api.Models.Profile { public static class ProfileStatus { public const string Unverified = "unverified"; public const string Verified = "verified"; public const string Blocked = "blocked"; } } ================================================ FILE: src/Mollie.Api/Models/Profile/Request/ProfileRequest.cs ================================================ using System.Collections.Generic; namespace Mollie.Api.Models.Profile.Request { public record ProfileRequest { /// /// The profile's name should reflect the tradename or brand name of the profile's website or application. /// public required string Name { get; set; } /// /// The URL to the profile's website or application. The URL should start with http:// or https://. /// public required string Website { get; set; } /// /// The email address associated with the profile's tradename or brand. /// public required string Email { get; set; } /// /// The phone number associated with the profile's tradename or brand. /// public required string Phone { get; set; } /// /// The products or services that the profile’s website offers. /// public string? Description { get; set; } /// /// The list of countries where you expect that the majority of the profile’s customers will live, in ISO 3166-1 alpha-2 format. /// public IEnumerable? CountriesOfActivity { get; set; } /// /// The industry associated with the profile’s trade name or brand. Please refer to the documentation of the business category /// for more information on which values are accepted. /// public string? BusinessCategory { get; set; } /// /// Optional – Creating a test profile by setting this parameter to test, enables you to start using the API without /// having to provide all your business info just yet. Defaults to live. /// public Mode? Mode { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Profile/Response/ApiKey.cs ================================================ using System; namespace Mollie.Api.Models.Profile.Response { public record ApiKey { /// /// Indicates the response contains an API key object. /// public required string Resource { get; set; } /// /// The API key's identifier. /// public required string Id { get; set; } /// /// The actual API key, which you'll use when creating payments or when otherwise communicating with the API. Never /// share the API key with anyone. /// public required string Key { get; set; } /// /// The API key's date and time of creation. /// public DateTime CreatedDatetime { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Profile/Response/EnableGiftCardIssuerResponse.cs ================================================ using System.Text.Json.Serialization; namespace Mollie.Api.Models.Profile.Response { public record EnableGiftCardIssuerResponse { /// /// Indicates the response contains an issuer object. Will always contain issuer for this endpoint. /// public required string Resource { get; set; } /// /// The unique identifier of the gift card issuer. /// public required string Id { get; set; } /// /// The full name of the gift card issuer. /// public required string Description { get; set; } /// /// The status that the issuer is in. Possible values: pending-issuer or activated. /// public required string Status { get; set; } /// /// An object with several URL objects relevant to the order. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required EnableGiftCardIssuerResponseLinks Links { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Profile/Response/EnableGiftCardIssuerResponseLinks.cs ================================================ using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Profile.Response { public record EnableGiftCardIssuerResponseLinks { /// /// The API resource URL of the gift card issuer itself. /// public required UrlLink Self { get; set; } /// /// The URL to the gift card issuer retrieval endpoint documentation. /// public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Profile/Response/EnableGiftCardIssuerStatus.cs ================================================ namespace Mollie.Api.Models.Profile.Response { /// /// The status that the issuer is in. Possible values: pending-issuer or activated. /// public static class EnableGiftCardIssuerStatus { /// /// The issuer is activated and ready for use. /// public const string Activated = "activated"; /// /// Activation of this issuer relies on you taking action with the issuer itself. /// public const string PendingIssuer = "pending-issuer"; } } ================================================ FILE: src/Mollie.Api/Models/Profile/Response/ProfileResponse.cs ================================================ using System; using System.Collections.Generic; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Profile.Response { public record ProfileResponse : IEntity { /// /// Indicates the response contains a profile object. Will always contain profile for this endpoint. /// public required string Resource { get; set; } /// /// The identifier uniquely referring to this profile, for example pfl_v9hTwCvYqw. /// public required string Id { get; set; } /// /// Indicates whether the payment profile is in test or production mode. /// Possible values: live or test /// public Mode Mode { get; set; } /// /// The payment profile's name, this will usually reflect the tradename or brand name of the profile's website or /// application. /// public required string Name { get; set; } /// /// The URL to the profile's website or application. /// public required string Website { get; set; } /// /// The products or services that the profile’s website offers. /// public string? Description { get; set; } /// /// The list of countries where you expect that the majority of the profile’s customers will live, in ISO 3166-1 alpha-2 format. /// public IEnumerable? CountriesOfActivity { get; set; } /// /// The email address associated with the profile's tradename or brand. /// public required string Email { get; set; } /// /// The phone number associated with the profile's tradename or brand. /// public required string Phone { get; set; } /// /// The industry associated with the profile’s trade name or brand. Please refer to the documentation of the business category /// for more information on which values are accepted. /// public string? BusinessCategory { get; set; } /// /// The industry associated with the profile's tradename or brand. /// [Obsolete("This parameter is deprecated and will be removed in 2022. Please use the businessCategory parameter instead.")] public int CategoryCode { get; set; } /// /// The profile status determines whether the payment profile is able to receive live payments. See the /// Mollie.Api.Models.Profile.ProfileStatus class for a full list of known values. /// public required string Status { get; set; } /// /// The presence of a review object indicates changes have been made that have not yet been approved by Mollie. /// Changes to test profiles are approved automatically, unless a switch to a live profile has been requested. /// The review object will therefore usually be null in test mode. /// public Review? Review { get; set; } /// /// The payment profile's date and time of creation. /// public required DateTime CreatedAt { get; set; } /// /// Useful URLs to related resources. /// [JsonPropertyName("_links")] public required ProfileResponseLinks Links { get; set; } } public record Review { /// /// The status of the requested profile changes. See the Mollie.Api.Models.Profile.ReviewStatus /// class for a full list of known values. /// public required string Status { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Profile/Response/ProfileResponseLinks.cs ================================================ using Mollie.Api.Models.Chargeback.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.PaymentMethod.Response; using Mollie.Api.Models.Refund.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Profile.Response { public record ProfileResponseLinks { public required UrlObjectLink Self { get; set; } public required UrlLink Dashboard { get; set; } public UrlObjectLink>? Chargebacks { get; set; } public UrlObjectLink>? Methods { get; set; } public UrlObjectLink>? Payments { get; set; } public UrlObjectLink>? Refunds { get; set; } public UrlLink? CheckoutPreviewUrl { get; set; } public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Profile/ReviewStatus.cs ================================================ namespace Mollie.Api.Models.Profile { public static class ReviewStatus { /// /// The changes are pending review. We will review your changes soon. /// public const string Pending = "pending"; /// /// We've reviewed and rejected your changes. /// public const string Rejected = "rejected"; } } ================================================ FILE: src/Mollie.Api/Models/Refund/Request/RefundRequest.cs ================================================ using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; using Mollie.Api.JsonConverters; namespace Mollie.Api.Models.Refund.Request { public record RefundRequest : ITestModeRequest { /// /// The amount to refund. For some payments, it can be up to €25.00 more than the original transaction amount. /// public required Amount Amount { get; set; } /// /// Optional – The description of the refund you are creating. This will be shown to the consumer on their card or bank /// statement when possible. Max 140 characters. /// public string? Description { get; set; } /// /// Provide any data you like, for example a string or a JSON object. We will save the data alongside the refund. Whenever /// you fetch the refund with our API, we’ll also include the metadata. You can use up to approximately 1kB. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// With Mollie Connect you can charge fees on payments that your app is processing on behalf of other Mollie merchants, /// by providing the routing object during payment creation. When creating refunds for these routed payments, by default /// the full amount is deducted from your balance.If you want to pull back the funds that were routed to the connected /// merchant(s), you can set this parameter to true when issuing a full refund. For more fine-grained control and for /// partial refunds, use the routingReversals parameter instead. /// public bool? ReverseRouting { get; set; } /// /// When creating refunds for routed payments, by default the full amount is deducted from your balance. If you want to /// pull back funds from the connected merchant(s), you can use this parameter to specify what amount needs to be /// reversed from which merchant(s). If you simply want to fully reverse the routed funds, you can also use the /// reverseRouting parameter instead. /// public IList? RoutingReversals { get; set; } /// /// Set this to true to refund a test mode payment. /// public bool? Testmode { get; set; } public void SetMetadata(object metadataObj, JsonSerializerOptions? jsonSerializerOptions = null) { Metadata = JsonSerializer.Serialize(metadataObj, jsonSerializerOptions); } } } ================================================ FILE: src/Mollie.Api/Models/Refund/Response/RefundResponse.cs ================================================ using System; using System.Collections.Generic; using System.Text.Json; using Mollie.Api.JsonConverters; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Refund.Response { public record RefundResponse : IEntity { /// /// Indicates the response contains a refund object. Will always contain refund for this endpoint. /// public required string Resource { get; set; } /// /// The refund's unique identifier, for example re_4qqhO89gsT. /// public required string Id { get; set; } /// /// The description of the refund that may be shown to the consumer, depending on the payment method used. /// public string? Description { get; set; } /// /// The amount refunded to the consumer with this refund. /// public required Amount Amount { get; set; } /// /// This optional field will contain the amount that will be deducted from your account balance, converted /// to the currency your account is settled in. It follows the same syntax as the amount property. Note that /// for refunds, the value key of settlementAmount will be negative. Any amounts not settled by Mollie will /// not be reflected in this amount, e.g. PayPal refunds. /// public Amount? SettlementAmount { get; set; } /// /// Provide any data you like, for example a string or a JSON object. We will save the data alongside the refund. Whenever /// you fetch the refund with our API, we’ll also include the metadata. You can use up to approximately 1kB. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// Since refunds may be delayed for certain payment methods, the refund carries a status field. See the /// Mollie.Api.Models.Refund.RefundStatus class for a full list of known values. /// public required string Status { get; set; } /// /// With Mollie Connect you can charge fees on payments that your app is processing on behalf of other Mollie merchants, /// by providing the routing object during payment creation. When creating refunds for these routed payments, by default /// the full amount is deducted from your balance.If you want to pull back the funds that were routed to the connected /// merchant(s), you can set this parameter to true when issuing a full refund. For more fine-grained control and for /// partial refunds, use the routingReversals parameter instead. /// public bool? ReverseRouting { get; set; } /// /// When creating refunds for routed payments, by default the full amount is deducted from your balance. If you want to /// pull back funds from the connected merchant(s), you can use this parameter to specify what amount needs to be /// reversed from which merchant(s). If you simply want to fully reverse the routed funds, you can also use the /// reverseRouting parameter instead. /// public IList? RoutingReversals { get; set; } /// /// The unique identifier of the payment this refund was created for. For example: tr_7UhSN1zuXS. The full /// payment object can be retrieved via the payment URL in the _links object. /// public required string PaymentId { get; set; } /// /// The identifier referring to the settlement this payment was settled with. For example, stl_BkEjN2eBb. /// This field is omitted if the refund is not settled (yet). /// public string? SettlementId { get; set; } /// /// The date and time the refund was issued, in ISO 8601 format. /// public DateTime? CreatedAt { get; set; } /// /// An object with several URL objects relevant to the refund. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required RefundResponseLinks Links { get; set; } public T? GetMetadata(JsonSerializerOptions? jsonSerializerOptions = null) { return Metadata != null ? JsonSerializer.Deserialize(Metadata, jsonSerializerOptions) : default; } } } ================================================ FILE: src/Mollie.Api/Models/Refund/Response/RefundResponseLinks.cs ================================================ using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Settlement.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Refund.Response { public record RefundResponseLinks { /// /// The API resource URL of the refund itself. /// public required UrlObjectLink Self { get; set; } /// /// The API resource URL of the payment the refund belongs to. /// public required UrlObjectLink Payment { get; set; } /// /// The API resource URL of the settlement this payment has been settled with. Not present if not yet settled. /// public UrlObjectLink? Settlement { get; set; } /// /// The URL to the refund retrieval endpoint documentation. /// public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Refund/Response/RefundStatus.cs ================================================ namespace Mollie.Api.Models.Refund.Response { public static class RefundStatus { public const string Pending = "pending"; public const string Processing = "processing"; public const string Refunded = "refunded"; public const string Queued = "queued"; public const string Failed = "failed"; } } ================================================ FILE: src/Mollie.Api/Models/Refund/RoutingReversal.cs ================================================ using Mollie.Api.Models.Payment; namespace Mollie.Api.Models.Refund; public record RoutingReversal { /// /// The amount that will be pulled back. /// public required Amount Amount { get; set; } /// /// Where the funds will be pulled back from. /// public required RoutingDestination Source { get; set; } } ================================================ FILE: src/Mollie.Api/Models/SalesInvoice/EmailDetails.cs ================================================ namespace Mollie.Api.Models.SalesInvoice; public record EmailDetails { /// /// The subject of the email to be sent. /// public required string Subject { get; set; } /// /// The body of the email to be sent. To add newline characters, you can use \n. /// public required string Body { get; set; } } ================================================ FILE: src/Mollie.Api/Models/SalesInvoice/PaymentDetails.cs ================================================ namespace Mollie.Api.Models.SalesInvoice; public record PaymentDetails { /// /// The way through which the invoice is to be set to paid. See the /// Mollie.Api.Models.SalesInvoice.PaymentDetailSource class for a full list of known values. /// public required string Source { get; set; } /// /// A reference to the payment the sales invoice is paid by. Required for source values payment-link and payment. /// public string? SourceReference { get; set; } } ================================================ FILE: src/Mollie.Api/Models/SalesInvoice/PaymentDetailsSource.cs ================================================ namespace Mollie.Api.Models.SalesInvoice; /// /// The way through which the invoice is to be set to paid. /// public static class PaymentDetailsSource { /// /// The invoice is to be paid manually. /// public const string Manual = "manual"; /// /// The invoice is to be paid through a payment link. /// public const string PaymentLink = "payment-link"; /// /// The invoice is to be paid through a payment. /// public const string Payment = "payment"; } ================================================ FILE: src/Mollie.Api/Models/SalesInvoice/PaymentTerm.cs ================================================ namespace Mollie.Api.Models.SalesInvoice; /// /// The payment term to be set on the invoice. Default: 30 days /// public static class PaymentTerm { public const string Days7 = "7 days"; public const string Days14 = "14 days"; public const string Days30 = "30 days"; public const string Days45 = "45 days"; public const string Days60 = "60 days"; public const string Days90 = "90 days"; public const string Days120 = "120 days"; } ================================================ FILE: src/Mollie.Api/Models/SalesInvoice/Recipient.cs ================================================ namespace Mollie.Api.Models.SalesInvoice; public record Recipient { /// /// The type of recipient, either consumer or business. This will determine what further fields are required on /// the recipient object. See the Mollie.Api.Models.SalesInvoice.RecipientType class for a full list of known /// values. /// public required string Type { get; set; } /// /// The title of the consumer type recipient, for example Mr. or Mrs.. /// public string? Title { get; set; } /// /// The given name (first name) of the consumer type recipient should be at least two characters and cannot contain /// only numbers. /// public string? GivenName { get; set; } /// /// The given name (last name) of the consumer type recipient should be at least two characters and cannot contain /// only numbers. /// public string? FamilyName { get; set; } /// /// The trading name of the business type recipient. /// public string? OrganizationName { get; set; } /// /// The Chamber of Commerce number of the organization for a business type recipient. Either this or vatNumber has /// to be provided. /// public string? OrganizationNumber { get; set; } /// /// The VAT number of the organization for a business type recipient. Either this or organizationNumber has to be /// provided. /// public string? VatNumber { get; set; } /// /// The email address of the recipient. /// public required string Email { get; set; } /// /// The phone number of the recipient. /// public string? Phone { get; set; } /// /// A street and street number. /// public required string StreetAndNumber { get; set; } /// /// Any additional addressing details, for example an apartment number. /// public string? StreetAdditional { get; set; } /// /// A postal code. /// public required string PostalCode { get; set; } /// /// The recipient's city. /// public required string City { get; set; } /// /// The recipient's region. /// public string? Region { get; set; } /// /// A country code in ISO 3166-1 alpha-2 format. /// public required string Country { get; set; } /// /// The locale for the recipient, to be used for translations in PDF generation and payment pages. /// public required string Locale { get; set; } } ================================================ FILE: src/Mollie.Api/Models/SalesInvoice/RecipientType.cs ================================================ namespace Mollie.Api.Models.SalesInvoice; /// /// The type of recipient, either consumer or business. This will determine what further fields are required on the /// recipient object. /// public static class RecipientType { /// /// The recipient is a consumer. /// public const string Consumer = "consumer"; /// /// The recipient is a business. /// public const string Business = "business"; } ================================================ FILE: src/Mollie.Api/Models/SalesInvoice/Request/SalesInvoiceRequest.cs ================================================ using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; using Mollie.Api.JsonConverters; namespace Mollie.Api.Models.SalesInvoice.Request; public record SalesInvoiceRequest : ITestModeRequest, IProfileRequest { /// /// Whether to create the entity in test mode or live mode. Most API credentials are specifically created for /// either live mode or test mode, in which case this parameter can be omitted. For organization-level credentials /// such as OAuth access tokens, you can enable test mode by setting testmode to true. /// public bool? Testmode { get; set; } /// /// The identifier referring to the profile this entity belongs to. Most API credentials are linked to a single /// profile. In these cases the profileId can be omitted in the creation request. For organization-level /// credentials such as OAuth access tokens however, the profileId parameter is required. /// public string? ProfileId { get; set; } /// /// The status for the invoice to end up in. See the Mollie.Api.Models.SalesInvoice.SalesInvoiceStatus class for /// a full list of known values. /// public required string Status { get; set; } /// /// The VAT scheme to create the invoice for. You must be enrolled with One Stop Shop enabled to use it. See the /// Mollie.Api.Models.VatScheme class for a full list of known values. /// public string? VatScheme { get; set; } /// /// The VAT mode to use for VAT calculation. exclusive mode means we will apply the relevant VAT on top of the /// price. inclusive means the prices you are providing to us already contain the VAT you want to apply. See the /// Mollie.Api.Models.VatMode class for a full list of known values. /// public string? VatMode { get; set; } /// /// A free-form memo you can set on the invoice, and will be shown on the invoice PDF. /// public string? Memo { get; set; } /// /// Provide any data you like, for example a string or a JSON object. We will save the data alongside the payment. Whenever /// you fetch the payment with our API, we’ll also include the metadata. You can use up to approximately 1kB. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// The payment term to be set on the invoice. See the Mollie.Api.Models.SalesInvoice.PaymentTerm class for a full /// list of known values. /// public required string PaymentTerm { get; set; } /// /// Used when setting an invoice to status of paid, and will store a payment that fully pays the invoice with /// the provided details. Required for paid status. /// public PaymentDetails? PaymentDetails { get; set; } /// /// Used when setting an invoice to status of either issued or paid. Will be used to issue the invoice to the /// recipient with the provided subject and body. Required for issued status. /// public EmailDetails? EmailDetails { get; set; } /// /// The identifier referring to the customer you want to attempt an automated payment for. If provided, /// mandateId becomes required as well. Only allowed for invoices with status paid. /// public string? CustomerId { get; set; } /// /// The identifier referring to the mandate you want to use for the automated payment. If provided, customerId /// becomes required as well. Only allowed for invoices with status paid. /// public string? MandateId { get; set; } /// /// An identifier tied to the recipient data. This should be a unique value based on data your system contains, /// so that both you and us know who we're referring to. It is a value you provide to us so that recipient /// management is not required to send a first invoice to a recipient. /// public required string RecipientIdentifier { get; set; } /// /// The recipient details /// public required Recipient Recipient { get; set; } /// /// Provide the line items for the invoice. Each line contains details such as a description of the item ordered /// and its price. All lines must have the same currency as the invoice. /// public required IEnumerable? Lines { get; set; } /// /// The webhook URL where we will send invoice status updates to. The webhookUrl is optional, but without a webhook /// you will miss out on important status changes to your invoice. The webhookUrl must be reachable from Mollie's /// point of view, so you cannot use localhost. If you want to use webhook during development on localhost, you /// must use a tool like ngrok to have the webhooks delivered to your local machine. /// public string? WebhookUrl { get; set; } /// /// The discount to be applied to the entire invoice, possibly on top of the line item discounts. /// public Amount? Discount { get; set; } /// /// This indicates whether the invoice is an e-invoice. The default value is false and can't be changed after the /// invoice has been issued. When emailDetails is provided, an additional email is sent to the recipient. E-invoicing /// is only available for merchants based in Belgium, Germany, and the Netherlands, and only when the recipient is /// also located in one of these countries. /// public bool IsEInvoice { get; set; } public void SetMetadata(object metadataObj, JsonSerializerOptions? jsonSerializerOptions = null) { Metadata = JsonSerializer.Serialize(metadataObj, jsonSerializerOptions); } } ================================================ FILE: src/Mollie.Api/Models/SalesInvoice/Request/SalesInvoiceUpdateRequest.cs ================================================ using System.Collections.Generic; namespace Mollie.Api.Models.SalesInvoice.Request; public record SalesInvoiceUpdateRequest : ITestModeRequest { /// /// The status for the invoice to end up in. Dependent parameters: paymentDetails for paid, emailDetails for issued /// and paid. See the Mollie.Api.Models.SalesInvoice.SalesInvoiceStatus class for a full list of known values. /// public string? Status { get; set; } /// /// A free-form memo you can set on the invoice, and will be shown on the invoice PDF. /// public string? Memo { get; set; } /// /// The payment term to be set on the invoice. See the Mollie.Api.Models.SalesInvoice.PaymentTerm class for a full /// list of known values. /// public string? PaymentTerm { get; set; } /// /// Used when setting an invoice to status of paid, and will store a payment that fully pays the invoice with the /// provided details. Required for paid status. /// public PaymentDetails? PaymentDetails { get; set; } /// /// Used when setting an invoice to status of either issued or paid. Will be used to issue the invoice to the /// recipient with the provided subject and body. Required for issued status. /// public EmailDetails? EmailDetails { get; set; } /// /// An identifier tied to the recipient data. This should be a unique value based on data your system contains, /// so that both you and us know who we're referring to. It is a value you provide to us so that recipient /// management is not required to send a first invoice to a recipient. /// public string? RecipientIdentifier { get; set; } /// /// The recipient object should contain all the information relevant to create an invoice for an intended recipient. /// This data will be stored, updated, and re-used as appropriate, based on the recipientIdentifier. /// public Recipient? Recipient { get; set; } /// /// Provide the line items for the invoice. Each line contains details such as a description of the item ordered /// and its price. All lines must have the same currency as the invoice. /// public IEnumerable? Lines { get; set; } /// /// The webhook URL where we will send invoice status updates to. The webhookUrl is optional, but without a webhook /// you will miss out on important status changes to your invoice. The webhookUrl must be reachable from Mollie's /// point of view, so you cannot use localhost. If you want to use webhook during development on localhost, you /// must use a tool like ngrok to have the webhooks delivered to your local machine. /// public string? WebhookUrl { get; set; } /// /// The discount to be applied to the entire invoice, possibly on top of the line item discounts. /// public Amount? Discount { get; set; } /// /// Most API credentials are specifically created for either live mode or test mode. For organization-level /// credentials such as OAuth access tokens, you can enable test mode by setting testmode to true. /// public bool? Testmode { get; set; } } ================================================ FILE: src/Mollie.Api/Models/SalesInvoice/Response/SalesInvoiceResponse.cs ================================================ using System; using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; using Mollie.Api.JsonConverters; namespace Mollie.Api.Models.SalesInvoice.Response; public record SalesInvoiceResponse : IEntity { /// /// Indicates the response contains a sales invoice object. Will always contain the string sales-invoice for /// this endpoint. /// public required string Resource { get; set; } /// /// The identifier uniquely referring to this invoice. Example: invoice_4Y0eZitmBnQ6IDoMqZQKh. /// public required string Id { get; set; } /// /// The mode used to create this sales invoice /// public required Mode Mode { get; set; } /// /// When issued, an invoice number will be set for the sales invoice. /// public string? InvoiceNumber { get; set; } /// /// The identifier referring to the profile this entity belongs to. /// public string? ProfileId { get; set; } /// /// A three-character ISO 4217 currency code. /// public string? Currency { get; set; } /// /// The status for the invoice to end up in. See the Mollie.Api.Models.SalesInvoice.SalesInvoiceStatus class for /// a full list of known values. /// public required string Status { get; set; } /// /// The VAT scheme to create the invoice for. You must be enrolled with One Stop Shop enabled to use it. See the /// Mollie.Api.Models.VatScheme class for a full list of known values. /// public string? VatScheme { get; set; } /// /// The VAT mode to use for VAT calculation. exclusive mode means we will apply the relevant VAT on top of the /// price. inclusive means the prices you are providing to us already contain the VAT you want to apply. See the /// Mollie.Api.Models.VatMode class for a full list of known values. /// public string? VatMode { get; set; } /// /// A free-form memo you can set on the invoice, and will be shown on the invoice PDF. /// public string? Memo { get; set; } /// /// The optional metadata you provided upon payment creation. Metadata can be used to link an order to a payment. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// The payment term to be set on the invoice. See the Mollie.Api.Models.SalesInvoice.PaymentTerm class for a full /// list of known values. /// public string? PaymentTerm { get; set; } /// /// Used when setting an invoice to status of paid, and will store a payment that fully pays the invoice with /// the provided details. Required for paid status. /// public PaymentDetails? PaymentDetails { get; set; } /// /// Used when setting an invoice to status of either issued or paid. Will be used to issue the invoice to the /// recipient with the provided subject and body. Required for issued status. /// public EmailDetails? EmailDetails { get; set; } /// /// The identifier referring to the customer you want to attempt an automated payment for. If provided, /// mandateId becomes required as well. Only allowed for invoices with status paid. /// public string? CustomerId { get; set; } /// /// The identifier referring to the mandate you want to use for the automated payment. If provided, customerId /// becomes required as well. Only allowed for invoices with status paid. /// public string? MandateId { get; set; } /// /// An identifier tied to the recipient data. This should be a unique value based on data your system contains, /// so that both you and us know who we're referring to. It is a value you provide to us so that recipient /// management is not required to send a first invoice to a recipient. /// public required string RecipientIdentifier { get; set; } /// /// The recipient details /// public Recipient? Recipient { get; set; } /// /// Provide the line items for the invoice. Each line contains details such as a description of the item ordered /// and its price. All lines must have the same currency as the invoice. /// public required IEnumerable? Lines { get; set; } /// /// The webhook URL where we will send invoice status updates to. The webhookUrl is optional, but without a webhook /// you will miss out on important status changes to your invoice. The webhookUrl must be reachable from Mollie's /// point of view, so you cannot use localhost. If you want to use webhook during development on localhost, you /// must use a tool like ngrok to have the webhooks delivered to your local machine. /// public string? WebhookUrl { get; set; } /// /// The discount to be applied to the entire invoice, possibly on top of the line item discounts. /// public Amount? Discount { get; set; } /// /// This indicates whether the invoice is an e-invoice. The default value is false and can't be changed after the /// invoice has been issued. When emailDetails is provided, an additional email is sent to the recipient. E-invoicing /// is only available for merchants based in Belgium, Germany, and the Netherlands, and only when the recipient is /// also located in one of these countries. /// public bool IsEInvoice { get; set; } /// /// The amount that is left to be paid. /// public required Amount AmountDue { get; set; } /// /// The total amount without VAT. /// public required Amount SubtotalAmount { get; set; } /// /// The total amount with VAT. /// public required Amount TotalAmount { get; set; } /// /// The discounted subtotal amount without VAT. /// public required Amount DiscountedSubtotalAmount { get; set; } /// /// The entity's date and time of creation. /// public required DateTime CreatedAt { get; set; } /// /// If issued, the date when the sales invoice was issued. /// public DateTime? IssuedAt { get; set; } /// /// If issued, the date when the sales invoice payment is due. /// public DateTime? DueAt { get; set; } /// /// An object with several relevant URLs. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required SalesInvoiceResponseLinks Links { get; set; } public T? GetMetadata(JsonSerializerOptions? jsonSerializerOptions = null) { return Metadata != null ? JsonSerializer.Deserialize(Metadata, jsonSerializerOptions) : default; } } ================================================ FILE: src/Mollie.Api/Models/SalesInvoice/Response/SalesInvoiceResponseLinks.cs ================================================ using Mollie.Api.Models.Url; namespace Mollie.Api.Models.SalesInvoice.Response; public record SalesInvoiceResponseLinks { /// /// In v2 endpoints, URLs are commonly represented as objects with an href and type field. /// public required UrlObjectLink Self { get; set; } /// /// The URL your customer should visit to make payment for the invoice. This is where you should redirect the /// customer to unless the status is set to paid. /// public required UrlLink InvoicePayment { get; set; } /// /// The URL the invoice is available at, if generated. /// public UrlLink? PdfLink { get; set; } /// /// In v2 endpoints, URLs are commonly represented as objects with an href and type field. /// public required UrlLink Documentation { get; set; } } ================================================ FILE: src/Mollie.Api/Models/SalesInvoice/SalesInvoiceLine.cs ================================================ namespace Mollie.Api.Models.SalesInvoice; public record SalesInvoiceLine { /// /// A description of the line item. For example LEGO 4440 Forest Police Station. /// public required string Description { get; set; } /// /// The number of items. /// public required int Quantity { get; set; } /// /// The vat rate to be applied to this line item. /// public required string VatRate { get; set; } /// /// The price of a single item excluding VAT. /// public required Amount UnitPrice { get; set; } /// /// The discount to be applied to the line item. /// public Amount? Discount { get; set; } } ================================================ FILE: src/Mollie.Api/Models/SalesInvoice/SalesInvoiceStatus.cs ================================================ namespace Mollie.Api.Models.SalesInvoice; public static class SalesInvoiceStatus { public const string Draft = "draft"; public const string Issued = "issued"; public const string Paid = "paid"; } ================================================ FILE: src/Mollie.Api/Models/Session/Request/SessionRequest.cs ================================================ using System; using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; using Mollie.Api.JsonConverters; using Mollie.Api.Models.Payment; namespace Mollie.Api.Models.Session.Request { public record SessionRequest : ITestModeRequest, IProfileRequest { /// /// The amount that you want to charge, e.g. {currency:"EUR", value:"1000.00"} if you would want to charge €1000.00. /// public required Amount Amount { get; set; } /// /// The description of the session. /// public required string Description { get; set; } /// /// Required - The URL the consumer will be redirected to after the payment process. It could make sense for the /// redirectURL to contain a unique /// identifier – like your order ID – so you can show the right page referencing the order when the consumer returns. /// public required string RedirectUrl { get; set; } /// /// The URL your consumer will be redirected to when the consumer explicitly cancels the payment. If this URL is not /// provided, the consumer will be redirected to the redirectUrl instead — see above. /// Mollie will always give you status updates via webhooks, including for the canceled status. This parameter is /// therefore entirely optional, but can be useful when implementing a dedicated consumer-facing flow to handle payment /// cancellations. /// public string? CancelUrl { get; set; } /// /// Optionally provide the order lines for the payment. Each line contains details such as a description of the item ordered and its price. /// All lines must have the same currency as the payment. /// public List? Lines { get; set; } /// /// The customer's billing address details. We advise to provide these details to improve fraud protection and conversion. This is /// particularly relevant for card payments. /// public PaymentAddressDetails? BillingAddress { get; set; } /// /// The customer's shipping address details. We advise to provide these details to improve fraud protection and conversion. This is /// particularly relevant for card payments. /// public PaymentAddressDetails? ShippingAddress { get; set; } public SessionPaymentDetails? Payment { get; set; } /// /// The ID of the Customer for whom the payment is being created. This is used for recurring payments and single click payments. /// public string? CustomerId { get; set; } /// /// Indicate which type of payment this is in a recurring sequence. If set to first, a first payment is created for the /// customer, allowing the customer to agree to automatic recurring charges taking place on their account in the future. /// If set to recurring, the customer’s card is charged automatically. Defaults to oneoff, which is a regular non-recurring /// payment(see also: Recurring). See the Mollie.Api.Models.Payment.SequenceType class for a full list of known values. /// public string? SequenceType { get; set; } /// /// The website profile’s unique identifier, for example pfl_3RkSN1zuPE. /// public string? ProfileId { get; set; } /// /// Provide any data you like, and we will save the data alongside the session. Whenever you fetch the session /// with our API, we’ll also include the metadata. You can use up to 1kB of JSON. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// Oauth only - Optional – Set this to true to make this session a test session. /// public bool? Testmode { get; set; } public void SetMetadata(object metadataObj, JsonSerializerOptions? jsonSerializerOptions = null) { Metadata = JsonSerializer.Serialize(metadataObj, jsonSerializerOptions); } } } ================================================ FILE: src/Mollie.Api/Models/Session/Response/SessionResponse.cs ================================================ using System; using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; using Mollie.Api.JsonConverters; using Mollie.Api.Models.Payment; namespace Mollie.Api.Models.Session.Response { public record SessionResponse : IEntity { /// /// Indicates the response contains a session object. /// public required string Resource { get; set; } /// /// The session's unique identifier, for example sub_rVKGtNd6s3. /// public required string Id { get; set; } /// /// The mode used to create this session. Mode determines whether the payments are real or test payments. /// public Mode Mode { get; set; } /// /// The client access token for the session. /// public required string ClientAccessToken { get; set; } /// /// The session's current status, depends on whether the customer has a pending, valid or invalid mandate. /// See the Mollie.Api.Models.Session.SessionStatus class for a full list of known values. /// public required string Status { get; set; } /// /// The constant amount that is charged with each session payment. /// public required Amount Amount { get; set; } /// /// The description of the session. /// public string? Description { get; set; } /// /// The URL your customer will be redirected to after the payment process. /// It could make sense for the redirectUrl to contain a unique identifier – like your order ID – so you can show the right page referencing the order when your customer returns. /// public string? RedirectUrl { get; set; } /// /// The identifier referring to the profile this entity belongs to. /// public required string ProfileId { get; set; } /// /// The URL your customer will be redirected to when the customer explicitly cancels the payment. /// If this URL is not provided, the customer will be redirected to the redirectUrl instead — see above. /// public string? CancelUrl { get; set; } public SessionPaymentDetails? Payment { get; set; } /// /// Optionally provide the order lines for the payment. Each line contains details such as a description of the item ordered and its price. /// All lines must have the same currency as the payment. /// public List? Lines { get; set; } /// /// The customer's billing address details. We advise to provide these details to improve fraud protection and conversion. This is /// particularly relevant for card payments. /// public PaymentAddressDetails? BillingAddress { get; set; } /// /// The customer's shipping address details. We advise to provide these details to improve fraud protection and conversion. This is /// particularly relevant for card payments. /// public PaymentAddressDetails? ShippingAddress { get; set; } /// /// The customer this session belongs to. /// public required string CustomerId { get; set; } /// /// Indicates which type of payment this is in a recurring sequence. Set to first for first payments that allow the customer to agree /// to automatic recurring charges taking place on their account in the future. Set to recurring for payments where the customer’s card /// is charged automatically. See the Mollie.Api.Models.Payment.SequenceType class for a full list of known values. /// public required string SequenceType { get; set; } /// /// The optional metadata you provided upon session creation. Metadata can for example be used to link a plan to a /// session. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// An object with several URL objects relevant to the session. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required SessionResponseLinks Links { get; set; } public T? GetMetadata(JsonSerializerOptions? jsonSerializerOptions = null) { return Metadata != null ? JsonSerializer.Deserialize(Metadata, jsonSerializerOptions) : default; } } } ================================================ FILE: src/Mollie.Api/Models/Session/Response/SessionResponseLinks.cs ================================================ using Mollie.Api.Models.Customer.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Profile.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Session.Response { public record SessionResponseLinks { /// /// The API resource URL of the session itself. /// public required UrlObjectLink Self { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Session/Response/SessionStatus.cs ================================================ namespace Mollie.Api.Models.Session.Response { public static class SessionStatus { public const string Open = "open"; public const string Expired = "expired"; public const string Completed = "completed"; } } ================================================ FILE: src/Mollie.Api/Models/Session/SessionLine.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Mollie.Api.Models.Payment; namespace Mollie.Api.Models.Session; public record SessionLine : PaymentLine { /// /// The details of subsequent recurring billing cycles. These parameters are used in the Mollie Checkout to inform the shopper of the details for recurring products in the payments. /// public SessionLineRecurringDetails? Recurring { get; set; } } ================================================ FILE: src/Mollie.Api/Models/Session/SessionLineRecurringDetails.cs ================================================ using System; using System.Collections.Generic; using System.Text; using System.Text.Json.Serialization; using Mollie.Api.JsonConverters; using Mollie.Api.Models.Payment; namespace Mollie.Api.Models.Session; public record SessionLineRecurringDetails { /// /// A description of the recurring item. If not present, the main description of the item will be used. /// public string? Description { get; set; } /// /// Cadence unit of the recurring item. For example: 12 months, 52 weeks or 365 days. /// public required string Interval { get; set; } /// /// Total amount and currency of the recurring item. /// public required Amount Amount { get; set; } /// /// Optional – Total number of charges for the subscription to complete. Leave empty for ongoing subscription. /// public int? Times { get; set; } /// /// Optional – The start date of the subscription if it does not start right away (format YYYY-MM-DD) /// [JsonConverter(typeof(DateJsonConverter))] public DateTime? StartDate { get; set; } } ================================================ FILE: src/Mollie.Api/Models/Session/SessionPaymentDetails.cs ================================================ using System; using System.Text.Json.Serialization; using Mollie.Api.JsonConverters; namespace Mollie.Api.Models.Session; public record SessionPaymentDetails { /// /// The webhook URL where we will send payment status updates to. /// This URL will be automatically set as the webhook URL for all payments created for this session. /// public string? WebhookUrl { get; set; } } ================================================ FILE: src/Mollie.Api/Models/Settlement/Response/SettlementPeriod.cs ================================================ using System.Collections.Generic; namespace Mollie.Api.Models.Settlement.Response { public record SettlementPeriod { /// /// The total revenue for each payment method during this period. /// public required List Revenue { get; set; } /// /// The fees withheld for each payment method during this period. /// public required List Costs { get; set; } /// /// The ID of the invoice that was created to invoice specifically the costs in this month/period. /// If an individual month/period has not been invoiced yet, then this field will not be present until /// that invoice is created. /// public string? InvoiceId { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Settlement/Response/SettlementPeriodCosts.cs ================================================ namespace Mollie.Api.Models.Settlement.Response { public record SettlementPeriodCosts { /// /// A description of the subtotal. /// public required string Description { get; set; } /// /// The net total of received funds for this payment method (excludes VAT). /// public required Amount AmountNet { get; set; } /// /// The VAT amount applicable to the revenue. /// public required Amount AmountVat { get; set; } /// /// The gross total of received funds for this payment method (includes VAT). /// public required Amount AmountGross { get; set; } /// /// The number of payments received for this payment method. /// public required int Count { get; set; } /// /// The service rates, further divided into fixed and variable costs. /// public required SettlementPeriodCostsRate Rate { get; set; } /// /// The payment method ID, if applicable - See the Mollie.Api.Models.Payment.PaymentMethod /// class for a full list of known values. /// public string? Method { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Settlement/Response/SettlementPeriodCostsRate.cs ================================================ namespace Mollie.Api.Models.Settlement.Response { public record SettlementPeriodCostsRate { /// /// An amount object describing the fixed costs. /// public required Amount Fixed { get; set; } /// /// A string describing the variable costs as a percentage. /// public required string Percentage { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Settlement/Response/SettlementPeriodRevenue.cs ================================================ namespace Mollie.Api.Models.Settlement.Response { public record SettlementPeriodRevenue { /// /// A description of the subtotal. /// public required string Description { get; set; } /// /// The net total of received funds for this payment method (excludes VAT). /// public required Amount AmountNet { get; set; } /// /// The VAT amount applicable to the revenue. /// public required Amount AmountVat { get; set; } /// /// The gross total of received funds for this payment method (includes VAT). /// public required Amount AmountGross { get; set; } /// /// The number of times costs were made for this payment method. /// public int Count { get; set; } /// /// The payment method ID, if applicable - See the Mollie.Api.Models.Payment.PaymentMethod /// class for a full list of known values. /// public string? Method { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Settlement/Response/SettlementResponse.cs ================================================ using System; using System.Collections.Generic; using Mollie.Api.JsonConverters; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Settlement.Response { public record SettlementResponse : IEntity { /// /// Indicates the response contains a settlement object. Will always contain settlement for this endpoint. /// public required string Resource { get; set; } /// /// The settlement's identifier, for example stl_jDk30akdN. /// public required string Id { get; set; } /// /// The settlement's bank reference, as found on your invoice and in your Mollie account. /// public required string Reference { get; set; } /// /// The date on which the settlement was created. /// When requesting the next settlement the returned date signifies the expected settlement date. /// public required DateTime CreatedAt { get; set; } /// /// The date on which the settlement was settled. /// When requesting the open settlement or next settlement the return value is null. /// public DateTime? SettledAt { get; set; } /// /// The status of the settlement - See the Mollie.Api.Models.Settlement.SettlementStatus /// class for a full list of known values. /// public required string Status { get; set; } /// /// The total amount paid out with this settlement. /// public required Amount Amount { get; set; } /// /// This object is a collection of Period objects, which describe the settlement by month in full detail. /// Please refer to the Period object section below. /// [JsonConverter(typeof(SettlementPeriodConverter))] public required Dictionary> Periods { get; set; } /// /// An object with several URL objects relevant to the settlement. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required SettlementResponseLinks Links { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Settlement/Response/SettlementResponseLinks.cs ================================================ using Mollie.Api.Models.Chargeback.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Refund.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Settlement.Response { public record SettlementResponseLinks { /// /// The API resource URL of the settlement itself. /// public required UrlObjectLink Self { get; set; } /// /// The API resource URL of the payments that are included in this settlement. /// public required UrlObjectLink> Payments { get; set; } /// /// The API resource URL of the refunds that are included in this settlement. /// public required UrlObjectLink> Refunds { get; set; } /// /// The API resource URL of the chargebacks that are included in this settlement. /// public required UrlObjectLink> Chargebacks { get; set; } /// /// The URL to the settlement retrieval endpoint documentation. /// public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Settlement/Response/SettlementStatus.cs ================================================ namespace Mollie.Api.Models.Settlement.Response { public static class SettlementStatus { public const string Open = "open"; public const string Pending = "pending"; public const string PaidOut = "paidout"; public const string Failed = "failed"; } } ================================================ FILE: src/Mollie.Api/Models/Shipment/Request/ShipmentLineRequest.cs ================================================ namespace Mollie.Api.Models.Shipment.Request { public record ShipmentLineRequest { /// /// The API resource token of the order line, for example: odl_jp31jz. /// public required string Id { get; set; } /// /// The number of items that should be shipped for this order line. /// public int? Quantity { get; set; } /// /// The amount that you want to ship. In almost all cases, Mollie can determine the amount automatically. /// public Amount? Amount { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Shipment/Request/ShipmentRequest.cs ================================================ using System.Collections.Generic; namespace Mollie.Api.Models.Shipment.Request { public record ShipmentRequest : ITestModeRequest { /// /// The total amount of the order, including VAT and discounts. This is the amount that will be charged /// to your customer. /// public TrackingObject? Tracking { get; set; } /// /// The lines in the order. Each line contains details such as a description of the item ordered, its /// price et cetera. If you send an empty array, the entire order will be shipped /// public IEnumerable? Lines { get; set; } /// /// Oauth only - Optional – Set this to true to make this shipment a test shipment. /// public bool? Testmode { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Shipment/Request/ShipmentUpdateRequest.cs ================================================ namespace Mollie.Api.Models.Shipment.Request { public record ShipmentUpdateRequest : ITestModeRequest { public required TrackingObject Tracking { get; set; } /// /// Oauth only - Optional – Set this to true to make this shipment a test shipment. /// public bool? Testmode { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Shipment/Response/ShipmentResponse.cs ================================================ using System; using System.Collections.Generic; using System.Text.Json; using Mollie.Api.JsonConverters; using Mollie.Api.Models.Order.Response; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Shipment.Response { public record ShipmentResponse : IEntity { /// /// Indicates the response contains a shipment object. Will always contain shipment for this endpoint. /// public required string Resource { get; set; } /// /// The shipment’s unique identifier, for example shp_3wmsgCJN4U. /// public required string Id { get; set; } /// /// The order this shipment was created on, for example ord_8wmqcHMN4U. /// public required string OrderId { get; set; } /// /// The shipment’s date and time of creation, in ISO 8601 format. /// public required DateTime CreatedAt { get; set; } /// /// An object containing shipment tracking details. Will be omitted when no tracking details are available. /// public TrackingObject? Tracking { get; set; } /// /// The optional metadata you provided upon subscription creation. Metadata can for example be used to link a plan to a /// subscription. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// An array of order line objects /// public required IEnumerable Lines { get; set; } /// /// An object with several URL objects relevant to the order. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required ShipmentResponseLinks Links { get; set; } public T? GetMetadata(JsonSerializerOptions? jsonSerializerOptions = null) { return Metadata != null ? JsonSerializer.Deserialize(Metadata, jsonSerializerOptions) : default; } } } ================================================ FILE: src/Mollie.Api/Models/Shipment/Response/ShipmentResponseLinks.cs ================================================ using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Shipment.Response { public record ShipmentResponseLinks { /// /// The API resource URL of the order itself. /// public required UrlObjectLink Self { get; set; } /// /// The URL your customer should visit to make the payment for the order. /// This is where you should redirect the customer to after creating the order. /// public required UrlLink Checkout { get; set; } /// /// The URL to the order retrieval endpoint documentation. /// public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Shipment/TrackingObject.cs ================================================ namespace Mollie.Api.Models.Shipment{ public record TrackingObject { /// /// Name of the postal carrier (as specific as possible). For example PostNL. /// public required string Carrier { get; set; } /// /// The track and trace code of the shipment. For example 3SKABA000000000. /// public required string Code { get; set; } /// /// The URL where your customer can track the shipment /// public string? Url { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/SortDirection.cs ================================================ using System.Runtime.Serialization; namespace Mollie.Api.Models { public enum SortDirection { [EnumMember(Value = "desc")] Desc, [EnumMember(Value = "asc")] Asc } } ================================================ FILE: src/Mollie.Api/Models/StatusReason.cs ================================================ namespace Mollie.Api.Models; public record StatusReason { /// /// A machine-readable code that indicates the reason for the status. /// public required string Code { get; set; } /// /// A machine-readable code that indicates the reason for the status. /// public required string Message { get; set; } } ================================================ FILE: src/Mollie.Api/Models/Subscription/Request/SubscriptionRequest.cs ================================================ using System; using System.Text.Json; using System.Text.Json.Serialization; using Mollie.Api.JsonConverters; namespace Mollie.Api.Models.Subscription.Request { public record SubscriptionRequest : ITestModeRequest, IProfileRequest { /// /// The constant amount in EURO that you want to charge with each subscription payment, e.g. 100.00 if you would want /// to charge € 100,00. /// public required Amount Amount { get; set; } /// /// Optional – Total number of charges for the subscription to complete. Leave empty for an on-going subscription. /// public int? Times { get; set; } /// /// Interval to wait between charges, for example 1 month or 14 days. /// public required string Interval { get; set; } /// /// Optional – The start date of the subscription in yyyy-mm-dd format. This is the first day on which your customer /// will be charged. When /// this parameter is not provided, the current date will be used instead. /// [JsonConverter(typeof(DateJsonConverter))] public DateTime? StartDate { get; set; } /// /// A description unique per customer. This will be included in the payment description along with the charge date in /// Y-m-d format. /// public required string Description { get; set; } /// /// Optional – The payment method used for this subscription, either forced on creation or null if any of the /// customer's valid mandates may be used. See the Mollie.Api.Models.Payment.PaymentMethod class for a full /// list of known values. /// public string? Method { get; set; } /// /// The mandate used for this subscription. Please note that this parameter can not set together with method. /// public string? MandateId { get; set; } /// /// Optional – Use this parameter to set a webhook URL for all subscription payments. /// public string? WebhookUrl { get; set; } /// /// Adding an application fee allows you to charge the merchant for each payment in the subscription and /// transfer these amounts to your own account. /// public ApplicationFee? ApplicationFee { get; set; } /// /// The website profile’s unique identifier, for example pfl_3RkSN1zuPE. /// public string? ProfileId { get; set; } /// /// Provide any data you like, and we will save the data alongside the subscription. Whenever you fetch the subscription /// with our API, we’ll also include the metadata. You can use up to 1kB of JSON. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// Oauth only - Optional – Set this to true to make this subscription a test subscription. /// public bool? Testmode { get; set; } public void SetMetadata(object metadataObj, JsonSerializerOptions? jsonSerializerOptions = null) { Metadata = JsonSerializer.Serialize(metadataObj, jsonSerializerOptions); } } } ================================================ FILE: src/Mollie.Api/Models/Subscription/Request/SubscriptionUpdateRequest.cs ================================================ using System; using System.Text.Json; using System.Text.Json.Serialization; using Mollie.Api.JsonConverters; namespace Mollie.Api.Models.Subscription.Request { public record SubscriptionUpdateRequest : ITestModeRequest { /// /// The constant amount in EURO that you want to charge with each subscription payment, e.g. 100.00 if you would want /// to charge € 100,00. /// public Amount? Amount { get; set; } /// /// Optional – Total number of charges for the subscription to complete. Leave empty for an on-going subscription. /// public int? Times { get; set; } /// /// Optional – The start date of the subscription in yyyy-mm-dd format. This is the first day on which your customer /// will be charged. When /// this parameter is not provided, the current date will be used instead. /// [JsonConverter(typeof(DateJsonConverter))] public DateTime? StartDate { get; set; } /// /// A description unique per customer. This will be included in the payment description along with the charge date in /// Y-m-d format. /// public string? Description { get; set; } /// /// Interval to wait between charges, for example 1 month or 14 days. /// public string? Interval { get; set; } /// /// Optional – Use this parameter to set a webhook URL for all subscription payments. /// public string? WebhookUrl { get; set; } /// /// Provide any data you like, and we will save the data alongside the subscription. Whenever you fetch the subscription /// with our API, we’ll also include the metadata. You can use up to 1kB of JSON. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// The mandate used for this subscription. Please note that this parameter can not set together with method. /// public string? MandateId { get; set; } /// /// Oauth only - Optional – Set this to true to make this subscription a test subscription. /// public bool? Testmode { get; set; } public void SetMetadata(object metadataObj, JsonSerializerOptions? jsonSerializerOptions = null) { Metadata = JsonSerializer.Serialize(metadataObj, jsonSerializerOptions); } } } ================================================ FILE: src/Mollie.Api/Models/Subscription/Response/SubscriptionResponse.cs ================================================ using System; using System.Text.Json; using Mollie.Api.JsonConverters; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Subscription.Response { public record SubscriptionResponse : IEntity { /// /// Indicates the response contains a subscription object. /// public required string Resource { get; set; } /// /// The subscription's unique identifier, for example sub_rVKGtNd6s3. /// public required string Id { get; set; } /// /// The mode used to create this subscription. Mode determines whether the payments are real or test payments. /// public Mode Mode { get; set; } /// /// The subscription's date and time of creation, in ISO 8601 format. /// public required DateTime CreatedAt { get; set; } /// /// The subscription's current status, depends on whether the customer has a pending, valid or invalid mandate. /// See the Mollie.Api.Models.Subscription.SubscriptionStatus class for a full list of known values. /// public required string Status { get; set; } /// /// The constant amount that is charged with each subscription payment. /// public required Amount Amount { get; set; } /// /// Total number of charges for the subscription to complete. /// public int? Times { get; set; } /// /// Number of charges left for the subscription to complete. /// public int? TimesRemaining { get; set; } /// /// Interval to wait between charges like 1 month(s) or 14 days. /// public required string Interval { get; set; } /// /// The start date of the subscription in yyyy-mm-dd format. /// public DateTime? StartDate { get; set; } /// /// The date of the next scheduled payment in YYYY-MM-DD format. When there will be no next payment, for example /// when the subscription has ended, this parameter will not be returned. /// public DateTime? NextPaymentDate { get; set; } /// /// A description unique per customer. This will be included in the payment description along with the charge date in /// Y-m-d format. /// public required string Description { get; set; } /// /// The payment method used for this subscription, either forced on creation by specifying the method parameter, or /// null if any of the customer's valid mandates may be used. See the Mollie.Api.Models.Payment.PaymentMethod class /// for a full list of known values. /// public string? Method { get; set; } /// /// The mandate used for this subscription. Please note that this parameter can not set together with method. /// public string? MandateId { get; set; } /// /// The subscription's date of cancellation, in ISO 8601 format. /// public DateTime? CanceledAt { get; set; } /// /// The URL Mollie will call as soon a payment status change takes place. /// public string? WebhookUrl { get; set; } /// /// The customer this subscription belongs to. /// public required string CustomerId { get; set; } /// /// The optional metadata you provided upon subscription creation. Metadata can for example be used to link a plan to a /// subscription. /// [JsonConverter(typeof(RawJsonConverter))] public string? Metadata { get; set; } /// /// An object with several URL objects relevant to the subscription. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required SubscriptionResponseLinks Links { get; set; } /// /// Adding an application fee allows you to charge the merchant for each payment in the subscription and /// transfer these amounts to your own account. /// public ApplicationFee? ApplicationFee { get; set; } public T? GetMetadata(JsonSerializerOptions? jsonSerializerOptions = null) { return Metadata != null ? JsonSerializer.Deserialize(Metadata, jsonSerializerOptions) : default; } } } ================================================ FILE: src/Mollie.Api/Models/Subscription/Response/SubscriptionResponseLinks.cs ================================================ using Mollie.Api.Models.Customer.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Profile.Response; using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Subscription.Response { public record SubscriptionResponseLinks { /// /// The API resource URL of the subscription itself. /// public required UrlObjectLink Self { get; set; } /// /// The API resource URL of the customer the subscription is for. /// public required UrlObjectLink Customer { get; set; } /// /// The API resource URL of the payments that are created by this subscription. Not present /// if no payments yet created. /// public UrlObjectLink>? Payments { get; set; } /// /// The API resource URL of the website profile on which this subscription was created. /// public required UrlObjectLink Profile { get; set; } /// /// The URL to the subscription retrieval endpoint documentation. /// public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Subscription/Response/SubscriptionStatus.cs ================================================ namespace Mollie.Api.Models.Subscription.Response { public static class SubscriptionStatus { public const string Pending = "pending"; public const string Active = "active"; public const string Canceled = "canceled"; public const string Suspended = "suspended"; public const string Completed = "completed"; } } ================================================ FILE: src/Mollie.Api/Models/Terminal/Response/TerminalResponse.cs ================================================ using System; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Terminal.Response { /// /// Full documentation for this class can be found at https://docs.mollie.com/reference/v2/terminals-api/overview /// public record TerminalResponse : IEntity { /// /// Indicates the response contains a terminal object. Will always contain the string terminal for this endpoint. /// public required string Resource { get; set; } /// /// The unique identifier used for referring to a terminal. Mollie assigns this identifier at terminal creation time. /// For example term_7MgL4wea46qkRcoTZjWEH. This ID will be used by Mollie to refer to a certain terminal and will be used for assigning a payment to a specific terminal. /// public required string Id { get; set; } /// /// Whether this entity was created in live mode or in test mode. /// public required Mode Mode { get; set; } /// /// The identifier used for referring to the profile the terminal was created on. For example, pfl_QkEhN94Ba. /// public required string ProfileId { get; set; } /// /// The status of the terminal, which is a read-only value determined by Mollie, according to the actions performed for that terminal. /// Its values can be pending, active, inactive. pending means that the terminal has been created but not yet active. /// active means that the terminal is active and can take payments. inactive means that the terminal has been deactivated. /// public required string Status { get; set; } /// /// The brand of the terminal. /// public required string Brand { get; set; } /// /// The model of the terminal. /// public required string Model { get; set; } /// /// The serial number of the terminal. The serial number is provided at terminal creation time. /// public required string SerialNumber { get; set; } /// /// An ISO 4217 currency code. The currencies supported depend on the payment methods that are enabled on your account. /// public required string Currency { get; set; } /// /// The full name of the payment terminal. /// public required string Description { get; set; } /// /// The Terminal's date and time of creation, in ISO 8601 format. /// public DateTime CreatedAt { get; set; } /// /// The Terminal's date and time of creation, in ISO 8601 format. /// public DateTime? UpdatedAt { get; set; } /// /// An object with several URL objects relevant to the payment method. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required TerminalResponseLinks Links { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Terminal/Response/TerminalResponseLinks.cs ================================================ using Mollie.Api.Models.Url; namespace Mollie.Api.Models.Terminal.Response { /// /// This Sublass is part of the TerminalResponse Class, Full documentation for this class can be found at https://docs.mollie.com/reference/v2/terminals-api/overview /// public record TerminalResponseLinks { /// /// The API resource URL of the payment method itself. /// public required UrlObjectLink Self { get; set; } /// /// The URL to the payment method retrieval endpoint documentation. /// public required UrlLink Documentation { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/TestmodeModel.cs ================================================ namespace Mollie.Api.Models { public record TestmodeModel { public static TestmodeModel? Create(bool testmode) { return testmode ? new TestmodeModel { Testmode = testmode } : null; } /// /// Oauth only - Optional /// public bool? Testmode { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Url/UrlLink.cs ================================================ namespace Mollie.Api.Models.Url { public record UrlLink { public required string Href { get; set; } public required string Type { get; set; } public override string ToString() { return $"{Type} - {Href}"; } } } ================================================ FILE: src/Mollie.Api/Models/Url/UrlObjectLink.cs ================================================ namespace Mollie.Api.Models.Url { // ReSharper disable once UnusedTypeParameter public record UrlObjectLink : UrlLink; } ================================================ FILE: src/Mollie.Api/Models/VatMode.cs ================================================ namespace Mollie.Api.Models; /// /// The VAT mode to use for VAT calculation. /// public static class VatMode { /// /// Inclusive means the prices you are providing to us already contain the VAT you want to apply. /// public const string Inclusive = "inclusive"; /// /// Exclusive mode means we will apply the relevant VAT on top of the price. /// public const string Exclusive = "exclusive"; } ================================================ FILE: src/Mollie.Api/Models/VatScheme.cs ================================================ namespace Mollie.Api.Models; public static class VatScheme { public const string Standard = "standard"; public const string OneStopShop = "one-stop-shop"; } ================================================ FILE: src/Mollie.Api/Models/VoucherCategory.cs ================================================ namespace Mollie.Api.Models; public static class VoucherCategory { public const string Gift = "gift"; public const string Eco = "eco"; public const string Meal = "meal"; public const string SportCulture = "sport_culture"; } ================================================ FILE: src/Mollie.Api/Models/Wallet/Request/ApplePayPaymentSessionRequest.cs ================================================ namespace Mollie.Api.Models.Wallet.Request { public record ApplePayPaymentSessionRequest { /// /// The validationUrl you got from the ApplePayValidateMerchant event. /// A list of all valid host names for merchant validation is available. You should white list these in your /// application and reject any validationUrl that have a host name not in the list. /// public required string ValidationUrl { get; set; } /// /// The domain of your web shop, that is visible in the browser’s location bar. For example pay.myshop.com. /// public required string Domain { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Wallet/Response/ApplePayPaymentSessionResponse.cs ================================================ using System; using Mollie.Api.JsonConverters; using System.Text.Json.Serialization; namespace Mollie.Api.Models.Wallet.Response { public record ApplePayPaymentSessionResponse { [JsonConverter(typeof(MicrosecondEpochConverter))] public required DateTime EpochTimestamp { get; set; } [JsonConverter(typeof(MicrosecondEpochConverter))] public required DateTime ExpiresAt { get; set; } public required string MerchantSessionIdentifier { get; set; } public required string Nonce { get; set; } public required string MerchantIdentifier { get; set; } public required string DomainName { get; set; } public required string DisplayName { get; set; } public required string Signature { get; set; } } } ================================================ FILE: src/Mollie.Api/Models/Webhook/Request/WebhookRequest.cs ================================================ using System.Collections.Generic; using System.Text.Json.Serialization; using Mollie.Api.JsonConverters; namespace Mollie.Api.Models.Webhook.Request; public record WebhookRequest : ITestModeRequest { /// /// A name that identifies the webhook. /// public required string Name { get; set; } /// /// The URL Mollie will send the events to. This URL must be publicly accessible. /// public required string Url { get; set; } /// /// The list of events to enable for this webhook. You may specify '*' to add all events, except those that require /// explicit selection. Separate multiple event types with a comma. See the Mollie.Api.Models.Webhook.WebhookEventTypes /// class for a full list of known values /// [JsonConverter(typeof(CollectionToCommaSeparatedListConverter))] public required IList EventTypes { get; set; } /// /// Whether to create the entity in test mode or live mode. Most API credentials are specifically created for /// either live mode or test mode, in which case this parameter can be omitted. For organization-level credentials /// such as OAuth access tokens, you can enable test mode by setting testmode to true. /// public bool? Testmode { get; set; } } ================================================ FILE: src/Mollie.Api/Models/Webhook/Response/WebhookResponse.cs ================================================ using System; using System.Collections.Generic; namespace Mollie.Api.Models.Webhook.Response; public record WebhookResponse : IEntity { /// /// Indicates the response contains a webhook subscription object. Will always contain the string webhook for this /// endpoint. /// public required string Resource { get; set; } /// /// The identifier uniquely referring to this subscription. /// public required string Id { get; set; } /// /// The subscription's events destination. /// public required string Url { get; set; } /// /// The identifier uniquely referring to the profile that created the subscription. /// public required string ProfileId { get; set; } /// /// The subscription's date time of creation. /// public required DateTime CreatedAt { get; set; } /// /// The subscription's name. /// public required string Name { get; set; } /// /// The events types that are subscribed. See the Mollie.Api.Models.Webhook.WebhookEventTypes class for a full /// list of known values /// public required IEnumerable EventTypes { get; set; } /// /// The subscription's current status. /// public required string Status { get; set; } /// /// The subscription's mode. /// public required Mode Mode { get; set; } } ================================================ FILE: src/Mollie.Api/Models/Webhook/WebhookEventTypes.cs ================================================ namespace Mollie.Api.Models.Webhook; public static class WebhookEventTypes { /// /// A payment link has been paid. /// public const string PaymentLinkPaid = "payment-link.paid"; /// /// A sales invoice has been created. /// public const string SalesInvoiceCreated = "sales-invoice.created"; /// /// A sales invoice has been issued. /// public const string SalesInvoiceIssued = "sales-invoice.issued"; /// /// A sales invoice has been canceled. /// public const string SalesInvoiceCanceled = "sales-invoice.canceled"; /// /// A sales invoice has been paid. /// public const string SalesInvoicePaid = "sales-invoice.paid"; /// /// All event types /// public const string All = "*"; } ================================================ FILE: src/Mollie.Api/Models/WebhookEvent/Response/FullWebhookEventResponse.cs ================================================ using System.Text.Json.Serialization; using Mollie.Api.JsonConverters; namespace Mollie.Api.Models.WebhookEvent.Response; public record FullWebhookEventResponse : SimpleWebhookEventResponse where T : IEntity { /// /// Full payload of the event. /// [JsonConverter(typeof(WebhookEventEntityJsonConverter))] [JsonPropertyName("_embedded")] public required T Entity { get; set; } } public record FullWebhookEventResponse : SimpleWebhookEventResponse { /// /// Full payload of the event. /// [JsonConverter(typeof(WebhookEventEntityJsonConverter))] [JsonPropertyName("_embedded")] public required IEntity Entity { get; set; } } ================================================ FILE: src/Mollie.Api/Models/WebhookEvent/Response/SimpleWebhookEventResponse.cs ================================================ using System; using System.Text.Json.Serialization; namespace Mollie.Api.Models.WebhookEvent.Response; public record SimpleWebhookEventResponse : IEntity { /// /// Indicates the response contains a webhook event object. Will always contain the string event for this endpoint. /// public required string Resource { get; set; } /// /// The identifier uniquely referring to this event. /// public required string Id { get; set; } /// /// The event's type. /// public required string Type { get; set; } /// /// The entity token that triggered the event /// public required string EntityId { get; set; } /// /// The event's date time of creation. /// public required DateTime CreatedAt { get; set; } /// /// An object with several relevant URLs. Every URL object will contain an href and a type field. /// [JsonPropertyName("_links")] public required WebhookEventResponseLinks Links { get; set; } } ================================================ FILE: src/Mollie.Api/Models/WebhookEvent/Response/WebhookEventResponseLinks.cs ================================================ using Mollie.Api.Models.Url; namespace Mollie.Api.Models.WebhookEvent.Response; /// /// An object with several relevant URLs. Every URL object will contain an href and a type field. /// public record WebhookEventResponseLinks { /// /// In v2 endpoints, URLs are commonly represented as objects with an href and type field. /// public required UrlObjectLink Self { get; set; } /// /// In v2 endpoints, URLs are commonly represented as objects with an href and type field. /// public required UrlLink Documentation { get; set; } /// /// The API resource URL of the entity that this event belongs to. /// public required UrlObjectLink Entity { get; set; } } ================================================ FILE: src/Mollie.Api/Mollie.Api.csproj ================================================  netstandard2.0;net8.0;net10.0 enable 12 True true This is a wrapper for the Mollie REST webservice. All payment methods and webservice calls are supported. runtime; build; native; contentfiles; analyzers; buildtransitive all all runtime; build; native; contentfiles; analyzers; buildtransitive <_Parameter1>Mollie.Tests.Unit <_Parameter1>Mollie.Tests.Integration <_Parameter1>Mollie.Api.AspNet <_Parameter1>Mollie.Tests.Unit, PublicKey=$(MollieStrongNamePublicKey) <_Parameter1>Mollie.Tests.Integration, PublicKey=$(MollieStrongNamePublicKey) <_Parameter1>Mollie.Api.AspNet, PublicKey=$(MollieStrongNamePublicKey) ================================================ FILE: src/Mollie.Api/Options/MollieClientOptions.cs ================================================ using Mollie.Api.Client; namespace Mollie.Api.Options; public class MollieClientOptions { /// /// Your API-key or OAuth token /// public required string ApiKey { get; set; } = string.Empty; /// /// (Optional) The default user agent is "Mollie.Api.NET {version}". When this property is set, the custom user /// agent will be appended to the default user agent. /// public string? CustomUserAgent { get; set; } /// /// (Optional) Enable test mode for all requests /// public bool? Testmode { get; set; } /// /// (Optional) The profile ID to be used for all requests /// public string? ProfileId { get; set; } /// /// (Optional) ClientId used by Connect API /// public string? ClientId { get; set; } /// /// (Optional) ClientSecret used by Connect API /// /// public string? ClientSecret { get; set; } /// /// The base URL for all API requests. Can be overridden for testing purposes. /// public string ApiBaseUrl { get; set; } = BaseMollieClient.DefaultBaseApiEndPoint; /// /// The authorize endpoint for the Connect client. Can be overridden for testing purposes. /// public string ConnectOAuthAuthorizeEndPoint { get; set; } = ConnectClient.DefaultAuthorizeEndpoint; /// /// The token endpoint for the Connect client. Can be overridden for testing purposes. /// public string ConnectTokenEndPoint { get; set; } = ConnectClient.DefaultTokenEndpoint; } ================================================ FILE: src/Mollie.Api/Options/MollieOptions.cs ================================================ using System; using System.Net.Http; using Mollie.Api.Client; using Mollie.Api.Framework.Authentication.Abstract; using Polly; namespace Mollie.Api.Options { public record MollieOptions { /// /// Your API-key or OAuth token /// public string ApiKey { get; set; } = string.Empty; /// /// (Optional) ClientId used by Connect API /// public string? ClientId { get; set; } = string.Empty; /// /// (Optional) ClientSecret used by Connect API /// /// public string? ClientSecret { get; set; } = string.Empty; /// /// The base URL for all API requests. Can be overridden for testing purposes. /// public string ApiBaseUrl { get; set; } = BaseMollieClient.DefaultBaseApiEndPoint; /// /// The authorize endpoint for the Connect client. Can be overridden for testing purposes. /// public string ConnectOAuthAuthorizeEndPoint { get; set; } = ConnectClient.DefaultAuthorizeEndpoint; /// /// The token endpoint for the Connect client. Can be overridden for testing purposes. /// public string ConnectTokenEndPoint { get; set; } = ConnectClient.DefaultTokenEndpoint; /// /// (Optional) Polly retry policy for failed requests /// public IAsyncPolicy? RetryPolicy { get; set; } /// /// (Optional) The default user agent is "Mollie.Api.NET {version}". When this property is set, the custom user /// agent will be appended to the default user agent. /// public string? CustomUserAgent { get; set; } /// /// (Optional) Enable test mode for all requests /// public bool? Testmode { get; set; } /// /// (Optional) The profile ID to be used for all requests /// public string? ProfileId { get; set; } /// /// (Optional) A custom secret manager that you can override to implement advanced multi-tenant scenario's /// public Type? CustomMollieSecretManager { get; private set; } /// /// Set a custom secret manager that you can override to implement advanced multi-tenant scenario's /// public MollieOptions SetCustomMollieSecretManager() where T : IMollieSecretManager { CustomMollieSecretManager = typeof(T); return this; } } } ================================================ FILE: src/Mollie.Api.AspNet/DependencyInjection.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Mollie.Api.AspNet.Webhooks.Authorization; using Mollie.Api.AspNet.Webhooks.Options; namespace Mollie.Api.AspNet; public static class DependencyInjection { public static IServiceCollection AddMollieWebhook( this IServiceCollection services, Action optionsDelegate) { MollieWebhookOptions options = new(); optionsDelegate.Invoke(options); services.AddSingleton(options); services.AddSingleton(); #if NET7_0_OR_GREATER services.AddScoped(); #endif services.AddScoped(); return services; } } ================================================ FILE: src/Mollie.Api.AspNet/Mollie.Api.AspNet.csproj ================================================ net6.0;net8.0;net10.0 enable enable 11 True true ASP.NET Core extensions for the Mollie.Api client. Provides ready-to-use model binders, filters, and middleware to securely process Mollie webhooks and integrate payments into your ASP.NET Core MVC or Minimal API applications. ================================================ FILE: src/Mollie.Api.AspNet/Webhooks/Authorization/MollieSignatureEndpointFilter.cs ================================================ using Microsoft.AspNetCore.Http; namespace Mollie.Api.AspNet.Webhooks.Authorization; #if NET7_0_OR_GREATER public class MollieSignatureEndpointFilter : IEndpointFilter { private readonly MollieSignatureValidator _signatureValidator; public MollieSignatureEndpointFilter(MollieSignatureValidator signatureValidator) { _signatureValidator = signatureValidator; } public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) { var request = context.HttpContext.Request; bool isValid = await _signatureValidator.Validate(request); if (!isValid) { return Results.Unauthorized(); } return await next(context); } } #endif ================================================ FILE: src/Mollie.Api.AspNet/Webhooks/Authorization/MollieSignatureFilter.cs ================================================ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; namespace Mollie.Api.AspNet.Webhooks.Authorization; public class MollieSignatureFilter : IAsyncAuthorizationFilter { private readonly MollieSignatureValidator _signatureValidator; public MollieSignatureFilter(MollieSignatureValidator signatureValidator) { _signatureValidator = signatureValidator; } public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { var request = context.HttpContext.Request; bool isValid = await _signatureValidator.Validate(request); if (!isValid) { context.Result = new UnauthorizedResult(); } } } ================================================ FILE: src/Mollie.Api.AspNet/Webhooks/Authorization/MollieSignatureValidator.cs ================================================ using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.Http; using Mollie.Api.AspNet.Webhooks.Options; namespace Mollie.Api.AspNet.Webhooks.Authorization; public class MollieSignatureValidator { private readonly MollieWebhookOptions _options; public MollieSignatureValidator(MollieWebhookOptions options) { _options = options; if (options == null) { throw new ArgumentNullException(nameof(options)); } if (string.IsNullOrWhiteSpace(options.Secret)) { throw new ArgumentException("Webhook signing secret cannot be null or empty.", nameof(MollieWebhookOptions.Secret)); } } /// /// Validates the Mollie HMAC webhook signature in the given HTTP request. /// /// Incoming HTTP request. /// True if the signature is valid; otherwise false. public async Task Validate(HttpRequest request) { if (!request.Headers.TryGetValue("X-Mollie-Signature", out var headerValues)) { return false; } var headerValue = headerValues.FirstOrDefault(); if (string.IsNullOrWhiteSpace(headerValue)) { return false; } var bodyBytes = await GetRequestBodyBytes(request); var isValid = ValidateHmacSignature(headerValue, bodyBytes, _options.Secret); return isValid; } private bool ValidateHmacSignature(string headerValue, byte[] bodyBytes, string secret) { if (string.IsNullOrWhiteSpace(headerValue) || string.IsNullOrWhiteSpace(secret)) { return false; } // Step 1: Remove optional prefix "sha256=" (case-insensitive) const string prefix = "sha256="; var sig = headerValue.Trim(); if (sig.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { sig = sig.Substring(prefix.Length); } // Step 2: parse header signature bytes byte[] signatureBytes; try { signatureBytes = HexToBytes(sig); } catch { return false; } // Step 3: compute HMAC-SHA256 over raw body bytes (secret as UTF8) using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); var computed = hmac.ComputeHash(bodyBytes); // Step 4: timing-safe compare if (computed.Length != signatureBytes.Length) { return false; } return CryptographicOperations.FixedTimeEquals(computed, signatureBytes); } private static byte[] HexToBytes(string hex) { var len = hex.Length / 2; var bytes = new byte[len]; for (int i = 0; i < len; i++) { bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16); } return bytes; } private async Task GetRequestBodyBytes(HttpRequest request) { request.EnableBuffering(); request.Body.Position = 0; var ms = new MemoryStream(); await request.Body.CopyToAsync(ms); var bodyBytes = ms.ToArray(); request.Body.Seek(0, SeekOrigin.Begin); return bodyBytes; } } ================================================ FILE: src/Mollie.Api.AspNet/Webhooks/ModelBinding/FromMollieWebhookAttribute.cs ================================================ using Microsoft.AspNetCore.Mvc; namespace Mollie.Api.AspNet.Webhooks.ModelBinding; public class FromMollieWebhookAttribute : ModelBinderAttribute { public FromMollieWebhookAttribute() : base(typeof(FromMollieWebhookModelBinder)) { } } ================================================ FILE: src/Mollie.Api.AspNet/Webhooks/ModelBinding/FromMollieWebhookModelBinder.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; using Mollie.Api.Framework; namespace Mollie.Api.AspNet.Webhooks.ModelBinding; public class FromMollieWebhookModelBinder : IModelBinder { private readonly JsonConverterService _jsonConverterService; public FromMollieWebhookModelBinder() { _jsonConverterService = new(); } public async Task BindModelAsync(ModelBindingContext context) { var request = context.HttpContext.Request; request.EnableBuffering(); using var reader = new StreamReader(request.Body); var body = await reader.ReadToEndAsync(); request.Body.Position = 0; try { var result = _jsonConverterService.Deserialize(body, context.ModelType); context.Result = ModelBindingResult.Success(result); } catch (Exception ex) { context.ModelState.AddModelError(context.ModelName, ex.Message); } } } ================================================ FILE: src/Mollie.Api.AspNet/Webhooks/ModelBinding/MollieModelBinder.cs ================================================ using System.Reflection; using Microsoft.AspNetCore.Http; using Mollie.Api.Framework; using Mollie.Api.Models; namespace Mollie.Api.AspNet.Webhooks.ModelBinding; public class MollieModelBinder where T : IEntity { public T? Model { get; init; } public static async ValueTask?> BindAsync(HttpContext context, ParameterInfo parameter) { JsonConverterService jsonConverterService = new(); var request = context.Request; request.EnableBuffering(); var reader = new StreamReader(request.Body); var body = await reader.ReadToEndAsync(); request.Body.Position = 0; try { var result = jsonConverterService.Deserialize(body); return new MollieModelBinder() { Model = result }; } catch (Exception) { return null; } } } ================================================ FILE: src/Mollie.Api.AspNet/Webhooks/Options/MollieWebhookOptions.cs ================================================ namespace Mollie.Api.AspNet.Webhooks.Options; public record MollieWebhookOptions { public string Secret { get; set; } = string.Empty; } ================================================ FILE: tests/Mollie.Tests.Integration/Api/ApiExceptionTests.cs ================================================ using System.Net; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Client.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.Connect.Request; using Mollie.Api.Models.Payment.Request; using Mollie.Tests.Integration.Framework; using Xunit; namespace Mollie.Tests.Integration.Api; public class ApiExceptionTests : BaseMollieApiTestClass { private readonly IPaymentClient _paymentClient; private readonly IConnectClient _connectClient; public ApiExceptionTests(IPaymentClient paymentClient, IConnectClient connectClient) { _paymentClient = paymentClient; _connectClient = connectClient; } [Fact] public async Task CreatePayment_WithInvalidParameters_ShouldThrowMollieApiException() { // Given: we create a payment request with invalid parameters var paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = string.Empty, RedirectUrl = null }; // Then: Send the payment request to the Mollie Api, this should throw a mollie api exception MollieApiException apiException = await Assert.ThrowsAsync(() => _paymentClient.CreatePaymentAsync(paymentRequest)); apiException.ShouldNotBeNull(); apiException.Details.ShouldNotBeNull(); apiException.Details.Status.ShouldBe(422); apiException.Details.Title.ShouldBe("Unprocessable Entity"); apiException.Details.Detail.ShouldBe("The description is invalid"); } [Fact] public async Task RevokeTokenAsync_WithInvalidToken_ShouldThrowMollieApiException() { // Given var tokenRequest = new RevokeTokenRequest { Token = "token", TokenTypeHint = "hint" }; // Then MollieApiException apiException = await Assert.ThrowsAsync(() => _connectClient.RevokeTokenAsync(tokenRequest)); apiException.Details.Title.ShouldBe("invalid_request"); } } ================================================ FILE: tests/Mollie.Tests.Integration/Api/BalanceTests.cs ================================================ using System; using System.Linq; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Client.Abstract; using Mollie.Api.Models.Balance.Response.BalanceReport; using Mollie.Api.Models.Balance.Response.BalanceReport.Specific.StatusBalance; using Mollie.Api.Models.Balance.Response.BalanceReport.Specific.TransactionCategories; using Mollie.Tests.Integration.Framework; using Xunit; namespace Mollie.Tests.Integration.Api; [Trait("TestCategory", "LocalIntegrationTests")] public class BalanceTests : BaseMollieApiTestClass, IDisposable { private readonly IBalanceClient _balanceClient; public BalanceTests(IBalanceClient balanceClient) { _balanceClient = balanceClient; } [Fact] public async Task GetPrimaryBalanceAsync_IsParsedCorrectly() { // When: We retrieve the primary balance from the Mollie API var result = await _balanceClient.GetPrimaryBalanceAsync(); // Then: Make sure we can parse the result result.ShouldNotBeNull(); result.Resource.ShouldBe("balance"); result.Currency.ShouldNotBeNull(); result.Id.ShouldNotBeNull(); result.Links.Documentation.Href.ShouldBe("https://docs.mollie.com/reference/v2/balances-api/get-primary-balance"); result.Links.Self.Href.ShouldBe("https://api.mollie.com/v2/balances/{result.Id}"); result.TransferFrequency.ShouldNotBeNull(); result.AvailableAmount.ShouldNotBeNull(); result.PendingAmount.ShouldNotBeNull(); result.TransferThreshold.ShouldNotBeNull(); } [Fact] public async Task GetBalanceAsync_IsParsedCorrectly() { // Given: We get a balance id from the list balances endpoint var balanceList = await _balanceClient.GetBalanceListAsync(); if (balanceList.Count == 0) { Assert.Fail("No balance found to retrieve"); } var firstBalance = balanceList.Items.First(); // When: We retrieve a specific balance from the Mollie API var result = await _balanceClient.GetBalanceAsync(firstBalance.Id); // Then: Make sure we can parse the result result.ShouldNotBeNull(); result.Resource.ShouldBe("balance"); result.AvailableAmount.ShouldBe(firstBalance.AvailableAmount); result.Id.ShouldBe(firstBalance.Id); result.Links.Documentation.Href.ShouldBe("https://docs.mollie.com/reference/v2/balances-api/get-balance"); result.Links.Self.Href.ShouldBe($"https://api.mollie.com/v2/balances/{result.Id}"); result.Currency.ShouldBe(firstBalance.Currency); result.TransferFrequency.ShouldBe(firstBalance.TransferFrequency); result.AvailableAmount.ShouldBe(firstBalance.AvailableAmount); result.PendingAmount.ShouldBe(firstBalance.PendingAmount); result.TransferThreshold.ShouldBe(firstBalance.TransferThreshold); } [Fact] public async Task ListBalancesAsync_IsParsedCorrectly() { // When: We retrieve the list of balances var result = await _balanceClient.GetBalanceListAsync(); // Then: Make sure we can parse the result result.ShouldNotBeNull(); result.Items.Count.ShouldBe(result.Count); } [Theory] [InlineData(ReportGrouping.TransactionCategories, typeof(TransactionCategoriesReportResponse))] [InlineData(ReportGrouping.StatusBalances, typeof(StatusBalanceReportResponse))] public async Task GetBalanceReportAsync_IsParsedCorrectly(string grouping, Type expectedObjectType) { // Given: We retrieve the primary balance var from = new DateTime(2022, 11, 1); var until = new DateTime(2022, 11, 30); var primaryBalance = await _balanceClient.GetPrimaryBalanceAsync(); // When: We retrieve the primary balance report var result = await _balanceClient.GetBalanceReportAsync( balanceId: primaryBalance.Id, from: from, until: until, grouping: grouping); // Then: Make sure we can parse the result result.ShouldNotBeNull(); result.ShouldBeOfType(expectedObjectType); result.Resource.ShouldBe("balance-report"); result.BalanceId.ShouldBe(primaryBalance.Id); result.From.ShouldBe(from); result.Until.ShouldBe(until); result.Grouping.ShouldBe(grouping); } [Fact] public async Task ListBalanceTransactionsAsync_IsParsedCorrectly() { // Given var balanceId = "bal_CKjKwQdjCwCSArXFAJNFH"; var from = "baltr_9S8yk4FFqqi2Qm6K3rqRH"; var limit = 250; // When: We list the balance transactions var result = await _balanceClient.GetBalanceTransactionListAsync(balanceId, from, limit); // Then: Make sure we can parse the result result.ShouldNotBeNull(); result.Items.ShouldNotBeNull(); result.Links.ShouldNotBeNull(); result.Links.Self.Href.ShouldBe($"https://api.mollie.com/v2/balances/{balanceId}/transactions?from={from}&limit={limit}"); } [Fact] public async Task ListPrimaryBalanceTransactionsAsync_IsParsedCorrectly() { // Given var from = "baltr_9S8yk4FFqqi2Qm6K3rqRH"; var limit = 250; // When: We list the balance transactions var result = await _balanceClient.GetPrimaryBalanceTransactionListAsync(from, limit); // Then: Make sure we can parse the result result.ShouldNotBeNull(); result.Items.ShouldNotBeNull(); } public void Dispose() { _balanceClient?.Dispose(); } } ================================================ FILE: tests/Mollie.Tests.Integration/Api/CaptureTests.cs ================================================ using System; using System.Linq; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Client.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.Capture; using Mollie.Api.Models.Capture.Request; using Mollie.Api.Models.Payment; using Mollie.Api.Models.Payment.Request; using Mollie.Tests.Integration.Framework; using Xunit; namespace Mollie.Tests.Integration.Api; public class CaptureTests : BaseMollieApiTestClass, IDisposable { private readonly ICaptureClient _captureClient; private readonly IPaymentClient _paymentClient; public CaptureTests( ICaptureClient captureClient, IPaymentClient paymentClient) { _captureClient = captureClient; _paymentClient = paymentClient; } [Fact(Skip = "We can only test this in debug mode, because we actually have to use the PaymentUrl" + " to make the payment, since Mollie can only capture payments that have been authorized")] public async Task CanCreateCaptureForPaymentWithManualCaptureMode() { // Given: We create a payment with captureMode set to manual PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, 1000.00m), Description = "Description", RedirectUrl = DefaultRedirectUrl, Method = PaymentMethod.CreditCard, CaptureMode = CaptureMode.Manual }; var payment = await _paymentClient.CreatePaymentAsync(paymentRequest); // When: We create a capture for the payment var captureRequest = new CaptureRequest { Amount = new Amount(Currency.EUR, 0.01m), Description = "my capture", Metadata = "my-metadata string" }; var capture = await _captureClient.CreateCapture(payment.Id, captureRequest); // Then: The capture should be created capture.Status.ShouldBe("pending"); capture.PaymentId.ShouldBe(payment.Id); capture.Resource.ShouldBe("capture"); capture.Metadata.ShouldBe(captureRequest.Metadata); } [Fact(Skip = "We can only test this in debug mode, because we actually have to use the PaymentUrl" + " to make the payment, since Mollie can only capture payments that have been authorized")] public async Task CanRetrieveCaptureListForPayment() { // Given: we create a payment and capture PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, 1000.00m), Description = "Description", RedirectUrl = DefaultRedirectUrl, Method = PaymentMethod.CreditCard, CaptureMode = CaptureMode.Manual }; var payment = await _paymentClient.CreatePaymentAsync(paymentRequest); var captureRequest = new CaptureRequest { Amount = new Amount(Currency.EUR, 0.01m), Description = "my capture", Metadata = "my-metadata string" }; await _captureClient.CreateCapture(payment.Id, captureRequest); // When: we retrieve the captures of the payment var captureList = await _captureClient.GetCaptureListAsync(payment.Id); // Then captureList.Count.ShouldBe(1); var capture = captureList.Items.Single(); capture.Status.ShouldBe("succeeded"); capture.PaymentId.ShouldBe(payment.Id); capture.Resource.ShouldBe("capture"); capture.Metadata.ShouldBe(captureRequest.Metadata); } public void Dispose() { _captureClient?.Dispose(); _paymentClient?.Dispose(); } } ================================================ FILE: tests/Mollie.Tests.Integration/Api/ConnectTests.cs ================================================ using Mollie.Api.Client; using Mollie.Tests.Integration.Framework; using System.Collections.Generic; using System.Threading.Tasks; using Mollie.Api.Client.Abstract; using Shouldly; using Mollie.Api.Models.Connect.Request; using Mollie.Api.Models.Connect.Response; using Xunit; namespace Mollie.Tests.Integration.Api; public class ConnectTests : BaseMollieApiTestClass { private readonly IConnectClient _connectClient; public ConnectTests(IConnectClient connectClient) { _connectClient = connectClient; } [Fact] public void GetAuthorizationUrl_WithSingleScope_GeneratesAuthorizationUrl() { // When: We get the authorization URL string authorizationUrl = _connectClient.GetAuthorizationUrl("abcde", new List() { AppPermissions.PaymentsRead }); // Then: string expectedUrl = $"https://my.mollie.com/oauth2/authorize?client_id={ClientId}&state=abcde&scope=payments.read&response_type=code&approval_prompt=auto"; authorizationUrl.ShouldBe(expectedUrl); } [Fact] public void GetAuthorizationUrl_WithMultipleScopes_GeneratesAuthorizationUrl() { // When: We get the authorization URL string authorizationUrl = _connectClient.GetAuthorizationUrl("abcdef", new List() { AppPermissions.PaymentsRead, AppPermissions.PaymentsWrite, AppPermissions.ProfilesRead, AppPermissions.ProfilesWrite }); // Then: string expectedUrl = $"https://my.mollie.com/oauth2/authorize?client_id={ClientId}" + $"&state=abcdef&scope=payments.read+payments.write+profiles.read+profiles.write&response_type=code&approval_prompt=auto"; authorizationUrl.ShouldBe(expectedUrl); } [Fact(Skip = "We can only test this in debug mode, because we login to the mollie dashboard and login to get the auth token")] public async Task GetAccessTokenAsync_WithValidTokenRequest_ReturnsAccessToken() { // Given: We fetch create a token request string authCode = "abcde"; // Set a valid access token here using ConnectClient connectClient = new ConnectClient(ClientId, ClientSecret); TokenRequest tokenRequest = new TokenRequest(authCode, DefaultRedirectUrl); // When: We request the auth code TokenResponse tokenResponse = await connectClient.GetAccessTokenAsync(tokenRequest); // Then: The access token should not be null tokenResponse.AccessToken.ShouldNotBeNullOrEmpty(); } [Fact(Skip = "We can only test this in debug mode, because we need a valid access token")] public async Task RevokeAccessTokenAsync_WithValidToken_DoesNotThrowError() { // Given: We create a revoke token request string accessToken = "abcde"; using ConnectClient connectClient = new ConnectClient(ClientId, ClientSecret); RevokeTokenRequest revokeTokenRequest = new RevokeTokenRequest() { TokenTypeHint = TokenType.AccessToken, Token = accessToken }; // When: we send the request await connectClient.RevokeTokenAsync(revokeTokenRequest); } } ================================================ FILE: tests/Mollie.Tests.Integration/Api/CustomerTests.cs ================================================ using System; using System.Linq; using System.Net; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Client.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.Customer.Request; using Mollie.Api.Models.Customer.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment; using Mollie.Api.Models.Payment.Request; using Mollie.Api.Models.Payment.Response; using Mollie.Tests.Integration.Framework; using Xunit; namespace Mollie.Tests.Integration.Api; public class CustomerTests : BaseMollieApiTestClass, IDisposable { private readonly ICustomerClient _customerClient; public CustomerTests(ICustomerClient customerClient) { _customerClient = customerClient; } [Fact] public async Task CanRetrieveCustomerList() { // When: Retrieve customer list with default settings ListResponse response = await _customerClient.GetCustomerListAsync(); // Then response.ShouldNotBeNull(); response.Items.ShouldNotBeNull(); } [Fact] public async Task ListCustomersNeverReturnsMoreCustomersThenTheNumberOfRequestedCustomers() { // If: Number of customers requested is 5 int numberOfCustomers = 5; // When: Retrieve 5 customers ListResponse response = await _customerClient.GetCustomerListAsync(null, numberOfCustomers); // Then numberOfCustomers.ShouldBe(response.Items.Count); } [Fact] public async Task CanCreateNewCustomer() { // If: We create a customer request with only the required parameters string name = "Smit"; string email = "johnsmit@mollie.com"; // When: We send the customer request to Mollie CustomerResponse result = await CreateCustomer(name, email); // Then: Make sure the requested parameters match the response parameter values result.ShouldNotBeNull(); result.Name.ShouldBe(name); result.Email.ShouldBe(email); } [Fact] public async Task CanUpdateCustomer() { // If: We retrieve the customer list ListResponse response = await _customerClient.GetCustomerListAsync(); // When: We update one of the customers in the list string customerIdToUpdate = response.Items.First().Id; string newCustomerName = DateTime.Now.ToShortTimeString(); CustomerRequest updateParameters = new CustomerRequest() { Name = newCustomerName }; CustomerResponse result = await _customerClient.UpdateCustomerAsync(customerIdToUpdate, updateParameters); // Then: Make sure the new name is updated result.ShouldNotBeNull(); result.Name.ShouldBe(newCustomerName); } [Fact] public async Task CanDeleteCustomer() { // If: We retrieve the customer list ListResponse response = await _customerClient.GetCustomerListAsync(); // When: We delete one of the customers in the list string customerIdToDelete = response.Items.First().Id; await _customerClient.DeleteCustomerAsync(customerIdToDelete); // Then: Make sure its deleted after one second await Task.Delay(TimeSpan.FromSeconds(1)); MollieApiException apiException = await Assert.ThrowsAsync(() => _customerClient.GetCustomerAsync(customerIdToDelete)); apiException.Details.Status.ShouldBe((int)HttpStatusCode.Gone); } [Fact] public async Task CanGetCustomerByUrlObject() { // If: We create a customer request with only the required parameters string name = "Smit"; string email = "johnsmit@mollie.com"; CustomerResponse createdCustomer = await CreateCustomer(name, email); // When: We try to retrieve the customer by Url object CustomerResponse retrievedCustomer = await ExecuteWithRetry(() => _customerClient.GetCustomerAsync(createdCustomer.Links.Self)); // Then: Make sure it's retrieved retrievedCustomer.Name.ShouldBe(createdCustomer.Name); retrievedCustomer.Email.ShouldBe(createdCustomer.Email); } [Fact] public async Task CanCreateCustomerWithJsonMetadata() { // If: We create a customer request with json metadata var customerRequest = new CustomerRequest() { Email = "johnsmit@mollie.com", Name = "Smit", Metadata = "{\"order_id\":\"4.40\"}", Locale = Locale.nl_NL }; // When: We try to retrieve the customer by Url object CustomerResponse retrievedCustomer = await _customerClient.CreateCustomerAsync(customerRequest); // Then: Make sure it's retrieved IsJsonResultEqual(customerRequest.Metadata, retrievedCustomer.Metadata).ShouldBeTrue(); } [Fact] public async Task CanCreateCustomerWithStringMetadata() { // If: We create a customer request with string metadata CustomerRequest customerRequest = new CustomerRequest() { Email = "johnsmit@mollie.com", Name = "Smit", Metadata = "This is my metadata", Locale = Locale.nl_NL }; // When: We try to retrieve the customer by Url object CustomerResponse retrievedCustomer = await _customerClient.CreateCustomerAsync(customerRequest); // Then: Make sure it's retrieved retrievedCustomer.Metadata.ShouldBe(customerRequest.Metadata); } [Fact] public async Task CanCreateNewCustomerPayment() { // If: We create a customer request with only the required parameters string name = "Smit"; string email = "johnsmit@mollie.com"; CustomerResponse customer = await CreateCustomer(name, email); PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = DefaultRedirectUrl }; // When: We create a payment request for this customer to Mollie PaymentResponse paymentResponse = await _customerClient.CreateCustomerPayment(customer.Id, paymentRequest); // Then: Make sure the requested parameters match the response parameter values paymentResponse.ShouldNotBeNull(); paymentResponse.CustomerId.ShouldBe(customer.Id); } private async Task CreateCustomer(string name, string email) { CustomerRequest customerRequest = new CustomerRequest() { Email = email, Name = name, Locale = Locale.nl_NL }; return await _customerClient.CreateCustomerAsync(customerRequest); } public void Dispose() { _customerClient?.Dispose(); } } ================================================ FILE: tests/Mollie.Tests.Integration/Api/MandateTests.cs ================================================ using System; using System.Linq; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Client.Abstract; using Mollie.Api.Models.Customer.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Mandate.Request; using Mollie.Api.Models.Mandate.Request.PaymentSpecificParameters; using Mollie.Api.Models.Mandate.Response; using Mollie.Api.Models.Mandate.Response.PaymentSpecificParameters; using Mollie.Api.Models.Payment; using Mollie.Api.Models.Payment.Response.PaymentSpecificParameters; using Mollie.Tests.Integration.Framework; using Xunit; namespace Mollie.Tests.Integration.Api; public class MandateTests : BaseMollieApiTestClass, IDisposable { private readonly IMandateClient _mandateClient; private readonly ICustomerClient _customerClient; public MandateTests(IMandateClient mandateClient, ICustomerClient customerClient) { _mandateClient = mandateClient; _customerClient = customerClient; } [Fact] public async Task CanRetrieveMandateList() { // We can only test this if there are customers ListResponse customers = await _customerClient.GetCustomerListAsync(); if (customers.Count > 0) { // When: Retrieve mandate list with default settings ListResponse response = await _mandateClient.GetMandateListAsync(customers.Items.First().Id); // Then response.ShouldNotBeNull(); response.Items.ShouldNotBeNull(); } } [Fact] public async Task ListMandatesNeverReturnsMoreCustomersThenTheNumberOfRequestedMandates() { // We can only test this if there are customers ListResponse customers = await _customerClient.GetCustomerListAsync(); if (customers.Count > 0) { // If: Number of customers requested is 5 int numberOfMandates = 5; // When: Retrieve 5 mandates ListResponse response = await _mandateClient.GetMandateListAsync(customers.Items.First().Id, null, numberOfMandates); // Then numberOfMandates.ShouldBeGreaterThanOrEqualTo(response.Items.Count); } } [Fact] public async Task CanCreateSepaDirectDebitMandate() { // We can only test this if there are customers ListResponse customers = await _customerClient.GetCustomerListAsync(); if (customers.Count > 0) { // If: We create a new mandate request SepaDirectDebitMandateRequest mandateRequest = new () { ConsumerAccount = "NL26ABNA0516682814", ConsumerName = "John Doe", Method = PaymentMethod.DirectDebit }; // When: We send the mandate request MandateResponse mandateResponse = await _mandateClient.CreateMandateAsync(customers.Items.First().Id, mandateRequest); // Then: Make sure we created a new mandate mandateResponse.ShouldBeOfType(); var sepaDirectDebitResponse = (SepaDirectDebitMandateResponse)mandateResponse; sepaDirectDebitResponse.Details.ConsumerAccount.ShouldBe(mandateRequest.ConsumerAccount); sepaDirectDebitResponse.Details.ConsumerName.ShouldBe(mandateRequest.ConsumerName); } } public void Dispose() { _mandateClient?.Dispose(); _customerClient?.Dispose(); } } ================================================ FILE: tests/Mollie.Tests.Integration/Api/OrderTests.cs ================================================ using System; #pragma warning disable CS0618 using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Client.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Order; using Mollie.Api.Models.Order.Request; using Mollie.Api.Models.Order.Request.ManageOrderLines; using Mollie.Api.Models.Order.Request.PaymentSpecificParameters; using Mollie.Api.Models.Order.Response; using Mollie.Api.Models.Payment; using Mollie.Tests.Integration.Framework; using Xunit; using SortDirection = Mollie.Api.Models.SortDirection; namespace Mollie.Tests.Integration.Api; public class OrderTests : BaseMollieApiTestClass, IDisposable { private readonly IOrderClient _orderClient; public OrderTests(IOrderClient orderClient) { _orderClient = orderClient; } [Fact] public async Task GetOrderListAsync_WithoutSortOrder_ReturnsOrdersInDescendingOrder() { // Act var orders = await _orderClient.GetOrderListAsync(); // Assert if (orders.Items.Any()) { orders.Items.Select(x => x.CreatedAt).ShouldBeInOrder(Shouldly.SortDirection.Descending); } } [Fact] public async Task GetOrderListAsync_InDescendingOrder_ReturnsOrdersInDescendingOrder() { // Act var orders = await _orderClient.GetOrderListAsync(sort: SortDirection.Desc); // Assert if (orders.Items.Any()) { orders.Items.Select(x => x.CreatedAt).ShouldBeInOrder(Shouldly.SortDirection.Descending); } } [Fact] public async Task GetOrderListAsync_InAscendingOrder_ReturnsOrdersInAscendingOrder() { // Act var orders = await _orderClient.GetOrderListAsync(sort: SortDirection.Asc); // Assert if (orders.Items.Any()) { orders.Items.Select(x => x.CreatedAt).ShouldBeInOrder(Shouldly.SortDirection.Ascending); } } [Fact] public async Task CreateOrderAsync_OrderWithRequiredFields_OrderIsCreated() { // If: we create a order request with only the required parameters OrderRequest orderRequest = CreateOrder(); // When: We send the order request to Mollie OrderResponse result = await _orderClient.CreateOrderAsync(orderRequest); // Then: Make sure we get a valid response result.ShouldNotBeNull(); result.Amount.ShouldBe(orderRequest.Amount); result.OrderNumber.ShouldBe(orderRequest.OrderNumber); result.Lines.Count().ShouldBe(orderRequest.Lines.Count()); result.Links.ShouldNotBeNull(); OrderLineRequest orderLineRequest = orderRequest.Lines.First(); OrderLineResponse orderResponseLine = result.Lines.First(); orderResponseLine.Type.ShouldBe(orderLineRequest.Type); orderResponseLine.Links.ImageUrl!.Href.ShouldBe(orderLineRequest.ImageUrl); orderResponseLine.Links.ProductUrl!.Href.ShouldBe(orderLineRequest.ProductUrl); var expectedMetadataString = result.Lines.First().Metadata; orderResponseLine.Metadata.ShouldBe(expectedMetadataString); } [Fact] public async Task CreateOrderAsync_OrderWithExtendedFields_OrderIsCreated() { // If: we create a order request OrderRequest orderRequest = CreateOrder(); orderRequest.ConsumerDateOfBirth = new DateTime(1980, 1, 1); orderRequest.ExpiresAt = DateTime.Now.AddDays(2); // When: We send the order request to Mollie OrderResponse result = await _orderClient.CreateOrderAsync(orderRequest); // Then: Make sure we get a valid response result.ShouldNotBeNull(); result.ConsumerDateOfBirth.ShouldBe(orderRequest.ConsumerDateOfBirth); result.ExpiresAt!.Value.Date.ShouldBe(orderRequest.ExpiresAt.Value.Date); } [Fact] public async Task CreateOrderAsync_OrderWithApplicationFee_OrderIsCreated() { // If: we create a order request with only the required parameters OrderRequest orderRequest = CreateOrder() with { Payment = new OrderPaymentParameters { ApplicationFee = new ApplicationFee { Amount = new Amount(Currency.EUR, 0.25m), Description = "Test" } } }; // When: We send the order request to Mollie OrderResponse result = await _orderClient.CreateOrderAsync(orderRequest); // Then: Make sure we get a valid response result.ShouldNotBeNull(); result.Amount.ShouldBe(orderRequest.Amount); result.OrderNumber.ShouldBe(orderRequest.OrderNumber); result.Lines.Count().ShouldBe(orderRequest.Lines.Count()); result.Links.ShouldNotBeNull(); OrderLineRequest orderLineRequest = orderRequest.Lines.First(); OrderLineResponse orderResponseLine = result.Lines.First(); orderResponseLine.Type.ShouldBe(orderLineRequest.Type); orderResponseLine.Links.ImageUrl!.Href.ShouldBe(orderLineRequest.ImageUrl); orderResponseLine.Links.ProductUrl!.Href.ShouldBe(orderLineRequest.ProductUrl); var expectedMetadataString = result.Lines.First().Metadata; orderResponseLine.Metadata.ShouldBe(expectedMetadataString); } [Fact] public async Task CreateOrderAsync_WithMultiplePaymentMethods_OrderIsCreated() { // When: we create a order request and specify multiple payment methods OrderRequest orderRequest = CreateOrder(); orderRequest.Methods = new List() { PaymentMethod.Ideal, PaymentMethod.CreditCard, PaymentMethod.DirectDebit }; // When: We send the order request to Mollie OrderResponse result = await _orderClient.CreateOrderAsync(orderRequest); // Then: Make sure we get a valid response result.ShouldNotBeNull(); result.Amount.ShouldBe(orderRequest.Amount); result.OrderNumber.ShouldBe(orderRequest.OrderNumber); } [Fact] public async Task CreateOrderAsync_WithSinglePaymentMethod_OrderIsCreated() { // When: we create a order request and specify a single payment method OrderRequest orderRequest = CreateOrder(); orderRequest.Method = PaymentMethod.CreditCard; // When: We send the order request to Mollie OrderResponse result = await _orderClient.CreateOrderAsync(orderRequest); // Then: Make sure we get a valid response orderRequest.Method.ShouldBe(PaymentMethod.CreditCard); orderRequest.Methods!.First().ShouldBe(PaymentMethod.CreditCard); result.ShouldNotBeNull(); result.Amount.ShouldBe(orderRequest.Amount); result.OrderNumber.ShouldBe(orderRequest.OrderNumber); } public static IEnumerable PaymentSpecificParameters => new List { new object[] { new KlarnaSpecificParameters { ExtraMerchantData = new { payment_history_simple = new[] { new { unique_account_identifier = "Adam Adamsson", paid_before = true } } } } }, new object[] { new BillieSpecificParameters { Company = new CompanyObject { EntityType = CompanyEntityType.LimitedCompany, RegistrationNumber = "registration-number", VatNumber = "vat-number" } } }, new object[] { new GiftcardSpecificParameters() { Issuer = "boekenbon", VoucherNumber = "voucher-number", VoucherPin = "1234" } }, new object[] { new IDealSpecificParameters { Issuer = "ideal_INGBNL2A" } }, new object[] { new KbcSpecificParameters { Issuer = "ideal_INGBNL2A" } }, new object[] { new PaySafeCardSpecificParameters { CustomerReference = "customer-reference" } }, new object[] { new SepaDirectDebitSpecificParameters { ConsumerAccount = "Consumer account" } } }; [Theory] [MemberData(nameof(PaymentSpecificParameters))] public async Task CreateOrderAsync_WithPaymentSpecificParameters_OrderIsCreated( OrderPaymentParameters paymentSpecificParameters) { // If: we create a order request with payment specific parameters OrderRequest orderRequest = CreateOrder(); orderRequest.BillingAddress!.Country = "DE"; // Billie only works in Germany orderRequest.BillingAddress.OrganizationName = "Mollie"; // Billie requires a organization name orderRequest.Payment = paymentSpecificParameters; // When: We send the order request to Mollie OrderResponse result = await _orderClient.CreateOrderAsync(orderRequest); // Then: Make sure we get a valid response result.ShouldNotBeNull(); result.Amount.ShouldBe(orderRequest.Amount); result.OrderNumber.ShouldBe(orderRequest.OrderNumber); } [Fact] public async Task GetOrderAsync_OrderIsCreated_OrderCanBeRetrieved() { // If: we create a new order OrderRequest orderRequest = CreateOrder(); OrderResponse createdOrder = await _orderClient.CreateOrderAsync(orderRequest); // When: We attempt to retrieve the order OrderResponse retrievedOrder = await _orderClient.GetOrderAsync(createdOrder.Id); // Then: Make sure we get a valid response retrievedOrder.ShouldNotBeNull(); retrievedOrder.Id.ShouldBe(createdOrder.Id); } [Fact] public async Task GetOrderAsync_WithUrlObject_OrderCanBeRetrieved() { // If: we create a new order OrderRequest orderRequest = CreateOrder(); OrderResponse createdOrder = await _orderClient.CreateOrderAsync(orderRequest); // When: We attempt to retrieve the order OrderResponse retrievedOrder = await _orderClient.GetOrderAsync(createdOrder.Links.Self); // Then: Make sure we get a valid response retrievedOrder.ShouldNotBeNull(); retrievedOrder.Id.ShouldBe(createdOrder.Id); } [Fact] public async Task GetOrderAsync_WithIncludeParameters_OrderIsRetrievedWithEmbeddedData() { // If: we create a new order OrderRequest orderRequest = CreateOrder(); OrderResponse createdOrder = await _orderClient.CreateOrderAsync(orderRequest); // When: We attempt to retrieve the order and add the include parameters OrderResponse retrievedOrder = await _orderClient.GetOrderAsync(createdOrder.Id, embedPayments: true, embedShipments: true, embedRefunds: true); // Then: Make sure we get a valid response retrievedOrder.ShouldNotBeNull(); retrievedOrder.Id.ShouldBe(createdOrder.Id); retrievedOrder.Embedded.ShouldNotBeNull(); retrievedOrder.Embedded!.Payments.ShouldNotBeNull(); retrievedOrder.Embedded.Shipments.ShouldNotBeNull(); retrievedOrder.Embedded.Refunds.ShouldNotBeNull(); } [Fact] public async Task UpdateOrderAsync_OrderIsUpdated_OrderIsUpdated() { // If: we create a new order OrderRequest orderRequest = CreateOrder(); OrderResponse createdOrder = await _orderClient.CreateOrderAsync(orderRequest); // When: We attempt to update the order OrderUpdateRequest orderUpdateRequest = new() { OrderNumber = "1337", BillingAddress = createdOrder.BillingAddress }; OrderResponse updatedOrder = await _orderClient.UpdateOrderAsync(createdOrder.Id, orderUpdateRequest); // Then: Make sure the order is updated updatedOrder.OrderNumber.ShouldBe(orderUpdateRequest.OrderNumber); } [Fact(Skip = "Broken - Reported to Mollie: https://discordapp.com/channels/1037712581407817839/1180467187677401198/1180467187677401198")] public async Task UpdateOrderLinesAsync_WhenOrderLineIsUpdated_UpdatedPropertiesCanBeRetrieved() { // If: we create a new order OrderRequest orderRequest = CreateOrder(); OrderResponse createdOrder = await _orderClient.CreateOrderAsync(orderRequest); // When: We update the order line OrderLineUpdateRequest updateRequest = new() { Name = "A fluffy bear" }; OrderResponse updatedOrder = await _orderClient.UpdateOrderLinesAsync(createdOrder.Id, createdOrder.Lines.First().Id, updateRequest); // Then: The name of the order line should be updated updatedOrder.Lines.First().Name.ShouldBe(updateRequest.Name); } [Fact] public async Task ManageOrderLinesAsync_AddOperation_OrderLineIsAdded() { // If: we create a new order OrderRequest orderRequest = CreateOrder(); OrderResponse createdOrder = await _orderClient.CreateOrderAsync(orderRequest); // When: We use the manager order lines endpoint to add a order line ManageOrderLinesAddOperationData newOrderLineRequest = new() { Name = "LEGO Batman mobile", Type = OrderLineDetailsType.Physical, Category = VoucherCategory.Gift, Quantity = 1, UnitPrice = new Amount(Currency.EUR, 100.00m), TotalAmount = new Amount(Currency.EUR, 100.00m), VatRate = "21.00", VatAmount = new Amount(Currency.EUR, 17.36m), ImageUrl = "http://www.google.com/legobatmanimage", ProductUrl = "http://www.mollie.nl/legobatmanproduct", Metadata = "{\"is_lego_awesome\":\"fosho\"}", }; ManageOrderLinesRequest manageOrderLinesRequest = new() { Operations = new List { new ManageOrderLinesAddOperation { Data = newOrderLineRequest } } }; OrderResponse updatedOrder = await _orderClient.ManageOrderLinesAsync(createdOrder.Id, manageOrderLinesRequest); // Then: The order line should be added updatedOrder.Lines.Count().ShouldBe(2); var addedOrderLineRequest = updatedOrder.Lines.SingleOrDefault(line => line.Name == newOrderLineRequest.Name); addedOrderLineRequest.ShouldNotBeNull(); addedOrderLineRequest!.Type.ShouldBe(newOrderLineRequest.Type); addedOrderLineRequest.Quantity.ShouldBe(newOrderLineRequest.Quantity); addedOrderLineRequest.UnitPrice.ShouldBe(newOrderLineRequest.UnitPrice); addedOrderLineRequest.TotalAmount.ShouldBe(newOrderLineRequest.TotalAmount); addedOrderLineRequest.VatRate.ShouldBe(newOrderLineRequest.VatRate); addedOrderLineRequest.VatAmount.ShouldBe(newOrderLineRequest.VatAmount); var newMetaData = addedOrderLineRequest.Metadata! .Replace(Environment.NewLine, "") .Replace(" ", ""); newMetaData.ShouldBe(newOrderLineRequest.Metadata); } [Fact] public async Task ManageOrderLinesAsync_UpdateOperation_OrderLineIsUpdated() { // If: we create a new order OrderRequest orderRequest = CreateOrder(); OrderResponse createdOrder = await _orderClient.CreateOrderAsync(orderRequest); // When: We use the manager order lines endpoint to update a order line ManageOrderLinesUpdateOperationData orderLineUpdateRequest = new() { Id = createdOrder.Lines.First().Id, Name = "LEGO Batman mobile", Quantity = 1, UnitPrice = new Amount(Currency.EUR, 100.00m), TotalAmount = new Amount(Currency.EUR, 90.00m), VatRate = "21.00", VatAmount = new Amount(Currency.EUR, 15.62m), ImageUrl = "http://www.google.com/legobatmanimage", ProductUrl = "http://www.mollie.nl/legobatmanproduct", Metadata = "{\"is_lego_awesome\":\"fosho\"}", Sku = "Sku", DiscountAmount = new Amount(Currency.EUR, 10m) }; ManageOrderLinesRequest manageOrderLinesRequest = new() { Operations = new List { new ManageOrderLinesUpdateOperation { Data = orderLineUpdateRequest } } }; OrderResponse updatedOrder = await _orderClient.ManageOrderLinesAsync(createdOrder.Id, manageOrderLinesRequest); // Then: The order line should be updated updatedOrder.Lines.Count().ShouldBe(1); var addedOrderLineRequest = updatedOrder.Lines.SingleOrDefault(line => line.Name == orderLineUpdateRequest.Name); addedOrderLineRequest.ShouldNotBeNull(); addedOrderLineRequest!.Quantity.ShouldBe(orderLineUpdateRequest.Quantity.Value); addedOrderLineRequest.UnitPrice.ShouldBe(orderLineUpdateRequest.UnitPrice); addedOrderLineRequest.TotalAmount.ShouldBe(orderLineUpdateRequest.TotalAmount); addedOrderLineRequest.VatRate.ShouldBe(orderLineUpdateRequest.VatRate); addedOrderLineRequest.VatAmount.ShouldBe(orderLineUpdateRequest.VatAmount); addedOrderLineRequest.Metadata! .Replace(Environment.NewLine, "") .Replace(" ", "") .ShouldBe(orderLineUpdateRequest.Metadata); } [Fact] public async Task ManageOrderLinesAsync_CancelOperation_OrderLineIsCanceled() { // If: we create a new order OrderRequest orderRequest = CreateOrder(); OrderResponse createdOrder = await _orderClient.CreateOrderAsync(orderRequest); // When: We use the manager order lines endpoint to cancel a order line ManagerOrderLinesCancelOperationData orderLineCancelRequest = new() { Id = createdOrder.Lines.First().Id, Quantity = 1 }; ManageOrderLinesRequest manageOrderLinesRequest = new() { Operations = new List { new ManageOrderLinesCancelOperation { Data = orderLineCancelRequest } } }; OrderResponse updatedOrder = await _orderClient.ManageOrderLinesAsync(createdOrder.Id, manageOrderLinesRequest); // Then: The order line should be canceled updatedOrder.Lines.Count().ShouldBe(1); var updatedOrderLineRequest = updatedOrder.Lines.Single(); updatedOrderLineRequest.Status.ShouldBe(OrderStatus.Canceled); } [Fact] public async Task GetOrderListAsync_NoParameters_OrderListIsRetrieved() { // When: Retrieve orders list with default settings ListResponse response = await _orderClient.GetOrderListAsync(); // Then response.ShouldNotBeNull(); response.Items.ShouldNotBeNull(); } [Fact] public async Task GetOrderListAsync_WithMaximumNumberOfItems_MaximumNumberOfOrdersIsReturned() { // If: Number of orders requested is 5 int numberOfOrders = 5; // When: Retrieve 5 orders ListResponse response = await _orderClient.GetOrderListAsync(null, numberOfOrders); // Then response.Items.Count.ShouldBeLessThanOrEqualTo(numberOfOrders); } private OrderRequest CreateOrder() { return new OrderRequest() { Amount = new Amount(Currency.EUR, "100.00"), OrderNumber = "16738", Lines = new List() { new() { Name = "A box of chocolates", Type = OrderLineDetailsType.Physical, Category = VoucherCategory.Gift, Quantity = 1, UnitPrice = new Amount(Currency.EUR, "100.00"), TotalAmount = new Amount(Currency.EUR, "100.00"), VatRate = "21.00", VatAmount = new Amount(Currency.EUR, "17.36"), ImageUrl = "http://www.google.com/", ProductUrl = "http://www.mollie.nl/", Metadata = "{\"order_id\":\"4.40\"}", } }, BillingAddress = new OrderAddressDetails() { GivenName = "John", FamilyName = "Smit", Email = "johnsmit@gmail.com", City = "Rotterdam", Country = "NL", PostalCode = "0000AA", Region = "Zuid-Holland", StreetAndNumber = "Coolsingel 1" }, RedirectUrl = "http://www.google.nl", Locale = Locale.nl_NL }; } public void Dispose() { _orderClient?.Dispose(); } } ================================================ FILE: tests/Mollie.Tests.Integration/Api/PaymentLinkTests.cs ================================================ using System; using System.Collections.Generic; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Client.Abstract; using Mollie.Api.Extensions; using Mollie.Api.Models; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Order.Request; using Mollie.Api.Models.Payment; using Mollie.Api.Models.PaymentLink.Request; using Mollie.Api.Models.PaymentLink.Response; using Mollie.Tests.Integration.Framework; using Xunit; namespace Mollie.Tests.Integration.Api; public class PaymentLinkTests : BaseMollieApiTestClass, IDisposable { private readonly IPaymentLinkClient _paymentLinkClient; public PaymentLinkTests(IPaymentLinkClient paymentLinkClient) { _paymentLinkClient = paymentLinkClient; } [Fact] public async Task CanRetrievePaymentLinkList() { // When: Retrieve payment list with default settings ListResponse response = await _paymentLinkClient.GetPaymentLinkListAsync(); // Then response.ShouldNotBeNull(); response.Items.ShouldNotBeNull(); } [Fact] public async Task CanCreatePaymentLinkAndRetrieveIt() { // Given: We create a new payment link var address = CreateAddress(); PaymentLinkRequest paymentLinkRequest = new() { Description = "Test", Amount = new Amount(Currency.EUR, 50), WebhookUrl = DefaultWebhookUrl, RedirectUrl = DefaultRedirectUrl, Reusable = true, ExpiresAt = DateTime.Now.AddDays(1), BillingAddress = address, ShippingAddress = address }; var createdPaymentLinkResponse = await _paymentLinkClient.CreatePaymentLinkAsync(paymentLinkRequest); // When: We retrieve it var retrievePaymentLinkResponse = await _paymentLinkClient.GetPaymentLinkAsync(createdPaymentLinkResponse.Id); // Then: We expect a payment link with the expected properties var verifyPaymentLinkResponse = new Action(response => { var expiresAtWithoutMs = paymentLinkRequest.ExpiresAt.Value.Truncate(TimeSpan.FromSeconds(1)); response.Amount.ShouldBe(paymentLinkRequest.Amount); response.MinimumAmount.ShouldBeNull(); response.ExpiresAt.ShouldBe(expiresAtWithoutMs); response.Description.ShouldBe(paymentLinkRequest.Description); response.RedirectUrl.ShouldBe(paymentLinkRequest.RedirectUrl); response.Archived.ShouldBeFalse(); response.Reusable.ShouldBe(paymentLinkRequest.Reusable); response.BillingAddress.ShouldBe(paymentLinkRequest.BillingAddress); response.ShippingAddress.ShouldBe(paymentLinkRequest.ShippingAddress); }); verifyPaymentLinkResponse(createdPaymentLinkResponse); verifyPaymentLinkResponse(retrievePaymentLinkResponse); } [Fact] public async Task CanCreatePaymentLinkWithMinimumAmount() { // Given: We create a new payment link PaymentLinkRequest paymentLinkRequest = new() { Description = "Test", MinimumAmount = new Amount(Currency.EUR, 50), WebhookUrl = DefaultWebhookUrl, RedirectUrl = DefaultRedirectUrl, Reusable = true, ExpiresAt = DateTime.Now.AddDays(1) }; var createdPaymentLinkResponse = await _paymentLinkClient.CreatePaymentLinkAsync(paymentLinkRequest); // When: We retrieve it var retrievePaymentLinkResponse = await _paymentLinkClient.GetPaymentLinkAsync(createdPaymentLinkResponse.Id); // Then: We expect a payment link with the expected properties var verifyPaymentLinkResponse = new Action(response => { var expiresAtWithoutMs = paymentLinkRequest.ExpiresAt.Value.Truncate(TimeSpan.FromSeconds(1)); response.Amount.ShouldBeNull(); response.MinimumAmount.ShouldBe(paymentLinkRequest.MinimumAmount); response.ExpiresAt.ShouldBe(expiresAtWithoutMs); response.Description.ShouldBe(paymentLinkRequest.Description); response.RedirectUrl.ShouldBe(paymentLinkRequest.RedirectUrl); response.Archived.ShouldBeFalse(); response.Reusable.ShouldBe(paymentLinkRequest.Reusable); }); verifyPaymentLinkResponse(createdPaymentLinkResponse); verifyPaymentLinkResponse(retrievePaymentLinkResponse); } [Fact] public async Task CanCreatePaymentLinkForRecurringPayments() { // Given: We create a new payment link PaymentLinkRequest paymentLinkRequest = new() { Description = "Test", Amount = new Amount(Currency.EUR, 50), WebhookUrl = DefaultWebhookUrl, RedirectUrl = DefaultRedirectUrl, Reusable = true, ExpiresAt = DateTime.Now.AddDays(1), SequenceType = SequenceType.First }; var createdPaymentLinkResponse = await _paymentLinkClient.CreatePaymentLinkAsync(paymentLinkRequest); // When: We retrieve it var retrievePaymentLinkResponse = await _paymentLinkClient.GetPaymentLinkAsync(createdPaymentLinkResponse.Id); // Then: We expect a payment link with the expected properties var verifyPaymentLinkResponse = new Action(response => { var expiresAtWithoutMs = paymentLinkRequest.ExpiresAt.Value.Truncate(TimeSpan.FromSeconds(1)); response.Amount.ShouldBe(paymentLinkRequest.Amount); response.ExpiresAt.ShouldBe(expiresAtWithoutMs); response.Description.ShouldBe(paymentLinkRequest.Description); response.RedirectUrl.ShouldBe(paymentLinkRequest.RedirectUrl); response.Archived.ShouldBeFalse(); response.Reusable.ShouldBe(paymentLinkRequest.Reusable); response.SequenceType.ShouldBe(paymentLinkRequest.SequenceType); }); verifyPaymentLinkResponse(createdPaymentLinkResponse); verifyPaymentLinkResponse(retrievePaymentLinkResponse); } [Fact] public async Task CanCreatePaymentLinkWithLines() { // Given: We create a new payment link PaymentLinkRequest paymentLinkRequest = new() { Description = "Test", Amount = new Amount(Currency.EUR, 90m), WebhookUrl = DefaultWebhookUrl, RedirectUrl = DefaultRedirectUrl, Reusable = false, ExpiresAt = DateTime.Now.AddDays(1), Lines = new List { new() { Type = OrderLineDetailsType.Digital, Description = "Star wars lego", Quantity = 1, QuantityUnit = "pcs", UnitPrice = new Amount(Currency.EUR, 100m), TotalAmount = new Amount(Currency.EUR, 90m), DiscountAmount = new Amount(Currency.EUR, 10m), ProductUrl = "http://www.lego.com/starwars", ImageUrl = "http://www.lego.com/starwars.jpg", Sku = "my-sku", VatAmount = new Amount(Currency.EUR, 15.62m), VatRate = "21.00" } } }; var createdPaymentLinkResponse = await _paymentLinkClient.CreatePaymentLinkAsync(paymentLinkRequest); // When: We retrieve it var retrievePaymentLinkResponse = await _paymentLinkClient.GetPaymentLinkAsync(createdPaymentLinkResponse.Id); // Then: We expect a payment link with the expected properties var verifyPaymentLinkResponse = new Action(response => { var expiresAtWithoutMs = paymentLinkRequest.ExpiresAt.Value.Truncate(TimeSpan.FromSeconds(1)); response.Amount.ShouldBe(paymentLinkRequest.Amount); response.ExpiresAt.ShouldBe(expiresAtWithoutMs); response.Description.ShouldBe(paymentLinkRequest.Description); response.RedirectUrl.ShouldBe(paymentLinkRequest.RedirectUrl); response.Archived.ShouldBeFalse(); response.Reusable.ShouldBe(paymentLinkRequest.Reusable); response.Lines.ShouldBe(paymentLinkRequest.Lines); }); verifyPaymentLinkResponse(createdPaymentLinkResponse); verifyPaymentLinkResponse(retrievePaymentLinkResponse); } [Fact] public async Task CanCreatePaymentLinkWithNullAmount() { // Given: We create a new payment link PaymentLinkRequest paymentLinkRequest = new() { Description = "Test", Amount = null, WebhookUrl = DefaultWebhookUrl, RedirectUrl = DefaultRedirectUrl, ExpiresAt = DateTime.Now.AddDays(1) }; var createdPaymentLinkResponse = await _paymentLinkClient.CreatePaymentLinkAsync(paymentLinkRequest); // When: We retrieve it var retrievePaymentLinkResponse = await _paymentLinkClient.GetPaymentLinkAsync(createdPaymentLinkResponse.Id); // Then: We expect a payment link with the expected properties var verifyPaymentLinkResponse = new Action(response => { var expiresAtWithoutMs = paymentLinkRequest.ExpiresAt.Value.Truncate(TimeSpan.FromSeconds(1)); response.Amount.ShouldBeNull(); response.ExpiresAt.ShouldBe(expiresAtWithoutMs); response.Description.ShouldBe(paymentLinkRequest.Description); response.RedirectUrl.ShouldBe(paymentLinkRequest.RedirectUrl); }); verifyPaymentLinkResponse(createdPaymentLinkResponse); verifyPaymentLinkResponse(retrievePaymentLinkResponse); } [Fact] public async Task CanCreatePaymentLinkWithSpecificPaymentMethods() { // Given: We create a new payment link PaymentLinkRequest paymentLinkRequest = new() { Description = "Test", Amount = new Amount(Currency.EUR, 50), WebhookUrl = DefaultWebhookUrl, RedirectUrl = DefaultRedirectUrl, ExpiresAt = DateTime.Now.AddDays(1), AllowedMethods = [PaymentMethod.Ideal, PaymentMethod.CreditCard] }; var createdPaymentLinkResponse = await _paymentLinkClient.CreatePaymentLinkAsync(paymentLinkRequest); // When: We retrieve it var retrievePaymentLinkResponse = await _paymentLinkClient.GetPaymentLinkAsync(createdPaymentLinkResponse.Id); // Then: We expect a payment link with the expected properties var verifyPaymentLinkResponse = new Action(response => { var expiresAtWithoutMs = paymentLinkRequest.ExpiresAt.Value.Truncate(TimeSpan.FromSeconds(1)); response.Amount.ShouldBe(paymentLinkRequest.Amount); response.ExpiresAt.ShouldBe(expiresAtWithoutMs); response.Description.ShouldBe(paymentLinkRequest.Description); response.RedirectUrl.ShouldBe(paymentLinkRequest.RedirectUrl); response.Archived.ShouldBeFalse(); response.AllowedMethods.ShouldBe(paymentLinkRequest.AllowedMethods); }); verifyPaymentLinkResponse(createdPaymentLinkResponse); verifyPaymentLinkResponse(retrievePaymentLinkResponse); } [Fact] public async Task CanUpdatePaymentLink() { // Given: We create a new payment link PaymentLinkRequest paymentLinkRequest = new() { Description = "Test", Amount = new Amount(Currency.EUR, 50), WebhookUrl = DefaultWebhookUrl, RedirectUrl = DefaultRedirectUrl, ExpiresAt = DateTime.Now.AddDays(1) }; var createdPaymentLinkResponse = await _paymentLinkClient.CreatePaymentLinkAsync(paymentLinkRequest); // When: We update the payment link PaymentLinkUpdateRequest paymentLinkUpdateRequest = new() { Description = "Updated description", Archived = true, AllowedMethods = [PaymentMethod.CreditCard] }; var updatedPaymentLinkResponse = await _paymentLinkClient.UpdatePaymentLinkAsync( createdPaymentLinkResponse.Id, paymentLinkUpdateRequest); // Then: We expect the payment link to be updated updatedPaymentLinkResponse.Description.ShouldBe(paymentLinkUpdateRequest.Description); updatedPaymentLinkResponse.Archived.ShouldBe(paymentLinkUpdateRequest.Archived); updatedPaymentLinkResponse.AllowedMethods.ShouldBe(paymentLinkUpdateRequest.AllowedMethods); } [Fact] public async Task CanDeletePaymentLink() { // Given: We create a new payment link PaymentLinkRequest paymentLinkRequest = new() { Description = "Test", Amount = new Amount(Currency.EUR, 50), WebhookUrl = DefaultWebhookUrl, RedirectUrl = DefaultRedirectUrl, ExpiresAt = DateTime.Now.AddDays(1) }; var createdPaymentLinkResponse = await _paymentLinkClient.CreatePaymentLinkAsync(paymentLinkRequest); // When: We delete the payment link await _paymentLinkClient.DeletePaymentLinkAsync(createdPaymentLinkResponse.Id); // Then: We expect the payment link to be updated MollieApiException exception = await Assert.ThrowsAsync(() => _paymentLinkClient.GetPaymentLinkAsync(createdPaymentLinkResponse.Id)); exception.Details.Status.ShouldBe(404); exception.Details.Detail.ShouldBe("Resource not found"); } [Fact] public async Task CanListPaymentLinkPayments() { // Given: We create a new payment link PaymentLinkRequest paymentLinkRequest = new() { Description = "Test", Amount = new Amount(Currency.EUR, 50), WebhookUrl = DefaultWebhookUrl, RedirectUrl = DefaultRedirectUrl, ExpiresAt = DateTime.Now.AddDays(1) }; var createdPaymentLinkResponse = await _paymentLinkClient.CreatePaymentLinkAsync(paymentLinkRequest); // When: We get the payment list of the payment link var result = await _paymentLinkClient.GetPaymentLinkPaymentListAsync(createdPaymentLinkResponse.Id); // Then: We expect the payment list to be returned result.ShouldNotBeNull(); result.Items.Count.ShouldBe(0); } private PaymentAddressDetails CreateAddress() { return new PaymentAddressDetails { Title = "Mr", GivenName = "John", FamilyName = "Doe", OrganizationName = "Mollie", StreetAndNumber = "Keizersgracht 126", Email = "johndoe@mollie.com", City = "Amsterdam", Country = "NL", Phone = "+31600000000", Region = "Zuid-Holland", PostalCode = "1015CW" }; } public void Dispose() { _paymentLinkClient?.Dispose(); } } ================================================ FILE: tests/Mollie.Tests.Integration/Api/PaymentMethodTests.cs ================================================ using System; using System.Linq; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Client.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment; using Mollie.Api.Models.PaymentMethod.Response; using Mollie.Tests.Integration.Framework; using Xunit; namespace Mollie.Tests.Integration.Api; public class PaymentMethodTests : BaseMollieApiTestClass, IDisposable { private readonly IPaymentMethodClient _paymentMethodClient; public PaymentMethodTests(IPaymentMethodClient paymentMethodClient) { _paymentMethodClient = paymentMethodClient; } [Fact] public async Task CanRetrievePaymentMethodList() { // When: Retrieve payment list with default settings ListResponse response = await _paymentMethodClient.GetPaymentMethodListAsync(); // Then: Make sure it can be retrieved response.ShouldNotBeNull(); response.Items.ShouldNotBeNull(); } [Fact] public async Task CanRetrievePaymentMethodListIncludeWallets() { // When: Retrieve payment list with default settings ListResponse response = await _paymentMethodClient.GetPaymentMethodListAsync(includeWallets: "applepay"); // Then: Make sure it can be retrieved response.ShouldNotBeNull(); response.Items.ShouldNotBeNull(); } [Theory] [InlineData(PaymentMethod.GooglePay)] public async Task CanRetrieveSinglePaymentMethod(string method) { // When: retrieving a payment method PaymentMethodResponse paymentMethod = await _paymentMethodClient.GetPaymentMethodAsync(method); // Then: Make sure it can be retrieved paymentMethod.ShouldNotBeNull(); paymentMethod.Id.ShouldBe(method); } [Fact] public async Task CanRetrieveKbcIssuers() { // When: retrieving the ideal method we can include the issuers PaymentMethodResponse paymentMethod = await _paymentMethodClient.GetPaymentMethodAsync(PaymentMethod.Kbc, true); // Then: We should have one or multiple issuers paymentMethod.ShouldNotBeNull(); paymentMethod.Issuers.ShouldNotBeEmpty(); } [Fact] public async Task DoNotRetrieveIssuersWhenIncludeIsFalse() { // When: retrieving the ideal method with the include parameter set to false PaymentMethodResponse paymentMethod = await _paymentMethodClient.GetPaymentMethodAsync(PaymentMethod.Kbc); // Then: Issuers should not be included paymentMethod.Issuers.ShouldBeNull(); } [Fact] public async Task CanRetrieveAllMethods() { // When: retrieving the all mollie payment methods ListResponse paymentMethods = await _paymentMethodClient.GetAllPaymentMethodListAsync(); // Then: We should have multiple issuers paymentMethods.ShouldNotBeNull(); paymentMethods.Items.ShouldNotBeEmpty(); } [Fact] public async Task CanRetrievePricingForAllMethods() { // When: retrieving the ideal method we can include the issuers ListResponse paymentMethods = await _paymentMethodClient.GetAllPaymentMethodListAsync(includePricing: true); // Then: We should have prices available paymentMethods.Items.All(x => x.Pricing != null && x.Pricing.Any(y => y.Fixed.Value > 0)).ShouldBeTrue(); } [Fact] public async Task CanRetrieveIssuersForAllMethods() { // When: retrieving the all mollie payment methods we can include the issuers ListResponse paymentMethods = await _paymentMethodClient.GetAllPaymentMethodListAsync(includeIssuers: true); // Then: We should have one or multiple issuers paymentMethods.Items.ShouldContain(x => x.Issuers != null); } [Fact] public async Task CanRetrieveIssuersAndPricingInformation() { // When: retrieving the all mollie payment methods we can include the issuers ListResponse paymentMethods = await _paymentMethodClient.GetAllPaymentMethodListAsync(includeIssuers: true, includePricing: true); // Then: We should have one or multiple issuers paymentMethods.Items.ShouldContain(x => x.Issuers != null); paymentMethods.Items.ShouldContain(x => x.Pricing != null && x.Pricing.Any(y => y.Fixed.Value > 0)); } [Theory] [InlineData("JPY", 249)] [InlineData("EUR", 50.25)] public async Task GetPaymentMethodListAsync_WithVariousCurrencies_ReturnsAvailablePaymentMethods(string currency, decimal value) { // When: Retrieving the payment methods for a currency and amount var amount = new Amount(currency, value); var paymentMethods = await _paymentMethodClient.GetPaymentMethodListAsync(amount: amount); // Then: We should have multiple payment methods paymentMethods.Count.ShouldBeGreaterThan(0); } public void Dispose() { _paymentMethodClient?.Dispose(); } } ================================================ FILE: tests/Mollie.Tests.Integration/Api/PaymentTests.cs ================================================ using System; using System.Net.Http; using System.Threading.Tasks; using Mollie.Api.Client; using Mollie.Api.Client.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.Capture; using Mollie.Api.Models.Capture.Request; using Mollie.Api.Models.Payment; using Mollie.Api.Models.Payment.Request; using Mollie.Api.Models.Payment.Response; using Mollie.Tests.Integration.Framework; using System.Collections.Generic; using System.Linq; using Shouldly; using Mollie.Api.Models.Capture.Response; using Mollie.Api.Models.Customer.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Mandate.Response; using Mollie.Api.Models.Order.Request; using Mollie.Api.Models.Payment.Request.PaymentSpecificParameters; using Mollie.Api.Models.Payment.Response.PaymentSpecificParameters; using Mollie.Api.Models.Terminal.Response; using Xunit; using SortDirection = Mollie.Api.Models.SortDirection; namespace Mollie.Tests.Integration.Api; public class PaymentTests : BaseMollieApiTestClass, IDisposable { private readonly IPaymentClient _paymentClient; private readonly ICustomerClient _customerClient; private readonly IMandateClient _mandateClient; private readonly ITerminalClient _terminalClient; private readonly ICaptureClient _captureClient; public PaymentTests( IPaymentClient paymentClient, ICustomerClient customerClient, IMandateClient mandateClient, ITerminalClient terminalClient, ICaptureClient captureClient) { _paymentClient = paymentClient; _customerClient = customerClient; _mandateClient = mandateClient; _terminalClient = terminalClient; _captureClient = captureClient; } [Fact] public async Task CanRetrievePaymentList() { // When: Retrieve payment list with default settings ListResponse response = await _paymentClient.GetPaymentListAsync(); // Then response.ShouldNotBeNull(); response.Items.ShouldNotBeNull(); response.Items.Select(x => x.CreatedAt).ShouldBeInOrder(Shouldly.SortDirection.Descending); } [Fact] public async Task CanRetrievePaymentListInDescendingOrder() { // When: Retrieve payment list in ascending order ListResponse response = await _paymentClient.GetPaymentListAsync(sort: SortDirection.Desc); // Then response.ShouldNotBeNull(); response.Items.ShouldNotBeNull(); response.Items.Select(x => x.CreatedAt).ShouldBeInOrder(Shouldly.SortDirection.Descending); } [Fact] public async Task CanRetrievePaymentListInAscendingOrder() { // When: Retrieve payment list in ascending order ListResponse response = await _paymentClient.GetPaymentListAsync(sort: SortDirection.Asc); // Then response.ShouldNotBeNull(); response.Items.ShouldNotBeNull(); response.Items.Select(x => x.CreatedAt).ShouldBeInOrder(Shouldly.SortDirection.Ascending); } [Fact] public async Task ListPaymentsNeverReturnsMorePaymentsThenTheNumberOfRequestedPayments() { // Given: Number of payments requested is 5 int numberOfPayments = 5; // When: Retrieve 5 payments ListResponse response = await _paymentClient.GetPaymentListAsync(null, numberOfPayments); // Then response.Items.Count.ShouldBeLessThanOrEqualTo(numberOfPayments); } [Fact] public async Task CanCreateDefaultPaymentWithOnlyRequiredFields() { // Given: we create a payment request with only the required parameters var paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = DefaultRedirectUrl }; // When: We send the payment request to Mollie PaymentResponse result = await _paymentClient.CreatePaymentAsync(paymentRequest); // Then: Make sure we get a valid response result.ShouldNotBeNull(); result.Amount.ShouldBe(paymentRequest.Amount); result.Description.ShouldBe(paymentRequest.Description); result.RedirectUrl.ShouldBe(paymentRequest.RedirectUrl); } [Fact] public async Task CanCreateDefaultPaymentWithCustomIdempotencyKey() { // Given: we create a payment request with only the required parameters PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = DefaultRedirectUrl }; // When: We send the payment request to Mollie using (_paymentClient.WithIdempotencyKey("my-idempotency-key")) { PaymentResponse firstAttempt = await _paymentClient.CreatePaymentAsync(paymentRequest); PaymentResponse secondAttempt = await _paymentClient.CreatePaymentAsync(paymentRequest); // Then: Make sure the responses have the same payment Id firstAttempt.Id.ShouldBe(secondAttempt.Id); } } [Fact] public async Task CanCreateDefaultPaymentWithAllFields() { // Given: we create a payment request where all parameters have a value PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = DefaultRedirectUrl, Locale = Locale.nl_NL, Metadata = "{\"firstName\":\"John\",\"lastName\":\"Doe\"}", Method = PaymentMethod.BankTransfer, WebhookUrl = DefaultWebhookUrl }; // When: We send the payment request to Mollie PaymentResponse result = await _paymentClient.CreatePaymentAsync(paymentRequest); // Then: Make sure all requested parameters match the response parameter values result.ShouldNotBeNull(); result.Amount.ShouldBe(paymentRequest.Amount); result.Description.ShouldBe(paymentRequest.Description); result.RedirectUrl.ShouldBe(paymentRequest.RedirectUrl); result.Locale.ShouldBe(paymentRequest.Locale); result.WebhookUrl.ShouldBe(paymentRequest.WebhookUrl); IsJsonResultEqual(result.Metadata, paymentRequest.Metadata).ShouldBeTrue(); } [Fact] public async Task CanUpdatePayment() { // Given: We create a payment with only the required parameters PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = DefaultRedirectUrl }; PaymentResponse result = await _paymentClient.CreatePaymentAsync(paymentRequest); // When: We update this payment PaymentUpdateRequest paymentUpdateRequest = new PaymentUpdateRequest() { Description = "Updated description", Metadata = "My metadata" }; PaymentResponse updatedPayment = await _paymentClient.UpdatePaymentAsync(result.Id, paymentUpdateRequest); // Then: Make sure the payment is updated updatedPayment.Description.ShouldBe(paymentUpdateRequest.Description); updatedPayment.Metadata.ShouldBe(paymentUpdateRequest.Metadata); } [Fact] public async Task CanCreatePaymentWithSinglePaymentMethod() { // Given: we create a payment request and specify multiple payment methods PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = DefaultRedirectUrl, Method = PaymentMethod.CreditCard }; // When: We send the payment request to Mollie PaymentResponse result = await _paymentClient.CreatePaymentAsync(paymentRequest); // Then: Make sure we get a valid response result.ShouldNotBeNull(); result.Amount.ShouldBe(paymentRequest.Amount); result.Description.ShouldBe(paymentRequest.Description); result.RedirectUrl.ShouldBe(paymentRequest.RedirectUrl); result.Method.ShouldBe(paymentRequest.Method); } [Fact] public async Task CanCreatePaymentWithMultiplePaymentMethods() { // When: we create a payment request and specify multiple payment methods PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = DefaultRedirectUrl, Methods = new List() { PaymentMethod.Ideal, PaymentMethod.CreditCard, PaymentMethod.DirectDebit } }; // When: We send the payment request to Mollie PaymentResponse result = await _paymentClient.CreatePaymentAsync(paymentRequest); // Then: Make sure we get a valid response result.ShouldNotBeNull(); result.Amount.ShouldBe(paymentRequest.Amount); result.Description.ShouldBe(paymentRequest.Description); result.RedirectUrl.ShouldBe(paymentRequest.RedirectUrl); result.Method.ShouldBeNull(); } [Theory] [InlineData(typeof(PaymentRequest), PaymentMethod.Bancontact, typeof(BancontactPaymentResponse))] [InlineData(typeof(BankTransferPaymentRequest), PaymentMethod.BankTransfer, typeof(BankTransferPaymentResponse))] [InlineData(typeof(PayPalPaymentRequest), PaymentMethod.PayPal, typeof(PayPalPaymentResponse))] [InlineData(typeof(PaymentRequest), PaymentMethod.Belfius, typeof(BelfiusPaymentResponse))] [InlineData(typeof(PaymentRequest), PaymentMethod.Eps, typeof(EpsPaymentResponse))] [InlineData(typeof(PaymentRequest), null, typeof(PaymentResponse))] public async Task CanCreateSpecificPaymentType(Type paymentType, string paymentMethod, Type expectedResponseType) { // When: we create a specific payment type with some bank transfer specific values PaymentRequest paymentRequest = (PaymentRequest)Activator.CreateInstance(paymentType)!; paymentRequest.Amount = new Amount(Currency.EUR, "100.00"); paymentRequest.Description = "Description"; paymentRequest.RedirectUrl = DefaultRedirectUrl; paymentRequest.Method = paymentMethod; // Set required billing email for Przelewy24 if (paymentRequest is Przelewy24PaymentRequest request) { request.BillingEmail = "example@example.com"; } // When: We send the payment request to Mollie PaymentResponse result = await _paymentClient.CreatePaymentAsync(paymentRequest); // Then: Make sure all requested parameters match the response parameter values result.ShouldNotBeNull(); result.ShouldBeOfType(expectedResponseType); result.Amount.ShouldBe(paymentRequest.Amount); result.Description.ShouldBe(paymentRequest.Description); result.RedirectUrl.ShouldBe(paymentRequest.RedirectUrl); result.Method.ShouldBe(paymentRequest.Method); result.Links.ShouldNotBeNull(); } [Fact] public async Task CanCreatePaymentAndRetrieveIt() { // When: we create a new payment request PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = DefaultRedirectUrl, Locale = Locale.de_DE }; // When: We send the payment request to Mollie and attempt to retrieve it PaymentResponse paymentResponse = await _paymentClient.CreatePaymentAsync(paymentRequest); PaymentResponse result = await _paymentClient.GetPaymentAsync(paymentResponse.Id); // Then result.ShouldNotBeNull(); result.Id.ShouldBe(paymentResponse.Id); result.Amount.ShouldBe(paymentRequest.Amount); result.Description.ShouldBe(paymentRequest.Description); result.RedirectUrl.ShouldBe(paymentRequest.RedirectUrl); result.Method.ShouldBe(paymentRequest.Method); } [Fact] public async Task CanCreateRecurringPaymentAndRetrieveIt() { // When: we create a new recurring payment MandateResponse? mandate = await GetFirstValidMandate(); if (mandate != null) { CustomerResponse customer = await _customerClient.GetCustomerAsync(mandate.Links.Customer); PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = DefaultRedirectUrl, SequenceType = SequenceType.First, CustomerId = customer.Id }; // When: We send the payment request to Mollie and attempt to retrieve it PaymentResponse paymentResponse = await _paymentClient.CreatePaymentAsync(paymentRequest); PaymentResponse result = await _paymentClient.GetPaymentAsync(paymentResponse.Id); // Then: Make sure the recurringtype parameter is entered result.SequenceType.ShouldBe(SequenceType.First); } } [Fact] public async Task CanCreatePaymentWithMetaData() { // When: We create a payment with meta data string metadata = "this is my metadata"; PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = DefaultRedirectUrl, Metadata = metadata }; // When: We send the payment request to Mollie PaymentResponse result = await _paymentClient.CreatePaymentAsync(paymentRequest); // Then: Make sure we get the same json result as metadata result.Metadata.ShouldBe(metadata); } [Fact] public async Task CanCreatePaymentWithJsonMetaData() { // When: We create a payment with meta data string json = "{\"order_id\":\"4.40\"}"; PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = DefaultRedirectUrl, Metadata = json }; // When: We send the payment request to Mollie PaymentResponse result = await _paymentClient.CreatePaymentAsync(paymentRequest); // Then: Make sure we get the same json result as metadata IsJsonResultEqual(result.Metadata, json).ShouldBeTrue(); } [Fact] public async Task CanCreatePaymentWithCustomMetaDataClass() { // When: We create a payment with meta data CustomMetadataClass metadataRequest = new CustomMetadataClass() { OrderId = 1, Description = "Custom description" }; PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = DefaultRedirectUrl, }; paymentRequest.SetMetadata(metadataRequest); // When: We send the payment request to Mollie PaymentResponse result = await _paymentClient.CreatePaymentAsync(paymentRequest); CustomMetadataClass? metadataResponse = result.GetMetadata(); // Then: Make sure we get the same json result as metadata metadataResponse.ShouldNotBeNull(); metadataResponse.OrderId.ShouldBe(metadataRequest.OrderId); metadataResponse.Description.ShouldBe(metadataRequest.Description); } [Fact] public async Task CanCreatePaymentWithLines() { // Arrange var address = new PaymentAddressDetails { Title = "Mr", GivenName = "John", FamilyName = "Doe", OrganizationName = "Mollie", StreetAndNumber = "Keizersgracht 126", Email = "johndoe@mollie.com", City = "Amsterdam", Country = "NL", Phone = "+31600000000", Region = "Zuid-Holland", PostalCode = "1015CW" }; PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, 90m), Description = "Description", RedirectUrl = DefaultRedirectUrl, Lines = new List() { new() { Type = OrderLineDetailsType.Digital, Description = "Star wars lego", Quantity = 1, QuantityUnit = "pcs", UnitPrice = new Amount(Currency.EUR, 100m), TotalAmount = new Amount(Currency.EUR, 90m), DiscountAmount = new Amount(Currency.EUR, 10m), ProductUrl = "http://www.lego.com/starwars", ImageUrl = "http://www.lego.com/starwars.jpg", Sku = "my-sku", VatAmount = new Amount(Currency.EUR, 15.62m), VatRate = "21.00" } }, ShippingAddress = address, BillingAddress = address }; // Act PaymentResponse result = await _paymentClient.CreatePaymentAsync(paymentRequest); // Assert result.Lines.ShouldBeEquivalentTo(paymentRequest.Lines); result.BillingAddress.ShouldBeEquivalentTo(paymentRequest.BillingAddress); result.ShippingAddress.ShouldBeEquivalentTo(paymentRequest.ShippingAddress); } [Fact] public async Task CanCreatePaymentWithMandate() { // When: We create a payment with a mandate id MandateResponse? validMandate = await GetFirstValidMandate(); if (validMandate != null) { CustomerResponse customer = await _customerClient.GetCustomerAsync(validMandate.Links.Customer); PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = DefaultRedirectUrl, SequenceType = SequenceType.Recurring, CustomerId = customer.Id, MandateId = validMandate.Id }; // When: We send the payment request to Mollie PaymentResponse result = await _paymentClient.CreatePaymentAsync(paymentRequest); // Then: Make sure we get the mandate id back in the details result.MandateId.ShouldBe(validMandate.Id); result.Links.Mandate!.Href.ShouldEndWith(validMandate.Id); result.Links.Customer!.Href.ShouldEndWith(customer.Id); } } [Fact] public async Task CanCreatePaymentWithDecimalAmountAndRetrieveIt() { // When: we create a new payment request PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, 100.1235m), Description = "Description", RedirectUrl = DefaultRedirectUrl, Locale = Locale.de_DE }; // When: We send the payment request to Mollie and attempt to retrieve it PaymentResponse paymentResponse = await _paymentClient.CreatePaymentAsync(paymentRequest); PaymentResponse result = await _paymentClient.GetPaymentAsync(paymentResponse.Id); // Then result.ShouldNotBeNull(); result.Id.ShouldBe(paymentResponse.Id); result.Amount.ShouldBe(paymentRequest.Amount); result.Description.ShouldBe(paymentRequest.Description); result.RedirectUrl.ShouldBe(paymentRequest.RedirectUrl); } [Fact] public async Task CanCreatePaymentWithImplicitAmountCastAndRetrieveIt() { var initialAmount = 100.75m; // When: we create a new payment request PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, initialAmount), Description = "Description", RedirectUrl = DefaultRedirectUrl, Locale = Locale.de_DE }; // When: We send the payment request to Mollie and attempt to retrieve it PaymentResponse paymentResponse = await _paymentClient.CreatePaymentAsync(paymentRequest); PaymentResponse result = await _paymentClient.GetPaymentAsync(paymentResponse.Id); decimal responseAmount = paymentResponse.Amount; // Implicit cast decimal resultAmount = result.Amount; // Implicit cast // Then result.ShouldNotBeNull(); result.Id.ShouldBe(paymentResponse.Id); result.Amount.ShouldBe(paymentRequest.Amount); result.Description.ShouldBe(paymentRequest.Description); result.RedirectUrl.ShouldBe(paymentRequest.RedirectUrl); resultAmount.ShouldBe(responseAmount); resultAmount.ShouldBe(initialAmount); } [Fact] public async Task CanCreatePointOfSalePayment() { // Given ListResponse terminals = await _terminalClient.GetTerminalListAsync(); TerminalResponse? terminal = terminals.Items.FirstOrDefault(); if (terminal != null) { string terminalId = terminals.Items.First().Id; PointOfSalePaymentRequest paymentRequest = new() { Amount = new Amount(Currency.EUR, 10m), Description = "Description", Method = PaymentMethod.PointOfSale, TerminalId = terminalId }; // When PaymentResponse response = await _paymentClient.CreatePaymentAsync(paymentRequest); // Then response.ShouldNotBeNull(); response.Amount.ShouldBe(paymentRequest.Amount); response.Description.ShouldBe(paymentRequest.Description); response.RedirectUrl.ShouldBe(paymentRequest.RedirectUrl); response.ShouldBeOfType(); PointOfSalePaymentResponse posResponse = (PointOfSalePaymentResponse)response; posResponse.Details!.TerminalId.ShouldBe(paymentRequest.TerminalId); posResponse.Details.CardNumber.ShouldBeNull(); posResponse.Details.CardFingerprint.ShouldBeNull(); posResponse.Details.CardAudience.ShouldBeNull(); posResponse.Details.CardLabel.ShouldBeNull(); posResponse.Details.CardCountryCode.ShouldBeNull(); posResponse.Method.ShouldBe(PaymentMethod.PointOfSale); } } [Fact(Skip = "We can only test this in debug mode, because we have to set the payment status to authorized")] public async Task CanCreatePaymentWithManualCaptureMode() { // Given PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, 10m), Description = "Description", RedirectUrl = DefaultRedirectUrl, Method = PaymentMethod.CreditCard, CaptureMode = CaptureMode.Manual }; // When PaymentResponse paymentResponse = await _paymentClient.CreatePaymentAsync(paymentRequest); // Perform payment before API call paymentResponse = await _paymentClient.GetPaymentAsync(paymentResponse.Id); CaptureResponse captureResponse = await _captureClient.CreateCapture(paymentResponse.Id, new CaptureRequest { Amount = new Amount(Currency.EUR, 10m), Description = "capture" }); // Then captureResponse.ShouldNotBeNull(); paymentResponse.Status.ShouldBe(PaymentStatus.Authorized); paymentRequest.CaptureMode.ShouldBe(CaptureMode.Manual); paymentResponse.CaptureBefore.ShouldNotBeNull(); } [Fact] public async Task CanCreatePaymentWithCaptureDelay() { // Given PaymentRequest paymentRequest = new() { Amount = new Amount(Currency.EUR, 10m), Description = "Description", RedirectUrl = DefaultRedirectUrl, Method = PaymentMethod.CreditCard, CaptureDelay = "2 days" }; // When PaymentResponse paymentResponse = await _paymentClient.CreatePaymentAsync(paymentRequest); // Then paymentResponse.CaptureDelay.ShouldBe(paymentRequest.CaptureDelay); } private async Task GetFirstValidMandate() { ListResponse customers = await _customerClient.GetCustomerListAsync(); foreach (CustomerResponse customer in customers.Items) { ListResponse customerMandates = await _mandateClient.GetMandateListAsync(customer.Id); MandateResponse? firstValidMandate = customerMandates.Items.FirstOrDefault(x => x.Status == MandateStatus.Valid); if (firstValidMandate != null) { return firstValidMandate; } } return null; } public void Dispose() { _paymentClient.Dispose(); _customerClient.Dispose(); _mandateClient.Dispose(); _terminalClient.Dispose(); _captureClient.Dispose(); } } public record CustomMetadataClass { public required int OrderId { get; init; } public required string Description { get; init; } } ================================================ FILE: tests/Mollie.Tests.Integration/Api/ProfileTests.cs ================================================  using System; using Mollie.Api.Client; using Mollie.Api.Models.Payment; using Mollie.Api.Models.Profile.Response; using Mollie.Tests.Integration.Framework; using System.Linq; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.PaymentMethod.Response; using Mollie.Api.Models.Profile.Request; using Xunit; namespace Mollie.Tests.Integration.Api; public class ProfileTests : BaseMollieApiTestClass, IDisposable { private readonly IProfileClient _profileClient; public ProfileTests(IProfileClient profileClient) { _profileClient = profileClient; } [Fact] public async Task GetCurrentProfileAsync_ReturnsCurrentProfile() { // Given // When: We retrieve the current profile from the mollie API ProfileResponse profileResponse = await _profileClient.GetCurrentProfileAsync(); // Then: Make sure we get a valid response profileResponse.ShouldNotBeNull(); profileResponse.Id.ShouldNotBeNullOrEmpty(); profileResponse.Email.ShouldNotBeNullOrEmpty(); profileResponse.Status.ShouldNotBeNullOrEmpty(); } [Fact] public async Task EnablePaymentMethodAsync_WhenEnablingPaymentMethodForCurrentProfile_PaymentMethodIsReturned() { // Given // When: We enable a payment method for the current profile PaymentMethodResponse paymentMethodResponse = await _profileClient.EnablePaymentMethodAsync(PaymentMethod.CreditCard); // Then: Make sure a payment method is returned paymentMethodResponse.ShouldNotBeNull(); paymentMethodResponse.Id.ShouldBe(PaymentMethod.CreditCard); } [Fact(Skip = "We can only test this in debug mode, because we need to retrieve a oauth access token to test this method")] public async Task EnablePaymentMethodAsync_WhenEnablingPaymentMethodForProfile_PaymentMethodIsReturned() { // Given: We retrieve the profile from the API ProfileClient profileClient = new ProfileClient("abcde"); // Set access token ListResponse allProfiles = await profileClient.GetProfileListAsync(); if (allProfiles.Items.Count > 0) { ProfileResponse profileToTestWith = allProfiles.Items.First(); // When: We enable a payment method for the given profile PaymentMethodResponse paymentMethodResponse = await profileClient.EnablePaymentMethodAsync(profileToTestWith.Id, PaymentMethod.Ideal); // Then: Make sure a payment method is returned paymentMethodResponse.ShouldNotBeNull(); paymentMethodResponse.Id.ShouldBeNullOrEmpty(); } } [Fact(Skip = "We can only test this in debug mode, because we need to retrieve a oauth access token to test this method")] public async Task CreateProfileAsync_WithDefaultParameters_CreatesProfile() { // Given ProfileRequest profileRequest = new ProfileRequest { Email = "test@test.com", Mode = Mode.Test, Name = "testuser", BusinessCategory = "PET_SHOPS", Website = "http://github.com", Phone = "+31600000000" }; ProfileClient profileClient = new ProfileClient("accesstoken"); // Set access token // When: We create a new profile ProfileResponse profileResponse = await profileClient.CreateProfileAsync(profileRequest); // Then: Make sure the profile that is created matched the profile request profileResponse.ShouldNotBeNull(); } [Fact(Skip = "Don't disable payment methods, other tests might break")] public async Task DisablePaymentMethodAsync_WhenDisablingPaymentMethodForCurrentProfile_NoErrorIsThrown() { // Given // When: We disable a payment method for the current profile await _profileClient.DisablePaymentMethodAsync(PaymentMethod.CreditCard); // Then } [Fact] public async Task DisableGiftCardIssuerAsync_WhenDisablingGiftCardIssuerForCurrentProfile_NoErrorIsThrown() { // Given // When: We disable a issuer method for the current profile await _profileClient.DisableGiftCardIssuerAsync("festivalcadeau"); // Then } public void Dispose() { _profileClient?.Dispose(); } } ================================================ FILE: tests/Mollie.Tests.Integration/Api/RefundTests.cs ================================================ using System; using System.Diagnostics; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Client.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment.Request; using Mollie.Api.Models.Payment.Request.PaymentSpecificParameters; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Refund.Request; using Mollie.Api.Models.Refund.Response; using Mollie.Tests.Integration.Framework; using Xunit; namespace Mollie.Tests.Integration.Api; public class RefundTests : BaseMollieApiTestClass, IDisposable { private readonly IRefundClient _refundClient; private readonly IPaymentClient _paymentClient; public RefundTests(IRefundClient refundClient, IPaymentClient paymentClient) { _refundClient = refundClient; _paymentClient = paymentClient; } [Fact(Skip = "We can only test this in debug mode, because we actually have to use the PaymentUrl to make the payment, since Mollie can only refund payments that have been paid")] public async Task CanCreateRefund() { // If: We create a payment string amount = "100.00"; PaymentResponse payment = await CreatePayment(amount); // We can only test this if you make the payment using the payment.Links.Checkout property. // If you don't do this, this test will fail because we can only refund payments that have been paid Debugger.Break(); // When: We attempt to refund this payment RefundRequest refundRequest = new RefundRequest() { Amount = new Amount(Currency.EUR, amount) }; RefundResponse refundResponse = await _refundClient.CreatePaymentRefundAsync(payment.Id, refundRequest); // Then refundResponse.ShouldNotBeNull(); } [Fact(Skip = "We can only test this in debug mode, because we actually have to use the PaymentUrl to make the payment, since Mollie can only refund payments that have been paid")] public async Task CanCreatePartialRefund() { // If: We create a payment of 250 euro PaymentResponse payment = await CreatePayment("250.00"); // We can only test this if you make the payment using the payment.Links.PaymentUrl property. // If you don't do this, this test will fail because we can only refund payments that have been paid Debugger.Break(); // When: We attempt to refund 50 euro RefundRequest refundRequest = new RefundRequest() { Amount = new Amount(Currency.EUR, "50.00") }; RefundResponse refundResponse = await _refundClient.CreatePaymentRefundAsync(payment.Id, refundRequest); // Then refundResponse.Amount.ShouldBe(refundRequest.Amount); } [Fact(Skip = "We can only test this in debug mode, because we actually have to use the PaymentUrl to make the payment, since Mollie can only refund payments that have been paid")] public async Task CanRetrieveSingleRefund() { // If: We create a payment PaymentResponse payment = await CreatePayment(); // We can only test this if you make the payment using the payment.Links.PaymentUrl property. // If you don't do this, this test will fail because we can only refund payments that have been paid Debugger.Break(); RefundRequest refundRequest = new RefundRequest() { Amount = new Amount(Currency.EUR, "50.00") }; RefundResponse refundResponse = await _refundClient.CreatePaymentRefundAsync(payment.Id, refundRequest); // When: We attempt to retrieve this refund RefundResponse result = await _refundClient.GetPaymentRefundAsync(payment.Id, refundResponse.Id); // Then result.ShouldNotBeNull(); result.Id.ShouldBe(refundResponse.Id); refundResponse.Amount.ShouldBe(refundRequest.Amount); } [Fact] public async Task CanRetrieveRefundList() { // If: We create a payment PaymentResponse payment = await CreatePayment(); // When: Retrieve refund list for this payment after one second var test = await ExecuteWithRetry(() => _refundClient.GetPaymentRefundListAsync(payment.Id)); ListResponse refundList = await _refundClient.GetPaymentRefundListAsync(payment.Id); // Then refundList.ShouldNotBeNull(); refundList.Items.ShouldNotBeNull(); } [Fact(Skip = "We can only test this in debug mode, because we actually have to use the PaymentUrl to make the payment, since Mollie can only refund payments that have been paid")] public async Task CanCreateRefundWithMetaData() { // If: We create a payment string amount = "100.00"; PaymentResponse payment = await CreatePayment(amount); // We can only test this if you make the payment using the payment.Links.Checkout property. // If you don't do this, this test will fail because we can only refund payments that have been paid Debugger.Break(); // When: We attempt to refund this payment with meta data. var metadata = "this is my metadata"; RefundRequest refundRequest = new RefundRequest() { Amount = new Amount(Currency.EUR, amount), Metadata = metadata }; RefundResponse refundResponse = await _refundClient.CreatePaymentRefundAsync(payment.Id, refundRequest); // Then: Make sure we get the same json result as metadata refundResponse.Metadata.ShouldBe(metadata); } private async Task CreatePayment(string amount = "100.00") { PaymentRequest paymentRequest = new PayPalPaymentRequest { Amount = new Amount(Currency.EUR, amount), Description = "Description", RedirectUrl = DefaultRedirectUrl }; return await _paymentClient.CreatePaymentAsync(paymentRequest); } public void Dispose() { _refundClient?.Dispose(); _paymentClient?.Dispose(); } } ================================================ FILE: tests/Mollie.Tests.Integration/Api/SalesInvoiceTests.cs ================================================ using System; using System.Linq; using System.Net; using System.Threading.Tasks; using Mollie.Api.Client; using Mollie.Api.Client.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment; using Mollie.Api.Models.SalesInvoice; using Mollie.Api.Models.SalesInvoice.Request; using Mollie.Api.Models.SalesInvoice.Response; using Mollie.Api.Models.Url; using Mollie.Tests.Integration.Framework; using Shouldly; using Xunit; namespace Mollie.Tests.Integration.Api; [Trait("TestCategory", "LocalIntegrationTests")] public class SalesInvoiceTests : BaseMollieApiTestClass, IDisposable { private readonly ISalesInvoiceClient _salesInvoiceClient; public SalesInvoiceTests(ISalesInvoiceClient salesInvoiceClient) { _salesInvoiceClient = salesInvoiceClient; } [Fact] public async Task CreateSalesInvoiceAsync_WithRequiredFields_SalesInvoiceIsCreated() { // Given: We create a new sales invoice var request = CreateSalesInvoiceRequest(); // When var response = await _salesInvoiceClient.CreateSalesInvoiceAsync(request); // Then AssertSalesInvoice(request, response); } [Fact] public async Task GetSalesInvoiceListAsync_NoParameters_SalesInvoiceListIsRetrieved() { // When: Retrieve sales invoice list with default settings ListResponse response = await _salesInvoiceClient.GetSalesInvoiceListAsync(); // Then response.ShouldNotBeNull(); response.Items.ShouldNotBeNull(); response.Links.ShouldNotBeNull(); response.Links.Self.Href.ShouldEndWith("sales-invoices"); } [Fact] public async Task GetSalesInvoiceListAsync_WithObjectUrlLink_SalesInvoiceListIsRetrieved() { // When: Retrieve sales invoice list with object URL link var urlObjectLink = new UrlObjectLink> { Href = "https://api.mollie.com/v2/sales-invoices", Type = "application/hal+json" }; ListResponse response = await _salesInvoiceClient.GetSalesInvoiceListAsync(urlObjectLink); // Then response.ShouldNotBeNull(); response.Items.ShouldNotBeNull(); response.Links.ShouldNotBeNull(); response.Links.Self.Href.ShouldEndWith("sales-invoices"); } [Fact] public async Task GetSalesInvoiceListAsync_WithMaximumNumberOfItems_MaximumNumberOfSalesInvoicesIsReturned() { // Given: Number of sales invoices requested is 5 int numberOfSalesInvoices = 5; // When: Retrieve 5 sales invoices ListResponse response = await _salesInvoiceClient.GetSalesInvoiceListAsync(null, numberOfSalesInvoices); // Then response.Items.Count.ShouldBeLessThanOrEqualTo(numberOfSalesInvoices); } [Fact] public async Task GetSalesInvoiceAsync_SalesInvoiceCanBeRetrieved() { // Given: We create a new sales invoice var salesInvoiceRequest = CreateSalesInvoiceRequest(); var createdSalesInvoice = await _salesInvoiceClient.CreateSalesInvoiceAsync(salesInvoiceRequest); // When: We retrieve the sales invoice var retrievedSalesInvoice = await _salesInvoiceClient.GetSalesInvoiceAsync(createdSalesInvoice.Id); // Then: The retrieved sales invoice should match the created one AssertSalesInvoice(salesInvoiceRequest, retrievedSalesInvoice); } [Fact] public async Task GetSalesInvoiceAsync_WithObjectUrlLink_SalesInvoiceCanBeRetrieved() { // Given: We create a new sales invoice var salesInvoiceRequest = CreateSalesInvoiceRequest(); var createdSalesInvoice = await _salesInvoiceClient.CreateSalesInvoiceAsync(salesInvoiceRequest); // When: We retrieve the sales invoice var retrievedSalesInvoice = await _salesInvoiceClient.GetSalesInvoiceAsync(createdSalesInvoice.Links.Self); // Then: The retrieved sales invoice should match the created one AssertSalesInvoice(salesInvoiceRequest, retrievedSalesInvoice); } [Fact] public async Task UpdateSalesInvoiceAsync_UpdatesSalesInvoice() { // Given: We create a new sales invoice var salesInvoiceRequest = CreateSalesInvoiceRequest(); var createdSalesInvoice = await _salesInvoiceClient.CreateSalesInvoiceAsync(salesInvoiceRequest); // When: We update the sales invoice var updatedSalesInvoiceRequest = new SalesInvoiceUpdateRequest { Memo = "Updated memo" }; var updatedSalesInvoice = await _salesInvoiceClient.UpdateSalesInvoiceAsync(createdSalesInvoice.Id, updatedSalesInvoiceRequest); // Then: The updated sales invoice should match the updated request updatedSalesInvoice.ShouldNotBeNull(); updatedSalesInvoice.Memo.ShouldBe(updatedSalesInvoiceRequest.Memo); updatedSalesInvoice.Lines!.Count().ShouldBe(1); } [Fact] public async Task DeleteSalesInvoiceAsync_DeletesSalesInvoice() { // If: We retrieve a list of sales invoices ListResponse response = await _salesInvoiceClient.GetSalesInvoiceListAsync(); // When: We delete one of the sales invoices in the list var salesInvoiceToDelete = response.Items.FirstOrDefault(x => x.Status == SalesInvoiceStatus.Draft); if (salesInvoiceToDelete != null) { await _salesInvoiceClient.DeleteSalesInvoiceAsync(salesInvoiceToDelete.Id); // Then: Make sure the sales invoice is deleted MollieApiException apiException = await Assert.ThrowsAsync(() => _salesInvoiceClient.GetSalesInvoiceAsync(salesInvoiceToDelete.Id)); apiException.Details.Status.ShouldBe((int)HttpStatusCode.NotFound); } } private SalesInvoiceRequest CreateSalesInvoiceRequest() { return new SalesInvoiceRequest { WebhookUrl = "https://github.com/Viincenttt/MollieApi", Status = SalesInvoiceStatus.Draft, PaymentTerm = PaymentTerm.Days30, VatMode = VatMode.Exclusive, VatScheme = VatScheme.Standard, Lines = new[] { new SalesInvoiceLine { Description = "Lego Batman", Quantity = 1, VatRate = "21.00", UnitPrice = new Amount(Currency.EUR, 50m) } }, RecipientIdentifier = Guid.NewGuid().ToString(), Recipient = new Recipient { Type = RecipientType.Consumer, Email = "example@example.com", FamilyName = "Smit", GivenName = "Jan", StreetAndNumber = "Keizersgracht 313", PostalCode = "1234AB", City = "Amsterdam", Country = "NL", Locale = Locale.nl_NL } }; } private void AssertSalesInvoice(SalesInvoiceRequest request, SalesInvoiceResponse response) { response.ShouldNotBeNull(); response.Id.ShouldNotBeNullOrEmpty(); response.Resource.ShouldBe("sales-invoice"); response.ProfileId.ShouldStartWith("pfl_"); response.Status.ShouldBe(request.Status); response.PaymentTerm.ShouldBe(request.PaymentTerm); response.Lines.ShouldNotBeNull(); response.Lines.ShouldHaveSingleItem(); response.Lines.Single().ShouldBeEquivalentTo(response.Lines.Single()); response.Recipient.ShouldNotBeNull(); response.Recipient.Email.ShouldBe(request.Recipient.Email); response.Recipient.FamilyName.ShouldBe(request.Recipient.FamilyName); response.Recipient.GivenName.ShouldBe(request.Recipient.GivenName); response.Recipient.StreetAndNumber.ShouldBe(request.Recipient.StreetAndNumber); response.Recipient.PostalCode.ShouldBe(request.Recipient.PostalCode); response.Recipient.City.ShouldBe(request.Recipient.City); response.Recipient.Country.ShouldBe(request.Recipient.Country); response.RecipientIdentifier.ShouldBe(request.RecipientIdentifier); response.Recipient.Type.ShouldBe(RecipientType.Consumer); response.WebhookUrl.ShouldBe(request.WebhookUrl); } public void Dispose() { _salesInvoiceClient.Dispose(); } } ================================================ FILE: tests/Mollie.Tests.Integration/Api/SessionTests.cs ================================================ using System; using System.Threading.Tasks; using Mollie.Api.Client.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.Session.Request; using Mollie.Api.Models.Session.Response; using Mollie.Tests.Integration.Framework; using System.Collections.Generic; using Shouldly; using Mollie.Api.Models.Order.Request; using Xunit; using Mollie.Api.Models.Payment; namespace Mollie.Tests.Integration.Api; [Trait("TestCategory", "LocalIntegrationTests")] public class SessionTests : BaseMollieApiTestClass, IDisposable { private readonly ISessionClient _sessionClient; public SessionTests( ISessionClient sessionClient) { _sessionClient = sessionClient; } [Fact] public async Task CanCreateDefaultSessionWithOnlyRequiredFields() { // Given: we create a session request with only the required parameters var sessionRequest = new SessionRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = DefaultRedirectUrl }; // When: We send the session request to Mollie SessionResponse result = await _sessionClient.CreateSessionAsync(sessionRequest); // Then: Make sure we get a valid response result.ShouldNotBeNull(); result.Amount.ShouldBe(sessionRequest.Amount); result.Description.ShouldBe(sessionRequest.Description); result.RedirectUrl.ShouldBe(sessionRequest.RedirectUrl); } [Fact] public async Task CanCreateDefaultSessionWithCustomIdempotencyKey() { // Given: we create a session request with only the required parameters SessionRequest sessionRequest = new SessionRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = DefaultRedirectUrl }; // When: We send the session request to Mollie using (_sessionClient.WithIdempotencyKey("my-idempotency-key")) { SessionResponse firstAttempt = await _sessionClient.CreateSessionAsync(sessionRequest); SessionResponse secondAttempt = await _sessionClient.CreateSessionAsync(sessionRequest); // Then: Make sure the responses have the same session Id firstAttempt.Id.ShouldBe(secondAttempt.Id); } } [Fact] public async Task CanCreateSessionAndRetrieveIt() { // When: we create a new session request SessionRequest sessionRequest = new SessionRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = DefaultRedirectUrl }; // When: We send the session request to Mollie and attempt to retrieve it SessionResponse sessionResponse = await _sessionClient.CreateSessionAsync(sessionRequest); SessionResponse result = await _sessionClient.GetSessionAsync(sessionResponse.Id); // Then result.ShouldNotBeNull(); result.Id.ShouldBe(sessionResponse.Id); result.Amount.ShouldBe(sessionRequest.Amount); result.Description.ShouldBe(sessionRequest.Description); result.RedirectUrl.ShouldBe(sessionRequest.RedirectUrl); } [Fact] public async Task CanCreateSessionWithJsonMetaData() { // When: We create a session with meta data string json = "{\"order_id\":\"4.40\"}"; SessionRequest sessionRequest = new SessionRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = DefaultRedirectUrl, Metadata = json }; // When: We send the session request to Mollie SessionResponse result = await _sessionClient.CreateSessionAsync(sessionRequest); // Then: Make sure we get the same json result as metadata IsJsonResultEqual(result.Metadata, json).ShouldBeTrue(); } [Fact] public async Task CanCreateSessionWithCustomMetaDataClass() { // When: We create a session with meta data CustomMetadataClass metadataRequest = new CustomMetadataClass() { OrderId = 1, Description = "Custom description" }; SessionRequest sessionRequest = new SessionRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = DefaultRedirectUrl, }; sessionRequest.SetMetadata(metadataRequest); // When: We send the session request to Mollie SessionResponse result = await _sessionClient.CreateSessionAsync(sessionRequest); CustomMetadataClass? metadataResponse = result.GetMetadata(); // Then: Make sure we get the same json result as metadata metadataResponse.ShouldNotBeNull(); metadataResponse.OrderId.ShouldBe(metadataRequest.OrderId); metadataResponse.Description.ShouldBe(metadataRequest.Description); } [Fact] public async Task CanCreateSessionWithLines() { // Arrange var address = new PaymentAddressDetails { Title = "Mr", GivenName = "John", FamilyName = "Doe", OrganizationName = "Mollie", StreetAndNumber = "Keizersgracht 126", Email = "johndoe@mollie.com", City = "Amsterdam", Country = "NL", Phone = "+31600000000", Region = "Zuid-Holland", PostalCode = "1015CW" }; SessionRequest sessionRequest = new SessionRequest() { Amount = new Amount(Currency.EUR, 90m), Description = "Description", RedirectUrl = DefaultRedirectUrl, Lines = new List() { new() { Type = OrderLineDetailsType.Digital, Description = "Star wars lego", Quantity = 1, QuantityUnit = "pcs", UnitPrice = new Amount(Currency.EUR, 100m), TotalAmount = new Amount(Currency.EUR, 90m), DiscountAmount = new Amount(Currency.EUR, 10m), ProductUrl = "http://www.lego.com/starwars", ImageUrl = "http://www.lego.com/starwars.jpg", Sku = "my-sku", VatAmount = new Amount(Currency.EUR, 15.62m), VatRate = "21.00" } }, ShippingAddress = address, BillingAddress = address }; // Act SessionResponse result = await _sessionClient.CreateSessionAsync(sessionRequest); // Assert //result.Lines.ShouldBeEquivalentTo(sessionRequest.Lines); Lines are ignored by Mollie's bug result.BillingAddress.ShouldBeEquivalentTo(sessionRequest.BillingAddress); result.ShippingAddress.ShouldBeEquivalentTo(sessionRequest.ShippingAddress); } [Fact] public async Task CanCreateSessionWithDecimalAmountAndRetrieveIt() { // When: we create a new session request SessionRequest sessionRequest = new SessionRequest() { Amount = new Amount(Currency.EUR, 100.1235m), Description = "Description", RedirectUrl = DefaultRedirectUrl }; // When: We send the session request to Mollie and attempt to retrieve it SessionResponse sessionResponse = await _sessionClient.CreateSessionAsync(sessionRequest); SessionResponse result = await _sessionClient.GetSessionAsync(sessionResponse.Id); // Then result.ShouldNotBeNull(); result.Id.ShouldBe(sessionResponse.Id); result.Amount.ShouldBe(sessionRequest.Amount); result.Description.ShouldBe(sessionRequest.Description); result.RedirectUrl.ShouldBe(sessionRequest.RedirectUrl); } [Fact] public async Task CanCreateSessionWithImplicitAmountCastAndRetrieveIt() { var initialAmount = 100.75m; // When: we create a new session request SessionRequest sessionRequest = new SessionRequest() { Amount = new Amount(Currency.EUR, initialAmount), Description = "Description", RedirectUrl = DefaultRedirectUrl }; // When: We send the session request to Mollie and attempt to retrieve it SessionResponse sessionResponse = await _sessionClient.CreateSessionAsync(sessionRequest); SessionResponse result = await _sessionClient.GetSessionAsync(sessionResponse.Id); decimal responseAmount = sessionResponse.Amount; // Implicit cast decimal resultAmount = result.Amount; // Implicit cast // Then result.ShouldNotBeNull(); result.Id.ShouldBe(sessionResponse.Id); result.Amount.ShouldBe(sessionRequest.Amount); result.Description.ShouldBe(sessionRequest.Description); result.RedirectUrl.ShouldBe(sessionRequest.RedirectUrl); resultAmount.ShouldBe(responseAmount); resultAmount.ShouldBe(initialAmount); } public void Dispose() { _sessionClient.Dispose(); } } ================================================ FILE: tests/Mollie.Tests.Integration/Api/ShipmentTests.cs ================================================ using System; using System.Collections.Generic; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Client.Abstract; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Shipment.Request; using Mollie.Api.Models.Shipment.Response; using Mollie.Tests.Integration.Framework; using Xunit; namespace Mollie.Tests.Integration.Api; public class ShipmentTests : BaseMollieApiTestClass, IDisposable { private readonly IShipmentClient _shipmentClient; public ShipmentTests(IShipmentClient shipmentClient) { _shipmentClient = shipmentClient; } [Fact(Skip = "For manual testing only")] public async Task CanCreateShipmentWithOnlyRequiredFields() { // the order needs to be autorized to do a shipment on. this can only be done by waiting. string validOrderId = "XXXXX"; ShipmentRequest shipmentRequest = CreateShipmentWithOnlyRequiredFields(); ShipmentResponse result = await _shipmentClient.CreateShipmentAsync(validOrderId, shipmentRequest); // Then: Make sure we get a valid shipment response result.ShouldNotBeNull(); result.CreatedAt.ShouldBeGreaterThan(DateTime.Now); } [Fact(Skip = "For manual testing only")] public async Task CanListShipmentsForOrder(){ string validOrderId = "XXXXX"; ListResponse result = await _shipmentClient.GetShipmentListAsync(validOrderId); result.ShouldNotBeNull(); result.Count.ShouldBeGreaterThan(0); } private ShipmentRequest CreateShipmentWithOnlyRequiredFields() { return new ShipmentRequest() { Lines = new List() }; } public void Dispose() { _shipmentClient?.Dispose(); } } ================================================ FILE: tests/Mollie.Tests.Integration/Api/SubscriptionTests.cs ================================================ using System; using System.Linq; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.Customer.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Mandate.Response; using Mollie.Api.Models.Subscription.Request; using Mollie.Api.Models.Subscription.Response; using Mollie.Tests.Integration.Framework; using Xunit; namespace Mollie.Tests.Integration.Api; public class SubscriptionTests : BaseMollieApiTestClass, IDisposable { private readonly ISubscriptionClient _subscriptionClient; private readonly ICustomerClient _customerClient; private readonly IMandateClient _mandateClient; public SubscriptionTests( ISubscriptionClient subscriptionClient, ICustomerClient customerClient, IMandateClient mandateClient) { _subscriptionClient = subscriptionClient; _customerClient = customerClient; _mandateClient = mandateClient; } [Fact] public async Task CanRetrieveSubscriptionList() { // Given string? customerId = await GetFirstCustomerWithValidMandate(); // When: Retrieve subscription list with default settings if (customerId != null) { ListResponse response = await _subscriptionClient.GetSubscriptionListAsync(customerId); // Then response.ShouldNotBeNull(); response.Items.ShouldNotBeNull(); } } [Fact] public async Task CanRetrieveAllSubscriptionList() { // Given // When: Retrieve subscription list with default settings ListResponse response = await _subscriptionClient.GetAllSubscriptionList(); // Then response.ShouldNotBeNull(); response.Items.ShouldNotBeNull(); } [Fact] public async Task ListSubscriptionsNeverReturnsMoreCustomersThenTheNumberOfRequestedSubscriptions() { // Given: Number of customers requested is 5 string? customerId = await GetFirstCustomerWithValidMandate(); if (customerId != null) { int numberOfSubscriptions = 5; // When: Retrieve 5 subscriptions ListResponse response = await _subscriptionClient.GetSubscriptionListAsync(customerId, null, numberOfSubscriptions); // Then response.Items.Count.ShouldBeLessThanOrEqualTo(numberOfSubscriptions); } } [Fact] public async Task CanCreateSubscription() { // Given string? customerId = await GetFirstCustomerWithValidMandate(); if (customerId != null) { var subscriptionRequest = new SubscriptionRequest { Amount = new Amount(Currency.EUR, "100.00"), Times = 5, Interval = "1 month", Description = $"Subscription {Guid.NewGuid()}", // Subscriptions must have a unique name WebhookUrl = "http://www.google.nl", StartDate = DateTime.Now.AddDays(1) }; // When SubscriptionResponse subscriptionResponse = await _subscriptionClient.CreateSubscriptionAsync(customerId, subscriptionRequest); // Then subscriptionResponse.Amount.ShouldBe(subscriptionRequest.Amount); subscriptionResponse.Times.ShouldBe(subscriptionRequest.Times); subscriptionResponse.Interval.ShouldBe(subscriptionRequest.Interval); subscriptionResponse.Description.ShouldBe(subscriptionRequest.Description); subscriptionResponse.WebhookUrl.ShouldBe(subscriptionRequest.WebhookUrl); subscriptionResponse.StartDate.ShouldBe(subscriptionRequest.StartDate.Value.Date); } } [Fact] public async Task CanUpdateSubscription() { // Given var activeSubscription = await GetActiveSubscription(); // When if (activeSubscription != null) { var customerId = activeSubscription.CustomerId; SubscriptionUpdateRequest request = new () { Description = $"Updated subscription {Guid.NewGuid()}" }; SubscriptionResponse response = await _subscriptionClient.UpdateSubscriptionAsync(customerId, activeSubscription.Id, request); // Then response.Description.ShouldBe(request.Description); } } [Fact] public async Task CanCancelSubscription() { // Given: We have a customer with a mandate string? customerId = await GetFirstCustomerWithValidMandate(); if (customerId != null) { ListResponse subscriptions = await _subscriptionClient.GetSubscriptionListAsync(customerId); // When: That customer has a subscription that we can cancel SubscriptionResponse? subscriptionToCancel = subscriptions.Items .FirstOrDefault(s => s.Status != SubscriptionStatus.Canceled); if (subscriptionToCancel != null) { await _subscriptionClient.CancelSubscriptionAsync(customerId, subscriptionToCancel.Id); // Then: Make sure its canceled after one second await Task.Delay(TimeSpan.FromSeconds(1)); SubscriptionResponse cancelledSubscription = await _subscriptionClient.GetSubscriptionAsync(customerId, subscriptionToCancel.Id); cancelledSubscription.Status.ShouldBe(SubscriptionStatus.Canceled); } } } [Fact] public async Task CanCreateSubscriptionWithMetaData() { // If: We create a subscription with meta data string json = "{\"order_id\":\"4.40\"}"; string? customerId = await GetFirstCustomerWithValidMandate(); if (customerId != null) { SubscriptionRequest subscriptionRequest = new SubscriptionRequest { Amount = new Amount(Currency.EUR, "100.00"), Times = 5, Interval = "1 month", Description = $"Subscription {Guid.NewGuid()}", // Subscriptions must have a unique name WebhookUrl = "http://www.google.nl", StartDate = DateTime.Now.AddDays(1), Metadata = json }; // When We send the subscription request to Mollie SubscriptionResponse result = await _subscriptionClient.CreateSubscriptionAsync(customerId, subscriptionRequest); // Then: Make sure we get the same json result as metadata IsJsonResultEqual(result.Metadata, json).ShouldBeTrue(); } } private async Task GetFirstCustomerWithValidMandate() { ListResponse customers = await _customerClient.GetCustomerListAsync(); foreach (CustomerResponse customer in customers.Items) { ListResponse mandates = await _mandateClient.GetMandateListAsync(customer.Id); if (mandates.Items.Any(x => x.Status == MandateStatus.Valid)) { return customer.Id; } } return null; } private async Task GetActiveSubscription() { ListResponse customers = await _customerClient.GetCustomerListAsync(); foreach (CustomerResponse customer in customers.Items.OrderByDescending(x => x.CreatedAt)) { ListResponse subscriptions = await _subscriptionClient.GetSubscriptionListAsync(customer.Id); var activeSubscription = subscriptions.Items.FirstOrDefault(x => x.Status == SubscriptionStatus.Active); if (activeSubscription != null) { return activeSubscription; } } return null; } public void Dispose() { _subscriptionClient.Dispose(); _customerClient.Dispose(); _mandateClient.Dispose(); } } ================================================ FILE: tests/Mollie.Tests.Integration/Api/TerminalTests.cs ================================================ using System; using System.Linq; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Client.Abstract; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Terminal.Response; using Mollie.Tests.Integration.Framework; using Xunit; namespace Mollie.Tests.Integration.Api; public class TerminalTests : BaseMollieApiTestClass, IDisposable { private readonly ITerminalClient _terminalClient; public TerminalTests(ITerminalClient terminalClient) { _terminalClient = terminalClient; } [Fact] public async Task CanRetrieveTerminalList() { // Given // When: Retrieve terminal client list ListResponse response = await _terminalClient.GetTerminalListAsync(); // Then response.ShouldNotBeNull(); response.Items.ShouldNotBeNull(); } [Fact(Skip = "Not implemented by Mollie yet")] public async Task CanRetrieveSingleTerminal() { // Given ListResponse allTerminals = await _terminalClient.GetTerminalListAsync(); if (allTerminals.Count > 0) { TerminalResponse firstTerminal = allTerminals.Items.First(); // When: Retrieve terminal client list TerminalResponse response = await _terminalClient.GetTerminalAsync(firstTerminal.Id); // Then response.ShouldNotBeNull(); response.Id.ShouldBe(firstTerminal.Id); } } public void Dispose() { _terminalClient?.Dispose(); } } ================================================ FILE: tests/Mollie.Tests.Integration/Api/WebhookEventTests.cs ================================================ using System; using System.Threading.Tasks; using Mollie.Api.Client.Abstract; using Mollie.Api.Models.PaymentLink.Response; using Mollie.Tests.Integration.Framework; using Shouldly; using Xunit; namespace Mollie.Tests.Integration.Api; [Trait("TestCategory", "LocalIntegrationTests")] public class WebhookEventTests : BaseMollieApiTestClass, IDisposable { private readonly IWebhookEventClient _webhookEventClient; public WebhookEventTests(IWebhookEventClient webhookEventClient) { _webhookEventClient = webhookEventClient; } [Fact] public async Task CanRetrieveWebhookEvent() { // Arrange const string webhookEventIdToRetrieve = "event_GvJ8WHrp5isUdRub9CJyH"; // Act var webhookEvent = await _webhookEventClient.GetWebhookEventAsync(webhookEventIdToRetrieve); // Assert webhookEvent.ShouldNotBeNull(); } [Fact] public async Task CanRetrieveGenericWebhookEvent() { // Arrange const string webhookEventIdToRetrieve = "event_GvJ8WHrp5isUdRub9CJyH"; // Act var webhookEvent = await _webhookEventClient.GetWebhookEventAsync(webhookEventIdToRetrieve); // Assert webhookEvent.ShouldNotBeNull(); } public void Dispose() { _webhookEventClient.Dispose(); } } ================================================ FILE: tests/Mollie.Tests.Integration/Api/WebhookTests.cs ================================================ using System; using System.Threading.Tasks; using Mollie.Api.Client; using Mollie.Api.Client.Abstract; using Mollie.Api.Models; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Webhook; using Mollie.Api.Models.Webhook.Request; using Mollie.Api.Models.Webhook.Response; using Mollie.Tests.Integration.Framework; using Shouldly; using Xunit; namespace Mollie.Tests.Integration.Api; [Trait("TestCategory", "LocalIntegrationTests")] public class WebhookTests : BaseMollieApiTestClass, IDisposable, IAsyncLifetime { private readonly IWebhookClient _webhookClient; public WebhookTests(IWebhookClient webhookClient) { _webhookClient = webhookClient; } [Fact] public async Task CanCreateRetrieveAndDeleteWebhook() { // Given var request = new WebhookRequest { Name = "my-webhook", Url = "https://github.com/Viincenttt/MollieApi/", EventTypes = [WebhookEventTypes.PaymentLinkPaid, WebhookEventTypes.SalesInvoiceCreated], Testmode = true }; // When: The webhook is created WebhookResponse created = await _webhookClient.CreateWebhookAsync(request); // Then created.Name.ShouldBe(request.Name); created.Url.ShouldBe(request.Url); created.EventTypes.ShouldBe([WebhookEventTypes.PaymentLinkPaid, WebhookEventTypes.SalesInvoiceCreated]); created.Mode.ShouldBe(Mode.Test); created.Resource.ShouldBe("webhook"); created.Id.ShouldNotBeNullOrEmpty(); // Then: The webhook can be retrieved WebhookResponse retrieved = await _webhookClient.GetWebhookAsync(created.Id, testmode: true); retrieved.ShouldBeEquivalentTo(created); // Then: The webhook can be deleted await _webhookClient.DeleteWebhookAsync(created.Id, testmode: true); } [Fact] public async Task CanRetrieveWebhookList() { // Given // When: Retrieve webhook list ListResponse response = await _webhookClient.GetWebhookListAsync(testmode: true); // Then response.ShouldNotBeNull(); response.Items.ShouldNotBeNull(); } [Fact] public async Task CanUpdateWebhook() { // Given: Create a webhook var createRequest = new WebhookRequest { Name = "my-webhook", Url = "https://github.com/Viincenttt/MollieApi/", EventTypes = [WebhookEventTypes.PaymentLinkPaid], Testmode = true }; WebhookResponse created = await _webhookClient.CreateWebhookAsync(createRequest); var updateRequest = new WebhookRequest { Name = "my-webhook-updated", Url = "https://github.com/Viincenttt/MollieApi/-updated", EventTypes = [WebhookEventTypes.PaymentLinkPaid, WebhookEventTypes.SalesInvoiceCreated], Testmode = true }; // When: The webhook is updated WebhookResponse updated = await _webhookClient.UpdateWebhookAsync(created.Id, updateRequest); // Then updated.Name.ShouldBe(updateRequest.Name); updated.Url.ShouldBe(updateRequest.Url); updated.EventTypes.ShouldBe([WebhookEventTypes.PaymentLinkPaid, WebhookEventTypes.SalesInvoiceCreated]); updated.Mode.ShouldBe(Mode.Test); } [Fact] public async Task CanTestWebhook() { // Given: Create a webhook var createRequest = new WebhookRequest { Name = "my-webhook", Url = "https://github.com/Viincenttt/MollieApi/", EventTypes = [WebhookEventTypes.PaymentLinkPaid], Testmode = true }; WebhookResponse created = await _webhookClient.CreateWebhookAsync(createRequest); // When: The webhook is updated MollieApiException exception = await Assert.ThrowsAsync(() => _webhookClient.TestWebhookAsync(created.Id, testmode: true)); // Then: An exception is thrown as the URL can't be reached exception.Message.ShouldBe("Unprocessable Entity - Failed to ping the webhook subscription."); } public async Task InitializeAsync() { ListResponse webhooks = await _webhookClient.GetWebhookListAsync(testmode: true); foreach (var webhook in webhooks.Items) { await _webhookClient.DeleteWebhookAsync(webhook.Id, testmode: true); } } public Task DisposeAsync() => Task.CompletedTask; public void Dispose() => _webhookClient.Dispose(); } ================================================ FILE: tests/Mollie.Tests.Integration/Framework/BaseMollieApiTestClass.cs ================================================ using System; using System.Globalization; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Mollie.Api.Client; using Mollie.Api.Options; namespace Mollie.Tests.Integration.Framework { public abstract class BaseMollieApiTestClass { protected readonly string DefaultRedirectUrl = "http://mysite.com"; protected readonly string DefaultWebhookUrl = "http://mysite.com/webhook"; private readonly MollieOptions _configuration; protected string ApiKey => _configuration.ApiKey; protected string ClientId => _configuration.ClientId ?? "client-id"; protected string ClientSecret => _configuration.ClientSecret ?? "client-secret"; protected BaseMollieApiTestClass() { _configuration = ConfigurationFactory.GetConfiguration().GetSection("Mollie").Get()!; EnsureTestApiKey(ApiKey); // Mollie returns a 429 response code (Too many requests) if we send a lot of requests in a short timespan. // this is partially mitigated by using a custom http retry policy. However, the RetryAfter header gets // exceedingly long if we do too many requests without any delay. This is why we add a small delay between // each test. TimeSpan timeBetweenTests = TimeSpan.FromMilliseconds(500); Thread.Sleep(timeBetweenTests); } private void EnsureTestApiKey(string apiKey) { if (string.IsNullOrEmpty(apiKey)) { throw new ArgumentException("Please enter you API key in the BaseMollieApiTestClass class"); } if (!apiKey.StartsWith("access") && !apiKey.StartsWith("test")) { throw new ArgumentException("You should not run these tests on your live key!"); } } protected bool IsJsonResultEqual(string? json1, string? json2) { return string.Compare(json1, json2, CultureInfo.CurrentCulture, CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols) == 0; } protected async Task ExecuteWithRetry(Func> apiAction, int numberOfRetries = 3) { MollieApiException? exception = null; for (int i = 0; i < numberOfRetries; i++) { try { return await apiAction.Invoke(); } catch (MollieApiException ex) { exception = ex; await Task.Delay(TimeSpan.FromSeconds(1)); } } throw exception!; } } } ================================================ FILE: tests/Mollie.Tests.Integration/Framework/ConfigurationFactory.cs ================================================ using Microsoft.Extensions.Configuration; using System.IO; namespace Mollie.Tests.Integration.Framework { public static class ConfigurationFactory { public static IConfiguration GetConfiguration() { var builder = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .AddUserSecrets(typeof(ConfigurationFactory).Assembly) .AddEnvironmentVariables(); var configuration = builder.Build(); return configuration; } } } ================================================ FILE: tests/Mollie.Tests.Integration/Framework/MollieIntegrationTestHttpRetryPolicies.cs ================================================ using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; using Mollie.Api.Client; using Polly; namespace Mollie.Tests.Integration.Framework; public static class MollieIntegrationTestHttpRetryPolicies { public static IAsyncPolicy TooManyRequestRetryPolicy() { var retryPolicy = Policy .Handle(x => x.Details.Status == (int)HttpStatusCode.TooManyRequests) .OrResult(r => r?.Headers?.RetryAfter != null) .WaitAndRetryAsync( 3, sleepDurationProvider: (_, response, _) => response.Result.Headers.RetryAfter?.Delta?.Add(TimeSpan.FromSeconds(1)) ?? TimeSpan.FromSeconds(5), onRetryAsync: (response, _, _, _) => { // If we send a retry with the same idempotency key, we always get a 429 back... response.Result.RequestMessage?.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString()); return Task.CompletedTask; } ); return retryPolicy; } } ================================================ FILE: tests/Mollie.Tests.Integration/Mollie.Tests.Integration.csproj ================================================  net10.0 false 51f1afd9-6752-484d-845c-119df206c6e7 enable Always PreserveNewest all runtime; build; native; contentfiles; analyzers; buildtransitive PreserveNewest ================================================ FILE: tests/Mollie.Tests.Integration/Startup.cs ================================================ using System.IO; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Mollie.Api; using Mollie.Tests.Integration.Framework; using Polly; namespace Mollie.Tests.Integration; public class Startup { public void ConfigureHost(IHostBuilder hostBuilder) => hostBuilder .ConfigureHostConfiguration(options => { options .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .AddUserSecrets(typeof(ConfigurationFactory).Assembly) .AddEnvironmentVariables(); }) .ConfigureServices((context, services) => { services.AddMollieApi(options => { options.ApiKey = context.Configuration["Mollie:ApiKey"]!; options.ClientId = context.Configuration["Mollie:ClientId"]!; options.ClientSecret = context.Configuration["Mollie:ClientSecret"]!; options.RetryPolicy = MollieIntegrationTestHttpRetryPolicies.TooManyRequestRetryPolicy(); }); }); } ================================================ FILE: tests/Mollie.Tests.Integration/appsettings.json ================================================ { "Mollie": { "ApiKey": "", "ClientId": "", "ClientSecret": "", "AccessKey": "" } } ================================================ FILE: tests/Mollie.Tests.Integration/xunit.runner.json ================================================ { "parallelizeAssembly": false, "parallelizeTestCollections": false } ================================================ FILE: tests/Mollie.Tests.Unit/Client/BalanceClientTests.cs ================================================ using System; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Mollie.Api.Client; using Mollie.Api.Models; using Mollie.Api.Models.Balance.Response; using Mollie.Api.Models.Balance.Response.BalanceReport; using Mollie.Api.Models.Balance.Response.BalanceReport.Specific.StatusBalance; using Mollie.Api.Models.Balance.Response.BalanceReport.Specific.TransactionCategories; using Mollie.Api.Models.Balance.Response.BalanceTransaction.Specific; using RichardSzalay.MockHttp; using Shouldly; using Xunit; namespace Mollie.Tests.Unit.Client { public class BalanceClientTests : BaseClientTests { [Fact] public async Task GetBalanceAsync_DefaultBehaviour_ResponseIsParsed() { // Given: We request a specific balance var getBalanceResponseFactory = new GetBalanceResponseFactory(); var getBalanceResponse = getBalanceResponseFactory.CreateGetBalanceResponse(); string expectedUrl = $"{BaseMollieClient.DefaultBaseApiEndPoint}balances/{getBalanceResponseFactory.BalanceId}"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, expectedUrl, getBalanceResponse); HttpClient httpClient = mockHttp.ToHttpClient(); BalanceClient balanceClient = new BalanceClient("api-key", httpClient); // When: We make the request BalanceResponse balanceResponse = await balanceClient.GetBalanceAsync(getBalanceResponseFactory.BalanceId); // Then: Response should be parsed mockHttp.VerifyNoOutstandingExpectation(); balanceResponse.ShouldNotBeNull(); balanceResponse.Id.ShouldBe(getBalanceResponseFactory.BalanceId); balanceResponse.CreatedAt.ToUniversalTime().ShouldBe(getBalanceResponseFactory.CreatedAt); balanceResponse.TransferThreshold.Currency.ShouldBe(getBalanceResponseFactory.TransferThreshold.Currency); balanceResponse.Currency.ShouldBe(getBalanceResponseFactory.Currency); balanceResponse.Status.ShouldBe(getBalanceResponseFactory.Status); balanceResponse.AvailableAmount.Currency.ShouldBe(getBalanceResponseFactory.AvailableAmount.Currency); balanceResponse.AvailableAmount.Value.ShouldBe(getBalanceResponseFactory.AvailableAmount.Value); balanceResponse.PendingAmount.Value.ShouldBe(getBalanceResponseFactory.PendingAmount.Value); balanceResponse.PendingAmount.Currency.ShouldBe(getBalanceResponseFactory.PendingAmount.Currency); balanceResponse.TransferFrequency.ShouldBe(getBalanceResponseFactory.TransferFrequency); balanceResponse.TransferReference.ShouldBe(getBalanceResponseFactory.TransferReference); balanceResponse.TransferThreshold.Currency.ShouldBe(getBalanceResponseFactory.TransferThreshold.Currency); balanceResponse.TransferThreshold.Value.ShouldBe(getBalanceResponseFactory.TransferThreshold.Value); balanceResponse.TransferDestination.Type.ShouldBe(getBalanceResponseFactory.TransferDestination.Type); balanceResponse.TransferDestination.BankAccount.ShouldBe(getBalanceResponseFactory.TransferDestination.BankAccount); balanceResponse.TransferDestination.BeneficiaryName.ShouldBe(getBalanceResponseFactory.TransferDestination.BeneficiaryName); balanceResponse.Links.ShouldNotBeNull(); balanceResponse.Links.Self.Href.ShouldBe($"https://api.mollie.com/v2/balances/{getBalanceResponseFactory.BalanceId}"); balanceResponse.Links.Self.Type.ShouldBe("application/hal+json"); balanceResponse.Links.Documentation.Href.ShouldBe($"https://docs.mollie.com/reference/v2/balances-api/get-balance"); balanceResponse.Links.Documentation.Type.ShouldBe("text/html"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetBalanceAsync_NoBalanceIdIsGiven_ArgumentExceptionIsThrown(string? balanceId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); BalanceClient balanceClient = new BalanceClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await balanceClient.GetBalanceAsync(balanceId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'balanceId' is null or empty"); } [Fact] public async Task GetPrimaryBalanceAsync_DefaultBehaviour_ResponseIsParsed() { // Given: We request the primary balance var getBalanceResponseFactory = new GetBalanceResponseFactory(); var getBalanceResponse = getBalanceResponseFactory.CreateGetBalanceResponse(); string expectedUrl = $"{BaseMollieClient.DefaultBaseApiEndPoint}balances/primary"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, expectedUrl, getBalanceResponse); HttpClient httpClient = mockHttp.ToHttpClient(); BalanceClient balanceClient = new BalanceClient("api-key", httpClient); // When: We make the request BalanceResponse balanceResponse = await balanceClient.GetPrimaryBalanceAsync(); // Then: Response should be parsed mockHttp.VerifyNoOutstandingExpectation(); balanceResponse.ShouldNotBeNull(); balanceResponse.ShouldNotBeNull(); balanceResponse.Id.ShouldBe(getBalanceResponseFactory.BalanceId); balanceResponse.CreatedAt.ToUniversalTime().ShouldBe(getBalanceResponseFactory.CreatedAt); balanceResponse.TransferThreshold.Currency.ShouldBe(getBalanceResponseFactory.TransferThreshold.Currency); balanceResponse.Currency.ShouldBe(getBalanceResponseFactory.Currency); balanceResponse.Status.ShouldBe(getBalanceResponseFactory.Status); balanceResponse.AvailableAmount.Currency.ShouldBe(getBalanceResponseFactory.AvailableAmount.Currency); balanceResponse.AvailableAmount.Value.ShouldBe(getBalanceResponseFactory.AvailableAmount.Value); balanceResponse.PendingAmount.Value.ShouldBe(getBalanceResponseFactory.PendingAmount.Value); balanceResponse.PendingAmount.Currency.ShouldBe(getBalanceResponseFactory.PendingAmount.Currency); balanceResponse.TransferFrequency.ShouldBe(getBalanceResponseFactory.TransferFrequency); balanceResponse.TransferReference.ShouldBe(getBalanceResponseFactory.TransferReference); balanceResponse.TransferThreshold.Currency.ShouldBe(getBalanceResponseFactory.TransferThreshold.Currency); balanceResponse.TransferThreshold.Value.ShouldBe(getBalanceResponseFactory.TransferThreshold.Value); balanceResponse.TransferDestination.Type.ShouldBe(getBalanceResponseFactory.TransferDestination.Type); balanceResponse.TransferDestination.BankAccount.ShouldBe(getBalanceResponseFactory.TransferDestination.BankAccount); balanceResponse.TransferDestination.BeneficiaryName.ShouldBe(getBalanceResponseFactory.TransferDestination.BeneficiaryName); } [Fact] public async Task ListBalancesAsync_DefaultBehaviour_ResponseIsParsed() { // Given: We request a list of balances string expectedUrl = $"{BaseMollieClient.DefaultBaseApiEndPoint}balances"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, expectedUrl, DefaultListBalancesResponse); HttpClient httpClient = mockHttp.ToHttpClient(); BalanceClient balanceClient = new BalanceClient("api-key", httpClient); // When: We make the request var balances = await balanceClient.GetBalanceListAsync(); // Then: Response should be parsed mockHttp.VerifyNoOutstandingExpectation(); balances.ShouldNotBeNull(); balances.Count.ShouldBe(2); balances.Items.Count.ShouldBe(2); } [Fact] public async Task GetBalanceReportAsync_TransactionCategories_ResponseIsParsed() { // Given: We request a balance report string balanceId = "bal_CKjKwQdjCwCSArXFAJNFH"; DateTime from = new DateTime(2022, 11, 1); DateTime until = new DateTime(2022, 11, 30); string grouping = ReportGrouping.TransactionCategories; string expectedUrl = $"{BaseMollieClient.DefaultBaseApiEndPoint}balances/{balanceId}/report" + $"?from={from.ToString("yyyy-MM-dd")}&until={until.ToString("yyyy-MM-dd")}&grouping={grouping}"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, expectedUrl, DefaultGetBalanceReportTransactionCategoriesResponse); HttpClient httpClient = mockHttp.ToHttpClient(); BalanceClient balanceClient = new BalanceClient("api-key", httpClient); // When: We make the request var balanceReport = await balanceClient.GetBalanceReportAsync(balanceId, from, until, grouping); // Then: Response should be parsed mockHttp.VerifyNoOutstandingExpectation(); balanceReport.ShouldNotBeNull(); balanceReport.ShouldBeOfType(); var specificBalanceReport = (TransactionCategoriesReportResponse)balanceReport; specificBalanceReport.Grouping.ShouldBe(grouping); specificBalanceReport.BalanceId.ShouldBe(balanceId); specificBalanceReport.Resource.ShouldBe("balance-report"); specificBalanceReport.From.ShouldBe(from); specificBalanceReport.Until.ShouldBe(until); specificBalanceReport.Totals.ShouldNotBeNull(); specificBalanceReport.Totals.Open.Pending.Amount.Value.ShouldBe("5.30"); specificBalanceReport.Totals.Open.Pending.Amount.Currency.ShouldBe("EUR"); specificBalanceReport.Totals.Open.Available.Amount.Value.ShouldBe("0.11"); specificBalanceReport.Totals.Open.Available.Amount.Currency.ShouldBe("EUR"); var childSubTotals = specificBalanceReport.Totals.Payments.Pending.Subtotals.First(); childSubTotals.TransactionType.ShouldBe("payment"); childSubTotals.Count.ShouldBe(36); var childChildSubTotals = childSubTotals.Subtotals!.First(); childChildSubTotals.Method.ShouldBe("ideal"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetBalanceReportAsync_NoBalanceIdIsGiven_ArgumentExceptionIsThrown(string? balanceId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); BalanceClient balanceClient = new BalanceClient("api-key", httpClient); DateTime from = new DateTime(2022, 11, 1); DateTime until = new DateTime(2022, 11, 30); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await balanceClient.GetBalanceReportAsync(balanceId, from, until)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'balanceId' is null or empty"); } [Fact] public async Task GetBalanceReportAsync_StatusBalances_ResponseIsParsed() { // Given: We request a balance report string balanceId = "bal_CKjKwQdjCwCSArXFAJNFH"; DateTime from = new DateTime(2022, 11, 1); DateTime until = new DateTime(2022, 11, 30); string grouping = ReportGrouping.StatusBalances; string expectedUrl = $"{BaseMollieClient.DefaultBaseApiEndPoint}balances/{balanceId}/report" + $"?from={from.ToString("yyyy-MM-dd")}&until={until.ToString("yyyy-MM-dd")}&grouping={grouping}"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, expectedUrl, DefaultGetBalanceReportStatusBalancesResponse); HttpClient httpClient = mockHttp.ToHttpClient(); BalanceClient balanceClient = new BalanceClient("api-key", httpClient); // When: We make the request var balanceReport = await balanceClient.GetBalanceReportAsync(balanceId, from, until, grouping); // Then: Response should be parsed mockHttp.VerifyNoOutstandingExpectation(); balanceReport.ShouldNotBeNull(); balanceReport.ShouldBeOfType(); var specificBalanceReport = (StatusBalanceReportResponse)balanceReport; specificBalanceReport.Grouping.ShouldBe(grouping); specificBalanceReport.BalanceId.ShouldBe(balanceId); specificBalanceReport.Resource.ShouldBe("balance-report"); specificBalanceReport.From.ShouldBe(from); specificBalanceReport.Until.ShouldBe(until); specificBalanceReport.Totals.ShouldNotBeNull(); specificBalanceReport.Totals.PendingBalance.Open.Amount.Value.ShouldBe("5.30"); specificBalanceReport.Totals.PendingBalance.Open.Amount.Currency.ShouldBe(Currency.EUR); specificBalanceReport.Totals.AvailableBalance.MovedFromPending.Amount.Value.ShouldBe("3.38"); specificBalanceReport.Totals.AvailableBalance.MovedFromPending.Amount.Currency.ShouldBe(Currency.EUR); var childSubTotals = specificBalanceReport.Totals.AvailableBalance.MovedFromPending.Subtotals.First(); childSubTotals.TransactionType.ShouldBe("payment"); var childChildSubtotals = childSubTotals.Subtotals!.First(); childChildSubtotals.Method.ShouldBe("ideal"); } [Fact] public async Task ListBalanceTransactionsAsync_StatusBalances_ResponseIsParsed() { // Given string balanceId = "bal_CKjKwQdjCwCSArXFAJNFH"; string expectedUrl = $"{BaseMollieClient.DefaultBaseApiEndPoint}balances/{balanceId}/transactions"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, expectedUrl, DefaultListBalanceTransactionsResponse); HttpClient httpClient = mockHttp.ToHttpClient(); BalanceClient balanceClient = new BalanceClient("api-key", httpClient); // When: We make the request var balanceTransactions = await balanceClient.GetBalanceTransactionListAsync(balanceId); // Then: Response should be parsed mockHttp.VerifyNoOutstandingExpectation(); balanceTransactions.Count.ShouldBe(balanceTransactions.Items.Count); var transaction = balanceTransactions.Items.First(); transaction.Resource.ShouldBe("balance_transactions"); transaction.Id.ShouldBe("baltr_9S8yk4FFqqi2Qm6K3rqRH"); transaction.Type.ShouldBe("outgoing-transfer"); transaction.ResultAmount.Value.ShouldBe("-7.76"); transaction.ResultAmount.Currency.ShouldBe(Currency.EUR); transaction.InitialAmount.Value.ShouldBe("-7.76"); transaction.InitialAmount.Currency.ShouldBe(Currency.EUR); transaction.ShouldBeOfType(); var transactionContext = (SettlementBalanceTransactionResponse)transaction; transactionContext.Context.SettlementId.ShouldBe("stl_ma2vu8"); transactionContext.Context.TransferId.ShouldBe("trf_ma2vu8"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task ListBalanceTransactionsAsync_NoBalanceIdIsGiven_ArgumentExceptionIsThrown(string? balanceId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); BalanceClient balanceClient = new BalanceClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await balanceClient.GetBalanceTransactionListAsync(balanceId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'balanceId' is null or empty"); } [Fact] public async Task ListPrimaryBalanceTransactionsAsync_StatusBalances_ResponseIsParsed() { // Given string expectedUrl = $"{BaseMollieClient.DefaultBaseApiEndPoint}balances/primary/transactions"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, expectedUrl, DefaultListBalanceTransactionsResponse); HttpClient httpClient = mockHttp.ToHttpClient(); BalanceClient balanceClient = new BalanceClient("api-key", httpClient); // When: We make the request var balanceTransactions = await balanceClient.GetPrimaryBalanceTransactionListAsync(); // Then: Response should be parsed mockHttp.VerifyNoOutstandingExpectation(); balanceTransactions.Count.ShouldBe(balanceTransactions.Items.Count); } private readonly string DefaultListBalanceTransactionsResponse = @" { ""_embedded"": { ""balance_transactions"": [{ ""resource"": ""balance_transactions"", ""id"": ""baltr_9S8yk4FFqqi2Qm6K3rqRH"", ""type"": ""outgoing-transfer"", ""resultAmount"": { ""value"": ""-7.76"", ""currency"": ""EUR"" }, ""initialAmount"": { ""value"": ""-7.76"", ""currency"": ""EUR"" }, ""createdAt"": ""2022-12-21T05:02:50+00:00"", ""context"": { ""settlementId"": ""stl_ma2vu8"", ""transferId"": ""trf_ma2vu8"" } }, { ""resource"": ""balance_transactions"", ""id"": ""baltr_2cXFV9Wd9AjYqHa8i2qRH"", ""type"": ""payment"", ""resultAmount"": { ""value"": ""0.15"", ""currency"": ""EUR"" }, ""initialAmount"": { ""value"": ""0.15"", ""currency"": ""EUR"" }, ""createdAt"": ""2022-12-20T21:21:09+00:00"", ""context"": { ""paymentId"": ""tr_JzT2KxV7hZ"" } } ] }, ""count"": 2, ""_links"": { ""documentation"": { ""href"": ""https://docs.mollie.com/reference/v2/balances-api/list-balance-transactions"", ""type"": ""text/html"" }, ""self"": { ""href"": ""https://api.mollie.com/v2/balances/bal_CKjKwQdjCwCSArXFAJNFH/transactions?limit=50"", ""type"": ""application/hal+json"" }, ""previous"": null, ""next"": { ""href"": ""https://api.mollie.com/v2/balances/bal_CKjKwQdjCwCSArXFAJNFH/transactions?from=baltr_pEM6remSK3UGHDRAuzVRH\u0026limit=50"", ""type"": ""application/hal+json"" } } }"; private readonly string DefaultListBalancesResponse = $@"{{ ""count"": 2, ""_embedded"": {{ ""balances"": [ {{ ""resource"": ""balance"", ""id"": ""bal_gVMhHKqSSRYJyPsuoPNFH"", ""mode"": ""live"", ""createdAt"": ""2019-01-10T12:06:28+00:00"", ""currency"": ""EUR"", ""status"": ""active"", ""availableAmount"": {{ ""value"": ""0.00"", ""currency"": ""EUR"" }}, ""pendingAmount"": {{ ""value"": ""0.00"", ""currency"": ""EUR"" }}, ""transferFrequency"": ""daily"", ""transferThreshold"": {{ ""value"": ""40.00"", ""currency"": ""EUR"" }}, ""transferReference"": ""Mollie payout"", ""transferDestination"": {{ ""type"": ""bank-account"", ""beneficiaryName"": ""Jack Bauer"", ""bankAccount"": ""NL53INGB0654422370"", ""bankAccountId"": ""bnk_jrty3f"" }}, ""_links"": {{ ""self"": {{ ""href"": ""https://api.mollie.com/v2/balances/bal_gVMhHKqSSRYJyPsuoPNFH"", ""type"": ""application/hal+json"" }} }} }}, {{ ""resource"": ""balance"", ""id"": ""bal_gVMhHKqSSRYJyPsuoPABC"", ""mode"": ""live"", ""createdAt"": ""2019-01-10T10:23:41+00:00"", ""status"": ""active"", ""currency"": ""EUR"", ""availableAmount"": {{ ""value"": ""0.00"", ""currency"": ""EUR"" }}, ""pendingAmount"": {{ ""value"": ""0.00"", ""currency"": ""EUR"" }}, ""transferFrequency"": ""twice-a-month"", ""transferThreshold"": {{ ""value"": ""5.00"", ""currency"": ""EUR"" }}, ""transferReference"": ""Mollie payout"", ""transferDestination"": {{ ""type"": ""bank-account"", ""beneficiaryName"": ""Jack Bauer"", ""bankAccount"": ""NL97MOLL6351480700"", ""bankAccountId"": ""bnk_jrty3e"" }}, ""_links"": {{ ""self"": {{ ""href"": ""https://api.mollie.com/v2/balances/bal_gVMhHKqSSRYJyPsuoPABC"", ""type"": ""application/hal+json"" }} }} }} ] }}, ""_links"": {{ ""documentation"": {{ ""href"": ""https://docs.mollie.com/reference/v2/balances-api/list-balances"", ""type"": ""text/html"" }}, ""self"": {{ ""href"": ""https://api.mollie.com/v2/balances?limit=5"", ""type"": ""application/hal+json"" }}, ""previous"": null, ""next"": {{ ""href"": ""https://api.mollie.com/v2/balances?from=bal_gVMhHKqSSRYJyPsuoPABC&limit=5"", ""type"": ""application/hal+json"" }} }} }}"; private readonly string DefaultGetBalanceReportTransactionCategoriesResponse = @"{ ""resource"": ""balance-report"", ""balanceId"": ""bal_CKjKwQdjCwCSArXFAJNFH"", ""timeZone"": ""Europe/Amsterdam"", ""from"": ""2022-11-01"", ""until"": ""2022-11-30"", ""grouping"": ""transaction-categories"", ""totals"": { ""open"": { ""pending"": { ""amount"": { ""value"": ""5.30"", ""currency"": ""EUR"" } }, ""available"": { ""amount"": { ""value"": ""0.11"", ""currency"": ""EUR"" } } }, ""payments"": { ""pending"": { ""amount"": { ""value"": ""3.57"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""transactionType"": ""payment"", ""count"": 36, ""amount"": { ""value"": ""1.07"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""method"": ""ideal"", ""count"": 1, ""amount"": { ""value"": ""0.02"", ""currency"": ""EUR"" } }, { ""method"": ""pointofsale"", ""count"": 35, ""amount"": { ""value"": ""1.05"", ""currency"": ""EUR"" } } ] }, { ""transactionType"": ""split-payment"", ""count"": 3, ""amount"": { ""value"": ""2.50"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""method"": ""ideal"", ""count"": 3, ""amount"": { ""value"": ""2.50"", ""currency"": ""EUR"" } } ] } ] }, ""movedToAvailable"": { ""amount"": { ""value"": ""3.66"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""transactionType"": ""payment"", ""count"": 36, ""amount"": { ""value"": ""1.07"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""method"": ""ideal"", ""count"": 1, ""amount"": { ""value"": ""0.02"", ""currency"": ""EUR"" } }, { ""method"": ""pointofsale"", ""count"": 35, ""amount"": { ""value"": ""1.05"", ""currency"": ""EUR"" } } ] }, { ""transactionType"": ""split-payment"", ""count"": 3, ""amount"": { ""value"": ""2.50"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""method"": ""ideal"", ""count"": 3, ""amount"": { ""value"": ""2.50"", ""currency"": ""EUR"" } } ] }, { ""transactionType"": ""released-rolling-reserve"", ""amount"": { ""value"": ""0.09"", ""currency"": ""EUR"" } } ] }, ""immediatelyAvailable"": { ""amount"": { ""value"": ""0.00"", ""currency"": ""EUR"" } } }, ""refunds"": { ""pending"": { ""amount"": { ""value"": ""0.00"", ""currency"": ""EUR"" } }, ""movedToAvailable"": { ""amount"": { ""value"": ""0.00"", ""currency"": ""EUR"" } }, ""immediatelyAvailable"": { ""amount"": { ""value"": ""-1.00"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""transactionType"": ""refund"", ""count"": 1, ""amount"": { ""value"": ""-1.00"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""method"": ""creditcard"", ""count"": 1, ""amount"": { ""value"": ""-1.00"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""cardIssuer"": ""other"", ""cardAudience"": ""other"", ""cardRegion"": ""domestic"", ""count"": 1, ""amount"": { ""value"": ""-1.00"", ""currency"": ""EUR"" } } ] } ] } ] } }, ""chargebacks"": { ""pending"": { ""amount"": { ""value"": ""0.00"", ""currency"": ""EUR"" } }, ""movedToAvailable"": { ""amount"": { ""value"": ""0.00"", ""currency"": ""EUR"" } }, ""immediatelyAvailable"": { ""amount"": { ""value"": ""0.00"", ""currency"": ""EUR"" } } }, ""capital"": { ""pending"": { ""amount"": { ""value"": ""0.00"", ""currency"": ""EUR"" } }, ""movedToAvailable"": { ""amount"": { ""value"": ""0.00"", ""currency"": ""EUR"" } }, ""immediatelyAvailable"": { ""amount"": { ""value"": ""0.00"", ""currency"": ""EUR"" } } }, ""transfers"": { ""pending"": { ""amount"": { ""value"": ""0.00"", ""currency"": ""EUR"" } }, ""movedToAvailable"": { ""amount"": { ""value"": ""0.00"", ""currency"": ""EUR"" } }, ""immediatelyAvailable"": { ""amount"": { ""value"": ""0.00"", ""currency"": ""EUR"" } } }, ""fee-prepayments"": { ""pending"": { ""amount"": { ""value"": ""-0.28"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""prepaymentPartType"": ""fee"", ""count"": 1, ""amount"": { ""value"": ""-0.28"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""feeType"": ""payment-fee"", ""count"": 1, ""amount"": { ""value"": ""-0.28"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""method"": ""ideal"", ""count"": 1, ""amount"": { ""value"": ""-0.28"", ""currency"": ""EUR"" } } ] } ] } ] }, ""movedToAvailable"": { ""amount"": { ""value"": ""-0.28"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""prepaymentPartType"": ""fee"", ""count"": 1, ""amount"": { ""value"": ""-0.28"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""feeType"": ""payment-fee"", ""count"": 1, ""amount"": { ""value"": ""-0.28"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""method"": ""ideal"", ""count"": 1, ""amount"": { ""value"": ""-0.28"", ""currency"": ""EUR"" } } ] } ] } ] }, ""immediatelyAvailable"": { ""amount"": { ""value"": ""-0.24"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""prepaymentPartType"": ""fee"", ""count"": 1, ""amount"": { ""value"": ""-0.25"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""feeType"": ""refund-fee"", ""count"": 1, ""amount"": { ""value"": ""-0.25"", ""currency"": ""EUR"" } } ] }, { ""transactionType"": ""invoice-rounding-compensation"", ""count"": 1, ""amount"": { ""value"": ""0.01"", ""currency"": ""EUR"" } } ] } }, ""corrections"": { ""pending"": { ""amount"": { ""value"": ""0.00"", ""currency"": ""EUR"" } }, ""movedToAvailable"": { ""amount"": { ""value"": ""0.00"", ""currency"": ""EUR"" } }, ""immediatelyAvailable"": { ""amount"": { ""value"": ""0.00"", ""currency"": ""EUR"" } } }, ""close"": { ""pending"": { ""amount"": { ""value"": ""5.21"", ""currency"": ""EUR"" } }, ""available"": { ""amount"": { ""value"": ""2.25"", ""currency"": ""EUR"" } } } }, ""_links"": { ""self"": { ""href"": ""https://api.mollie.com/v2/balances/bal_CKjKwQdjCwCSArXFAJNFH/report"", ""type"": ""application/hal+json"" }, ""documentation"": { ""href"": ""https://docs.mollie.com/reference/v2/balances-api/get-balance-report"", ""type"": ""text/html"" } } }"; private readonly string DefaultGetBalanceReportStatusBalancesResponse = @"{ ""resource"": ""balance-report"", ""balanceId"": ""bal_CKjKwQdjCwCSArXFAJNFH"", ""timeZone"": ""Europe/Amsterdam"", ""from"": ""2022-11-01"", ""until"": ""2022-11-30"", ""grouping"": ""status-balances"", ""totals"": { ""pendingBalance"": { ""open"": { ""amount"": { ""value"": ""5.30"", ""currency"": ""EUR"" } }, ""pending"": { ""amount"": { ""value"": ""3.29"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""transactionType"": ""payment"", ""count"": 36, ""amount"": { ""value"": ""1.07"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""method"": ""ideal"", ""count"": 1, ""amount"": { ""value"": ""0.02"", ""currency"": ""EUR"" } }, { ""method"": ""pointofsale"", ""count"": 35, ""amount"": { ""value"": ""1.05"", ""currency"": ""EUR"" } } ] }, { ""transactionType"": ""split-payment"", ""count"": 3, ""amount"": { ""value"": ""2.50"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""method"": ""ideal"", ""count"": 3, ""amount"": { ""value"": ""2.50"", ""currency"": ""EUR"" } } ] }, { ""transactionType"": ""fee-prepayment"", ""amount"": { ""value"": ""-0.28"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""prepaymentPartType"": ""fee"", ""count"": 1, ""amount"": { ""value"": ""-0.28"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""feeType"": ""payment-fee"", ""count"": 1, ""amount"": { ""value"": ""-0.28"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""method"": ""ideal"", ""count"": 1, ""amount"": { ""value"": ""-0.28"", ""currency"": ""EUR"" } } ] } ] } ] } ] }, ""movedToAvailable"": { ""amount"": { ""value"": ""3.38"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""transactionType"": ""payment"", ""count"": 36, ""amount"": { ""value"": ""1.07"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""method"": ""ideal"", ""count"": 1, ""amount"": { ""value"": ""0.02"", ""currency"": ""EUR"" } }, { ""method"": ""pointofsale"", ""count"": 35, ""amount"": { ""value"": ""1.05"", ""currency"": ""EUR"" } } ] }, { ""transactionType"": ""split-payment"", ""count"": 3, ""amount"": { ""value"": ""2.50"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""method"": ""ideal"", ""count"": 3, ""amount"": { ""value"": ""2.50"", ""currency"": ""EUR"" } } ] }, { ""transactionType"": ""fee-prepayment"", ""amount"": { ""value"": ""-0.28"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""prepaymentPartType"": ""fee"", ""count"": 1, ""amount"": { ""value"": ""-0.28"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""feeType"": ""payment-fee"", ""count"": 1, ""amount"": { ""value"": ""-0.28"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""method"": ""ideal"", ""count"": 1, ""amount"": { ""value"": ""-0.28"", ""currency"": ""EUR"" } } ] } ] } ] }, { ""transactionType"": ""released-rolling-reserve"", ""amount"": { ""value"": ""0.09"", ""currency"": ""EUR"" } } ] }, ""close"": { ""amount"": { ""value"": ""5.21"", ""currency"": ""EUR"" } } }, ""availableBalance"": { ""open"": { ""amount"": { ""value"": ""0.11"", ""currency"": ""EUR"" } }, ""movedFromPending"": { ""amount"": { ""value"": ""3.38"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""transactionType"": ""payment"", ""count"": 36, ""amount"": { ""value"": ""1.07"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""method"": ""ideal"", ""count"": 1, ""amount"": { ""value"": ""0.02"", ""currency"": ""EUR"" } }, { ""method"": ""pointofsale"", ""count"": 35, ""amount"": { ""value"": ""1.05"", ""currency"": ""EUR"" } } ] }, { ""transactionType"": ""split-payment"", ""count"": 3, ""amount"": { ""value"": ""2.50"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""method"": ""ideal"", ""count"": 3, ""amount"": { ""value"": ""2.50"", ""currency"": ""EUR"" } } ] }, { ""transactionType"": ""fee-prepayment"", ""amount"": { ""value"": ""-0.28"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""prepaymentPartType"": ""fee"", ""count"": 1, ""amount"": { ""value"": ""-0.28"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""feeType"": ""payment-fee"", ""count"": 1, ""amount"": { ""value"": ""-0.28"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""method"": ""ideal"", ""count"": 1, ""amount"": { ""value"": ""-0.28"", ""currency"": ""EUR"" } } ] } ] } ] }, { ""transactionType"": ""released-rolling-reserve"", ""amount"": { ""value"": ""0.09"", ""currency"": ""EUR"" } } ] }, ""immediatelyAvailable"": { ""amount"": { ""value"": ""-1.24"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""transactionType"": ""refund"", ""count"": 1, ""amount"": { ""value"": ""-1.00"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""method"": ""creditcard"", ""count"": 1, ""amount"": { ""value"": ""-1.00"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""cardIssuer"": ""other"", ""cardAudience"": ""other"", ""cardRegion"": ""domestic"", ""count"": 1, ""amount"": { ""value"": ""-1.00"", ""currency"": ""EUR"" } } ] } ] }, { ""transactionType"": ""fee-prepayment"", ""amount"": { ""value"": ""-0.25"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""prepaymentPartType"": ""fee"", ""count"": 1, ""amount"": { ""value"": ""-0.25"", ""currency"": ""EUR"" }, ""subtotals"": [{ ""feeType"": ""refund-fee"", ""count"": 1, ""amount"": { ""value"": ""-0.25"", ""currency"": ""EUR"" } } ] } ] }, { ""transactionType"": ""invoice-rounding-compensation"", ""count"": 1, ""amount"": { ""value"": ""0.01"", ""currency"": ""EUR"" } } ] }, ""close"": { ""amount"": { ""value"": ""2.25"", ""currency"": ""EUR"" } } } }, ""_links"": { ""self"": { ""href"": ""https://api.mollie.com/v2/balances/bal_CKjKwQdjCwCSArXFAJNFH/report"", ""type"": ""application/hal+json"" }, ""documentation"": { ""href"": ""https://docs.mollie.com/reference/v2/balances-api/get-balance-report"", ""type"": ""text/html"" } } }"; private class GetBalanceResponseFactory { public string BalanceId { get; set; } = "bal_gVMhHKqSSRYJyPsuoPNFH"; public DateTime CreatedAt { get; set; } = DateTime.SpecifyKind(new DateTime(2022, 11, 22, 13, 15, 0), DateTimeKind.Utc); public string Currency { get; set; } = "EUR"; public BalanceResponseStatus Status { get; set; } = BalanceResponseStatus.Active; public Amount AvailableAmount { get; set; } = new Amount(Api.Models.Currency.EUR, 905.25m); public Amount PendingAmount { get; set; } = new Amount(Api.Models.Currency.EUR, 100); public string TransferFrequency { get; set; } = "twice-a-month"; public Amount TransferThreshold { get; set; } = new Amount(Api.Models.Currency.EUR, 5); public string TransferReference { get; set; } = "Mollie payout"; public BalanceTransferDestination TransferDestination { get; set; } = new BalanceTransferDestination { Type = "bank-account", BankAccount = "bank-account", BeneficiaryName = "Piet" }; public string CreateGetBalanceResponse() { return @$" {{ ""resource"": ""balance"", ""id"": ""{BalanceId}"", ""mode"": ""live"", ""createdAt"": ""{CreatedAt:yyyy-MM-ddTHH:mm:ss.fffffffzzz}"", ""currency"": ""{Currency}"", ""status"": ""{Status}"", ""availableAmount"": {{ ""value"": ""{AvailableAmount.Value}"", ""currency"": ""{AvailableAmount.Currency}"" }}, ""pendingAmount"": {{ ""value"": ""{PendingAmount.Value}"", ""currency"": ""{PendingAmount.Currency}"" }}, ""transferFrequency"": ""{TransferFrequency}"", ""transferThreshold"": {{ ""value"": ""{TransferThreshold.Value}"", ""currency"": ""{TransferThreshold.Currency}"" }}, ""transferReference"": ""{TransferReference}"", ""transferDestination"": {{ ""type"": ""{TransferDestination.Type}"", ""beneficiaryName"": ""{TransferDestination.BeneficiaryName}"", ""bankAccount"": ""{TransferDestination.BankAccount}"", ""bankAccountId"": ""bnk_jrty3f"" }}, ""_links"": {{ ""self"": {{ ""href"": ""https://api.mollie.com/v2/balances/bal_gVMhHKqSSRYJyPsuoPNFH"", ""type"": ""application/hal+json"" }}, ""documentation"": {{ ""href"": ""https://docs.mollie.com/reference/v2/balances-api/get-balance"", ""type"": ""text/html"" }} }} }}"; } } } } ================================================ FILE: tests/Mollie.Tests.Unit/Client/BalanceTransferClientTests.cs ================================================ using System; using System.Net.Http; using System.Threading.Tasks; using Mollie.Api.Client; using Mollie.Api.Models; using Mollie.Api.Models.BalanceTransfer; using Mollie.Api.Models.BalanceTransfer.Request; using Mollie.Api.Models.BalanceTransfer.Response; using RichardSzalay.MockHttp; using Shouldly; using Xunit; using SortDirection = Mollie.Api.Models.SortDirection; namespace Mollie.Tests.Unit.Client; public class BalanceTransferClientTests : BaseClientTests { [Fact] public async Task CreateBalanceTransferAsync_WithRequiredParameters_ResponseIsDeserializedInExpectedFormat() { // Given: we create a balance transfer request with only the required parameters const string balanceTransferId = "balance-transfer-id"; BalanceTransferRequest request = new() { Description = "Test Description", Amount = new Amount(Currency.EUR, 50), Source = new BalanceTransferParty { Id = "source", Description = "Test Source", Type = "organization" }, Destination = new BalanceTransferParty { Id = "destination", Description = "Test Destination", Type = "organization" } }; string jsonToReturnInMockResponse = CreateBalanceTransferJsonResponse(balanceTransferId, request); var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Post, $"{BaseMollieClient.DefaultBaseApiEndPoint}connect/balance-transfers", jsonToReturnInMockResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var client = new BalanceTransferClient("abcde", httpClient); // When: We send the request BalanceTransferResponse response = await client.CreateBalanceTransferAsync(request); // Then mockHttp.VerifyNoOutstandingExpectation(); response.ShouldNotBeNull(); response.Description.ShouldBe(request.Description); response.Amount.ShouldBeEquivalentTo(request.Amount); response.Source.ShouldBeEquivalentTo(response.Source); response.Destination.ShouldBeEquivalentTo(response.Destination); response.Status.ShouldBe("succeeded"); response.StatusReason.Code.ShouldBe("success"); response.StatusReason.Message.ShouldBe("Balance transfer completed successfully."); response.CreatedAt.ToUniversalTime().ShouldBe(new DateTime(2025, 5, 1, 10, 0, 0, DateTimeKind.Utc)); response.ExecutedAt!.Value.ToUniversalTime().ShouldBe(new DateTime(2025, 5, 1, 10, 5, 0, DateTimeKind.Utc)); response.Mode.ShouldBe(Mode.Live); } [Theory] [InlineData(null, null, false, null, "")] [InlineData("from", null, false, null, "?from=from")] [InlineData("from", 50, false, null, "?from=from&limit=50")] [InlineData(null, null, true, null, "?testmode=true")] [InlineData(null, null, true, SortDirection.Desc, "?testmode=true&sort=desc")] [InlineData(null, null, true, SortDirection.Asc, "?testmode=true&sort=asc")] public async Task GetBalanceTransferListAsync_TestModeParameterCase_QueryStringOnlyContainsTestModeParameterIfTrue( string? from, int? limit, bool testmode, SortDirection? sortDirection, string expectedQueryString) { // Given: We retrieve a list of customers var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}connect/balance-transfers{expectedQueryString}") .Respond("application/json", CreateBalanceTransferListJsonResponse()); HttpClient httpClient = mockHttp.ToHttpClient(); var client = new BalanceTransferClient("abcde", httpClient); // When: We send the request var result = await client.GetBalanceTransferListAsync(from, limit, sortDirection, testmode); // Then mockHttp.VerifyNoOutstandingExpectation(); result.ShouldNotBeNull(); } [Fact] public async Task GetBalanceTransferAsync_WithValidBalanceTransferId_ResponseIsDeserializedInExpectedFormat() { // Given: We have a valid balance transfer ID const string balanceTransferId = "balance-transfer-id"; BalanceTransferRequest request = new() { Description = "Test Description", Amount = new Amount(Currency.EUR, 50), Source = new BalanceTransferParty { Id = "source", Description = "Test Source", Type = "organization" }, Destination = new BalanceTransferParty { Id = "destination", Description = "Test Destination", Type = "organization" } }; string jsonToReturnInMockResponse = CreateBalanceTransferJsonResponse(balanceTransferId, request); var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}connect/balance-transfers/{balanceTransferId}", jsonToReturnInMockResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var client = new BalanceTransferClient("abcde", httpClient); // When: We attempt to retrieve the balance transfer BalanceTransferResponse response = await client.GetBalanceTransferAsync(balanceTransferId); // Then mockHttp.VerifyNoOutstandingExpectation(); response.ShouldNotBeNull(); response.Id.ShouldBe(balanceTransferId); response.Description.ShouldBe(request.Description); } [Fact] public async Task GetBalanceTransferAsync_WithTestmodeTrue_QueryStringContainsTestmodeParameter() { // Given: We have a valid balance transfer ID const string balanceTransferId = "balance-transfer-id"; BalanceTransferRequest request = new() { Description = "Test Description", Amount = new Amount(Currency.EUR, 50), Source = new BalanceTransferParty { Id = "source", Description = "Test Source", Type = "organization" }, Destination = new BalanceTransferParty { Id = "destination", Description = "Test Destination", Type = "organization" } }; string jsonToReturnInMockResponse = CreateBalanceTransferJsonResponse(balanceTransferId, request); var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}connect/balance-transfers/{balanceTransferId}?testmode=true", jsonToReturnInMockResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var client = new BalanceTransferClient("abcde", httpClient); // When: We attempt to retrieve the balance transfer BalanceTransferResponse response = await client.GetBalanceTransferAsync(balanceTransferId, testmode: true); // Then mockHttp.VerifyNoOutstandingExpectation(); response.ShouldNotBeNull(); response.Id.ShouldBe(balanceTransferId); response.Description.ShouldBe(request.Description); } [Fact] public async Task GetBalanceTransferAsync_WithEmptyBalanceTransferId_ThrowsException() { // Given: We have an empty balance transfer ID var client = new BalanceTransferClient("abcde", new HttpClient()); // When: We attempt to retrieve the balance transfer ArgumentException exception = await Should.ThrowAsync(async () => { await client.GetBalanceTransferAsync(""); }); // Then exception.Message.ShouldContain("balanceTransferId"); } private string CreateBalanceTransferListJsonResponse() { BalanceTransferRequest request = new() { Description = "Test Description", Amount = new Amount(Currency.EUR, 50), Source = new BalanceTransferParty { Id = "source", Description = "Test Source", Type = "organization" }, Destination = new BalanceTransferParty { Id = "destination", Description = "Test Destination", Type = "organization" } }; return $@"{{ ""count"": 1, ""_embedded"": {{ ""connect-balance-transfers"": [ {CreateBalanceTransferJsonResponse("balance-transfer-id-1", request)}, {CreateBalanceTransferJsonResponse("balance-transfer-id-2", request)}, ] }} }}"; } private string CreateBalanceTransferJsonResponse(string balanceTransferId, BalanceTransferRequest request) { return $@"{{ ""resource"": ""connect-balance-transfer"", ""id"": ""{balanceTransferId}"", ""amount"": {{ ""value"": ""{request.Amount.Value}"", ""currency"": ""{request.Amount.Currency}"" }}, ""source"": {{ ""type"": ""{request.Source.Type}"", ""id"": ""{request.Source.Id}"", ""description"": ""{request.Source.Description}"" }}, ""destination"": {{ ""type"": ""{request.Destination.Type}"", ""id"": ""{request.Destination.Id}"", ""description"": ""{request.Destination.Description}"" }}, ""description"": ""{request.Description}"", ""status"": ""succeeded"", ""statusReason"": {{ ""code"": ""success"", ""message"": ""Balance transfer completed successfully."" }}, ""metadata"": {{ ""order_id"": 12345, ""customer_id"": 9876 }}, ""createdAt"": ""2025-05-01T10:00:00+00:00"", ""executedAt"": ""2025-05-01T10:05:00+00:00"", ""mode"": ""live"" }}"; } } ================================================ FILE: tests/Mollie.Tests.Unit/Client/BaseClientTests.cs ================================================ using System.Net; using RichardSzalay.MockHttp; using System.Net.Http; using System.Net.Mime; namespace Mollie.Tests.Unit.Client { public abstract class BaseClientTests { protected const string DefaultRedirectUrl = "https://www.mollie.com"; protected MockHttpMessageHandler CreateMockHttpMessageHandler( HttpMethod httpMethod, string url, string response, string? expectedPartialContent = null, string responseContentType = MediaTypeNames.Application.Json, HttpStatusCode responseStatusCode = HttpStatusCode.OK) { MockHttpMessageHandler mockHttp = new(); MockedRequest mockedRequest = mockHttp.Expect(httpMethod, url) .Respond(responseStatusCode, responseContentType, response); if (!string.IsNullOrEmpty(expectedPartialContent)) { mockedRequest.With(x => { string expectedContent = RemoveWhiteSpaces(expectedPartialContent); string content = RemoveWhiteSpaces(x.Content!.ReadAsStringAsync().Result); return content.Contains(expectedContent); }); } return mockHttp; } private string RemoveWhiteSpaces(string input) { return input .Replace(System.Environment.NewLine, string.Empty) .Replace(" ", string.Empty); } } } ================================================ FILE: tests/Mollie.Tests.Unit/Client/BaseMollieClientTests.cs ================================================ using System.Linq; using System.Net; using System.Net.Http; using System.Net.Mime; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Framework.Authentication; using Mollie.Api.Models; using Mollie.Api.Models.Payment.Request; using Mollie.Api.Options; using RichardSzalay.MockHttp; using Xunit; namespace Mollie.Tests.Unit.Client; public class BaseMollieClientTests : BaseClientTests { [Fact] public async Task HttpResponseStatusCodeIsNotSuccesfull_ResponseBodyContainsMollieErrorDetails_MollieApiExceptionIsThrown() { // Arrange const string errorMessage = "A validation error occured"; const int errorStatus = (int)HttpStatusCode.UnprocessableEntity; string responseBody = @$"{{ ""_links"": {{ ""documentation"": {{ ""href"": ""https://docs.mollie.com/overview/handling-errors"", ""type"": ""text/html"" }} }}, ""detail"": ""{errorMessage}"", ""status"": {errorStatus}, ""title"": ""Error"" }}"; const string expectedUrl = $"{BaseMollieClient.DefaultBaseApiEndPoint}payments"; var mockHttp = CreateMockHttpMessageHandler( HttpMethod.Post, expectedUrl, responseBody, responseStatusCode: HttpStatusCode.UnprocessableEntity); HttpClient httpClient = mockHttp.ToHttpClient(); PaymentClient paymentClient = new("api-key", httpClient); PaymentRequest paymentRequest = new() { Amount = new Amount(Currency.EUR, 50m), Description = "description" }; // Act var exception = await Assert.ThrowsAsync(() => paymentClient.CreatePaymentAsync(paymentRequest)); // Assert exception.Details.Detail.ShouldBe(errorMessage); exception.Details.Status.ShouldBe(errorStatus); } [Fact] public async Task HttpResponseStatusCodeIsNotSuccesfull_ResponseBodyContainsHtml_MollieApiExceptionIsThrown() { // Arrange string responseBody = "Whoops!"; const string expectedUrl = $"{BaseMollieClient.DefaultBaseApiEndPoint}payments"; var mockHttp = CreateMockHttpMessageHandler( HttpMethod.Post, expectedUrl, responseBody, responseContentType: MediaTypeNames.Text.Html, responseStatusCode: HttpStatusCode.UnprocessableEntity); HttpClient httpClient = mockHttp.ToHttpClient(); PaymentClient paymentClient = new("api-key", httpClient); PaymentRequest paymentRequest = new() { Amount = new Amount(Currency.EUR, 50m), Description = "description" }; // Act var exception = await Assert.ThrowsAsync(() => paymentClient.CreatePaymentAsync(paymentRequest)); // Assert exception.Details.Detail.ShouldBe(responseBody); exception.Details.Status.ShouldBe((int)HttpStatusCode.UnprocessableEntity); } [Fact] public async Task CustomUserAgentIsSetInOptions_UserAgentIsAppendedToDefaultUserAgent() { // Arrange const string customUserAgent = "my-user-agent"; var mockHttp = new MockHttpMessageHandler(); mockHttp.Expect(HttpMethod.Get,$"{BaseMollieClient.DefaultBaseApiEndPoint}methods") .With(request => { var userAgent = request.Headers.UserAgent.ToArray(); userAgent.ShouldNotBeNull(); userAgent.Length.ShouldBe(2); userAgent[0].Product!.Name.ShouldStartWith("Mollie.Api.NET"); userAgent[1].Product!.Name.ShouldBe(customUserAgent); return true; }) .Respond("application/json", DefaultPaymentMethodJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var mollieClientOptions = new MollieClientOptions { CustomUserAgent = customUserAgent, ApiKey = "api-key" }; var secretManager = new DefaultMollieSecretManager(mollieClientOptions.ApiKey); using var paymentMethodClient = new PaymentMethodClient(mollieClientOptions, secretManager, httpClient); // Act await paymentMethodClient.GetPaymentMethodListAsync(); // Assert mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task NoCustomUserAgentIsSetInOptions_UserAgentIsAppendedToDefaultUserAgent() { // Arrange var mockHttp = new MockHttpMessageHandler(); mockHttp.Expect(HttpMethod.Get,$"{BaseMollieClient.DefaultBaseApiEndPoint}methods") .With(request => { var userAgent = request.Headers.UserAgent.ToArray(); userAgent.ShouldNotBeNull(); userAgent.Length.ShouldBe(1); userAgent[0].Product!.Name.ShouldStartWith("Mollie.Api.NET"); return true; }) .Respond("application/json", DefaultPaymentMethodJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var mollieClientOptions = new MollieClientOptions { CustomUserAgent = null, ApiKey = "api-key" }; var secretManager = new DefaultMollieSecretManager(mollieClientOptions.ApiKey); using var paymentMethodClient = new PaymentMethodClient(mollieClientOptions, secretManager, httpClient); // Act await paymentMethodClient.GetPaymentMethodListAsync(); // Assert mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task CustomApiBaseUrlIsSetInOptions_RequestsAreRoutedToCustomApiUrl() { // Arrange var mockHttp = new MockHttpMessageHandler(); var mollieClientOptions = new MollieClientOptions { ApiKey = "api-key", ApiBaseUrl = "https://custom-api-base.mollie.com/v2/" }; mockHttp.Expect(HttpMethod.Get,$"{mollieClientOptions.ApiBaseUrl}methods") .Respond("application/json", DefaultPaymentMethodJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var secretManager = new DefaultMollieSecretManager(mollieClientOptions.ApiKey); using var paymentMethodClient = new PaymentMethodClient(mollieClientOptions, secretManager, httpClient); // Act await paymentMethodClient.GetPaymentMethodListAsync(); // Assert mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task ProfileIdIsSetInOptions_ForPostRequest_RequestProfileIdIsSet() { // Arrange var mockHttp = new MockHttpMessageHandler(); var mollieClientOptions = new MollieClientOptions { ApiKey = "api-key", ProfileId = "my-profile-id" }; mockHttp.Expect(HttpMethod.Post, $"{mollieClientOptions.ApiBaseUrl}payments") .WithPartialContent($"\"profileId\":\"{mollieClientOptions.ProfileId}\"") .Respond("application/json", DefaultPaymentJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var secretManager = new DefaultMollieSecretManager(mollieClientOptions.ApiKey); using var paymentClient = new PaymentClient(mollieClientOptions, secretManager, httpClient); var request = new PaymentRequest { Amount = new Amount(Currency.EUR, 10m), Description = "Test payment" }; // Act await paymentClient.CreatePaymentAsync(request); // Assert mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task TestModeIsSetInOptions_ForPostRequest_RequestTestModeIsSet() { // Arrange var mockHttp = new MockHttpMessageHandler(); var mollieClientOptions = new MollieClientOptions { ApiKey = "api-key", Testmode = true }; mockHttp.Expect(HttpMethod.Post, $"{mollieClientOptions.ApiBaseUrl}payments") .WithPartialContent("\"testmode\":true") .Respond("application/json", DefaultPaymentJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var secretManager = new DefaultMollieSecretManager(mollieClientOptions.ApiKey); using var paymentClient = new PaymentClient(mollieClientOptions, secretManager, httpClient); var request = new PaymentRequest { Amount = new Amount(Currency.EUR, 10m), Description = "Test payment" }; // Act await paymentClient.CreatePaymentAsync(request); // Assert mockHttp.VerifyNoOutstandingExpectation(); } private const string DefaultPaymentMethodJsonResponse = @"{ ""count"": 13, ""_embedded"": { ""methods"": [ { ""resource"": ""method"", ""id"": ""ideal"", ""description"": ""iDEAL"", ""minimumAmount"": { ""value"": ""0.01"", ""currency"": ""EUR"" }, ""maximumAmount"": { ""value"": ""50000.00"", ""currency"": ""EUR"" }, ""image"": { ""size1x"": ""https://mollie.com/external/icons/payment-methods/ideal.png"", ""size2x"": ""https://mollie.com/external/icons/payment-methods/ideal%402x.png"", ""svg"": ""https://mollie.com/external/icons/payment-methods/ideal.svg"" }, ""status"": ""activated"" } ] } }"; private const string DefaultPaymentJsonResponse = @" { ""resource"": ""payment"", ""id"": ""tr_WDqYK6vllg"", ""mode"": ""test"", ""createdAt"": ""2018-03-20T13:13:37+00:00"", ""amount"":{ ""currency"":""EUR"", ""value"":""100.00"" }, ""description"":""Description"", ""method"": null, ""metadata"": { ""order_id"": ""12345"" }, ""status"": ""open"", ""isCancelable"": false, ""locale"": ""nl_NL"", ""restrictPaymentMethodsToCountry"": ""NL"", ""expiresAt"": ""2018-03-20T13:28:37+00:00"", ""details"": null, ""profileId"": ""pfl_QkEhN94Ba"", ""sequenceType"": ""oneoff"", ""redirectUrl"": ""https://webshop.example.org/order/12345/"", ""webhookUrl"": ""https://webshop.example.org/payments/webhook/"", ""authorizedAt"": ""2018-03-19T13:28:37+00:00"", ""paidAt"": ""2018-03-21T13:28:37+00:00"", ""canceledAt"": ""2018-03-22T13:28:37+00:00"", ""expiredAt"": ""2018-03-23T13:28:37+00:00"", ""failedAt"": ""2018-03-24T13:28:37+00:00"", ""captureBefore"": ""2018-03-25T13:28:37+00:00"", ""amountRefunded"": { ""currency"": ""EUR"", ""value"": ""10.00"" }, ""amountRemaining"": { ""currency"": ""EUR"", ""value"": ""90.00"" }, ""amountChargedBack"": { ""currency"": ""EUR"", ""value"": ""10.00"" }, ""cancelUrl"": ""https://webshop.example.org/order/12345/cancel"", ""countryCode"": ""NL"", ""settlementId"": ""stl_jDk30akdN"", ""subscriptionId"": ""sub_rVKGtNd6s3"", ""applicationFee"": { ""amount"": { ""currency"": ""EUR"", ""value"": ""1.00"" }, ""description"": ""description"" }, ""_links"": { ""self"": { ""href"": ""https://api.mollie.com/v2/payments/tr_WDqYK6vllg"", ""type"": ""application/hal+json"" }, ""checkout"": { ""href"": ""https://www.mollie.com/payscreen/select-method/WDqYK6vllg"", ""type"": ""text/html"" }, ""dashboard"": { ""href"": ""https://www.mollie.com/dashboard/org_12345678/payments/tr_WDqYK6vllg"", ""type"": ""text/html"" }, ""documentation"": { ""href"": ""https://docs.mollie.com/reference/v2/payments-api/get-payment"", ""type"": ""text/html"" } } }"; } ================================================ FILE: tests/Mollie.Tests.Unit/Client/CapabilityClientTests.cs ================================================ using System; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Mollie.Api.Client; using Mollie.Api.Models.Capability; using RichardSzalay.MockHttp; using Shouldly; using Xunit; namespace Mollie.Tests.Unit.Client; public class CapabilityClientTests { [Fact] public async Task GetCapabilityListAsync_ResponseIsDeserializedInExpectedFormat() { // Arrange var mockHttp = new MockHttpMessageHandler(); mockHttp.Expect(HttpMethod.Get,$"{BaseMollieClient.DefaultBaseApiEndPoint}capabilities") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", DefaultListCapabilitiesResponse); HttpClient httpClient = mockHttp.ToHttpClient(); using var capabilityClient = new CapabilityClient("access_1234", httpClient); // Act var result = await capabilityClient.GetCapabilitiesListAsync(); // Assert mockHttp.VerifyNoOutstandingRequest(); result.Count.ShouldBe(1); var capability = result.Items[0]; capability.Resource.ShouldBe("capability"); capability.Name.ShouldBe("payments"); capability.Status.ShouldBe(CapabilityStatus.Pending); capability.StatusReason.ShouldBe(CapabilityStatusReason.OnboardingInformtionNeeded); capability.Requirements.Count().ShouldBe(2); var requirement1 = capability.Requirements.First(); requirement1.Id.ShouldBe("legal-representatives"); requirement1.DueDate.ShouldBeNull(); requirement1.Status.ShouldBe(CapabilityRequirementStatus.Requested); requirement1.Links.ShouldNotBeNull(); requirement1.Links.Dashboard.Href.ShouldBe("https://my.mollie.com/dashboard/"); requirement1.Links.Dashboard.Type.ShouldBe("text/html"); var requirement2 = capability.Requirements.Skip(1).First(); requirement2.Id.ShouldBe("bank-account"); requirement2.DueDate.ShouldBe(new DateTime(2024, 5, 14, 1, 29, 9, DateTimeKind.Utc)); requirement2.Status.ShouldBe(CapabilityRequirementStatus.PastDue); requirement2.Links.ShouldNotBeNull(); requirement2.Links.Dashboard.Href.ShouldBe("https://my.mollie.com/dashboard/"); requirement2.Links.Dashboard.Type.ShouldBe("text/html"); } private const string DefaultListCapabilitiesResponse = @"{ ""count"": 1, ""_embedded"": { ""capabilities"": [ { ""resource"": ""capability"", ""name"": ""payments"", ""status"": ""pending"", ""statusReason"": ""onboarding-information-needed"", ""requirements"": [ { ""id"": ""legal-representatives"", ""dueDate"": null, ""status"": ""requested"", ""_links"": { ""dashboard"": { ""href"": ""https://my.mollie.com/dashboard/"", ""type"": ""text/html"" } } }, { ""id"": ""bank-account"", ""dueDate"": ""2024-05-14T01:29:09Z"", ""status"": ""past-due"", ""_links"": { ""dashboard"": { ""href"": ""https://my.mollie.com/dashboard/"", ""type"": ""text/html"" } } } ] } ] }, ""_links"": { ""documentation"": { ""href"": ""https://docs.mollie.com/reference/list-capabilities"", ""type"": ""text/html"" } } }"; } ================================================ FILE: tests/Mollie.Tests.Unit/Client/CaptureClientTests.cs ================================================ using System; using Mollie.Api.Client; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Models; using Mollie.Api.Models.Capture.Request; using Mollie.Api.Models.Capture.Response; using Mollie.Api.Models.List.Response; using RichardSzalay.MockHttp; using Xunit; namespace Mollie.Tests.Unit.Client { public class CaptureClientTests : BaseClientTests { private const string defaultCaptureId = "tr_ffh435dvgs"; private const string defaultPaymentId = "tr_WDqYK6vllg"; private const string defaultShipmentId = "shp_3wmsgCJN4U"; private const string defaultSettlementId = "settlementId"; private const string defaultAmountValue = "1027.99"; private const string defaultAmountCurrency = "EUR"; private const string defaultStatus = "succeeded"; private string defaultCaptureJsonResponse = $@"{{ ""resource"": ""capture"", ""id"": ""{defaultCaptureId}"", ""mode"": ""live"", ""amount"": {{ ""value"": ""{defaultAmountValue}"", ""currency"": ""{defaultAmountCurrency}"" }}, ""settlementAmount"": {{ ""value"": ""1027.99"", ""currency"": ""EUR"" }}, ""paymentId"": ""{defaultPaymentId}"", ""shipmentId"": ""{defaultShipmentId}"", ""settlementId"": ""{defaultSettlementId}"", ""status"": ""{defaultStatus}"", ""createdAt"": ""2018-08-02T09:29:56+00:00"", }}"; private string defaultCaptureListJsonResponse = $@"{{ ""_embedded"": {{ ""captures"": [ {{ ""resource"": ""capture"", ""id"": ""cpt_4qqhO89gsT"", ""mode"": ""live"", ""amount"": {{ ""value"": ""{defaultAmountValue}"", ""currency"": ""{defaultAmountCurrency}"" }}, ""settlementAmount"": {{ ""value"": ""1027.99"", ""currency"": ""EUR"" }}, ""paymentId"": ""{defaultPaymentId}"", ""shipmentId"": ""{defaultShipmentId}"", ""settlementId"": ""{defaultSettlementId}"", ""status"": ""{defaultStatus}"", ""createdAt"": ""2018-08-02T09:29:56+00:00"" }} ] }}, ""count"": 1 }}"; [Theory] [InlineData(true, "?testmode=true")] [InlineData(false, "")] public async Task GetCaptureAsync_CorrectQueryParametersAreAdded(bool testmode, string expectedQueryString) { // Given: We make a request to retrieve a capture const string paymentId = "payment-id"; const string captureId = "capture-id"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{paymentId}/captures/{captureId}{expectedQueryString}", defaultCaptureJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); CaptureClient captureClient = new CaptureClient("abcde", httpClient); // When: We send the request await captureClient.GetCaptureAsync(paymentId, captureId, testmode); // Then mockHttp.VerifyNoOutstandingRequest(); } [Theory] [InlineData(true, "?testmode=true")] [InlineData(false, "")] public async Task GetCapturesListAsync_CorrectQueryParametersAreAdded(bool testmode, string expectedQueryString) { // Given: We make a request to retrieve a capture const string paymentId = "payment-id"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{paymentId}/captures{expectedQueryString}", defaultCaptureJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); CaptureClient captureClient = new CaptureClient("abcde", httpClient); // When: We send the request await captureClient.GetCaptureListAsync(paymentId, testmode); // Then mockHttp.VerifyNoOutstandingRequest(); } [Fact] public async Task GetCaptureAsync_DefaultBehaviour_ResponseIsParsed() { // Given: We request a capture with a payment id and capture id string expectedUrl = $"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{defaultPaymentId}/captures/{defaultCaptureId}"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, expectedUrl, defaultCaptureJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); CaptureClient captureClient = new CaptureClient("api-key", httpClient); // When: We make the request CaptureResponse captureResponse = await captureClient.GetCaptureAsync(defaultPaymentId, defaultCaptureId); // Then: Response should be parsed mockHttp.VerifyNoOutstandingExpectation(); captureResponse.ShouldNotBeNull(); captureResponse.PaymentId.ShouldBe(defaultPaymentId); captureResponse.ShipmentId.ShouldBe(defaultShipmentId); captureResponse.SettlementId.ShouldBe(defaultSettlementId); captureResponse.Amount.Value.ShouldBe(defaultAmountValue); captureResponse.Amount.Currency.ShouldBe(defaultAmountCurrency); captureResponse.Status.ShouldBe(defaultStatus); } [Fact] public async Task GetCapturesListAsync_DefaultBehaviour_ResponseIsParsed() { // Given: We request a list of captures string expectedUrl = $"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{defaultPaymentId}/captures"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, expectedUrl, defaultCaptureListJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); CaptureClient captureClient = new CaptureClient("api-key", httpClient); // When: We make the request ListResponse listCaptureResponse = await captureClient.GetCaptureListAsync(defaultPaymentId); // Then: Response should be parsed mockHttp.VerifyNoOutstandingExpectation(); listCaptureResponse.ShouldNotBeNull(); listCaptureResponse.Count.ShouldBe(1); CaptureResponse captureResponse = listCaptureResponse.Items.First(); captureResponse.PaymentId.ShouldBe(defaultPaymentId); captureResponse.ShipmentId.ShouldBe(defaultShipmentId); captureResponse.SettlementId.ShouldBe(defaultSettlementId); captureResponse.Amount.Value.ShouldBe(defaultAmountValue); captureResponse.Amount.Currency.ShouldBe(defaultAmountCurrency); captureResponse.Status.ShouldBe(defaultStatus); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetCaptureAsync_NoPaymentIdIsGiven_ArgumentExceptionIsThrown(string? paymentId) { // Given var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); CaptureClient captureClient = new CaptureClient("abcde", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await captureClient.GetCaptureAsync(paymentId, "capture-id")); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'paymentId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetCaptureAsync_NoCaptureIdIsGiven_ArgumentExceptionIsThrown(string? captureId) { // Given var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); CaptureClient captureClient = new CaptureClient("abcde", httpClient); // When: We send the request var exception = await Assert.ThrowsAsync(async () => #pragma warning disable CS8604 // Possible null reference argument. await captureClient.GetCaptureAsync("payment-id", captureId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'captureId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetCapturesListAsync_NoPaymentIdIsGiven_ArgumentExceptionIsThrown(string? paymentId) { // Given var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); CaptureClient captureClient = new CaptureClient("abcde", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await captureClient.GetCaptureListAsync(paymentId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'paymentId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task CreateCapture_NoPaymentIdIsGiven_ArgumentExceptionIsThrown(string? paymentId) { // Given var captureRequest = new CaptureRequest { Amount = new Amount(Currency.EUR, 10m), Description = "capture-description" }; var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); CaptureClient captureClient = new CaptureClient("abcde", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await captureClient.CreateCapture(paymentId, captureRequest)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'paymentId' is null or empty"); } [Fact] public async Task CreateCapture_CaptureIsCreated_CaptureIsDeserialized() { // Given var captureRequest = new CaptureRequest { Amount = new Amount(defaultAmountCurrency, defaultAmountValue), Description = "capture-description" }; var mockHttp = CreateMockHttpMessageHandler( HttpMethod.Post, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{defaultPaymentId}/captures", defaultCaptureJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); CaptureClient captureClient = new CaptureClient("abcde", httpClient); // When CaptureResponse response = await captureClient.CreateCapture(defaultPaymentId, captureRequest); // Then mockHttp.VerifyNoOutstandingRequest(); response.Id.ShouldBe(defaultCaptureId); response.PaymentId.ShouldBe(defaultPaymentId); } } } ================================================ FILE: tests/Mollie.Tests.Unit/Client/ChargebackClientTests.cs ================================================ using System; using System.Net.Http; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Models.Chargeback.Response; using RichardSzalay.MockHttp; using Xunit; namespace Mollie.Tests.Unit.Client { public class ChargebackClientTests : BaseClientTests { private const string defaultPaymentId = "tr_WDqYK6vllg"; private const string defaultChargebackId = "chb_n9z0tp"; private const string defaultChargebackReasonCode = "AC01"; private const string defaultChargebackReason = "Account identifier incorrect (i.e. invalid IBAN)"; private string defaultGetChargebacksResponse = @$"{{ ""resource"": ""chargeback"", ""id"": ""{defaultChargebackId}"", ""amount"": {{ ""currency"": ""USD"", ""value"": ""43.38"" }}, ""settlementAmount"": {{ ""currency"": ""EUR"", ""value"": ""-35.07"" }}, ""createdAt"": ""2018-03-14T17:00:52.0Z"", ""reason"": {{ ""code"": ""AC01"", ""description"": ""Account identifier incorrect (i.e. invalid IBAN)"" }}, ""reversedAt"": null, ""paymentId"": ""{defaultPaymentId}"", ""_links"": {{ ""self"": {{ ""href"": ""https://api.mollie.com/v2/payments/tr_WDqYK6vllg/chargebacks/chb_n9z0tp"", ""type"": ""application/hal+json"" }}, ""payment"": {{ ""href"": ""https://api.mollie.com/v2/payments/tr_WDqYK6vllg"", ""type"": ""application/hal+json"" }}, ""documentation"": {{ ""href"": ""https://docs.mollie.com/reference/v2/chargebacks-api/get-chargeback"", ""type"": ""text/html"" }} }} }}"; [Fact] public async Task GetChargebackAsync_ResponseIsDeserializedInExpectedFormat() { // Given: we retrieve the chargeback by id and payment id var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}*") .Respond("application/json", defaultGetChargebacksResponse); HttpClient httpClient = mockHttp.ToHttpClient(); ChargebackClient chargebackClient = new ChargebackClient("abcde", httpClient); // When: We send the request ChargebackResponse chargebackResponse = await chargebackClient.GetChargebackAsync(defaultPaymentId, defaultChargebackId); // Then chargebackResponse.PaymentId.ShouldBe(defaultPaymentId); chargebackResponse.Id.ShouldBe(defaultChargebackId); chargebackResponse.Reason.ShouldNotBeNull(); chargebackResponse.Reason!.Code.ShouldBe(defaultChargebackReasonCode); chargebackResponse.Reason.Description.ShouldBe(defaultChargebackReason); } [Theory] [InlineData(false, "")] [InlineData(true, "?testmode=true")] public async Task GetOrderRefundListAsync_QueryParameterOptions_CorrectParametersAreAdded(bool testmode, string expectedQueryString) { // Given: we retrieve the chargeback by id and payment id var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{defaultPaymentId}/chargebacks/{defaultChargebackId}{expectedQueryString}") .Respond("application/json", defaultGetChargebacksResponse); HttpClient httpClient = mockHttp.ToHttpClient(); ChargebackClient chargebackClient = new ChargebackClient("abcde", httpClient); // When: We send the request await chargebackClient.GetChargebackAsync(defaultPaymentId, defaultChargebackId, testmode); // Then mockHttp.VerifyNoOutstandingRequest(); } [Theory] [InlineData(null, null, false, "")] [InlineData("from", null, false, "?from=from")] [InlineData("from", 50, false, "?from=from&limit=50")] [InlineData(null, null, true, "?testmode=true")] public async Task GetChargebacksListAsync_FromLimitTestmodeQueryParameterOptions_CorrectParametersAreAdded(string? from, int? limit, bool testmode, string expectedQueryString) { // Given: we retrieve the chargeback by id and payment id var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{defaultPaymentId}/chargebacks{expectedQueryString}") .Respond("application/json", defaultGetChargebacksResponse); HttpClient httpClient = mockHttp.ToHttpClient(); ChargebackClient chargebackClient = new ChargebackClient("abcde", httpClient); // When: We send the request await chargebackClient.GetChargebackListAsync(defaultPaymentId, from, limit, testmode); // Then mockHttp.VerifyNoOutstandingRequest(); } [Theory] [InlineData(null, false, "")] [InlineData("profileId", false, "?profileId=profileId")] [InlineData("profileId", true, "?profileId=profileId&testmode=true")] public async Task GetChargebacksListAsync_ProfileTestModeQueryParameterOptions_CorrectParametersAreAdded(string? profileId, bool testmode, string expectedQueryString) { // Given: we retrieve the chargeback by id and payment id var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}chargebacks{expectedQueryString}") .Respond("application/json", defaultGetChargebacksResponse); HttpClient httpClient = mockHttp.ToHttpClient(); ChargebackClient chargebackClient = new ChargebackClient("abcde", httpClient); // When: We send the request await chargebackClient.GetChargebackListAsync(profileId, testmode); // Then mockHttp.VerifyNoOutstandingRequest(); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetChargebackAsync_NoPaymentIdIsGiven_ArgumentExceptionIsThrown(string? paymentId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); ChargebackClient chargebackClient = new ChargebackClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await chargebackClient.GetChargebackAsync(paymentId, "chargeback-id")); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'paymentId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetChargebackAsync_NoChargeBackIdIsGiven_ArgumentExceptionIsThrown(string? chargebackId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); ChargebackClient chargebackClient = new ChargebackClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await chargebackClient.GetChargebackAsync("payment-id", chargebackId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'chargebackId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetChargebacksListAsync_NoPaymentIdIsGiven_ArgumentExceptionIsThrown(string? paymentId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); ChargebackClient chargebackClient = new ChargebackClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await chargebackClient.GetChargebackListAsync(paymentId: paymentId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'paymentId' is null or empty"); } } } ================================================ FILE: tests/Mollie.Tests.Unit/Client/ClientClientTests.cs ================================================ using System; using System.Net.Http; using System.Threading.Tasks; using Mollie.Api.Client; using RichardSzalay.MockHttp; using Shouldly; using Xunit; namespace Mollie.Tests.Unit.Client; public class ClientClientTests : BaseClientTests { [Fact] public async Task GetClientAsync_WithoutClientId_ThrowsArgumentException() { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); using var permissionClient = new ClientClient("access_abcde", httpClient); // Act var exception = await Assert.ThrowsAsync(() => permissionClient.GetClientAsync(string.Empty)); // Assert exception.Message.ShouldBe($"Required URL argument 'clientId' is null or empty"); } [Fact] public async Task GetClientAsync_WithClientId_ResponseIsDeserializedInExpectedFormat() { // Arrange const string clientId = "org_12345678"; var mockHttp = new MockHttpMessageHandler(); mockHttp.Expect(HttpMethod.Get,$"{BaseMollieClient.DefaultBaseApiEndPoint}clients/{clientId}") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", DefaultGetClientResponse); HttpClient httpClient = mockHttp.ToHttpClient(); using var clientClient = new ClientClient("access_1234", httpClient); // Act var result = await clientClient.GetClientAsync(clientId); // Assert mockHttp.VerifyNoOutstandingRequest(); result.Resource.ShouldBe("client"); result.Id.ShouldBe(clientId); result.Commission.ShouldNotBeNull().Count.ShouldBe(0); result.Links.ShouldNotBeNull(); result.Links.Self.Href.ShouldBe("https://api.mollie.com/v2/clients/org_12345678"); result.Links.Self.Type.ShouldBe("application/hal+json"); result.Links.Organization.Href.ShouldBe("https://api.mollie.com/v2/organizations/org_12345678"); result.Links.Organization.Type.ShouldBe("application/hal+json"); result.Links.Onboarding.Href.ShouldBe("https://api.mollie.com/v2/onboarding/org_12345678"); result.Links.Onboarding.Type.ShouldBe("application/hal+json"); result.Links.Documentation.Href.ShouldBe("https://docs.mollie.com/reference/clients-api"); result.Links.Documentation.Type.ShouldBe("text/html"); } [Theory] [InlineData(false, false, false, "")] [InlineData(true, true, true, "?embed=organization,onboarding,capabilities")] [InlineData(true, false, false, "?embed=organization")] [InlineData(false, true, false, "?embed=onboarding")] [InlineData(false, false, true, "?embed=capabilities")] public async Task GetClientAsync_WithEmbeddedParameters_GeneratesExpectedUrl( bool embedOrganization, bool embedOnboarding, bool embedCapabilities, string expectedQueryString) { // Arrange const string clientId = "org_12345678"; var mockHttp = new MockHttpMessageHandler(); mockHttp.Expect(HttpMethod.Get,$"{BaseMollieClient.DefaultBaseApiEndPoint}clients/{clientId}{expectedQueryString}") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", DefaultGetClientResponse); HttpClient httpClient = mockHttp.ToHttpClient(); using var clientClient = new ClientClient("access_1234", httpClient); // Act await clientClient.GetClientAsync(clientId, embedOrganization, embedOnboarding, embedCapabilities); // Assert mockHttp.VerifyNoOutstandingRequest(); } [Theory] [InlineData(null, null, false, false, false, "")] [InlineData("from", null, false, false, false, "?from=from")] [InlineData("from", 50, false, false, false, "?from=from&limit=50")] [InlineData("from", 50, true, false, false, "?from=from&limit=50&embed=organization")] [InlineData("from", 50, true, true, false, "?from=from&limit=50&embed=organization,onboarding")] [InlineData("from", 50, true, true, true, "?from=from&limit=50&embed=organization,onboarding,capabilities")] public async Task GetClientListAsync_WithQueryParameters_QueryStringOnlyContainsTestModeParameterIfTrue( string? from, int? limit, bool embedOrganization, bool embedOnboarding, bool embedCapabilities, string expectedQueryString) { // Given: We retrieve a list of clients var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}clients{expectedQueryString}") .Respond("application/json", DefaultGetClientListResponse); HttpClient httpClient = mockHttp.ToHttpClient(); using var clientClient = new ClientClient("access_1234", httpClient); // When: We send the request var result = await clientClient.GetClientListAsync( from, limit, embedOrganization, embedOnboarding, embedCapabilities); // Then mockHttp.VerifyNoOutstandingExpectation(); result.ShouldNotBeNull(); } [Fact] public async Task GetClienListAsync_ResponseIsDeserializedInExpectedFormat() { // Arrange var mockHttp = new MockHttpMessageHandler(); mockHttp.Expect(HttpMethod.Get,$"{BaseMollieClient.DefaultBaseApiEndPoint}clients") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", DefaultGetClientListResponse); HttpClient httpClient = mockHttp.ToHttpClient(); using var clientClient = new ClientClient("access_1234", httpClient); // Act var result = await clientClient.GetClientListAsync(); // Assert mockHttp.VerifyNoOutstandingRequest(); result.Count.ShouldBe(2); result.Items.ShouldNotBeNull(); result.Links.ShouldNotBeNull(); result.Links.ShouldNotBeNull(); result.Links.Self.Href.ShouldBe("https://api.mollie.com/v2/clients"); result.Links.Self.Type.ShouldBe("application/hal+json"); result.Links.Previous.ShouldBeNull(); result.Links.Next!.Href.ShouldBe("https://api.mollie.com/v2/clients?from=org_63916732&limit=5"); result.Links.Next.Type.ShouldBe("application/hal+json"); result.Links.Documentation.Href.ShouldBe("https://docs.mollie.com/reference/clients-api"); result.Links.Documentation.Type.ShouldBe("text/html"); } private const string DefaultGetClientResponse = @"{ ""resource"": ""client"", ""id"": ""org_12345678"", ""commission"": { ""count"": 0 }, ""organizationCreatedAt"": ""2024-10-03T10:47:38.457381+00:00"", ""_links"": { ""self"": { ""href"": ""https://api.mollie.com/v2/clients/org_12345678"", ""type"": ""application/hal+json"" }, ""organization"": { ""href"": ""https://api.mollie.com/v2/organizations/org_12345678"", ""type"": ""application/hal+json"" }, ""onboarding"": { ""href"": ""https://api.mollie.com/v2/onboarding/org_12345678"", ""type"": ""application/hal+json"" }, ""documentation"": { ""href"": ""https://docs.mollie.com/reference/clients-api"", ""type"": ""text/html"" } } }"; private const string DefaultGetClientListResponse = $@"{{ ""count"": 2, ""_embedded"": {{ ""clients"": [ {DefaultGetClientResponse}, {DefaultGetClientResponse} ] }}, ""_links"": {{ ""self"": {{ ""href"": ""https://api.mollie.com/v2/clients"", ""type"": ""application/hal+json"" }}, ""previous"": null, ""next"": {{ ""href"": ""https://api.mollie.com/v2/clients?from=org_63916732&limit=5"", ""type"": ""application/hal+json"" }}, ""documentation"": {{ ""href"": ""https://docs.mollie.com/reference/clients-api"", ""type"": ""text/html"" }} }} }}"; } ================================================ FILE: tests/Mollie.Tests.Unit/Client/ClientLinkClientTests.cs ================================================ using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Models; using Mollie.Api.Models.ClientLink.Request; using Mollie.Api.Models.ClientLink.Response; using RichardSzalay.MockHttp; using Xunit; namespace Mollie.Tests.Unit.Client; public class ClientLinkClientTests : BaseClientTests { [Fact] public async Task CreateClientLinkAsync_ResponseIsDeserializedInExpectedFormat() { // Given: We create a payment link const string clientLinkId = "csr_vZCnNQsV2UtfXxYifWKWH"; const string clientLinkUrl = "myurl"; string clientLinkResponseJson = CreateClientLinkResponseJson(clientLinkId, clientLinkUrl); var mockHttp = new MockHttpMessageHandler(); mockHttp.When( HttpMethod.Post, $"{BaseMollieClient.DefaultBaseApiEndPoint}client-links") .Respond("application/json", clientLinkResponseJson); HttpClient httpClient = mockHttp.ToHttpClient(); ClientLinkClient clientLinkClient = new ClientLinkClient("clientId", "access_1234", httpClient); var request = new ClientLinkRequest { Owner = new ClientLinkOwner { Email = "email", GivenName = "gicen-name", FamilyName = "family-name" }, Address = new AddressObject(), Name = "name" }; // When: We send the request ClientLinkResponse response = await clientLinkClient.CreateClientLinkAsync(request); // Then response.Id.ShouldBe(clientLinkId); response.Links.ClientLink.Href.ShouldBe(clientLinkUrl); mockHttp.VerifyNoOutstandingRequest(); } [Theory] [InlineData(true)] [InlineData(false)] public void GenerateClientLinkWithParameters_GeneratesExpectedUrl(bool forceApprovalPrompt) { // Arrange const string clientId = "app_j9Pakf56Ajta6Y65AkdTtAv"; const string clientLinkUrl = "https://my.mollie.com/dashboard/client-link/csr_vZCnNQsV2UtfXxYifWKWH"; const string state = "decafbad"; var scopes = new List() { "onboarding.read", "organizations.read", "payments.write", "payments.read", "profiles.write", }; ClientLinkClient clientLinkClient = new ClientLinkClient( clientId, "access_1234", new MockHttpMessageHandler().ToHttpClient()); // Act string result = clientLinkClient.GenerateClientLinkWithParameters( clientLinkUrl, state, scopes, forceApprovalPrompt); // Assert string expectedApprovalPrompt = forceApprovalPrompt ? "force" : "auto"; result.ShouldBe("https://my.mollie.com/dashboard/client-link/csr_vZCnNQsV2UtfXxYifWKWH" + $"?client_id={clientId}" + $"&state={state}" + "&scope=onboarding.read+organizations.read+payments.write+payments.read+profiles.write" + $"&approval_prompt={expectedApprovalPrompt}"); } private string CreateClientLinkResponseJson(string id, string clientLinkUrl) { return $@"{{ ""id"": ""{id}"", ""resource"": ""client-link"", ""_links"": {{ ""clientLink"": {{ ""href"": ""{clientLinkUrl}"", ""type"": ""text/html"" }}, ""documentation"": {{ ""href"": ""https://docs.mollie.com/reference/v2/clients-api/create-client-link"", ""type"": ""text/html"" }} }} }}"; } } ================================================ FILE: tests/Mollie.Tests.Unit/Client/ConnectClientTests.cs ================================================ using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Models.Connect.Request; using Mollie.Api.Models.Connect.Response; using Mollie.Api.Options; using RichardSzalay.MockHttp; using Xunit; namespace Mollie.Tests.Unit.Client; public class ConnectClientTests : BaseClientTests { private const string ClientId = "client-id"; private const string ClientSecret = "client-secret"; [Fact] public void GetAuthorizationUrl_WithCustomWithSingleScope_GeneratesAuthorizationUrl() { // Arrange var mollieClientOptions = new MollieClientOptions { ApiKey = string.Empty, ConnectOAuthAuthorizeEndPoint = "https://custom-authorize-endpoint.mollie.com/oauth2/authorize", ClientId = ClientId, ClientSecret = ClientSecret }; HttpClient httpClient = new(); ConnectClient connectClient = new(mollieClientOptions, httpClient); var scopes = new List {AppPermissions.PaymentsRead}; // Act string authorizationUrl = connectClient.GetAuthorizationUrl("abcde", scopes); // Assert string expectedUrl = $"{mollieClientOptions.ConnectOAuthAuthorizeEndPoint}?client_id={ClientId}&state=abcde&scope=payments.read&response_type=code&approval_prompt=auto"; authorizationUrl.ShouldBe(expectedUrl); } [Fact] public void GetAuthorizationUrl_WithSingleScope_GeneratesAuthorizationUrl() { // Arrange HttpClient httpClient = new HttpClient(); ConnectClient connectClient = new ConnectClient(ClientId, ClientSecret, httpClient); var scopes = new List {AppPermissions.PaymentsRead}; // Act string authorizationUrl = connectClient.GetAuthorizationUrl("abcde", scopes); // Assert string expectedUrl = $"https://my.mollie.com/oauth2/authorize?client_id={ClientId}&state=abcde&scope=payments.read&response_type=code&approval_prompt=auto"; authorizationUrl.ShouldBe(expectedUrl); } [Theory] [InlineData("refresh_abcde", "refresh_token")] [InlineData("abcde", "authorization_code")] public async Task GetAccessTokenAsync_WithRefreshToken_ResponseIsDeserializedInExpectedFormat(string refreshToken, string expectedGrantType) { // Arrange var mockHttp = new MockHttpMessageHandler(); mockHttp.Expect(HttpMethod.Post, "https://api.mollie.com/oauth2/tokens") .Respond("application/json", defaultGetTokenResponse); HttpClient httpClient = mockHttp.ToHttpClient(); ConnectClient connectClient = new ConnectClient(ClientId, ClientSecret, httpClient); var tokenRequest = new TokenRequest(refreshToken, DefaultRedirectUrl); // Act TokenResponse tokenResponse = await connectClient.GetAccessTokenAsync(tokenRequest); // Assert mockHttp.VerifyNoOutstandingExpectation(); tokenRequest.GrantType.ShouldBe(expectedGrantType); tokenResponse.ShouldNotBeNull(); tokenResponse.AccessToken.ShouldBe("access_46EUJ6x8jFJZZeAvhNH4JVey6qVpqR"); tokenResponse.RefreshToken.ShouldBe("refresh_FS4xc3Mgci2xQ5s5DzaLXh3HhaTZOP"); tokenResponse.ExpiresIn.ShouldBe(3600); tokenResponse.TokenType.ShouldBe("bearer"); tokenResponse.Scope.ShouldBe("payments.read organizations.read"); } [Fact] public async Task RevokeTokenAsync_SendsRequest() { // Arrange var mockHttp = new MockHttpMessageHandler(); mockHttp.Expect(HttpMethod.Delete, "https://api.mollie.com/oauth2/tokens") .Respond(HttpStatusCode.NoContent); HttpClient httpClient = mockHttp.ToHttpClient(); ConnectClient connectClient = new ConnectClient(ClientId, ClientSecret, httpClient); var revokeTokenRequest = new RevokeTokenRequest { Token = "access_46EUJ6x8jFJZZeAvhNH4JVey6qVpqR", TokenTypeHint = "refresh_token" }; // Act await connectClient.RevokeTokenAsync(revokeTokenRequest); // Assert mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task RevokeTokenAsync_WithCustomTokenEndpoints_SendsRequestToCustomEndpoint() { // Arrange var mollieClientOptions = new MollieClientOptions { ApiKey = string.Empty, ConnectTokenEndPoint = "https://custom-token-endpoint.mollie.com/oauth2/", ClientId = ClientId, ClientSecret = ClientSecret }; var mockHttp = new MockHttpMessageHandler(); mockHttp.Expect(HttpMethod.Delete, $"{mollieClientOptions.ConnectTokenEndPoint}tokens") .Respond(HttpStatusCode.NoContent); HttpClient httpClient = mockHttp.ToHttpClient(); ConnectClient connectClient = new(mollieClientOptions, httpClient); var revokeTokenRequest = new RevokeTokenRequest { Token = "access_46EUJ6x8jFJZZeAvhNH4JVey6qVpqR", TokenTypeHint = "refresh_token" }; // Act await connectClient.RevokeTokenAsync(revokeTokenRequest); // Assert mockHttp.VerifyNoOutstandingExpectation(); } private const string defaultGetTokenResponse = @" { ""access_token"": ""access_46EUJ6x8jFJZZeAvhNH4JVey6qVpqR"", ""refresh_token"": ""refresh_FS4xc3Mgci2xQ5s5DzaLXh3HhaTZOP"", ""expires_in"": 3600, ""token_type"": ""bearer"", ""scope"": ""payments.read organizations.read"" }"; } ================================================ FILE: tests/Mollie.Tests.Unit/Client/CustomerClientTests.cs ================================================ using System; using System.Net.Http; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Models; using Mollie.Api.Models.Customer.Request; using Mollie.Api.Models.Customer.Response; using Mollie.Api.Models.Payment.Request; using RichardSzalay.MockHttp; using Xunit; namespace Mollie.Tests.Unit.Client { public class CustomerClientTests : BaseClientTests { [Theory] [InlineData("customers/customer-id", false)] [InlineData("customers/customer-id?testmode=true", true)] public async Task GetCustomerAsync_TestModeParameterCase_QueryStringOnlyContainsTestModeParameterIfTrue(string expectedUrl, bool testModeParameter) { // Given: We retrieve a customer const string customerId = "customer-id"; var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}{expectedUrl}") .Respond("application/json", DefaultCustomerJsonToReturn); HttpClient httpClient = mockHttp.ToHttpClient(); var customerClient = new CustomerClient("abcde", httpClient); // When: We send the request CustomerResponse customerResponse = await customerClient.GetCustomerAsync(customerId, testModeParameter); // Then mockHttp.VerifyNoOutstandingExpectation(); customerResponse.ShouldNotBeNull(); } [Theory] [InlineData(null, null, false, "")] [InlineData("from", null, false, "?from=from")] [InlineData("from", 50, false, "?from=from&limit=50")] [InlineData(null, null, true, "?testmode=true")] public async Task GetCustomerListAsync_TestModeParameterCase_QueryStringOnlyContainsTestModeParameterIfTrue(string? from, int? limit, bool testmode, string expectedQueryString) { // Given: We retrieve a list of customers var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}customers{expectedQueryString}") .Respond("application/json", DefaultCustomerJsonToReturn); HttpClient httpClient = mockHttp.ToHttpClient(); var customerClient = new CustomerClient("abcde", httpClient); // When: We send the request var result = await customerClient.GetCustomerListAsync(from, limit, testmode); // Then mockHttp.VerifyNoOutstandingExpectation(); result.ShouldNotBeNull(); } [Theory] [InlineData(null, null, null, false, "")] [InlineData("from", null, null, false, "?from=from")] [InlineData("from", 50, null, false, "?from=from&limit=50")] [InlineData(null, null, null, true, "?testmode=true")] [InlineData(null, null, "profile-id", true, "?profileId=profile-id")] public async Task GetCustomerPaymentListAsync_TestModeParameterCase_QueryStringOnlyContainsTestModeParameterIfTrue(string? from, int? limit, string? profileId, bool testmode, string expectedQueryString) { // Given: We retrieve a list of customers const string customerId = "customer-id"; var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}customers/{customerId}/payments{expectedQueryString}") .Respond("application/json", DefaultCustomerJsonToReturn); HttpClient httpClient = mockHttp.ToHttpClient(); var customerClient = new CustomerClient("abcde", httpClient); // When: We send the request var result = await customerClient.GetCustomerPaymentListAsync(customerId, from, limit, profileId, testmode); // Then mockHttp.VerifyNoOutstandingExpectation(); result.ShouldNotBeNull(); } [Fact] public async Task DeleteCustomerAsync_TestmodeIsTrue_RequestContainsTestmodeModel() { // Given: We make a request to retrieve a payment with embedded refunds const string customerId = "customer-id"; string expectedContent = "\"testmode\":true"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Delete, $"{BaseMollieClient.DefaultBaseApiEndPoint}customers/{customerId}", DefaultCustomerJsonToReturn, expectedContent); HttpClient httpClient = mockHttp.ToHttpClient(); var customerClient = new CustomerClient("abcde", httpClient); // When: We send the request await customerClient.DeleteCustomerAsync(customerId, true); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task UpdateCustomerAsync_NoCustomerIdIsGiven_ArgumentExceptionIsThrown(string? customerId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var customerClient = new CustomerClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await customerClient.UpdateCustomerAsync(customerId, new CustomerRequest())); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'customerId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task DeleteCustomerAsync_NoCustomerIdIsGiven_ArgumentExceptionIsThrown(string? customerId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var customerClient = new CustomerClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await customerClient.DeleteCustomerAsync(customerId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'customerId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetCustomerAsync_NoCustomerIdIsGiven_ArgumentExceptionIsThrown(string? customerId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var customerClient = new CustomerClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await customerClient.GetCustomerAsync(customerId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'customerId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetCustomerPaymentListAsync_NoCustomerIdIsGiven_ArgumentExceptionIsThrown(string? customerId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var customerClient = new CustomerClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await customerClient.GetCustomerPaymentListAsync(customerId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'customerId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task CreateCustomerPayment_NoCustomerIdIsGiven_ArgumentExceptionIsThrown(string? customerId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var customerClient = new CustomerClient("api-key", httpClient); var paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, 100), Description = "Order #12345", }; // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await customerClient.CreateCustomerPayment(customerId, paymentRequest)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'customerId' is null or empty"); } private const string DefaultCustomerJsonToReturn = @"{ ""resource"": ""customer"", ""id"": ""customer-id"", ""mode"": ""test"", ""name"": ""Customer A"", ""email"": ""customer@example.org"", ""locale"": ""nl_NL"", ""metadata"": null, ""createdAt"": ""2018-04-06T13:23:21.0Z"" }"; } } ================================================ FILE: tests/Mollie.Tests.Unit/Client/InvoiceClientTests.cs ================================================ using System.Net.Http; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using RichardSzalay.MockHttp; using Xunit; namespace Mollie.Tests.Unit.Client; public class InvoiceClientTests : BaseClientTests { [Fact] public async Task GetInvoiceAsync_DefaultBehaviour_ResponseIsParsed() { // Given const string invoiceId = "inv_xBEbP9rvAq"; var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}invoices/{invoiceId}") .Respond("application/json", defaultInvoice); HttpClient httpClient = mockHttp.ToHttpClient(); using InvoiceClient invoiceClient = new InvoiceClient("access_abcde", httpClient); // When var result = await invoiceClient.GetInvoiceAsync(invoiceId); // Then mockHttp.VerifyNoOutstandingExpectation(); result.ShouldNotBeNull(); result.Resource.ShouldBe("invoice"); result.Id.ShouldBe(invoiceId); result.Reference.ShouldBe("2016.10000"); result.VatNumber.ShouldBe("NL001234567B01"); result.Status.ShouldBe("open"); } [Theory] [InlineData(null, null, null, null, "")] [InlineData("my-reference", null, null, null, "?reference=my-reference")] [InlineData("my-reference", 2023, null, null, "?reference=my-reference&year=2023")] [InlineData("my-reference", 2023, "abcde", null, "?reference=my-reference&year=2023&from=abcde")] [InlineData("my-reference", 2023, "abcde", 10, "?reference=my-reference&year=2023&from=abcde&limit=10")] public async Task GetInvoiceListAsync_WithVariousParameters_QueryStringMatchesExpectedValue( string? reference, int? year, string? from, int? limit, string expectedQueryString) { // Given: We retrieve a list of customers var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}invoices{expectedQueryString}") .Respond("application/json", defaultInvoiceList); HttpClient httpClient = mockHttp.ToHttpClient(); using InvoiceClient invoiceClient = new InvoiceClient("access_abcde", httpClient); // When: We send the request var result = await invoiceClient.GetInvoiceListAsync( reference, year, from, limit); // Then mockHttp.VerifyNoOutstandingExpectation(); result.ShouldNotBeNull(); } private const string defaultInvoice = @"{ ""resource"": ""invoice"", ""id"": ""inv_xBEbP9rvAq"", ""reference"": ""2016.10000"", ""vatNumber"": ""NL001234567B01"", ""status"": ""open"", ""issuedAt"": ""2016-08-31"", ""dueAt"": ""2016-09-14"", ""netAmount"": { ""value"": ""45.00"", ""currency"": ""EUR"" }, ""vatAmount"": { ""value"": ""9.45"", ""currency"": ""EUR"" }, ""grossAmount"": { ""value"": ""54.45"", ""currency"": ""EUR"" }, ""lines"":[ { ""period"": ""2016-09"", ""description"": ""iDEAL transactiekosten"", ""count"": 100, ""vatPercentage"": 21, ""amount"": { ""value"": ""45.00"", ""currency"": ""EUR"" } } ], ""_links"": { ""self"": { ""href"": ""https://api.mollie.com/v2/invoices/inv_xBEbP9rvAq"", ""type"": ""application/hal+json"" }, ""pdf"": { ""href"": ""https://www.mollie.com/merchant/download/invoice/xBEbP9rvAq/2ab44d60b35b1d06090bba955fa2c602"", ""type"": ""application/pdf"", ""expiresAt"": ""2018-11-09T14:10:36+00:00"" } } }"; private const string defaultInvoiceList = @"{ ""count"": 5, ""_embedded"": { ""invoices"": [ { ""resource"": ""invoice"", ""id"": ""inv_xBEbP9rvAq"", ""reference"": ""2016.10000"", ""vatNumber"": ""NL001234567B01"", ""status"": ""open"", ""issuedAt"": ""2016-08-31"", ""dueAt"": ""2016-09-14"", ""netAmount"": { ""value"": ""45.00"", ""currency"": ""EUR"" }, ""vatAmount"": { ""value"": ""9.45"", ""currency"": ""EUR"" }, ""grossAmount"": { ""value"": ""54.45"", ""currency"": ""EUR"" }, ""lines"":[ { ""period"": ""2016-09"", ""description"": ""iDEAL transactiekosten"", ""count"": 100, ""vatPercentage"": 21, ""amount"": { ""value"": ""45.00"", ""currency"": ""EUR"" } } ], ""_links"": { ""self"": { ""href"": ""https://api.mollie.com/v2/invoices/inv_xBEbP9rvAq"", ""type"": ""application/hal+json"" }, ""pdf"": { ""href"": ""https://www.mollie.com/merchant/download/invoice/xBEbP9rvAq/2ab44d60b35955fa2c602"", ""type"": ""application/pdf"", ""expiresAt"": ""2018-11-09T14:10:36+00:00"" } } }, { }, { }, { }, { } ] }, ""_links"": { ""self"": { ""href"": ""https://api.mollie.com/v2/invoices?limit=5"", ""type"": ""application/hal+json"" }, ""previous"": null, ""next"": { ""href"": ""https://api.mollie.com/v2/invoices?from=inv_xBEbP9rvAq&limit=5"", ""type"": ""application/hal+json"" }, ""documentation"": { ""href"": ""https://docs.mollie.com/reference/v2/invoices-api/list-invoices"", ""type"": ""text/html"" } } }"; } ================================================ FILE: tests/Mollie.Tests.Unit/Client/MandateClientTests.cs ================================================ using System; using System.Net.Http; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Models.Mandate.Request; using Mollie.Api.Models.Payment; using RichardSzalay.MockHttp; using Xunit; namespace Mollie.Tests.Unit.Client { public class MandateClientTests : BaseClientTests { [Theory] [InlineData("customers/customer-id/mandates/mandate-id", false)] [InlineData("customers/customer-id/mandates/mandate-id?testmode=true", true)] public async Task GetMandateAsync_TestModeParameterCase_QueryStringOnlyContainsTestModeParameterIfTrue(string expectedUrl, bool testModeParameter) { // Given: We retrieve a mandate const string customerId = "customer-id"; const string mandateId = "mandate-id"; var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}{expectedUrl}") .Respond("application/json", DefaultMandateJsonToReturn); HttpClient httpClient = mockHttp.ToHttpClient(); MandateClient mandateClient = new MandateClient("abcde", httpClient); // When: We send the request var result = await mandateClient.GetMandateAsync(customerId, mandateId, testModeParameter); // Then mockHttp.VerifyNoOutstandingExpectation(); result.ShouldNotBeNull(); } [Theory] [InlineData(null, null, false, "")] [InlineData("from", null, false, "?from=from")] [InlineData("from", 50, false, "?from=from&limit=50")] [InlineData(null, null, true, "?testmode=true")] public async Task GetMandateListAsync_TestModeParameterCase_QueryStringOnlyContainsTestModeParameterIfTrue( string? from, int? limit, bool testmode, string expectedQueryString) { // Given: We retrieve a list of mandates const string customerId = "customer-id"; var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}customers/{customerId}/mandates{expectedQueryString}") .Respond("application/json", DefaultMandateJsonToReturn); HttpClient httpClient = mockHttp.ToHttpClient(); MandateClient mandateClient = new MandateClient("abcde", httpClient); // When: We send the request var result = await mandateClient.GetMandateListAsync(customerId, from, limit, testmode); // Then mockHttp.VerifyNoOutstandingExpectation(); result.ShouldNotBeNull(); } [Fact] public async Task RevokeMandate_TestmodeIsTrue_RequestContainsTestmodeModel() { // Given: We make a request to retrieve a payment with embedded refunds const string customerId = "customer-id"; const string mandateId = "mandate-id"; string expectedContent = "\"testmode\":true"; var mockHttp = CreateMockHttpMessageHandler( HttpMethod.Delete, $"{BaseMollieClient.DefaultBaseApiEndPoint}customers/{customerId}/mandates/{mandateId}", DefaultMandateJsonToReturn, expectedContent); HttpClient httpClient = mockHttp.ToHttpClient(); MandateClient mandateClient = new MandateClient("abcde", httpClient); // When: We send the request await mandateClient.RevokeMandate(customerId, mandateId, true); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetMandateAsync_NoCustomerIdIsGiven_ArgumentExceptionIsThrown(string? mandateId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); MandateClient mandateClient = new MandateClient("api-key", httpClient); // When: We send the request var exception = await Assert.ThrowsAsync(async () => #pragma warning disable CS8604 // Possible null reference argument. await mandateClient.GetMandateAsync(mandateId, "mandate-id")); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'customerId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetMandateAsync_NoMandateIdIsGiven_ArgumentExceptionIsThrown(string? mandateId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); MandateClient mandateClient = new MandateClient("api-key", httpClient); // When: We send the request var exception = await Assert.ThrowsAsync(async () => #pragma warning disable CS8604 // Possible null reference argument. await mandateClient.GetMandateAsync("customer-id", mandateId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'mandateId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetMandateListAsync_NoCustomerIdIsGiven_ArgumentExceptionIsThrown(string? mandateId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); MandateClient mandateClient = new MandateClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await mandateClient.GetMandateListAsync(mandateId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'customerId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task CreateMandateAsync_NoCustomerIdIsGiven_ArgumentExceptionIsThrown(string? mandateId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); MandateClient mandateClient = new MandateClient("api-key", httpClient); MandateRequest mandateRequest = new MandateRequest { ConsumerName = "John Doe", Method = PaymentMethod.DirectDebit }; // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await mandateClient.CreateMandateAsync(mandateId, mandateRequest)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'customerId' is null or empty"); } private const string DefaultMandateJsonToReturn = @"{ ""resource"": ""mandate"", ""id"": ""mdt_h3gAaD5zP"", ""mode"": ""test"", ""status"": ""valid"", ""method"": ""directdebit"", ""details"": { ""consumerName"": ""John Doe"", ""consumerAccount"": ""NL55INGB0000000000"", ""consumerBic"": ""INGBNL2A"" }, ""mandateReference"": ""YOUR-COMPANY-MD1380"", ""signatureDate"": ""2018-05-07"", ""createdAt"": ""2018-05-07T10:49:08+00:00"" }"; } } ================================================ FILE: tests/Mollie.Tests.Unit/Client/OnboardingClientTests.cs ================================================ using Mollie.Api.Client; using Mollie.Api.Models; using Mollie.Api.Models.Onboarding.Request; using Mollie.Api.Models.Onboarding.Response; using System.Net.Http; using System.Threading.Tasks; using Shouldly; using Xunit; namespace Mollie.Tests.Unit.Client { public class OnboardingClientTests : BaseClientTests { public const string defaultName = "Mollie API Unit Test"; public const string defaultStatus = OnboardingStatus.Completed; public const string canReceivePayments = "true"; public const string canReceiveSettlements = "true"; public const string defaultStreetAndNumber = "My address"; public static readonly string defaultOnboardingStatusJsonResponse = $@"{{ ""resource"": ""onboarding"", ""name"": ""{defaultName}"", ""signedUpAt"": ""2018-12-20T10:49:08+00:00"", ""status"": ""{defaultStatus}"", ""canReceivePayments"": {canReceivePayments}, ""canReceiveSettlements"": {canReceiveSettlements}, }}"; [Fact] public async Task GetOnboardingStatusAsync_DefaultBehaviour_ResponseIsParsed() { // Given: We request the onboarding status string expectedUrl = $"{BaseMollieClient.DefaultBaseApiEndPoint}onboarding/me"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, expectedUrl, defaultOnboardingStatusJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); OnboardingClient onboardingClient = new OnboardingClient("api-key", httpClient); // When: We make the request OnboardingStatusResponse onboardingResponse = await onboardingClient.GetOnboardingStatusAsync(); // Then: Response should be parsed mockHttp.VerifyNoOutstandingExpectation(); onboardingResponse.ShouldNotBeNull(); onboardingResponse.Name.ShouldBe(defaultName); onboardingResponse.Status.ShouldBe(defaultStatus); onboardingResponse.CanReceivePayments.ToString().ToLower().ShouldBe(canReceivePayments); onboardingResponse.CanReceiveSettlements.ToString().ToLower().ShouldBe(canReceiveSettlements); } [Fact] public async Task SubmitOnboardingDataAsync_DefaultBehaviour_RequestIsParsed() { // Given: We submit an onboarding status request string expectedUrl = $"{BaseMollieClient.DefaultBaseApiEndPoint}onboarding/me"; SubmitOnboardingDataRequest submitOnboardingDataRequest = new SubmitOnboardingDataRequest() { Organization = new OnboardingOrganizationRequest() { Name = defaultName, Address = new AddressObject() { StreetAndNumber = defaultStreetAndNumber } }, Profile = new OnboardingProfileRequest() { Name = defaultName } }; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Post, expectedUrl, string.Empty); HttpClient httpClient = mockHttp.ToHttpClient(); OnboardingClient onboardingClient = new OnboardingClient("api-key", httpClient); // When: We make the request await onboardingClient.SubmitOnboardingDataAsync(submitOnboardingDataRequest); // Then: There should be no outstanding requests mockHttp.VerifyNoOutstandingExpectation(); } } } ================================================ FILE: tests/Mollie.Tests.Unit/Client/OrderClientTests.cs ================================================ using System; #pragma warning disable CS0618 using Mollie.Api.Client; using Mollie.Api.Models; using Mollie.Api.Models.Order; using Mollie.Api.Models.Payment; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Models.Order.Request; using Mollie.Api.Models.Order.Request.ManageOrderLines; using Mollie.Api.Models.Order.Response; using RichardSzalay.MockHttp; using Xunit; using SortDirection = Mollie.Api.Models.SortDirection; namespace Mollie.Tests.Unit.Client { public class OrderClientTests : BaseClientTests { [Fact] public async Task GetOrderAsync_NoEmbedParameters_QueryStringIsEmpty() { // Given: We make a request to retrieve a order without wanting any extra data const string orderId = "abcde"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}orders/{orderId}", defaultOrderJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); OrderClient orderClient = new OrderClient("abcde", httpClient); // When: We send the request await orderClient.GetOrderAsync(orderId); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task GetOrderAsync_SingleEmbedParameters_QueryStringContainsEmbedParameter() { // Given: We make a request to retrieve a order with a single embed parameter const string orderId = "abcde"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}orders/{orderId}?embed=payments", defaultOrderJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); OrderClient orderClient = new OrderClient("abcde", httpClient); // When: We send the request await orderClient.GetOrderAsync(orderId, embedPayments: true); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task GetOrderAsync_MultipleEmbedParameters_QueryStringContainsMultipleParameters() { // Given: We make a request to retrieve a order with a single embed parameter const string orderId = "abcde"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}orders/{orderId}?embed=payments,refunds,shipments", defaultOrderJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); OrderClient orderClient = new OrderClient("abcde", httpClient); // When: We send the request await orderClient.GetOrderAsync(orderId, embedPayments: true, embedRefunds: true, embedShipments: true); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task GetOrderAsync_WithTestModeParameter_QueryStringContainsTestModeParameter() { // Given: We make a request to retrieve a order with a single embed parameter const string orderId = "abcde"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}orders/{orderId}?testmode=true", defaultOrderJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); OrderClient orderClient = new OrderClient("abcde", httpClient); // When: We send the request await orderClient.GetOrderAsync(orderId, testmode: true); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Theory] [InlineData(null, null, null, false, null, "")] [InlineData("from", null, null, false, null, "?from=from")] [InlineData("from", 50, null, false, null, "?from=from&limit=50")] [InlineData(null, null, "profile-id", false, null, "?profileId=profile-id")] [InlineData(null, null, "profile-id", true, null, "?profileId=profile-id&testmode=true")] [InlineData(null, null, "profile-id", true, SortDirection.Desc, "?profileId=profile-id&testmode=true&sort=desc")] [InlineData(null, null, "profile-id", true, SortDirection.Asc, "?profileId=profile-id&testmode=true&sort=asc")] public async Task GetOrderListAsync_QueryParameterOptions_CorrectParametersAreAdded( string? from, int? limit, string? profileId, bool testmode, SortDirection? sortDirection, string expectedQueryString) { // Given: We make a request to retrieve the list of orders var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}orders{expectedQueryString}", defaultOrderListJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); OrderClient orderClient = new OrderClient("abcde", httpClient); // When: We send the request await orderClient.GetOrderListAsync(from, limit, profileId, testmode, sortDirection); // Then mockHttp.VerifyNoOutstandingRequest(); } [Fact] public async Task CreateOrderAsync_SinglePaymentMethod_RequestIsSerializedInExpectedFormat() { // Given: we create a order with a single payment method OrderRequest orderRequest = CreateOrderRequestWithOnlyRequiredFields(); orderRequest.Method = PaymentMethod.Ideal; string expectedPaymentMethodJson = $"\"method\":[\"{PaymentMethod.Ideal}"; const string jsonResponse = defaultOrderJsonResponse; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Post, $"{BaseMollieClient.DefaultBaseApiEndPoint}orders", jsonResponse, expectedPaymentMethodJson); HttpClient httpClient = mockHttp.ToHttpClient(); OrderClient orderClient = new ("abcde", httpClient); // When: We send the request OrderResponse orderResponse = await orderClient.CreateOrderAsync(orderRequest); // Then mockHttp.VerifyNoOutstandingExpectation(); orderResponse.Method.ShouldBe(orderRequest.Method); } [Fact] public async Task CreateOrderAsync_MultiplePaymentMethods_RequestIsSerializedInExpectedFormat() { // Given: we create a order with a single payment method OrderRequest orderRequest = CreateOrderRequestWithOnlyRequiredFields(); orderRequest.Methods = new List() { PaymentMethod.Ideal, PaymentMethod.CreditCard, PaymentMethod.DirectDebit }; string expectedPaymentMethodJson = $"\"method\":[\"{PaymentMethod.Ideal}\",\"{PaymentMethod.CreditCard}\",\"{PaymentMethod.DirectDebit}\"]"; const string jsonResponse = defaultOrderJsonResponse; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Post, $"{BaseMollieClient.DefaultBaseApiEndPoint}orders", jsonResponse, expectedPaymentMethodJson); HttpClient httpClient = mockHttp.ToHttpClient(); OrderClient orderClient = new OrderClient("abcde", httpClient); // When: We send the request await orderClient.CreateOrderAsync(orderRequest); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task CreateOrderPaymentAsync_PaymentWithSinglePaymentMethod_RequestIsSerializedInExpectedFormat() { // Given: We create a payment request with multiple payment methods OrderPaymentRequest orderPaymentRequest = new OrderPaymentRequest() { CustomerId = "customer-id", Testmode = true, MandateId = "mandate-id", Methods = new List() { PaymentMethod.Ideal } }; const string orderId = "order-id"; string url = $"{BaseMollieClient.DefaultBaseApiEndPoint}orders/{orderId}/payments"; string expectedPaymentMethodJson = $"\"method\":[\"{PaymentMethod.Ideal}\"]"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Post, url, defaultPaymentJsonResponse, expectedPaymentMethodJson); HttpClient httpClient = mockHttp.ToHttpClient(); OrderClient orderClient = new OrderClient("abcde", httpClient); // When: We send the request await orderClient.CreateOrderPaymentAsync(orderId, orderPaymentRequest); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task CreateOrderPaymentAsync_PaymentWithMultiplePaymentMethods_RequestIsSerializedInExpectedFormat() { // Given: We create a payment request with multiple payment methods OrderPaymentRequest orderPaymentRequest = new () { CustomerId = "customer-id", Testmode = true, MandateId = "mandate-id", Methods = new List() { PaymentMethod.Ideal, PaymentMethod.CreditCard, PaymentMethod.DirectDebit } }; const string orderId = "order-id"; string url = $"{BaseMollieClient.DefaultBaseApiEndPoint}orders/{orderId}/payments"; string expectedPaymentMethodJson = $"\"method\":[\"{PaymentMethod.Ideal}\",\"{PaymentMethod.CreditCard}\",\"{PaymentMethod.DirectDebit}\"]"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Post, url, defaultPaymentJsonResponse, expectedPaymentMethodJson); HttpClient httpClient = mockHttp.ToHttpClient(); OrderClient orderClient = new ("abcde", httpClient); // When: We send the request await orderClient.CreateOrderPaymentAsync(orderId, orderPaymentRequest); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetOrderAsync_NoOrderIdIsGiven_ArgumentExceptionIsThrown(string? orderId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); OrderClient orderClient = new OrderClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await orderClient.GetOrderAsync(orderId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'orderId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task UpdateOrderAsync_NoOrderIdIsGiven_ArgumentExceptionIsThrown(string? orderId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); OrderClient orderClient = new OrderClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await orderClient.UpdateOrderAsync(orderId, new OrderUpdateRequest())); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'orderId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task UpdateOrderLinesAsync_NoOrderIdIsGiven_ArgumentExceptionIsThrown(string? orderId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); OrderClient orderClient = new OrderClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await orderClient.UpdateOrderLinesAsync(orderId, "order-line-id", new OrderLineUpdateRequest())); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'orderId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task UpdateOrderLinesAsync_NoOrderLineIdIsGiven_ArgumentExceptionIsThrown(string? orderLineId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); OrderClient orderClient = new OrderClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await orderClient.UpdateOrderLinesAsync("order-id", orderLineId, new OrderLineUpdateRequest())); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'orderLineId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task ManageOrderLinesAsync_NoOrderIdIsGiven_ArgumentExceptionIsThrown(string? orderId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); OrderClient orderClient = new OrderClient("api-key", httpClient); ManageOrderLinesRequest request = new ManageOrderLinesRequest { Operations = new List { new ManageOrderLinesUpdateOperation { Data = new ManageOrderLinesUpdateOperationData { Id = "id" } } } }; // When: We send the request var exception = await Assert.ThrowsAsync(async () => #pragma warning disable CS8604 // Possible null reference argument. await orderClient.ManageOrderLinesAsync(orderId, request)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'orderId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task CancelOrderAsync_NoOrderIdIsGiven_ArgumentExceptionIsThrown(string? orderId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); OrderClient orderClient = new OrderClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await orderClient.CancelOrderAsync(orderId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'orderId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task CreateOrderPaymentAsync_NoOrderIdIsGiven_ArgumentExceptionIsThrown(string? orderId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); OrderClient orderClient = new OrderClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await orderClient.CreateOrderPaymentAsync(orderId, new OrderPaymentRequest())); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'orderId' is null or empty"); } private OrderRequest CreateOrderRequestWithOnlyRequiredFields() { return new OrderRequest() { Amount = new Amount(Currency.EUR, "100.00"), OrderNumber = "16738", Lines = new List() { new OrderLineRequest() { Name = "A box of chocolates", Quantity = 1, UnitPrice = new Amount(Currency.EUR, "100.00"), TotalAmount = new Amount(Currency.EUR, "100.00"), VatRate = "21.00", VatAmount = new Amount(Currency.EUR, "17.36") } }, BillingAddress = new OrderAddressDetails() { GivenName = "John", FamilyName = "Smit", Email = "johnsmit@gmail.com", City = "Rotterdam", Country = "NL", PostalCode = "0000AA", Region = "Zuid-Holland", StreetAndNumber = "Coolsingel 1" }, RedirectUrl = "http://www.google.nl", Locale = Locale.nl_NL }; } private const string defaultOrderListJsonResponse = @"{ ""count"": 5, ""_embedded"": { ""orders"": [] }, ""_links"": { ""self"": { ""href"": ""..."", ""type"": ""application/hal+json"" }, ""previous"": null, ""next"": { ""href"": ""https://api.mollie.com/v2/orders?from=ord_vsKJpSsabw&limit=5"", ""type"": ""application/hal+json"" }, ""documentation"": { ""href"": ""..."", ""type"": ""text/html"" } } }"; private const string defaultOrderJsonResponse = @"{ ""resource"": ""order"", ""id"": ""ord_kEn1PlbGa"", ""profileId"": ""pfl_URR55HPMGx"", ""method"": ""ideal"", ""status"": ""created"", ""isCancelable"": false, ""orderId"": ""ord_pbjz8x"", ""createdAt"": ""2023-08-02T09:29:56.0Z"", ""locale"": ""nl_NL"", ""orderNumber"": ""34629"", ""lines"": [], ""amount"": { ""value"": ""1027.99"", ""currency"": ""EUR"" }, ""_links"": { ""self"": { ""href"": ""..."", ""type"": ""application/hal+json"" }, ""checkout"": { ""href"": ""https://www.mollie.com/checkout/select-method/7UhSN1zuXS"", ""type"": ""text/html"" }, ""dashboard"": { ""href"": ""https://www.mollie.com/dashboard/org_123456789/orders/ord_pbjz8x"", ""type"": ""text/html"" }, ""documentation"": { ""href"": ""..."", ""type"": ""text/html"" } } }"; private const string defaultPaymentJsonResponse = @"{ ""resource"": ""payment"", ""id"": ""tr_WDqYK6vllg"", ""mode"": ""test"", ""status"": ""open"", ""createdAt"": ""2018-03-20T13:13:37+00:00"", ""profileId"": ""pfl_QkEhN94Ba"", ""amount"":{ ""currency"":""EUR"", ""value"":""100.00"" }, ""description"":""Description"", ""sequenceType"": ""oneoff"", ""redirectUrl"":""http://www.mollie.com""}"; } } ================================================ FILE: tests/Mollie.Tests.Unit/Client/OrganizationClientTests.cs ================================================ using System; using System.Net.Http; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Models.Organization; using RichardSzalay.MockHttp; using Xunit; namespace Mollie.Tests.Unit.Client; public class OrganizationClientTests : BaseClientTests { [Fact] public async Task GetCurrentOrganizationAsync_ResponseIsDeserializedInExpectedFormat() { // Arrange var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}organizations/me") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", defaultOrganizationResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var organizationsClient = new OrganizationClient("access_abcde", httpClient); // Act var result = await organizationsClient.GetCurrentOrganizationAsync(); // Assert AssertDefaultOrganization(result); } [Fact] public async Task GetOrganizationAsync_ResponseIsDeserializedInExpectedFormat() { // Arrange const string organizationId = "organization-id"; var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}organizations/{organizationId}") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", defaultOrganizationResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var organizationsClient = new OrganizationClient("access_abcde", httpClient); // Act var result = await organizationsClient.GetOrganizationAsync(organizationId); // Assert AssertDefaultOrganization(result); } [Fact] public async Task GetOrganizationsListAsync_ResponseIsDeserializedInExpectedFormat() { // Arrange var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}organizations") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", defaultOrganizationListResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var organizationsClient = new OrganizationClient("access_abcde", httpClient); // Act var result = await organizationsClient.GetOrganizationListAsync(); // Assert result.Count.ShouldBe(2); result.Items.Count.ShouldBe(2); } [Fact] public async Task GetPartnerStatusAsync_ResponseIsDeserializedInExpectedFormat() { // Arrange var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}organizations/me/partner") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", defaultGetPartnerStatusResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var organizationsClient = new OrganizationClient("access_abcde", httpClient); // Act var result = await organizationsClient.GetPartnerStatusAsync(); // Assert result.ShouldNotBeNull(); result.Resource.ShouldBe("partner"); result.PartnerType.ShouldBe(PartnerTypes.SignupLink); result.PartnerContractSignedAt.ShouldBe(new DateTimeOffset(2024, 3, 20, 13, 59, 02, TimeSpan.FromHours(0))); result.PartnerContractExpiresAt.ShouldBe(new DateTimeOffset(2024, 4, 19, 23, 59, 59, TimeSpan.FromHours(0))); result.Links.ShouldNotBeNull(); result.Links.Self.Href.ShouldBe("https://docs.mollie.com/reference/get-partner-status"); result.Links.Documentation.Href.ShouldBe("https://docs.mollie.com/reference/get-partner-status"); result.Links.Signuplink!.Href.ShouldBe("https://www.mollie.com/dashboard/signup/exampleCode"); } private void AssertDefaultOrganization(OrganizationResponse response) { response.Resource.ShouldBe("organization"); response.Id.ShouldBe("org_12345678"); response.Name.ShouldBe("Mollie B.V."); response.Email.ShouldBe("info@mollie.com"); response.Address.StreetAndNumber.ShouldBe("Keizersgracht 126"); response.Address.PostalCode.ShouldBe("1015 CW"); response.Address.City.ShouldBe("Amsterdam"); response.Address.Country.ShouldBe("NL"); response.RegistrationNumber.ShouldBe("30204462"); response.VatNumber.ShouldBe("NL815839091B01"); response.VatRegulation.ShouldBeNullOrEmpty(); response.Links.Self.Href.ShouldBe("https://api.mollie.com/v2/organizations/me"); response.Links.Chargebacks!.Href.ShouldBe("https://api.mollie.com/v2/chargebacks"); response.Links.Customers!.Href.ShouldBe("https://api.mollie.com/v2/customers"); response.Links.Invoices!.Href.ShouldBe("https://api.mollie.com/v2/invoices"); response.Links.Payments!.Href.ShouldBe("https://api.mollie.com/v2/payments"); response.Links.Profiles!.Href.ShouldBe("https://api.mollie.com/v2/profiles"); response.Links.Refunds!.Href.ShouldBe("https://api.mollie.com/v2/refunds"); response.Links.Settlements!.Href.ShouldBe("https://api.mollie.com/v2/settlements"); response.Links.Dashboard.Href.ShouldBe("https://mollie.com/dashboard/org_12345678"); response.Links.Documentation.Href.ShouldBe("https://docs.mollie.com/reference/v2/organizations-api/current-organization"); } private const string defaultOrganizationListResponse = @$" {{ ""count"": 2, ""_embedded"": {{ ""organizations"": [ {defaultOrganizationResponse}, {defaultOrganizationResponse} ] }} }}"; private const string defaultGetPartnerStatusResponse = @"{ ""resource"": ""partner"", ""partnerType"": ""signuplink"", ""partnerContractSignedAt"": ""2024-03-20T13:59:02+00:00"", ""partnerContractExpiresAt"": ""2024-04-19T23:59:59+00:00"", ""_links"": { ""self"": { ""href"": ""https://docs.mollie.com/reference/get-partner-status"", ""type"": ""application/hal+json"" }, ""signuplink"": { ""href"": ""https://www.mollie.com/dashboard/signup/exampleCode"", ""type"": ""text/html"" }, ""documentation"": { ""href"": ""https://docs.mollie.com/reference/get-partner-status"", ""type"": ""text/html"" } } }"; private const string defaultOrganizationResponse = @"{ ""resource"": ""organization"", ""id"": ""org_12345678"", ""name"": ""Mollie B.V."", ""email"": ""info@mollie.com"", ""locale"": ""nl_NL"", ""address"": { ""streetAndNumber"" : ""Keizersgracht 126"", ""postalCode"": ""1015 CW"", ""city"": ""Amsterdam"", ""country"": ""NL"" }, ""registrationNumber"": ""30204462"", ""vatNumber"": ""NL815839091B01"", ""_links"": { ""self"": { ""href"": ""https://api.mollie.com/v2/organizations/me"", ""type"": ""application/hal+json"" }, ""chargebacks"": { ""href"": ""https://api.mollie.com/v2/chargebacks"", ""type"": ""application/hal+json"" }, ""customers"": { ""href"": ""https://api.mollie.com/v2/customers"", ""type"": ""application/hal+json"" }, ""invoices"": { ""href"": ""https://api.mollie.com/v2/invoices"", ""type"": ""application/hal+json"" }, ""payments"": { ""href"": ""https://api.mollie.com/v2/payments"", ""type"": ""application/hal+json"" }, ""profiles"": { ""href"": ""https://api.mollie.com/v2/profiles"", ""type"": ""application/hal+json"" }, ""refunds"": { ""href"": ""https://api.mollie.com/v2/refunds"", ""type"": ""application/hal+json"" }, ""settlements"": { ""href"": ""https://api.mollie.com/v2/settlements"", ""type"": ""application/hal+json"" }, ""dashboard"": { ""href"": ""https://mollie.com/dashboard/org_12345678"", ""type"": ""text/html"" }, ""documentation"": { ""href"": ""https://docs.mollie.com/reference/v2/organizations-api/current-organization"", ""type"": ""text/html"" } } }"; } ================================================ FILE: tests/Mollie.Tests.Unit/Client/PaymentClientTests.cs ================================================ using System; using Mollie.Api.Client; using Mollie.Api.Models; using Mollie.Api.Models.Payment; using Mollie.Api.Models.Payment.Request; using Mollie.Api.Models.Payment.Response; using RichardSzalay.MockHttp; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Models.Payment.Request.PaymentSpecificParameters; using Mollie.Api.Models.Payment.Response.PaymentSpecificParameters; using Xunit; using SortDirection = Mollie.Api.Models.SortDirection; namespace Mollie.Tests.Unit.Client; public class PaymentClientTests : BaseClientTests { [Fact] public async Task CreatePaymentAsync_WithCustomIdempotencyKey_CustomIdemPotencyKeyIsSent() { // Given: We create a payment request with only the required parameters var paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = "http://www.mollie.com" }; const string customIdempotencyKey1 = "my-idempotency-key-1"; const string customIdempotencyKey2 = "my-idempotency-key-2"; const string jsonToReturnInMockResponse = defaultPaymentJsonResponse; var mockHttp = new MockHttpMessageHandler(); mockHttp.Expect(HttpMethod.Post, $"{BaseMollieClient.DefaultBaseApiEndPoint}*") .WithHeaders("Idempotency-Key", customIdempotencyKey1) .Respond("application/json", jsonToReturnInMockResponse); mockHttp.Expect(HttpMethod.Post, $"{BaseMollieClient.DefaultBaseApiEndPoint}*") .WithHeaders("Idempotency-Key", customIdempotencyKey2) .Respond("application/json", jsonToReturnInMockResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // Act using (paymentClient.WithIdempotencyKey(customIdempotencyKey1)) { await paymentClient.CreatePaymentAsync(paymentRequest); } using (paymentClient.WithIdempotencyKey(customIdempotencyKey2)) { await paymentClient.CreatePaymentAsync(paymentRequest); } // Assert mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task CreatePaymentAsync_PaymentWithRequiredParameters_ResponseIsDeserializedInExpectedFormat() { // Given: we create a payment request with only the required parameters PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = "http://www.mollie.com" }; const string jsonToReturnInMockResponse = defaultPaymentJsonResponse; var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}*") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", jsonToReturnInMockResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request PaymentResponse paymentResponse = await paymentClient.CreatePaymentAsync(paymentRequest); // Then AssertPaymentIsEqual(paymentRequest, paymentResponse); paymentResponse.AuthorizedAt!.Value.ToUniversalTime().ShouldBe(DateTime.SpecifyKind(new DateTime(2018, 3, 19, 13, 28, 37), DateTimeKind.Utc)); paymentResponse.CreatedAt!.ToUniversalTime().ShouldBe(DateTime.SpecifyKind(new DateTime(2018, 3, 20, 13, 13, 37), DateTimeKind.Utc)); paymentResponse.PaidAt!.Value.ToUniversalTime().ShouldBe(DateTime.SpecifyKind(new DateTime(2018, 3, 21, 13, 28, 37), DateTimeKind.Utc)); paymentResponse.CanceledAt!.Value.ToUniversalTime().ShouldBe(DateTime.SpecifyKind(new DateTime(2018, 3, 22, 13, 28, 37), DateTimeKind.Utc)); paymentResponse.ExpiredAt!.Value.ToUniversalTime().ShouldBe(DateTime.SpecifyKind(new DateTime(2018, 3, 23, 13, 28, 37), DateTimeKind.Utc)); paymentResponse.FailedAt!.Value.ToUniversalTime().ShouldBe(DateTime.SpecifyKind(new DateTime(2018, 3, 24, 13, 28, 37), DateTimeKind.Utc)); paymentResponse.CaptureBefore!.Value.ToUniversalTime().ShouldBe(DateTime.SpecifyKind(new DateTime(2018, 3, 25, 13, 28, 37), DateTimeKind.Utc)); paymentResponse.AmountRefunded!.Value.ShouldBe("10.00"); paymentResponse.AmountRefunded.Currency.ShouldBe(Currency.EUR); paymentResponse.AmountRemaining!.Value.ShouldBe("90.00"); paymentResponse.AmountRemaining.Currency.ShouldBe(Currency.EUR); paymentResponse.AmountChargedBack!.Value.ShouldBe("10.00"); paymentResponse.AmountChargedBack.Currency.ShouldBe(Currency.EUR); paymentResponse.CancelUrl.ShouldBe("https://webshop.example.org/order/12345/cancel"); paymentResponse.CountryCode.ShouldBe("NL"); paymentResponse.SettlementId.ShouldBe("stl_jDk30akdN"); paymentResponse.SubscriptionId.ShouldBe("sub_rVKGtNd6s3"); paymentResponse.ApplicationFee.ShouldNotBeNull(); paymentResponse.ApplicationFee!.Amount.Value.ShouldBe("1.00"); paymentResponse.ApplicationFee.Amount.Currency.ShouldBe(Currency.EUR); paymentResponse.ApplicationFee.Description.ShouldBe("description"); } [Fact] public async Task CreatePaymentAsync_PaymentWithSinglePaymentMethod_RequestIsSerializedInExpectedFormat() { // Given: We create a payment request with a single payment method PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = "http://www.mollie.com", Method = PaymentMethod.Ideal }; string expectedPaymentMethodJson = $"\"method\":[\"{PaymentMethod.Ideal}\""; const string jsonResponse = @"{ ""amount"":{ ""currency"":""EUR"", ""value"":""100.00"" }, ""description"":""Description"", ""method"":""ideal"", ""redirectUrl"":""http://www.mollie.com""}"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Post, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments", jsonResponse, expectedPaymentMethodJson); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request PaymentResponse paymentResponse = await paymentClient.CreatePaymentAsync(paymentRequest); // Then mockHttp.VerifyNoOutstandingExpectation(); AssertPaymentIsEqual(paymentRequest, paymentResponse); paymentResponse.Method.ShouldBe(paymentRequest.Method); } [Fact] public async Task CreatePaymentAsync_PaymentWithMultiplePaymentMethods_RequestIsSerializedInExpectedFormat() { // Given: We create a payment request with multiple payment methods PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = "http://www.mollie.com", Methods = new List() { PaymentMethod.Ideal, PaymentMethod.CreditCard, PaymentMethod.DirectDebit } }; string expectedPaymentMethodJson = $"\"method\":[\"{PaymentMethod.Ideal}\",\"{PaymentMethod.CreditCard}\",\"{PaymentMethod.DirectDebit}\"]"; const string expectedJsonResponse = @"{ ""amount"":{ ""currency"":""EUR"", ""value"":""100.00"" }, ""description"":""Description"", ""method"": null, ""redirectUrl"":""http://www.mollie.com""}"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Post, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments", expectedJsonResponse, expectedPaymentMethodJson); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request PaymentResponse paymentResponse = await paymentClient.CreatePaymentAsync(paymentRequest); // Then mockHttp.VerifyNoOutstandingExpectation(); AssertPaymentIsEqual(paymentRequest, paymentResponse); paymentResponse.Method.ShouldBeNull(); } [Fact] public async Task CreatePayment_WithRoutingInformation_RequestIsSerializedInExpectedFormat() { // Given: We create a payment request with the routing request PaymentRoutingRequest routingRequest = new PaymentRoutingRequest { Amount = new Amount("EUR", 100), Destination = new RoutingDestination { Type = "organization", OrganizationId = "organization-id" }, ReleaseDate = new DateTime(2022, 1, 14) }; PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = "http://www.mollie.com", Routings = new List { routingRequest } }; string expectedRoutingInformation = $"\"routing\":[{{\"amount\":{{\"currency\":\"EUR\",\"value\":\"100.00\"}},\"destination\":{{\"type\":\"organization\",\"organizationId\":\"organization-id\"}},\"releaseDate\":\"2022-01-14\"}}]}}"; const string expectedJsonResponse = @"{ ""resource"": ""payment"", ""id"": ""tr_5B8cwPMGnU6qLbRvo7qEZo"", ""mode"": ""live"", ""createdAt"": ""2024-03-20T09:13:37.0Z"", ""status"": ""open"", ""webhookUrl"": ""https://webshop.example.org/payments/webhook/"", ""profileId"": ""pfl_QkEhN94Ba"", ""sequenceType"": ""oneoff"", ""amount"":{ ""currency"":""EUR"", ""value"":""100.00"" }, ""description"":""Description"", ""method"": null, ""redirectUrl"":""http://www.mollie.com"", ""routing"": [{ ""resource"": ""route"", ""id"": ""crt_tntKsr6tffuVdqnEvhq3J"", ""amount"": { ""currency"": ""EUR"", ""value"": ""100.00"" }, ""destination"": { ""type"": ""organization"", ""organizationId"": ""organization-id"" }, ""releaseDate"": ""2022-01-14"" } ]}"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Post, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments", expectedJsonResponse, expectedRoutingInformation); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request PaymentResponse paymentResponse = await paymentClient.CreatePaymentAsync(paymentRequest); // Then mockHttp.VerifyNoOutstandingExpectation(); AssertPaymentIsEqual(paymentRequest, paymentResponse); paymentResponse.Method.ShouldBeNull(); } [Fact] public async Task CreatePaymentAsync_IncludeQrCode_QueryStringContainsIncludeQrCodeParameter() { // Given: We make a request to create a payment and include the QR code PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = "http://www.mollie.com", Method = PaymentMethod.Ideal }; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Post, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments?include=details.qrCode", defaultPaymentJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request await paymentClient.CreatePaymentAsync(paymentRequest, includeQrCode: true); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task GetPaymentAsync_NoIncludeParameters_RequestIsDeserializedInExpectedFormat() { // Given: We make a request to retrieve a payment without wanting any extra data const string paymentId = "tr_WDqYK6vllg"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{paymentId}", defaultPaymentJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request var payment = await paymentClient.GetPaymentAsync(paymentId); // Then mockHttp.VerifyNoOutstandingExpectation(); payment.Resource.ShouldBe("payment"); payment.Id.ShouldBe(paymentId); payment.Amount.Value.ShouldBe("100.00"); payment.Amount.Currency.ShouldBe(Currency.EUR); payment.Description.ShouldBe("Description"); payment.Method.ShouldBeNull(); payment.Status.ShouldBe(PaymentStatus.Open); payment.IsCancelable.ShouldBe(false); payment.Locale.ShouldBe("nl_NL"); payment.ExpiresAt!.Value.ToUniversalTime().ShouldBe(DateTime.SpecifyKind(new DateTime(2018, 3, 20, 13, 28, 37), DateTimeKind.Utc)); payment.ProfileId.ShouldBe("pfl_QkEhN94Ba"); payment.SequenceType.ShouldBe(SequenceType.OneOff); payment.RedirectUrl.ShouldBe("https://webshop.example.org/order/12345/"); payment.WebhookUrl.ShouldBe("https://webshop.example.org/payments/webhook/"); payment.Links.ShouldNotBeNull(); payment.Links.Self.Href.ShouldBe("https://api.mollie.com/v2/payments/tr_WDqYK6vllg"); payment.Links.Self.Type.ShouldBe("application/hal+json"); payment.Links.Checkout!.Href.ShouldBe("https://www.mollie.com/payscreen/select-method/WDqYK6vllg"); payment.Links.Checkout.Type.ShouldBe("text/html"); payment.Links.Dashboard.Href.ShouldBe("https://www.mollie.com/dashboard/org_12345678/payments/tr_WDqYK6vllg"); payment.Links.Dashboard.Type.ShouldBe("text/html"); payment.Links.Documentation.Href.ShouldBe("https://docs.mollie.com/reference/v2/payments-api/get-payment"); payment.Links.Documentation.Type.ShouldBe("text/html"); } [Fact] public async Task GetPaymentAsync_ForBankTransferPayment_DetailsAreDeserialized() { // Given: We make a request to retrieve a bank transfer payment const string paymentId = "tr_WDqYK6vllg"; const string jsonResponse = @"{ ""resource"": ""payment"", ""id"": ""tr_WDqYK6vllg"", ""mode"": ""test"", ""createdAt"": ""2018-03-20T13:13:37+00:00"", ""amount"":{ ""currency"":""EUR"", ""value"":""100.00"" }, ""description"":""Description"", ""method"": ""banktransfer"", ""expiresAt"": ""2018-03-20T13:28:37+00:00"", ""details"": { ""bankName"": ""bank-name"", ""bankAccount"": ""bank-account"", ""bankBic"": ""bank-bic"", ""transferReference"": ""transfer-reference"", ""consumerName"": ""consumer-name"", ""consumerAccount"": ""consumer-account"", ""consumerBic"": ""consumer-bic"", ""billingEmail"": ""billing-email"", ""qrCode"":{ ""height"": 5, ""width"": 10, ""src"": ""https://www.mollie.com/qr/12345678.png"" } }, ""_links"": { ""status"": { ""href"": ""https://api.mollie.com/v2/payments/tr_WDqYK6vllg"", ""type"": ""application/hal+json"" }, ""payOnline"": { ""href"": ""https://www.mollie.com/payscreen/select-method/WDqYK6vllg"", ""type"": ""text/html"" } } }"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{paymentId}", jsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request var payment = await paymentClient.GetPaymentAsync(paymentId); // Then payment.ShouldBeOfType(); var bankTransferPayment = payment as BankTransferPaymentResponse; bankTransferPayment!.Details!.BankName.ShouldBe("bank-name"); bankTransferPayment.Details.BankAccount.ShouldBe("bank-account"); bankTransferPayment.Details.BankBic.ShouldBe("bank-bic"); bankTransferPayment.Details.TransferReference.ShouldBe("transfer-reference"); bankTransferPayment.Details.ConsumerName.ShouldBe("consumer-name"); bankTransferPayment.Details.ConsumerAccount.ShouldBe("consumer-account"); bankTransferPayment.Details.ConsumerBic.ShouldBe("consumer-bic"); bankTransferPayment.Details.BillingEmail.ShouldBe("billing-email"); bankTransferPayment.Details.QrCode.ShouldNotBeNull(); bankTransferPayment.Details.QrCode.Height.ShouldBe(5); bankTransferPayment.Details.QrCode.Width.ShouldBe(10); bankTransferPayment.Details.QrCode.Src.ShouldBe("https://www.mollie.com/qr/12345678.png"); bankTransferPayment.Links.ShouldNotBeNull(); bankTransferPayment.Links.Status.ShouldNotBeNull(); bankTransferPayment.Links.Status.Href.ShouldBe("https://api.mollie.com/v2/payments/tr_WDqYK6vllg"); bankTransferPayment.Links.Status.Type.ShouldBe("application/hal+json"); bankTransferPayment.Links.PayOnline.ShouldNotBeNull(); bankTransferPayment.Links.PayOnline.Href.ShouldBe("https://www.mollie.com/payscreen/select-method/WDqYK6vllg"); bankTransferPayment.Links.PayOnline.Type.ShouldBe("text/html"); } [Fact] public async Task GetPaymentAsync_ForBanContactPayment_DetailsAreDeserialized() { // Given: We make a request to retrieve a bancontact payment const string paymentId = "tr_WDqYK6vllg"; const string jsonResponse = @"{ ""resource"": ""payment"", ""id"": ""tr_WDqYK6vllg"", ""mode"": ""test"", ""createdAt"": ""2018-03-20T13:13:37+00:00"", ""amount"":{ ""currency"":""EUR"", ""value"":""100.00"" }, ""description"":""Description"", ""method"": ""bancontact"", ""expiresAt"": ""2018-03-20T13:28:37+00:00"", ""details"": { ""cardNumber"": ""1234567890123456"", ""cardFingerprint"": ""fingerprint"", ""qrCode"":{ ""height"": 5, ""width"": 10, ""src"": ""https://www.mollie.com/qr/12345678.png"" }, ""consumerName"": ""consumer-name"", ""consumerAccount"": ""consumer-account"", ""consumerBic"": ""consumer-bic"", ""failureReason"": ""failure-reason"" } }"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{paymentId}", jsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request var result = await paymentClient.GetPaymentAsync(paymentId); // Then result.ShouldBeOfType(); var banContactPayment = result as BancontactPaymentResponse; banContactPayment!.Details!.CardNumber.ShouldBe("1234567890123456"); banContactPayment.Details.QrCode.ShouldNotBeNull(); banContactPayment.Details.QrCode.Height.ShouldBe(5); banContactPayment.Details.QrCode.Width.ShouldBe(10); banContactPayment.Details.QrCode.Src.ShouldBe("https://www.mollie.com/qr/12345678.png"); banContactPayment.Details.ConsumerName.ShouldBe("consumer-name"); banContactPayment.Details.ConsumerAccount.ShouldBe("consumer-account"); banContactPayment.Details.ConsumerBic.ShouldBe("consumer-bic"); banContactPayment.Details.FailureReason.ShouldBe("failure-reason"); } [Fact] public async Task CreatePaymentAsync_SepaDirectDebit_RequestAndResponseAreConvertedToExpectedJsonFormat() { // Given we create a creditcard specific payment request var paymentRequest = new SepaDirectDebitRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", Method = PaymentMethod.Ideal, RedirectUrl = "http://www.mollie.com", WebhookUrl = "http://www.mollie.com/webhook", ConsumerName = "consumer-name", ConsumerAccount = "consumer-account" }; const string jsonRequest = @"{ ""consumerName"": ""consumer-name"", ""consumerAccount"": ""consumer-account"", ""amount"": { ""currency"": ""EUR"", ""value"": ""100.00"" }, ""description"": ""Description"", ""redirectUrl"": ""http://www.mollie.com"", ""webhookUrl"": ""http://www.mollie.com/webhook"", ""method"": [ ""ideal"" ] }"; const string jsonResponse = @"{ ""resource"": ""payment"", ""id"": ""tr_WDqYK6vllg"", ""mode"": ""test"", ""createdAt"": ""2018-03-20T13:13:37+00:00"", ""amount"":{ ""currency"":""EUR"", ""value"":""100.00"" }, ""description"":""Description"", ""method"": ""directdebit"", ""expiresAt"": ""2018-03-20T13:28:37+00:00"", ""details"": { ""consumerName"": ""consumer-name"", ""consumerAccount"": ""consumer-account"", ""consumerBic"": ""consumer-bic"", ""transferReference"": ""transfer-reference"", ""bankReasonCode"": ""bank-reason-code"", ""bankReason"": ""bank-reason"", ""batchReference"": ""batch-reference"", ""mandateReference"": ""mandate-reference"", ""creditorIdentifier"": ""creditor-identifier"", ""dueDate"": ""2018-03-20"", ""signatureDate"": ""2018-03-20"", ""endToEndIdentifier"": ""end-to-end-identifier"", ""batchReference"": ""batch-reference"", ""fileReference"": ""file-reference"" } }"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Post, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments", jsonResponse, jsonRequest); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request var result = await paymentClient.CreatePaymentAsync(paymentRequest); // Then mockHttp.VerifyNoOutstandingExpectation(); var specificPaymentResponse = result as SepaDirectDebitResponse; specificPaymentResponse.ShouldNotBeNull(); specificPaymentResponse.Details!.ConsumerName.ShouldBe("consumer-name"); specificPaymentResponse.Details.ConsumerAccount.ShouldBe("consumer-account"); specificPaymentResponse.Details.ConsumerBic.ShouldBe("consumer-bic"); specificPaymentResponse.Details.TransferReference.ShouldBe("transfer-reference"); specificPaymentResponse.Details.BankReasonCode.ShouldBe("bank-reason-code"); specificPaymentResponse.Details.BankReason.ShouldBe("bank-reason"); specificPaymentResponse.Details.BatchReference.ShouldBe("batch-reference"); specificPaymentResponse.Details.MandateReference.ShouldBe("mandate-reference"); specificPaymentResponse.Details.CreditorIdentifier.ShouldBe("creditor-identifier"); specificPaymentResponse.Details.DueDate.ShouldBe("2018-03-20"); specificPaymentResponse.Details.SignatureDate.ShouldBe("2018-03-20"); specificPaymentResponse.Details.EndToEndIdentifier.ShouldBe("end-to-end-identifier"); specificPaymentResponse.Details.BatchReference.ShouldBe("batch-reference"); specificPaymentResponse.Details.FileReference.ShouldBe("file-reference"); } [Fact] public async Task GetPaymentAsync_ForPayPalPayment_DetailsAreDeserialized() { // Given: We make a request to retrieve a paypal payment const string paymentId = "tr_WDqYK6vllg"; const string jsonResponse = @"{ ""resource"": ""payment"", ""id"": ""tr_WDqYK6vllg"", ""mode"": ""test"", ""createdAt"": ""2018-03-20T13:13:37+00:00"", ""amount"":{ ""currency"":""EUR"", ""value"":""100.00"" }, ""description"":""Description"", ""method"": ""paypal"", ""expiresAt"": ""2018-03-20T13:28:37+00:00"", ""details"": { ""consumerName"": ""consumer-name"", ""consumerAccount"": ""consumer-account"", ""paypalReference"": ""paypal-ref"", ""paypalPayerId"": ""paypal-payer-id"", ""sellerProtection"": ""Eligible"", ""shippingAddress"": { ""streetAndNumber"": ""street-and-number"", ""streetAdditional"": ""street-additional"", ""postalCode"": ""postal-code"", ""city"": ""city"", ""region"": ""region"", ""country"": ""country"" }, ""paypalFee"": { ""currency"": ""EUR"", ""value"": ""100.00"" } } }"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{paymentId}", jsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request var result = await paymentClient.GetPaymentAsync(paymentId); // Then result.ShouldBeOfType(); var payPalPayment = result as PayPalPaymentResponse; payPalPayment!.Details!.ConsumerName.ShouldBe("consumer-name"); payPalPayment.Details.ConsumerAccount.ShouldBe("consumer-account"); payPalPayment.Details.PayPalReference.ShouldBe("paypal-ref"); payPalPayment.Details.PaypalPayerId.ShouldBe("paypal-payer-id"); payPalPayment.Details.SellerProtection.ShouldBe("Eligible"); payPalPayment.Details.ShippingAddress.ShouldNotBeNull(); payPalPayment.Details.ShippingAddress.StreetAndNumber.ShouldBe("street-and-number"); payPalPayment.Details.ShippingAddress.StreetAdditional.ShouldBe("street-additional"); payPalPayment.Details.ShippingAddress.PostalCode.ShouldBe("postal-code"); payPalPayment.Details.ShippingAddress.City.ShouldBe("city"); payPalPayment.Details.ShippingAddress.Region.ShouldBe("region"); payPalPayment.Details.ShippingAddress.Country.ShouldBe("country"); payPalPayment.Details.PaypalFee.ShouldNotBeNull(); payPalPayment.Details.PaypalFee.Currency.ShouldBe("EUR"); payPalPayment.Details.PaypalFee.Value.ShouldBe("100.00"); } [Fact] public async Task CreatePaymentAsync_CreditcardPayment_RequestAndResponseAreConvertedToExpectedJsonFormat() { // Given we create a creditcard specific payment request var paymentRequest = new CreditCardPaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", Method = PaymentMethod.Ideal, RedirectUrl = "http://www.mollie.com", WebhookUrl = "http://www.mollie.com/webhook", BillingAddress = new PaymentAddressDetails() { City = "Amsterdam", Country = "NL", PostalCode = "1000AA", Region = "Noord-Holland", StreetAndNumber = "Keizersgracht 313" }, ShippingAddress = new PaymentAddressDetails() { City = "Amsterdam", Country = "NL", PostalCode = "1000AA", Region = "Noord-Holland", StreetAndNumber = "Keizersgracht 313" }, CardToken = "card-token" }; const string jsonRequest = @"{ ""cardToken"": ""card-token"", ""amount"": { ""currency"": ""EUR"", ""value"": ""100.00"" }, ""description"": ""Description"", ""redirectUrl"": ""http://www.mollie.com"", ""webhookUrl"": ""http://www.mollie.com/webhook"", ""billingAddress"": { ""streetAndNumber"": ""Keizersgracht 313"", ""postalCode"": ""1000AA"", ""city"": ""Amsterdam"", ""region"": ""Noord-Holland"", ""country"": ""NL"" }, ""shippingAddress"": { ""streetAndNumber"": ""Keizersgracht 313"", ""postalCode"": ""1000AA"", ""city"": ""Amsterdam"", ""region"": ""Noord-Holland"", ""country"": ""NL"" }, ""method"": [ ""ideal"" ] }"; const string jsonResponse = @"{ ""resource"": ""payment"", ""id"": ""tr_WDqYK6vllg"", ""mode"": ""test"", ""status"": ""open"", ""webhookUrl"": ""https://webshop.example.org/payments/webhook/"", ""profileId"": ""pfl_QkEhN94Ba"", ""sequenceType"": ""oneoff"", ""createdAt"": ""2018-03-20T13:13:37+00:00"", ""amount"":{ ""currency"":""EUR"", ""value"":""100.00"" }, ""description"":""Description"", ""method"": ""creditcard"", ""expiresAt"": ""2018-03-20T13:28:37+00:00"", ""details"": { ""cardNumber"": ""1234567890123456"", ""cardHolder"": ""John Doe"", ""cardFingerprint"": ""fingerprint"", ""cardAudience"": ""audience"", ""cardLabel"": ""American Express"", ""cardCountryCode"": ""NL"", ""cardSecurity"": ""security"", ""feeRegion"": ""american-express"", ""failureReason"": ""unknown_reason"", ""failureMessage"": ""faulure-message"", ""wallet"": ""applepay"" } }"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Post, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments", jsonResponse, jsonRequest); HttpClient httpClient = mockHttp.ToHttpClient(); PaymentClient paymentClient = new("abcde", httpClient); // When: We send the request var result = await paymentClient.CreatePaymentAsync(paymentRequest); // Then mockHttp.VerifyNoOutstandingExpectation(); var specificPaymentResponse = result as CreditCardPaymentResponse; specificPaymentResponse.ShouldNotBeNull(); specificPaymentResponse.Details!.CardNumber.ShouldBe("1234567890123456"); specificPaymentResponse.Details.CardHolder.ShouldBe("John Doe"); specificPaymentResponse.Details.CardFingerprint.ShouldBe("fingerprint"); specificPaymentResponse.Details.CardAudience.ShouldBe("audience"); specificPaymentResponse.Details.CardLabel.ShouldBe("American Express"); specificPaymentResponse.Details.CardCountryCode.ShouldBe("NL"); specificPaymentResponse.Details.CardSecurity.ShouldBe("security"); specificPaymentResponse.Details.FeeRegion.ShouldBe("american-express"); specificPaymentResponse.Details.FailureReason.ShouldBe("unknown_reason"); specificPaymentResponse.Details.FailureMessage.ShouldBe("faulure-message"); specificPaymentResponse.Details.Wallet.ShouldBe("applepay"); } [Fact] public async Task CreatePaymentAsync_GiftcardPayment_RequestAndResponseAreConvertedToExpectedJsonFormat() { // Given we create a giftcard specific payment request var paymentRequest = new GiftcardPaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", Method = PaymentMethod.GiftCard, RedirectUrl = "http://www.mollie.com", WebhookUrl = "http://www.mollie.com/webhook", Issuer = "issuer", VoucherNumber = "voucher-number", VoucherPin = "voucher-pin" }; const string jsonRequest = @"{ ""voucherNumber"" : ""voucher-number"", ""voucherPin"" : ""voucher-pin"", ""amount"" : { ""currency"" : ""EUR"", ""value"" : ""100.00"" }, ""description"" : ""Description"", ""redirectUrl"" : ""http://www.mollie.com"", ""webhookUrl"" : ""http://www.mollie.com/webhook"", ""method"" : [ ""giftcard"" ], ""issuer"" : ""issuer"" }"; const string jsonResponse = @"{ ""resource"": ""payment"", ""id"": ""tr_WDqYK6vllg"", ""mode"": ""test"", ""createdAt"": ""2018-03-20T13:13:37+00:00"", ""amount"":{ ""currency"":""EUR"", ""value"":""100.00"" }, ""description"":""Description"", ""method"": ""giftcard"", ""expiresAt"": ""2018-03-20T13:28:37+00:00"", ""details"": { ""voucherNumber"": ""voucher-number"", ""giftcards"": [ { ""issuer"": ""issuer"", ""amount"": { ""currency"": ""EUR"", ""value"": ""100.00"" }, ""voucherNumber"": ""voucher-number"" } ], ""remainderAmount"": { ""currency"": ""EUR"", ""value"": ""100.00"" }, ""remainderMethod"": ""ideal"" } }"; var mockHttp = CreateMockHttpMessageHandler( HttpMethod.Post, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments", jsonResponse, jsonRequest); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request var result = await paymentClient.CreatePaymentAsync(paymentRequest); // Then mockHttp.VerifyNoOutstandingExpectation(); var specificPaymentResponse = result as GiftcardPaymentResponse; specificPaymentResponse.ShouldNotBeNull(); specificPaymentResponse!.Details!.VoucherNumber.ShouldBe("voucher-number"); specificPaymentResponse.Details.Giftcards.ShouldNotBeNull(); specificPaymentResponse.Details.Giftcards.Count.ShouldBe(1); specificPaymentResponse.Details.Giftcards[0].Issuer.ShouldBe("issuer"); specificPaymentResponse.Details.Giftcards[0].Amount.ShouldNotBeNull(); specificPaymentResponse.Details.Giftcards[0].Amount.Currency.ShouldBe("EUR"); specificPaymentResponse.Details.Giftcards[0].Amount.Value.ShouldBe("100.00"); specificPaymentResponse.Details.Giftcards[0].VoucherNumber.ShouldBe("voucher-number"); specificPaymentResponse.Details.RemainderAmount.ShouldNotBeNull(); specificPaymentResponse.Details.RemainderAmount.Currency.ShouldBe("EUR"); specificPaymentResponse.Details.RemainderAmount.Value.ShouldBe("100.00"); specificPaymentResponse.Details.RemainderMethod.ShouldBe("ideal"); } [Fact] public async Task GetPaymentAsync_ForBelfiusPayment_DetailsAreDeserialized() { // Given: We make a request to retrieve a belfius payment const string paymentId = "tr_WDqYK6vllg"; const string jsonResponse = @"{ ""resource"": ""payment"", ""id"": ""tr_WDqYK6vllg"", ""mode"": ""test"", ""createdAt"": ""2018-03-20T13:13:37+00:00"", ""amount"":{ ""currency"":""EUR"", ""value"":""100.00"" }, ""description"":""Description"", ""method"": ""belfius"", ""expiresAt"": ""2018-03-20T13:28:37+00:00"", ""details"": { ""consumerName"": ""consumer-name"", ""consumerAccount"": ""consumer-account"", ""consumerBic"": ""consumer-bic"" } }"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{paymentId}", jsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request var result = await paymentClient.GetPaymentAsync(paymentId); // Then result.ShouldBeOfType(); var belfiusPayment = result as BelfiusPaymentResponse; belfiusPayment!.Details!.ConsumerName.ShouldBe("consumer-name"); belfiusPayment.Details.ConsumerAccount.ShouldBe("consumer-account"); belfiusPayment.Details.ConsumerBic.ShouldBe("consumer-bic"); } [Fact] public async Task GetPaymentAsync_ForIngHomePay_DetailsAreDeserialized() { // Given: We make a request to retrieve a ing home pay payment const string paymentId = "tr_WDqYK6vllg"; const string jsonResponse = @"{ ""resource"": ""payment"", ""id"": ""tr_WDqYK6vllg"", ""mode"": ""test"", ""createdAt"": ""2018-03-20T13:13:37+00:00"", ""amount"":{ ""currency"":""EUR"", ""value"":""100.00"" }, ""description"":""Description"", ""method"": ""inghomepay"", ""expiresAt"": ""2018-03-20T13:28:37+00:00"", ""details"": { ""consumerName"": ""consumer-name"", ""consumerAccount"": ""consumer-account"", ""consumerBic"": ""consumer-bic"" } }"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{paymentId}", jsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request var result = await paymentClient.GetPaymentAsync(paymentId); // Then result.ShouldBeOfType(); var ingHomePayPayment = result as IngHomePayPaymentResponse; ingHomePayPayment!.Details!.ConsumerName.ShouldBe("consumer-name"); ingHomePayPayment.Details.ConsumerAccount.ShouldBe("consumer-account"); ingHomePayPayment.Details.ConsumerBic.ShouldBe("consumer-bic"); } [Fact] public async Task GetPaymentAsync_ForKbcPayment_DetailsAreDeserialized() { // Given: We make a request to retrieve a ing home pay payment const string paymentId = "tr_WDqYK6vllg"; const string jsonResponse = @"{ ""resource"": ""payment"", ""id"": ""tr_WDqYK6vllg"", ""mode"": ""test"", ""createdAt"": ""2018-03-20T13:13:37+00:00"", ""amount"":{ ""currency"":""EUR"", ""value"":""100.00"" }, ""description"":""Description"", ""method"": ""kbc"", ""expiresAt"": ""2018-03-20T13:28:37+00:00"", ""details"": { ""consumerName"": ""consumer-name"", ""consumerAccount"": ""consumer-account"", ""consumerBic"": ""consumer-bic"" } }"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{paymentId}", jsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request var result = await paymentClient.GetPaymentAsync(paymentId); // Then result.ShouldBeOfType(); var kbcPayment = result as KbcPaymentResponse; kbcPayment!.Details!.ConsumerName.ShouldBe("consumer-name"); kbcPayment.Details.ConsumerAccount.ShouldBe("consumer-account"); kbcPayment.Details.ConsumerBic.ShouldBe("consumer-bic"); } [Fact] public async Task GetPaymentAsync_ForSofortPayment_DetailsAreDeserialized() { // Given: We make a request to retrieve a ing home pay payment const string paymentId = "tr_WDqYK6vllg"; const string jsonResponse = @"{ ""resource"": ""payment"", ""id"": ""tr_WDqYK6vllg"", ""mode"": ""test"", ""createdAt"": ""2018-03-20T13:13:37+00:00"", ""amount"":{ ""currency"":""EUR"", ""value"":""100.00"" }, ""description"":""Description"", ""method"": ""sofort"", ""expiresAt"": ""2018-03-20T13:28:37+00:00"", ""details"": { ""consumerName"": ""consumer-name"", ""consumerAccount"": ""consumer-account"", ""consumerBic"": ""consumer-bic"" } }"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{paymentId}", jsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request var result = await paymentClient.GetPaymentAsync(paymentId); // Then result.ShouldBeOfType(); var sofortPayment = result as SofortPaymentResponse; sofortPayment!.Details!.ConsumerName.ShouldBe("consumer-name"); sofortPayment.Details.ConsumerAccount.ShouldBe("consumer-account"); sofortPayment.Details.ConsumerBic.ShouldBe("consumer-bic"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetPaymentAsync_NoPaymentIdIsGiven_ArgumentExceptionIsThrown(string? paymentId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await paymentClient.GetPaymentAsync(paymentId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'paymentId' is null or empty"); } [Fact] public async Task GetPaymentAsync_IncludeQrCode_QueryStringContainsIncludeQrCodeParameter() { // Given: We make a request to retrieve a payment without wanting any extra data const string paymentId = "abcde"; const string jsonResponse = @"{ ""resource"": ""payment"", ""id"": ""tr_WDqYK6vllg"", ""amount"":{ ""currency"":""EUR"", ""value"":""100.00"" }, ""description"":""Description"", ""method"": ""ideal"", ""details"": { ""qrCode"":{ ""height"": 5, ""width"": 5, ""src"": ""https://www.mollie.com/qr/12345678.png"" } } }"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{paymentId}?include=details.qrCode", jsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request var result = await paymentClient.GetPaymentAsync(paymentId, includeQrCode: true); // Then mockHttp.VerifyNoOutstandingExpectation(); result.ShouldBeOfType(); var paymentResponse = result as IdealPaymentResponse; paymentResponse!.Details!.QrCode.ShouldNotBeNull(); paymentResponse.Details.QrCode.Height.ShouldBe(5); paymentResponse.Details.QrCode.Width.ShouldBe(5); paymentResponse.Details.QrCode.Src.ShouldBe("https://www.mollie.com/qr/12345678.png"); } [Fact] public async Task GetPaymentAsync_IncludeRemainderDetails_QueryStringContainsIncludeRemainderDetailsParameter() { // Given: We make a request to retrieve a payment without wanting any extra data const string paymentId = "abcde"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{paymentId}?include=details.remainderDetails", defaultPaymentJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request await paymentClient.GetPaymentAsync(paymentId, includeRemainderDetails: true); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task GetPaymentListAsync_IncludeQrCode_QueryStringContainsIncludeQrCodeParameter() { // Given: We make a request to retrieve a payment without wanting any extra data var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments?include=details.qrCode", defaultPaymentJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request await paymentClient.GetPaymentListAsync(includeQrCode: true); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task GetPaymentAsync_EmbedRefunds_QueryStringContainsEmbedRefundsParameter() { // Given: We make a request to retrieve a payment with embedded refunds const string paymentId = "abcde"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{paymentId}?embed=refunds", defaultPaymentJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request await paymentClient.GetPaymentAsync(paymentId, embedRefunds: true); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task GetPaymentListAsync_EmbedRefunds_QueryStringContainsEmbedRefundsParameter() { // Given: We make a request to retrieve a payment with embedded refunds var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments?embed=refunds", defaultPaymentJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request await paymentClient.GetPaymentListAsync(embedRefunds: true); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task GetPaymentAsync_EmbedChargebacks_QueryStringContainsEmbedChargebacksParameter() { // Given: We make a request to retrieve a payment with embedded refunds const string paymentId = "abcde"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{paymentId}?embed=chargebacks", defaultPaymentJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request await paymentClient.GetPaymentAsync(paymentId, embedChargebacks: true); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task GetPaymentListAsync_EmbedChargebacks_QueryStringContainsEmbedChargebacksParameter() { // Given: We make a request to retrieve a payment with embedded refunds var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments?embed=chargebacks", defaultPaymentJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request await paymentClient.GetPaymentListAsync(embedChargebacks: true); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Theory] [InlineData(null, "")] [InlineData(SortDirection.Desc, "?sort=desc")] [InlineData(SortDirection.Asc, "?sort=asc")] public async Task GetPaymentListAsync_AddSortDirection_QueryStringContainsSortDirection(SortDirection? sortDirection, string expectedQueryString) { // Given: We make a request to retrieve a payment with embedded refunds var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments{expectedQueryString}", defaultPaymentJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request await paymentClient.GetPaymentListAsync(embedChargebacks: true, sort: sortDirection); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task DeletePaymentAsync_TestmodeIsTrue_RequestContainsTestmodeModel() { // Given: We make a request to retrieve a payment with embedded refunds const string paymentId = "payment-id"; string expectedContent = "\"testmode\":true"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Delete, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{paymentId}", defaultPaymentJsonResponse, expectedContent); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request await paymentClient.CancelPaymentAsync(paymentId, true); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task ReleasePaymentAuthorizationAsync_NoPaymentIdIsGiven_ArgumentExceptionIsThrown(string? paymentId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await paymentClient.ReleasePaymentAuthorization(paymentId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'paymentId' is null or empty"); } [Fact] public async Task ReleasePaymentAuthorizationAsync_WithTestModeParameter_QueryStringContainsTestModeParameter() { // Given: We make a request to retrieve a payment with a the test mode parameter const string paymentId = "abcde"; var mockHttp = CreateMockHttpMessageHandler( HttpMethod.Post, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{paymentId}/release-authorization?testmode=true", string.Empty); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request await paymentClient.ReleasePaymentAuthorization(paymentId, testmode: true); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task DeletePaymentAsync_NoPaymentIdIsGiven_ArgumentExceptionIsThrown(string? paymentId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await paymentClient.CancelPaymentAsync(paymentId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'paymentId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task UpdatePaymentAsync_NoPaymentIdIsGiven_ArgumentExceptionIsThrown(string? paymentId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentClient = new PaymentClient("abcde", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await paymentClient.UpdatePaymentAsync(paymentId, new PaymentUpdateRequest())); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'paymentId' is null or empty"); } private void AssertPaymentIsEqual(PaymentRequest paymentRequest, PaymentResponse paymentResponse) { paymentResponse.Amount.Value.ShouldBe(paymentRequest.Amount.Value); paymentResponse.Amount.Currency.ShouldBe(paymentRequest.Amount.Currency); paymentResponse.Description.ShouldBe(paymentRequest.Description); if (paymentRequest.Routings != null) { paymentResponse.Routings!.Count.ShouldBe(paymentRequest.Routings.Count); for (int i = 0; i < paymentRequest.Routings.Count; i++) { var paymentRequestRouting = paymentRequest.Routings[i]; var paymentResponseRouting = paymentResponse.Routings[i]; paymentResponseRouting.Amount.ShouldBe(paymentRequestRouting.Amount); paymentResponseRouting.Destination.Type.ShouldBe(paymentRequestRouting.Destination.Type); paymentResponseRouting.Destination.OrganizationId.ShouldBe(paymentRequestRouting.Destination.OrganizationId); paymentResponseRouting.ReleaseDate.ShouldBe(paymentRequestRouting.ReleaseDate); } } } private const string defaultPaymentJsonResponse = @" { ""resource"": ""payment"", ""id"": ""tr_WDqYK6vllg"", ""mode"": ""test"", ""createdAt"": ""2018-03-20T13:13:37+00:00"", ""amount"":{ ""currency"":""EUR"", ""value"":""100.00"" }, ""description"":""Description"", ""method"": null, ""metadata"": { ""order_id"": ""12345"" }, ""status"": ""open"", ""isCancelable"": false, ""locale"": ""nl_NL"", ""restrictPaymentMethodsToCountry"": ""NL"", ""expiresAt"": ""2018-03-20T13:28:37+00:00"", ""details"": null, ""profileId"": ""pfl_QkEhN94Ba"", ""sequenceType"": ""oneoff"", ""redirectUrl"": ""https://webshop.example.org/order/12345/"", ""webhookUrl"": ""https://webshop.example.org/payments/webhook/"", ""authorizedAt"": ""2018-03-19T13:28:37+00:00"", ""paidAt"": ""2018-03-21T13:28:37+00:00"", ""canceledAt"": ""2018-03-22T13:28:37+00:00"", ""expiredAt"": ""2018-03-23T13:28:37+00:00"", ""failedAt"": ""2018-03-24T13:28:37+00:00"", ""captureBefore"": ""2018-03-25T13:28:37+00:00"", ""amountRefunded"": { ""currency"": ""EUR"", ""value"": ""10.00"" }, ""amountRemaining"": { ""currency"": ""EUR"", ""value"": ""90.00"" }, ""amountChargedBack"": { ""currency"": ""EUR"", ""value"": ""10.00"" }, ""cancelUrl"": ""https://webshop.example.org/order/12345/cancel"", ""countryCode"": ""NL"", ""settlementId"": ""stl_jDk30akdN"", ""subscriptionId"": ""sub_rVKGtNd6s3"", ""applicationFee"": { ""amount"": { ""currency"": ""EUR"", ""value"": ""1.00"" }, ""description"": ""description"" }, ""_links"": { ""self"": { ""href"": ""https://api.mollie.com/v2/payments/tr_WDqYK6vllg"", ""type"": ""application/hal+json"" }, ""checkout"": { ""href"": ""https://www.mollie.com/payscreen/select-method/WDqYK6vllg"", ""type"": ""text/html"" }, ""dashboard"": { ""href"": ""https://www.mollie.com/dashboard/org_12345678/payments/tr_WDqYK6vllg"", ""type"": ""text/html"" }, ""documentation"": { ""href"": ""https://docs.mollie.com/reference/v2/payments-api/get-payment"", ""type"": ""text/html"" } } }"; } ================================================ FILE: tests/Mollie.Tests.Unit/Client/PaymentLinkClientTests.cs ================================================ using System; using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Models; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.PaymentLink.Request; using Mollie.Api.Models.PaymentLink.Response; using RichardSzalay.MockHttp; using Xunit; using SortDirection = Mollie.Api.Models.SortDirection; namespace Mollie.Tests.Unit.Client { public class PaymentLinkClientTests : BaseClientTests { private const decimal DefaultPaymentAmount = 50; private const string DefaultPaymentLinkId = "pl_4Y0eZitmBnQ6IDoMqZQKh"; private const string DefaultDescription = "A car"; private const string DefaultWebhookUrl = "https://www.mollie.com"; [Fact] public async Task CreatePaymentLinkAsync_PaymentLinkWithRequiredParameters_ResponseIsDeserializedInExpectedFormat() { // Given: we create a payment link request with only the required parameters PaymentLinkRequest paymentLinkRequest = new() { Description = "Test", Amount = new Amount(Currency.EUR, 50), WebhookUrl = "https://www.mollie.com", RedirectUrl = "https://www.mollie.com", ExpiresAt = DateTime.Now.AddDays(1) }; var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}*") .Respond("application/json", _defaultPaymentLinkJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); PaymentLinkClient paymentLinkClient = new PaymentLinkClient("api-key", httpClient); // When: We send the request PaymentLinkResponse response = await paymentLinkClient.CreatePaymentLinkAsync(paymentLinkRequest); // Then mockHttp.VerifyNoOutstandingExpectation(); VerifyPaymentLinkResponse(response); } [Fact] public async Task GetPaymentLinkAsync_ResponseIsDeserializedInExpectedFormat() { // Given: we retrieve a payment link var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}*") .Respond("application/json", _defaultPaymentLinkJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); PaymentLinkClient paymentLinkClient = new PaymentLinkClient("api-key", httpClient); // When: We send the request PaymentLinkResponse response = await paymentLinkClient.GetPaymentLinkAsync(DefaultPaymentLinkId); // Then mockHttp.VerifyNoOutstandingExpectation(); VerifyPaymentLinkResponse(response); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetPaymentLinkAsync_NoPaymentLinkIdIsGiven_ArgumentExceptionIsThrown(string? paymentLinkId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); PaymentLinkClient paymentLinkClient = new PaymentLinkClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await paymentLinkClient.GetPaymentLinkAsync(paymentLinkId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'paymentLinkId' is null or empty"); } [Theory] [InlineData(false, null, "")] [InlineData(false, "abcde", "?profileId=abcde")] [InlineData(true, "abcde", "?profileId=abcde")] public async Task DeletePaymentLinkAsync_QueryParameterOptions_CorrectParametersAreAdded( bool testMode, string? profileId, string expectedQueryString) { // Given: We make a request to delete a payment link var expectedPartialContent = testMode ? "\"testmode\":true" : null; var mockHttp = CreateMockHttpMessageHandler( HttpMethod.Delete, $"{BaseMollieClient.DefaultBaseApiEndPoint}payment-links/{DefaultPaymentLinkId}{expectedQueryString}", _defaultPaymentLinkPaymentsJsonResponse, expectedPartialContent); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentLinkClient = new PaymentLinkClient("access_abcde", httpClient); // When: We send the request await paymentLinkClient.DeletePaymentLinkAsync(DefaultPaymentLinkId, profileId, testMode); // Then mockHttp.VerifyNoOutstandingRequest(); } [Theory] [InlineData(null, null, false, null, "")] [InlineData("from", null, false, null, "?from=from")] [InlineData("from", 50, false, null, "?from=from&limit=50")] [InlineData(null, null, true, null, "?testmode=true")] [InlineData(null, null, true, SortDirection.Desc, "?testmode=true&sort=desc")] [InlineData(null, null, true, SortDirection.Asc, "?testmode=true&sort=asc")] public async Task GetPaymentLinkPaymentListAsync_QueryParameterOptions_CorrectParametersAreAdded( string? from, int? limit, bool testmode, SortDirection? sortDirection, string expectedQueryString) { // Given: We make a request to retrieve the list of payment links var mockHttp = CreateMockHttpMessageHandler( HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}payment-links/{DefaultPaymentLinkId}/payments{expectedQueryString}", _defaultPaymentLinkPaymentsJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentLinkClient = new PaymentLinkClient("access_abcde", httpClient); // When: We send the request await paymentLinkClient.GetPaymentLinkPaymentListAsync(DefaultPaymentLinkId, from, limit, testmode, sortDirection); // Then mockHttp.VerifyNoOutstandingRequest(); } [Fact] public async Task GetPaymentLinkPaymentListAsync_ResponseIsDeserializedInExpectedFormat() { // Given: We make a request to retrieve the list of payment links var mockHttp = CreateMockHttpMessageHandler( HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}payment-links/{DefaultPaymentLinkId}/payments", _defaultPaymentLinkPaymentsJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var paymentLinkClient = new PaymentLinkClient("abcde", httpClient); // When: We send the request ListResponse result = await paymentLinkClient.GetPaymentLinkPaymentListAsync(DefaultPaymentLinkId); // Then mockHttp.VerifyNoOutstandingRequest(); result.ShouldNotBeNull(); result.Count.ShouldBe(1); PaymentResponse payment = result.Items.Single(); payment.Id.ShouldBe("tr_7UhSN1zuXS"); payment.Amount.Value.ShouldBe(DefaultPaymentAmount.ToString(CultureInfo.InvariantCulture)); payment.Description.ShouldBe(DefaultDescription); payment.RedirectUrl.ShouldBe(DefaultRedirectUrl); payment.WebhookUrl.ShouldBe(DefaultWebhookUrl); } private void VerifyPaymentLinkResponse(PaymentLinkResponse response) { response.Amount!.Value.ShouldBe(DefaultPaymentAmount.ToString(CultureInfo.InvariantCulture)); response.Description.ShouldBe(DefaultDescription); response.Id.ShouldBe(DefaultPaymentLinkId); response.RedirectUrl.ShouldBe(DefaultRedirectUrl); response.WebhookUrl.ShouldBe(DefaultWebhookUrl); } private readonly string _defaultPaymentLinkPaymentsJsonResponse = $@"{{ ""count"": 1, ""_embedded"": {{ ""payments"": [ {{ ""resource"": ""payment"", ""id"": ""tr_7UhSN1zuXS"", ""mode"": ""live"", ""status"": ""open"", ""isCancelable"": false, ""amount"": {{ ""value"": ""{DefaultPaymentAmount.ToString(CultureInfo.InvariantCulture)}"", ""currency"": ""EUR"" }}, ""description"": ""{DefaultDescription}"", ""method"": ""ideal"", ""metadata"": null, ""details"": null, ""profileId"": ""pfl_QkEhN94Ba"", ""redirectUrl"": ""{DefaultRedirectUrl}"", ""webhookUrl"": ""{DefaultWebhookUrl}"", ""createdAt"": ""2024-02-12T11:58:35.0Z"", ""expiresAt"": ""2024-02-12T12:13:35.0Z"", ""_links"": {{ ""self"": {{ ""href"": ""..."", ""type"": ""application/hal+json"" }}, ""checkout"": {{ ""href"": ""https://www.mollie.com/checkout/issuer/select/ideal/7UhSN1zuXS"", ""type"": ""text/html"" }}, ""dashboard"": {{ ""href"": ""https://www.mollie.com/dashboard/org_12345678/payments/tr_7UhSN1zuXS"", ""type"": ""text/html"" }} }} }} ] }}, ""_links"": {{ ""previous"": null, ""next"": {{ ""href"": ""https://api.mollie.com/v2/payment-links/pl_4Y0eZitmBnQ6IDoMqZQKh/payments?from=tr_SDkzMggpvx&limit=5"", ""type"": ""application/hal+json"" }} }} }}"; private readonly string _defaultPaymentLinkJsonResponse = @$"{{ ""resource"": ""payment-link"", ""id"": ""{DefaultPaymentLinkId}"", ""mode"": ""test"", ""profileId"": ""pfl_QkEhN94Ba"", ""createdAt"": ""2021-03-20T09:13:37+00:00"", ""paidAt"": null, ""updatedAt"": null, ""expiresAt"": ""2021-06-06T11:00:00+00:00"", ""amount"": {{ ""value"": ""{DefaultPaymentAmount.ToString(CultureInfo.InvariantCulture)}"", ""currency"": ""EUR"" }}, ""description"": ""{DefaultDescription}"", ""redirectUrl"": ""{DefaultRedirectUrl}"", ""webhookUrl"": ""{DefaultWebhookUrl}"", ""_links"": {{ ""self"": {{ ""href"": ""https://api.mollie.com/v2/payment-links/pl_4Y0eZitmBnQ6IDoMqZQKh"", ""type"": ""application/json"" }}, ""paymentLink"": {{ ""href"": ""https://useplink.com/payment/4Y0eZitmBnQ6IDoMqZQKh/"", ""type"": ""text/html"" }}, ""documentation"": {{ ""href"": ""https://docs.mollie.com/reference/v2/payment-links-api/create-payment-link"", ""type"": ""text/html"" }} }} }}"; } } ================================================ FILE: tests/Mollie.Tests.Unit/Client/PaymentMethodClientTests.cs ================================================ using System; using Mollie.Api.Client; using Mollie.Api.Models; using System.Net.Http; using System.Threading.Tasks; using Shouldly; using RichardSzalay.MockHttp; using Xunit; namespace Mollie.Tests.Unit.Client { public class PaymentMethodClientTests : BaseClientTests { private const string defaultPaymentMethodJsonResponse = @"{ ""count"": 13, ""_embedded"": { ""methods"": [ { ""resource"": ""method"", ""id"": ""ideal"", ""description"": ""iDEAL"" } ] } }"; [Fact] public async Task GetAllPaymentMethodListAsync_NoAmountParameter_QueryStringIsEmpty() { // Given: We make a request to retrieve all payment methods without any parameters var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}methods/all", defaultPaymentMethodJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); PaymentMethodClient paymentMethodClient = new PaymentMethodClient("abcde", httpClient); // When: We send the request await paymentMethodClient.GetAllPaymentMethodListAsync(); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task GetAllPaymentMethodListAsync_AmountParameterIsAdded_QueryStringContainsAmount() { // Given: We make a request to retrieve all payment methods with a amount parameter var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}methods/all?amount[value]=100.00&amount[currency]=EUR", defaultPaymentMethodJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); PaymentMethodClient paymentMethodClient = new PaymentMethodClient("abcde", httpClient); // When: We send the request await paymentMethodClient.GetAllPaymentMethodListAsync(amount: new Amount("EUR", 100)); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task GetAllPaymentMethodListAsync_ProfileIdParameterIsSpecified_QueryStringContainsProfileIdParameter() { // Given: We make a request to retrieve all payment methods with a profile id parameter var profileId = "myProfileId"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}methods/all?profileId={profileId}", defaultPaymentMethodJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); PaymentMethodClient paymentMethodClient = new PaymentMethodClient("abcde", httpClient); // When: We send the request await paymentMethodClient.GetAllPaymentMethodListAsync(profileId: profileId); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task GetPaymentMethodListAsync_IncludeWalletsParameterIsSpecified_QueryStringContainsIncludeWalletsParameter() { // Given: We make a request to retrieve the payment methods with a includeWallets parameter var includeWalletsValue = "includeWalletsValue"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}methods?includeWallets={includeWalletsValue}", defaultPaymentMethodJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); PaymentMethodClient paymentMethodClient = new PaymentMethodClient("abcde", httpClient); // When: We send the request await paymentMethodClient.GetPaymentMethodListAsync(includeWallets: includeWalletsValue); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetPaymentMethodAsync_NoPaymentMethodIsGiven_ArgumentExceptionIsThrown(string? paymentLinkId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); PaymentMethodClient paymentMethodClient = new PaymentMethodClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await paymentMethodClient.GetPaymentMethodAsync(paymentLinkId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'paymentMethod' is null or empty"); } } } ================================================ FILE: tests/Mollie.Tests.Unit/Client/PermissionClientTests.cs ================================================ using System; using System.Net.Http; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using RichardSzalay.MockHttp; using Xunit; namespace Mollie.Tests.Unit.Client; public class PermissionClientTests : BaseClientTests { [Fact] public async Task GetPermissionAsync_WithPermissionId_ResponseIsDeserializedInExpectedFormat() { // Arrange const string permissionId = "payments.read"; var mockHttp = new MockHttpMessageHandler(); mockHttp.Expect(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}permissions/{permissionId}") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", defaultGetPermissionResponse); HttpClient httpClient = mockHttp.ToHttpClient(); using var permissionClient = new PermissionClient("access_abcde", httpClient); // Act var response = await permissionClient.GetPermissionAsync(permissionId); // Assert mockHttp.VerifyNoOutstandingExpectation(); response.Resource.ShouldBe("permission"); response.Id.ShouldBe("payments.read"); response.Description.ShouldBe("View your payments"); response.Granted.ShouldBeTrue(); response.Links.ShouldNotBeNull(); response.Links.Self.Href.ShouldBe("https://api.mollie.com/v2/permissions/payments.read"); response.Links.Self.Type.ShouldBe("application/hal+json"); response.Links.Documentation.Href.ShouldBe("https://docs.mollie.com/reference/v2/permissions-api/get-permission"); response.Links.Documentation.Type.ShouldBe("text/html"); } [Fact] public async Task GetPermissionAsync_WithoutPermissionId_ThrowsArgumentException() { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); using var permissionClient = new PermissionClient("access_abcde", httpClient); // Act var exception = await Assert.ThrowsAsync(() => permissionClient.GetPermissionAsync(string.Empty)); // Assert exception.Message.ShouldBe($"Required URL argument 'permissionId' is null or empty"); } [Fact] public async Task GetPermissionListAsync_ResponseIsDeserializedInExpectedFormat() { // Arrange var mockHttp = new MockHttpMessageHandler(); mockHttp.Expect(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}permissions") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", defaultListPermissionsResponse); HttpClient httpClient = mockHttp.ToHttpClient(); using var permissionClient = new PermissionClient("access_abcde", httpClient); // Act var response = await permissionClient.GetPermissionListAsync(); // Assert mockHttp.VerifyNoOutstandingExpectation(); response.Count.ShouldBe(2); response.ShouldNotBeNull(); response.Items.Count.ShouldBe(2); response.Items[0].Resource.ShouldBe("permission"); response.Items[0].Id.ShouldBe("payments.write"); response.Items[0].Description.ShouldBe("Create new payments"); response.Items[0].Granted.ShouldBeFalse(); response.Items[0].Links.ShouldNotBeNull(); response.Items[0].Links.Self.Href.ShouldBe("https://api.mollie.com/v2/permissions/payments.write"); response.Items[0].Links.Self.Type.ShouldBe("application/hal+json"); response.Items[1].Resource.ShouldBe("permission"); response.Items[1].Id.ShouldBe("payments.read"); response.Items[1].Description.ShouldBe("View your payments"); response.Items[1].Granted.ShouldBeTrue(); response.Items[1].Links.ShouldNotBeNull(); response.Items[1].Links.Self.Href.ShouldBe("https://api.mollie.com/v2/permissions/payments.read"); response.Items[1].Links.Self.Type.ShouldBe("application/hal+json"); response.Links.ShouldNotBeNull(); response.Links.Self.Href.ShouldBe("https://api.mollie.com/v2/permissions"); response.Links.Self.Type.ShouldBe("application/hal+json"); response.Links.Documentation.Href.ShouldBe("https://docs.mollie.com/reference/v2/permissions-api/list-permissions"); response.Links.Documentation.Type.ShouldBe("text/html"); } private const string defaultGetPermissionResponse = @"{ ""resource"": ""permission"", ""id"": ""payments.read"", ""description"": ""View your payments"", ""granted"": true, ""_links"": { ""self"": { ""href"": ""https://api.mollie.com/v2/permissions/payments.read"", ""type"": ""application/hal+json"" }, ""documentation"": { ""href"": ""https://docs.mollie.com/reference/v2/permissions-api/get-permission"", ""type"": ""text/html"" } } }"; private const string defaultListPermissionsResponse = @"{ ""_embedded"": { ""permissions"": [ { ""resource"": ""permission"", ""id"": ""payments.write"", ""description"": ""Create new payments"", ""granted"": false, ""_links"": { ""self"": { ""href"": ""https://api.mollie.com/v2/permissions/payments.write"", ""type"": ""application/hal+json"" } } }, { ""resource"": ""permission"", ""id"": ""payments.read"", ""description"": ""View your payments"", ""granted"": true, ""_links"": { ""self"": { ""href"": ""https://api.mollie.com/v2/permissions/payments.read"", ""type"": ""application/hal+json"" } } } ] }, ""count"": 2, ""_links"": { ""documentation"": { ""href"": ""https://docs.mollie.com/reference/v2/permissions-api/list-permissions"", ""type"": ""text/html"" }, ""self"": { ""href"": ""https://api.mollie.com/v2/permissions"", ""type"": ""application/hal+json"" } } }"; } ================================================ FILE: tests/Mollie.Tests.Unit/Client/ProfileClientTests.cs ================================================ using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Models; using Mollie.Api.Models.Payment; using Mollie.Api.Models.Profile; using Mollie.Api.Models.Profile.Request; using Mollie.Api.Models.Profile.Response; using RichardSzalay.MockHttp; using Xunit; namespace Mollie.Tests.Unit.Client; public class ProfileClientTests : BaseClientTests { [Fact] public async Task CreateProfileAsync_WithRequiredParameters_ResponseIsDeserializedInExpectedFormat() { // Arrange ProfileRequest profileRequest = new() { Name = "My website name", Email = "info@mywebsite.com", Mode = Mode.Test, Phone = "+31208202070", Website = "https://www.mywebsite.com", Description = "Description", CountriesOfActivity = ["NL"], BusinessCategory = "OTHER_MERCHANDISE" }; var mockHttp = CreateMockHttpMessageHandler( HttpMethod.Post, $"{BaseMollieClient.DefaultBaseApiEndPoint}profiles", defaultProfileJsonResponse, "\"mode\": \"test\""); HttpClient httpClient = mockHttp.ToHttpClient(); using var profileClient = new ProfileClient("abcde", httpClient); // Act var result = await profileClient.CreateProfileAsync(profileRequest); // Assert mockHttp.VerifyNoOutstandingRequest(); AssertDefaultProfileResponse(result); result.Description.ShouldBe(profileRequest.Description); result.CountriesOfActivity.ShouldBe(profileRequest.CountriesOfActivity); } [Fact] public async Task GetProfileAsync_WithProfileId_ResponseIsDeserializedInExpectedFormat() { // Arrange const string profileId = "profile-id"; var mockHttp = new MockHttpMessageHandler(); mockHttp.Expect(HttpMethod.Get,$"{BaseMollieClient.DefaultBaseApiEndPoint}profiles/{profileId}") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", defaultProfileJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); using var profileClient = new ProfileClient("abcde", httpClient); // Act var result = await profileClient.GetProfileAsync(profileId); // Assert mockHttp.VerifyNoOutstandingRequest(); AssertDefaultProfileResponse(result); } [Fact] public async Task GetCurrentProfileAsync_ResponseIsDeserializedInExpectedFormat() { // Arrange var mockHttp = new MockHttpMessageHandler(); mockHttp.Expect(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}profiles/me") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", defaultProfileJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); using var profileClient = new ProfileClient("abcde", httpClient); // Act var result = await profileClient.GetCurrentProfileAsync(); // Assert mockHttp.VerifyNoOutstandingRequest(); AssertDefaultProfileResponse(result); } [Fact] public async Task GetProfileListAsync_WithNoParameters_ResponseIsDeserializedInExpectedFormat() { // Arrange var mockHttp = new MockHttpMessageHandler(); mockHttp.Expect(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}profiles") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", defaultGetProfileListJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); using var profileClient = new ProfileClient("abcde", httpClient); // Act var result = await profileClient.GetProfileListAsync(); // Assert mockHttp.VerifyNoOutstandingRequest(); result.Count.ShouldBe(1); var profile = result.Items[0]; profile.Resource.ShouldBe("profiles"); profile.Id.ShouldBe("pfl_v9hTwCvYqw"); profile.Mode.ShouldBe(Mode.Live); profile.Name.ShouldBe("My website name"); profile.Email.ShouldBe("info@mywebsite.com"); profile.Website.ShouldBe("https://www.mywebsite.com"); profile.Phone.ShouldBe("+31208202070"); profile.BusinessCategory.ShouldBe("OTHER_MERCHANDISE"); profile.Status.ShouldBe(ProfileStatus.Verified); profile.Review!.Status.ShouldBe(ReviewStatus.Pending); profile.CreatedAt.ShouldBe(DateTime.Parse("2018-03-20T09:28:37+00:00")); profile.Links.ShouldNotBeNull(); profile.Links.Self.Href.ShouldBe("https://api.mollie.com/v2/profiles/pfl_v9hTwCvYqw"); profile.Links.Self.Type.ShouldBe("application/hal+json"); profile.Links.Dashboard.Href.ShouldBe("https://www.mollie.com/dashboard/org_123456789/settings/profiles/pfl_v9hTwCvYqw"); profile.Links.Dashboard.Type.ShouldBe("text/html"); profile.Links.Chargebacks!.Href.ShouldBe("https://api.mollie.com/v2/chargebacks?profileId=pfl_v9hTwCvYqw"); profile.Links.Chargebacks.Type.ShouldBe("application/hal+json"); profile.Links.Methods!.Href.ShouldBe("https://api.mollie.com/v2/methods?profileId=pfl_v9hTwCvYqw"); profile.Links.Methods.Type.ShouldBe("application/hal+json"); profile.Links.Payments!.Href.ShouldBe("https://api.mollie.com/v2/payments?profileId=pfl_v9hTwCvYqw"); profile.Links.Payments.Type.ShouldBe("application/hal+json"); profile.Links.Refunds!.Href.ShouldBe("https://api.mollie.com/v2/refunds?profileId=pfl_v9hTwCvYqw"); profile.Links.Refunds.Type.ShouldBe("application/hal+json"); profile.Links.CheckoutPreviewUrl!.Href.ShouldBe("https://www.mollie.com/payscreen/preview/pfl_v9hTwCvYqw"); profile.Links.CheckoutPreviewUrl.Type.ShouldBe("text/html"); profile.Links.Documentation.Href.ShouldBe("https://docs.mollie.com/reference/v2/profiles-api/create-profile"); profile.Links.Documentation.Type.ShouldBe("text/html"); } [Fact] public async Task UpdateProfileAsync_WithRequiredParameters_ResponseIsDeserializedInExpectedFormat() { // Arrange const string profileId = "profileId"; ProfileRequest profileRequest = new ProfileRequest() { Name = "My website name", Email = "info@mywebsite.com", Mode = Mode.Test, Phone = "+31208202070", Website = "https://www.mywebsite.com", BusinessCategory = "OTHER_MERCHANDISE" }; var mockHttp = new MockHttpMessageHandler(); mockHttp.Expect(HttpMethod.Patch, $"{BaseMollieClient.DefaultBaseApiEndPoint}profiles/{profileId}") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", defaultProfileJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); using var profileClient = new ProfileClient("abcde", httpClient); // Act var result = await profileClient.UpdateProfileAsync(profileId, profileRequest); // Assert mockHttp.VerifyNoOutstandingRequest(); AssertDefaultProfileResponse(result); } [Fact] public async Task UpdateProfileAsync_WithMissingProfileIdParameter_ThrowsArgumentException() { // Arrange const string profileId = ""; var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); using var profileClient = new ProfileClient("abcde", httpClient); ProfileRequest profileRequest = new ProfileRequest() { Name = "My website name", Email = "info@mywebsite.com", Mode = Mode.Test, Phone = "+31208202070", Website = "https://www.mywebsite.com", BusinessCategory = "OTHER_MERCHANDISE" }; // Act var exception = await Assert.ThrowsAsync(() => profileClient.UpdateProfileAsync(profileId, profileRequest)); // Assert exception.Message.ShouldBe($"Required URL argument '{nameof(profileId)}' is null or empty"); } [Fact] public async Task EnablePaymentMethodAsync_ForCurrentProfile_ResponseIsDeserializedInExpectedFormat() { // Arrange const string paymentMethod = PaymentMethod.Ideal; var mockHttp = new MockHttpMessageHandler(); mockHttp.Expect(HttpMethod.Post,$"{BaseMollieClient.DefaultBaseApiEndPoint}profiles/me/methods/{paymentMethod}") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", defaultPaymentMethodResponse); HttpClient httpClient = mockHttp.ToHttpClient(); using var profileClient = new ProfileClient("abcde", httpClient); // Act var result = await profileClient.EnablePaymentMethodAsync(paymentMethod); // Assert mockHttp.VerifyNoOutstandingRequest(); result.Resource.ShouldBe("method"); result.Id.ShouldBe(paymentMethod); } [Fact] public async Task EnablePaymentMethodAsync_ForCurrentProfileWithMissingPaymentMethodParameter_ThrowsArgumentException() { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); using var profileClient = new ProfileClient("abcde", httpClient); // Act var exception = await Assert.ThrowsAsync(() => profileClient.EnablePaymentMethodAsync(string.Empty)); // Assert exception.Message.ShouldBe($"Required URL argument 'paymentMethod' is null or empty"); } [Fact] public async Task DisablePaymentMethodAsync_ForCurrentProfile_SendsRequest() { // Arrange const string paymentMethod = PaymentMethod.Ideal; var mockHttp = new MockHttpMessageHandler(); mockHttp.Expect(HttpMethod.Delete, $"{BaseMollieClient.DefaultBaseApiEndPoint}profiles/me/methods/{paymentMethod}") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond(HttpStatusCode.NoContent); HttpClient httpClient = mockHttp.ToHttpClient(); using var profileClient = new ProfileClient("abcde", httpClient); // Act await profileClient.DisablePaymentMethodAsync(paymentMethod); // Assert mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task DisablePaymentMethodAsync_ForCurrentProfileWithMissingPaymentMethodParameter_ThrowsArgumentException() { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); using var profileClient = new ProfileClient("abcde", httpClient); // Act var exception = await Assert.ThrowsAsync(() => profileClient.DisablePaymentMethodAsync(string.Empty)); // Assert exception.Message.ShouldBe($"Required URL argument 'paymentMethod' is null or empty"); } [Fact] public async Task DeleteProfileAsync_ForGivenProfileId_SendsRequest() { // Arrange const string profileId = "profile-id"; var mockHttp = new MockHttpMessageHandler(); mockHttp.Expect(HttpMethod.Delete, $"{BaseMollieClient.DefaultBaseApiEndPoint}profiles/{profileId}") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond(HttpStatusCode.NoContent); HttpClient httpClient = mockHttp.ToHttpClient(); using var profileClient = new ProfileClient("abcde", httpClient); // Act await profileClient.DeleteProfileAsync(profileId); // Assert mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task DeleteProfileAsync_WithMissingProfileIdParameter_ThrowsArgumentException() { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); using var profileClient = new ProfileClient("abcde", httpClient); // Act var exception = await Assert.ThrowsAsync(() => profileClient.DeleteProfileAsync(string.Empty)); // Assert exception.Message.ShouldBe($"Required URL argument 'profileId' is null or empty"); } [Fact] public async Task EnableGiftCardIssuerAsync_ForCurrentProfile_ResponseIsDeserializedInExpectedFormat() { // Arrange const string issuer = "festivalcadeau"; var mockHttp = new MockHttpMessageHandler(); mockHttp.Expect(HttpMethod.Post,$"{BaseMollieClient.DefaultBaseApiEndPoint}profiles/me/methods/giftcard/issuers/{issuer}") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", defaultEnableGiftcardIssuerResponse); HttpClient httpClient = mockHttp.ToHttpClient(); using var profileClient = new ProfileClient("abcde", httpClient); // Act var result = await profileClient.EnableGiftCardIssuerAsync(issuer); // Assert mockHttp.VerifyNoOutstandingRequest(); result.Resource.ShouldBe("issuer"); result.Id.ShouldBe(issuer); result.Description.ShouldBe("FestivalCadeau Giftcard"); result.Status.ShouldBe("pending-issuer"); } [Fact] public async Task EnableGiftCardIssuerAsync_ForCurrentProfileWithMissingIssuerParameter_ThrowsArgumentException() { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); using var profileClient = new ProfileClient("abcde", httpClient); // Act var exception = await Assert.ThrowsAsync(() => profileClient.EnableGiftCardIssuerAsync(string.Empty)); // Assert exception.Message.ShouldBe($"Required URL argument 'issuer' is null or empty"); } [Fact] public async Task DisableGiftCardIssuerAsync_ForCurrentProfile_SendsRequest() { // Arrange const string issuer = "festivalcadeau"; var mockHttp = new MockHttpMessageHandler(); mockHttp.Expect(HttpMethod.Delete, $"{BaseMollieClient.DefaultBaseApiEndPoint}profiles/me/methods/giftcard/issuers/{issuer}") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond(HttpStatusCode.NoContent); HttpClient httpClient = mockHttp.ToHttpClient(); using var profileClient = new ProfileClient("abcde", httpClient); // Act await profileClient.DisableGiftCardIssuerAsync(issuer); // Assert mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task DisableGiftCardIssuerAsync_ForCurrentProfileWithMissingIssuerParameter_ThrowsArgumentException() { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); using var profileClient = new ProfileClient("abcde", httpClient); // Act var exception = await Assert.ThrowsAsync(() => profileClient.DisableGiftCardIssuerAsync(string.Empty)); // Assert exception.Message.ShouldBe($"Required URL argument 'issuer' is null or empty"); } private void AssertDefaultProfileResponse(ProfileResponse result) { result.Resource.ShouldBe("profile"); result.Id.ShouldBe("pfl_v9hTwCvYqw"); result.Mode.ShouldBe(Mode.Test); result.Name.ShouldBe("My website name"); result.Email.ShouldBe("info@mywebsite.com"); result.Website.ShouldBe("https://www.mywebsite.com"); result.Phone.ShouldBe("+31208202070"); result.BusinessCategory.ShouldBe("OTHER_MERCHANDISE"); result.Status.ShouldBe(ProfileStatus.Unverified); } private const string defaultEnableGiftcardIssuerResponse = @"{ ""resource"": ""issuer"", ""id"": ""festivalcadeau"", ""description"": ""FestivalCadeau Giftcard"", ""status"": ""pending-issuer"", ""_links"": { ""self"": { ""href"": ""https://api.mollie.com/v2/issuers/festivalcadeau"", ""type"": ""application/hal+json"" }, ""documentation"": { ""href"": ""https://docs.mollie.com/reference/v2/profiles-api/enable-giftcard-issuer"", ""type"": ""text/html"" } } }"; private const string defaultPaymentMethodResponse = @"{ ""resource"": ""method"", ""id"": ""ideal"", ""description"": ""iDEAL"", ""minimumAmount"": { ""value"": ""0.01"", ""currency"": ""EUR"" }, ""maximumAmount"": { ""value"": ""50000.00"", ""currency"": ""EUR"" }, ""image"": { ""size1x"": ""https://www.mollie.com/external/icons/payment-methods/ideal.png"", ""size2x"": ""https://www.mollie.com/external/icons/payment-methods/ideal%402x.png"", ""svg"": ""https://www.mollie.com/external/icons/payment-methods/ideal.svg"" }, ""status"": ""activated"", ""_links"": { ""self"": { ""href"": ""https://api.mollie.com/v2/methods/ideal"", ""type"": ""application/hal+json"" }, ""documentation"": { ""href"": ""https://docs.mollie.com/reference/v2/profiles-api/enable-method"", ""type"": ""text/html"" } } }"; private const string defaultProfileJsonResponse = @"{ ""resource"": ""profile"", ""id"": ""pfl_v9hTwCvYqw"", ""mode"": ""test"", ""name"": ""My website name"", ""website"": ""https://www.mywebsite.com"", ""email"": ""info@mywebsite.com"", ""phone"": ""+31208202070"", ""description"": ""Description"", ""countriesOfActivity"": [""NL""], ""businessCategory"": ""OTHER_MERCHANDISE"", ""categoryCode"": 5399, ""status"": ""unverified"", ""createdAt"": ""2018-03-20T09:28:37+00:00"", ""_links"": { ""self"": { ""href"": ""https://api.mollie.com/v2/profiles/pfl_v9hTwCvYqw"", ""type"": ""application/hal+json"" }, ""dashboard"": { ""href"": ""https://www.mollie.com/dashboard/org_123456789/settings/profiles/pfl_v9hTwCvYqw"", ""type"": ""text/html"" }, ""chargebacks"": { ""href"": ""https://api.mollie.com/v2/chargebacks?profileId=pfl_v9hTwCvYqw"", ""type"": ""application/hal+json"" }, ""methods"": { ""href"": ""https://api.mollie.com/v2/methods?profileId=pfl_v9hTwCvYqw"", ""type"": ""application/hal+json"" }, ""payments"": { ""href"": ""https://api.mollie.com/v2/payments?profileId=pfl_v9hTwCvYqw"", ""type"": ""application/hal+json"" }, ""refunds"": { ""href"": ""https://api.mollie.com/v2/refunds?profileId=pfl_v9hTwCvYqw"", ""type"": ""application/hal+json"" }, ""checkoutPreviewUrl"": { ""href"": ""https://www.mollie.com/payscreen/preview/pfl_v9hTwCvYqw"", ""type"": ""text/html"" }, ""documentation"": { ""href"": ""https://docs.mollie.com/reference/v2/profiles-api/create-profile"", ""type"": ""text/html"" } } }"; private const string defaultGetProfileListJsonResponse = @"{ ""_embedded"": { ""profiles"": [ { ""resource"": ""profiles"", ""id"": ""pfl_v9hTwCvYqw"", ""mode"": ""live"", ""name"": ""My website name"", ""website"": ""https://www.mywebsite.com"", ""email"": ""info@mywebsite.com"", ""phone"": ""+31208202070"", ""businessCategory"": ""OTHER_MERCHANDISE"", ""categoryCode"": 5399, ""status"": ""verified"", ""review"": { ""status"": ""pending"" }, ""createdAt"": ""2018-03-20T09:28:37+00:00"", ""_links"": { ""self"": { ""href"": ""https://api.mollie.com/v2/profiles/pfl_v9hTwCvYqw"", ""type"": ""application/hal+json"" }, ""dashboard"": { ""href"": ""https://www.mollie.com/dashboard/org_123456789/settings/profiles/pfl_v9hTwCvYqw"", ""type"": ""text/html"" }, ""chargebacks"": { ""href"": ""https://api.mollie.com/v2/chargebacks?profileId=pfl_v9hTwCvYqw"", ""type"": ""application/hal+json"" }, ""methods"": { ""href"": ""https://api.mollie.com/v2/methods?profileId=pfl_v9hTwCvYqw"", ""type"": ""application/hal+json"" }, ""payments"": { ""href"": ""https://api.mollie.com/v2/payments?profileId=pfl_v9hTwCvYqw"", ""type"": ""application/hal+json"" }, ""refunds"": { ""href"": ""https://api.mollie.com/v2/refunds?profileId=pfl_v9hTwCvYqw"", ""type"": ""application/hal+json"" }, ""checkoutPreviewUrl"": { ""href"": ""https://www.mollie.com/payscreen/preview/pfl_v9hTwCvYqw"", ""type"": ""text/html"" }, ""documentation"": { ""href"": ""https://docs.mollie.com/reference/v2/profiles-api/create-profile"", ""type"": ""text/html"" } } } ] }, ""count"": 1, ""_links"": { ""documentation"": { ""href"": ""https://docs.mollie.com/reference/v2/profiles-api/list-profiles"", ""type"": ""text/html"" }, ""self"": { ""href"": ""https://api.mollie.com/v2/profiles?limit=5"", ""type"": ""application/hal+json"" }, ""previous"": null, ""next"": { ""href"": ""https://api.mollie.com/v2/profiles?from=pfl_3RkSN1zuPE&limit=5"", ""type"": ""application/hal+json"" } } }"; } ================================================ FILE: tests/Mollie.Tests.Unit/Client/RefundClientTests.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using Mollie.Api.Client; using System.Net.Http; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Models; using Mollie.Api.Models.Order.Request; using Mollie.Api.Models.Payment; using Mollie.Api.Models.Refund; using Mollie.Api.Models.Refund.Request; using Mollie.Api.Models.Refund.Response; using RichardSzalay.MockHttp; using Xunit; namespace Mollie.Tests.Unit.Client { public class RefundClientTests : BaseClientTests { private const string defaultSettlementId = "stl_BkEjN2eBb"; private readonly string defaultGetRefundResponse = @$"{{ ""resource"": ""refund"", ""id"": ""re_4qqhO89gsT"", ""amount"": {{ ""currency"": ""EUR"", ""value"": ""5.95"" }}, ""settlementId"": ""{defaultSettlementId}"", ""status"": ""pending"", ""createdAt"": ""2018-03-14T17:09:02.0Z"", ""description"": ""Order #33"", ""metadata"": {{ ""bookkeeping_id"": 12345 }}, ""paymentId"": ""tr_WDqYK6vllg"", ""_links"": {{ ""self"": {{ ""href"": ""https://api.mollie.com/v2/payments/tr_WDqYK6vllg/refunds/re_4qqhO89gsT"", ""type"": ""application/hal+json"" }}, ""payment"": {{ ""href"": ""https://api.mollie.com/v2/payments/tr_WDqYK6vllg"", ""type"": ""application/hal+json"" }}, ""documentation"": {{ ""href"": ""https://docs.mollie.com/reference/v2/refunds-api/get-refund"", ""type"": ""text/html"" }} }} }}"; [Theory] [InlineData("payments/paymentId/refunds/refundId", null)] [InlineData("payments/paymentId/refunds/refundId", false)] [InlineData("payments/paymentId/refunds/refundId?testmode=true", true)] public async Task GetRefundAsync_TestModeParameterCase_QueryStringOnlyContainsTestModeParameterIfTrue(string expectedUrl, bool? testModeParameter) { // Given: We make a request to retrieve a payment without wanting any extra data bool testMode = testModeParameter ?? false; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}{expectedUrl}", defaultGetRefundResponse); HttpClient httpClient = mockHttp.ToHttpClient(); RefundClient refundClient = new RefundClient("abcde", httpClient); // When: We send the request var refundResponse = await refundClient.GetPaymentRefundAsync("paymentId", "refundId", testmode: testMode); // Then mockHttp.VerifyNoOutstandingExpectation(); refundResponse.ShouldNotBeNull(); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task CreateRefundAsync_NoPaymentIdIsGiven_ArgumentExceptionIsThrown(string? paymentId) { // Given: We create a refund without specifying a paymentId var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); RefundClient refundClient = new RefundClient("api-key", httpClient); var refund = new RefundRequest { Amount = new Amount(Currency.EUR, 100m) }; // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await refundClient.CreatePaymentRefundAsync(paymentId, refund)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'paymentId' is null or empty"); } [Theory] [InlineData(true)] [InlineData(false)] public async Task CreateRefundAsync_WithReverseRouting_ResponseIsDeserializedInExpectedFormat(bool reverseRouting) { // Given: We create a refund with a routing destination const string paymentId = "tr_7UhSN1zuXS"; var refundRequest = new RefundRequest { Amount = new Amount(Currency.EUR, 100m), ReverseRouting = reverseRouting }; string expectedStringValue = reverseRouting.ToString().ToLowerInvariant(); string expectedRoutingInformation = $"\"reverseRouting\": {expectedStringValue}"; string expectedJsonResponse = @$"{{ ""resource"": ""refund"", ""id"": ""re_4qqhO89gsT"", ""description"": """", ""amount"": {{ ""currency"": ""EUR"", ""value"": ""100.00"" }}, ""reverseRouting"": {expectedStringValue}, ""status"": ""pending"", ""metadata"": null, ""paymentId"": ""{paymentId}"", ""createdAt"": ""2023-03-14T17:09:02.0Z"" }}"; var mockHttp = CreateMockHttpMessageHandler( HttpMethod.Post, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{paymentId}/refunds", expectedJsonResponse, expectedRoutingInformation); HttpClient httpClient = mockHttp.ToHttpClient(); RefundClient refundClient = new("api-key", httpClient); // When: We create the refund RefundResponse refundResponse = await refundClient.CreatePaymentRefundAsync(paymentId, refundRequest); // Then mockHttp.VerifyNoOutstandingExpectation(); refundResponse.ReverseRouting.ShouldBe(reverseRouting); refundResponse.RoutingReversals.ShouldBeNull(); } [Fact] public async Task CreateRefundAsync_WithRoutingInformation_ResponseIsDeserializedInExpectedFormat() { // Given: We create a refund with a routing destination const string paymentId = "tr_7UhSN1zuXS"; var refundRequest = new RefundRequest { Amount = new Amount(Currency.EUR, 100m), ReverseRouting = null, RoutingReversals = new List { new RoutingReversal { Amount = new Amount(Currency.EUR, 50m), Source = new RoutingDestination { Type = "organization", OrganizationId = "organization-id" } } } }; string expectedRoutingInformation = $"\"routingReversals\":[{{\"amount\":{{\"currency\":\"EUR\",\"value\":\"50.00\"}},\"source\":{{\"type\":\"organization\",\"organizationId\":\"organization-id\"}}}}]}}"; string expectedJsonResponse = @$"{{ ""resource"": ""refund"", ""id"": ""re_4qqhO89gsT"", ""description"": """", ""amount"": {{ ""currency"": ""EUR"", ""value"": ""100.00"" }}, ""routingReversals"": [ {{ ""amount"": {{ ""currency"": ""EUR"", ""value"": ""50.00"" }}, ""source"": {{ ""type"": ""organization"", ""organizationId"": ""organization-id"" }} }} ], ""status"": ""pending"", ""metadata"": null, ""paymentId"": ""{paymentId}"", ""createdAt"": ""2023-03-14T17:09:02.0Z"" }}"; var mockHttp = CreateMockHttpMessageHandler( HttpMethod.Post, $"{BaseMollieClient.DefaultBaseApiEndPoint}payments/{paymentId}/refunds", expectedJsonResponse, expectedRoutingInformation); HttpClient httpClient = mockHttp.ToHttpClient(); RefundClient refundClient = new("api-key", httpClient); // When: We create the refund RefundResponse refundResponse = await refundClient.CreatePaymentRefundAsync(paymentId, refundRequest); // Then mockHttp.VerifyNoOutstandingExpectation(); refundResponse.RoutingReversals.ShouldBeEquivalentTo(refundRequest.RoutingReversals); refundResponse.ReverseRouting.ShouldBeNull(); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetRefundListAsync_NoPaymentIdIsGiven_ArgumentExceptionIsThrown(string? paymentId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); RefundClient refundClient = new RefundClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await refundClient.GetPaymentRefundListAsync(paymentId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'paymentId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetRefundAsync_NoPaymentIdIsGiven_ArgumentExceptionIsThrown(string? paymentId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); RefundClient refundClient = new RefundClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await refundClient.GetPaymentRefundAsync(paymentId, "refund-id")); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'paymentId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetRefundAsync_NoRefundIsGiven_ArgumentExceptionIsThrown(string? refundId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); RefundClient refundClient = new RefundClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await refundClient.GetPaymentRefundAsync("payment-id", refundId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'refundId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task CancelRefundAsync_NoPaymentIdIsGiven_ArgumentExceptionIsThrown(string? paymentId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); RefundClient refundClient = new RefundClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await refundClient.CancelPaymentRefundAsync(paymentId, "refund-id")); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'paymentId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task CancelRefundAsync_NoRefundIsGiven_ArgumentExceptionIsThrown(string? refundId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); RefundClient refundClient = new RefundClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await refundClient.CancelPaymentRefundAsync("payment-id", refundId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'refundId' is null or empty"); } [Theory] [InlineData(null, null, false, "")] [InlineData("from", null, false, "?from=from")] [InlineData("from", 50, false, "?from=from&limit=50")] [InlineData(null, null, true, "?testmode=true")] public async Task GetOrderRefundListAsync_QueryParameterOptions_CorrectParametersAreAdded(string? from, int? limit, bool testmode, string expectedQueryString) { // Given: We make a request to retrieve the list of orders const string orderId = "abcde"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}orders/{orderId}/refunds{expectedQueryString}", defaultOrderJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); RefundClient refundClient = new RefundClient("api-key", httpClient); // When: We send the request await refundClient.GetOrderRefundListAsync(orderId, from, limit, testmode); // Then mockHttp.VerifyNoOutstandingRequest(); } [Fact] public async Task CreateOrderRefundAsync_WithRequiredParameters_ResponseIsDeserializedInExpectedFormat() { // Given: We create a refund request with only the required parameters const string orderId = "ord_stTC2WHAuS"; OrderRefundRequest orderRefundRequest = new OrderRefundRequest() { Description = "description", Lines = new[] { new OrderLineDetails() { Id = "odl_dgtxyl", Quantity = 1, Amount = new Amount(Currency.EUR, "399.00") } }, Metadata = "my-metadata" }; string url = $"{BaseMollieClient.DefaultBaseApiEndPoint}orders/{orderId}/refunds"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Post, url, defaultOrderRefundJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); RefundClient refundClient = new RefundClient("api-key", httpClient); // When: We send the request var response = await refundClient.CreateOrderRefundAsync(orderId, orderRefundRequest); // Then mockHttp.VerifyNoOutstandingExpectation(); response.Resource.ShouldBe("refund"); response.Id.ShouldBe("re_4qqhO89gsT"); response.Description.ShouldBe("description"); response.Status.ShouldBe("pending"); response.CreatedAt!.Value.ToUniversalTime().ShouldBe(DateTime.SpecifyKind(new DateTime(2018, 3, 14, 17, 09, 02), DateTimeKind.Utc)); response.PaymentId.ShouldBe("tr_WDqYK6vllg"); response.OrderId.ShouldBe(orderId); response.Lines.Count().ShouldBe(1); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task CreateOrderRefundAsync_NoOrderIdIsGiven_ArgumentExceptionIsThrown(string? orderId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); RefundClient refundClient = new RefundClient("api-key", httpClient); var request = new OrderRefundRequest() { Lines = new List() }; // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await refundClient.CreateOrderRefundAsync(orderId, request)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'orderId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetOrderRefundListAsync_NoOrderIdIsGiven_ArgumentExceptionIsThrown(string? orderId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); RefundClient refundClient = new RefundClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await refundClient.GetOrderRefundListAsync(orderId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'orderId' is null or empty"); } private const string defaultOrderJsonResponse = @"{ ""resource"": ""order"", ""id"": ""ord_kEn1PlbGa"", ""profileId"": ""pfl_URR55HPMGx"", ""method"": ""ideal"", ""amount"": { ""value"": ""1027.99"", ""currency"": ""EUR"" }, }"; private const string defaultOrderRefundJsonResponse = @"{ ""resource"": ""refund"", ""id"": ""re_4qqhO89gsT"", ""amount"": { ""currency"": ""EUR"", ""value"": ""698.00"" }, ""status"": ""pending"", ""createdAt"": ""2018-03-14T17:09:02.0Z"", ""description"": ""description"", ""metadata"": { ""bookkeeping_id"": 12345 }, ""paymentId"": ""tr_WDqYK6vllg"", ""orderId"": ""ord_stTC2WHAuS"", ""lines"": [ { ""resource"": ""orderline"", ""id"": ""odl_dgtxyl"", ""orderId"": ""ord_stTC2WHAuS"", ""name"": ""LEGO 42083 Bugatti Chiron"", ""sku"": ""5702016116977"", ""type"": ""physical"", ""status"": ""paid"", ""metadata"": null, ""quantity"": 1, ""unitPrice"": { ""value"": ""399.00"", ""currency"": ""EUR"" }, ""vatRate"": ""21.00"", ""vatAmount"": { ""value"": ""51.89"", ""currency"": ""EUR"" }, ""discountAmount"": { ""value"": ""100.00"", ""currency"": ""EUR"" }, ""totalAmount"": { ""value"": ""299.00"", ""currency"": ""EUR"" }, ""createdAt"": ""2018-08-02T09:29:56+00:00"", ""_links"": { ""productUrl"": { ""href"": ""https://shop.lego.com/nl-NL/Bugatti-Chiron-42083"", ""type"": ""text/html"" }, ""imageUrl"": { ""href"": ""https://sh-s7-live-s.legocdn.com/is/image//LEGO/42083_alt1?$main$"", ""type"": ""text/html"" } } } ], ""_links"": { ""self"": { ""href"": ""https://api.mollie.com/v2/payments/tr_WDqYK6vllg/refunds/re_4qqhO89gsT"", ""type"": ""application/hal+json"" }, ""payment"": { ""href"": ""https://api.mollie.com/v2/payments/tr_WDqYK6vllg"", ""type"": ""application/hal+json"" }, ""order"": { ""href"": ""https://api.mollie.com/v2/orders/ord_stTC2WHAuS"", ""type"": ""application/hal+json"" }, ""documentation"": { ""href"": ""https://docs.mollie.com/reference/v2/refunds-api/create-order-refund"", ""type"": ""text/html"" } } }"; } } ================================================ FILE: tests/Mollie.Tests.Unit/Client/SalesInvoiceClientTests.cs ================================================ using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Mollie.Api.Client; using Mollie.Api.Models; using Mollie.Api.Models.SalesInvoice; using Mollie.Api.Models.SalesInvoice.Request; using Shouldly; using Xunit; namespace Mollie.Tests.Unit.Client; public class SalesInvoiceClientTests : BaseClientTests { [Fact] public async Task CreateSalesInvoiceAsync_ShouldReturnSalesInvoiceResponse() { // Given: We create a new sales invoice var request = new SalesInvoiceRequest { Status = SalesInvoiceStatus.Draft, PaymentTerm = PaymentTerm.Days30, RecipientIdentifier = "123532354", Recipient = new Recipient { Type = RecipientType.Consumer, Email = "given.family@mollie.com", StreetAndNumber = "Street 1", PostalCode = "1000 AA", City = "Amsterdam", Country = "NL", Locale = "nl_NL", GivenName = "Given", FamilyName = "Family" }, Lines = [ new SalesInvoiceLine { Description = "LEGO 4440 Forest Police Station", Quantity = 1, VatRate = "21.00", UnitPrice = new Amount("89.00", "EUR") } ] }; string expectedUrl = $"{BaseMollieClient.DefaultBaseApiEndPoint}sales-invoices"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Post, expectedUrl, DefaultSalesInvoiceClientResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var salesInvoiceClient = new SalesInvoiceClient("api-key", httpClient); // When: We create the new invoice var result = await salesInvoiceClient.CreateSalesInvoiceAsync(request); // Then: We should get a valid response mockHttp.VerifyNoOutstandingExpectation(); result.Id.ShouldBe("invoice_4Y0eZitmBnQ6IDoMqZQKh"); result.Status.ShouldBe(SalesInvoiceStatus.Draft); result.Currency.ShouldBe(Currency.EUR); result.Lines.ShouldNotBeEmpty(); var orderLine = result.Lines.Single(); orderLine.Description.ShouldBe("LEGO 4440 Forest Police Station"); orderLine.Quantity.ShouldBe(1); orderLine.VatRate.ShouldBe("21.00"); orderLine.UnitPrice.Value.ShouldBe("89.00"); orderLine.UnitPrice.Currency.ShouldBe(Currency.EUR); orderLine.Discount.ShouldBeNull(); result.AmountDue.Value.ShouldBe("107.69"); result.AmountDue.Currency.ShouldBe(Currency.EUR); result.DiscountedSubtotalAmount.Value.ShouldBe("89.00"); result.DiscountedSubtotalAmount.Currency.ShouldBe(Currency.EUR); } [Fact] public async Task GetSalesInvoiceAsync_ShouldReturnSalesInvoiceResponse() { // Given: A sales invoice ID const string salesInvoiceId = "invoice_4Y0eZitmBnQ6IDoMqZQKh"; string expectedUrl = $"{BaseMollieClient.DefaultBaseApiEndPoint}sales-invoices/{salesInvoiceId}"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, expectedUrl, DefaultSalesInvoiceClientResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var salesInvoiceClient = new SalesInvoiceClient("api-key", httpClient); // When: We retrieve the sales invoice var result = await salesInvoiceClient.GetSalesInvoiceAsync(salesInvoiceId); // Then: The response should match the expected data mockHttp.VerifyNoOutstandingExpectation(); result.Id.ShouldBe(salesInvoiceId); result.Status.ShouldBe(SalesInvoiceStatus.Draft); result.Currency.ShouldBe(Currency.EUR); result.Lines.ShouldNotBeEmpty(); var orderLine = result.Lines.Single(); orderLine.Description.ShouldBe("LEGO 4440 Forest Police Station"); orderLine.Quantity.ShouldBe(1); orderLine.VatRate.ShouldBe("21.00"); orderLine.UnitPrice.Value.ShouldBe("89.00"); orderLine.UnitPrice.Currency.ShouldBe(Currency.EUR); orderLine.Discount.ShouldBeNull(); result.AmountDue.Value.ShouldBe("107.69"); result.AmountDue.Currency.ShouldBe(Currency.EUR); result.DiscountedSubtotalAmount.Value.ShouldBe("89.00"); result.DiscountedSubtotalAmount.Currency.ShouldBe(Currency.EUR); } [Fact] public async Task UpdateSalesInvoiceAsync_ShouldReturnUpdatedSalesInvoiceResponse() { // Given: A sales invoice ID and update request const string salesInvoiceId = "invoice_4Y0eZitmBnQ6IDoMqZQKh"; var updateRequest = new SalesInvoiceUpdateRequest { Memo = "Updated memo" }; string expectedUrl = $"{BaseMollieClient.DefaultBaseApiEndPoint}sales-invoices/{salesInvoiceId}"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Patch, expectedUrl, DefaultSalesInvoiceClientResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var salesInvoiceClient = new SalesInvoiceClient("api-key", httpClient); // When: We update the sales invoice await salesInvoiceClient.UpdateSalesInvoiceAsync(salesInvoiceId, updateRequest); // Then: The update sales invoice endpoint should be called mockHttp.VerifyNoOutstandingExpectation(); } [Fact] public async Task DeleteSalesInvoiceAsync_ShouldNotThrowException() { // Given: A sales invoice ID const string salesInvoiceId = "invoice_4Y0eZitmBnQ6IDoMqZQKh"; string expectedUrl = $"{BaseMollieClient.DefaultBaseApiEndPoint}sales-invoices/{salesInvoiceId}"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Delete, expectedUrl, "{}"); HttpClient httpClient = mockHttp.ToHttpClient(); var salesInvoiceClient = new SalesInvoiceClient("api-key", httpClient); // When: We delete the sales invoice await salesInvoiceClient.DeleteSalesInvoiceAsync(salesInvoiceId); // Then: No exception should be thrown mockHttp.VerifyNoOutstandingExpectation(); } private const string DefaultSalesInvoiceClientResponse = @"{ ""resource"": ""sales-invoice"", ""id"": ""invoice_4Y0eZitmBnQ6IDoMqZQKh"", ""profileId"": ""pfl_QkEhN94Ba"", ""invoiceNumber"": null, ""currency"": ""EUR"", ""status"": ""draft"", ""vatScheme"": ""standard"", ""paymentTerm"": ""30 days"", ""recipientIdentifier"": ""123532354"", ""recipient"": { ""type"": ""consumer"", ""title"": null, ""givenName"": ""Given"", ""familyName"": ""Family"", ""email"": ""given.family@mollie.com"", ""phone"": null, ""streetAndNumber"": ""Street 1"", ""streetAdditional"": null, ""postalCode"": ""1000 AA"", ""city"": ""Amsterdam"", ""region"": null, ""country"": ""NL"", ""locale"": ""nl_NL"" }, ""lines"": [ { ""description"": ""LEGO 4440 Forest Police Station"", ""quantity"": 1, ""vatRate"": ""21.00"", ""unitPrice"": { ""value"": ""89.00"", ""currency"": ""EUR"" }, ""discount"": null } ], ""discount"": null, ""amountDue"": { ""value"": ""107.69"", ""currency"": ""EUR"" }, ""subtotalAmount"": { ""value"": ""89.00"", ""currency"": ""EUR"" }, ""totalAmount"": { ""value"": ""107.69"", ""currency"": ""EUR"" }, ""totalVatAmount"": { ""value"": ""18.69"", ""currency"": ""EUR"" }, ""discountedSubtotalAmount"": { ""value"": ""89.00"", ""currency"": ""EUR"" }, ""createdAt"": ""2024-10-03T10:47:38.457381+00:00"", ""issuedAt"": null, ""dueAt"": null, ""memo"": null, ""metadata"": [], ""_links"": { ""self"": { ""href"": ""..."", ""type"": ""application/hal+json"" }, ""invoicePayment"": { ""href"": ""..."", ""type"": ""application/hal+json"" }, ""pdfLink"": { ""href"": ""..."", ""type"": ""application/hal+json"" }, ""documentation"": { ""href"": ""..."", ""type"": ""text/html"" } } }"; } ================================================ FILE: tests/Mollie.Tests.Unit/Client/SettlementClientTests.cs ================================================ using System; using Mollie.Api.Client; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Models.Capture.Response; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Settlement.Response; using RichardSzalay.MockHttp; using Xunit; namespace Mollie.Tests.Unit.Client { public class SettlementClientTests : BaseClientTests { [Fact] public async Task ListSettlementCaptures_DefaultBehaviour_ResponseIsParsed() { // Given: We request a list of captures string expectedUrl = $"{BaseMollieClient.DefaultBaseApiEndPoint}settlements/{defaultSettlementId}/captures"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, expectedUrl, defaultCaptureListJsonResponse); HttpClient httpClient = mockHttp.ToHttpClient(); SettlementClient settlementClient = new SettlementClient("api-key", httpClient); // When: We make the request ListResponse listCaptureResponse = await settlementClient.GetSettlementCaptureListAsync(defaultSettlementId); // Then: Response should be parsed mockHttp.VerifyNoOutstandingExpectation(); listCaptureResponse.ShouldNotBeNull(); listCaptureResponse.Count.ShouldBe(1); listCaptureResponse.Links.Self.Href.ShouldBe("https://api.mollie.com/v2/settlements/stl_jDk30akdN/captures?limit=50"); listCaptureResponse.Links.Self.Type.ShouldBe("application/hal+json"); listCaptureResponse.Links.Documentation.Href.ShouldBe("https://docs.mollie.com/reference/v2/settlements-api/list-settlement-captures"); listCaptureResponse.Links.Documentation.Type.ShouldBe("text/html"); CaptureResponse captureResponse = listCaptureResponse.Items.First(); captureResponse.PaymentId.ShouldBe(defaultPaymentId); captureResponse.ShipmentId.ShouldBe(defaultShipmentId); captureResponse.SettlementId.ShouldBe(defaultSettlementId); captureResponse.Amount.Value.ShouldBe(defaultAmountValue); captureResponse.Amount.Currency.ShouldBe(defaultAmountCurrency); captureResponse.Links.ShouldNotBeNull(); captureResponse.Links.Self.Href.ShouldBe($"https://api.mollie.com/v2/payments/{defaultPaymentId}/captures/cpt_4qqhO89gsT"); captureResponse.Links.Self.Type.ShouldBe("application/hal+json"); captureResponse.Links.Payment.Href.ShouldBe($"https://api.mollie.com/v2/payments/{defaultPaymentId}"); captureResponse.Links.Payment.Type.ShouldBe("application/hal+json"); captureResponse.Links.Shipment.Href.ShouldBe($"https://api.mollie.com/v2/orders/ord_8wmqcHMN4U/shipments/shp_3wmsgCJN4U"); captureResponse.Links.Shipment.Type.ShouldBe("application/hal+json"); captureResponse.Links.Settlement.Href.ShouldBe($"https://api.mollie.com/v2/settlements/stl_jDk30akdN"); captureResponse.Links.Settlement.Type.ShouldBe("application/hal+json"); captureResponse.Links.Documentation.Href.ShouldBe("https://docs.mollie.com/reference/v2/captures-api/get-capture"); captureResponse.Links.Documentation.Type.ShouldBe("text/html"); } [Fact] public async Task GetOpenSettlement_DefaultBehaviour_ResponseIsParsed() { // Given: We request a list of captures string expectedUrl = $"{BaseMollieClient.DefaultBaseApiEndPoint}settlements/open"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, expectedUrl, defaultGetSettlementResponse); HttpClient httpClient = mockHttp.ToHttpClient(); SettlementClient settlementClient = new SettlementClient("api-key", httpClient); // When: We make the request SettlementResponse settlementResponse = await settlementClient.GetOpenSettlement(); // Then: Response should be parsed mockHttp.VerifyNoOutstandingExpectation(); settlementResponse.ShouldNotBeNull(); settlementResponse.Amount.Value.ShouldBe(defaultAmountValue); settlementResponse.Amount.Currency.ShouldBe(defaultAmountCurrency); settlementResponse.Periods.Count.ShouldBe(1); settlementResponse.Periods[2018][4].InvoiceId.ShouldBe(defaultInvoiceId); settlementResponse.Periods[2018][4].Revenue.Count.ShouldBe(2); settlementResponse.Periods[2018][4].Revenue[0].Description.ShouldBe("iDEAL"); settlementResponse.Periods[2018][4].Revenue[0].Method.ShouldBe("ideal"); settlementResponse.Periods[2018][4].Revenue[0].Count.ShouldBe(6); settlementResponse.Periods[2018][4].Revenue[0].AmountNet.Value.ShouldBe("86.1000"); settlementResponse.Periods[2018][4].Revenue[0].AmountNet.Currency.ShouldBe("EUR"); settlementResponse.Periods[2018][4].Revenue[0].AmountVat.ShouldBeNull(); settlementResponse.Periods[2018][4].Revenue[0].AmountGross.Value.ShouldBe("86.1000"); settlementResponse.Periods[2018][4].Revenue[0].AmountGross.Currency.ShouldBe("EUR"); settlementResponse.Periods[2018][4].Revenue[1].Description.ShouldBe("Refunds iDEAL"); settlementResponse.Periods[2018][4].Revenue[1].Method.ShouldBe("refund"); settlementResponse.Periods[2018][4].Revenue[1].Count.ShouldBe(2); settlementResponse.Periods[2018][4].Revenue[1].AmountNet.Value.ShouldBe("-43.2000"); settlementResponse.Periods[2018][4].Revenue[1].AmountNet.Currency.ShouldBe("EUR"); settlementResponse.Periods[2018][4].Revenue[1].AmountVat.ShouldBeNull(); settlementResponse.Periods[2018][4].Revenue[1].AmountGross.Value.ShouldBe("43.2000"); settlementResponse.Periods[2018][4].Revenue[1].AmountGross.Currency.ShouldBe("EUR"); settlementResponse.Periods[2018][4].Costs.Count.ShouldBe(2); settlementResponse.Periods[2018][4].Costs[0].Description.ShouldBe("iDEAL"); settlementResponse.Periods[2018][4].Costs[0].Method.ShouldBe("ideal"); settlementResponse.Periods[2018][4].Costs[0].Count.ShouldBe(6); settlementResponse.Periods[2018][4].Costs[0].Rate.Fixed.Value.ShouldBe("0.3500"); settlementResponse.Periods[2018][4].Costs[0].Rate.Fixed.Currency.ShouldBe("EUR"); settlementResponse.Periods[2018][4].Costs[0].Rate.Percentage.ShouldBeNull(); settlementResponse.Periods[2018][4].Costs[0].AmountNet.Value.ShouldBe("2.1000"); settlementResponse.Periods[2018][4].Costs[0].AmountNet.Currency.ShouldBe("EUR"); settlementResponse.Periods[2018][4].Costs[0].AmountVat.Value.ShouldBe("0.4410"); settlementResponse.Periods[2018][4].Costs[0].AmountVat.Currency.ShouldBe("EUR"); settlementResponse.Periods[2018][4].Costs[0].AmountGross.Value.ShouldBe("2.5410"); settlementResponse.Periods[2018][4].Costs[0].AmountGross.Currency.ShouldBe("EUR"); settlementResponse.Periods[2018][4].Costs[1].Description.ShouldBe("Refunds iDEAL"); settlementResponse.Periods[2018][4].Costs[1].Method.ShouldBe("refund"); settlementResponse.Periods[2018][4].Costs[1].Count.ShouldBe(2); settlementResponse.Periods[2018][4].Costs[1].Rate.Fixed.Value.ShouldBe("0.2500"); settlementResponse.Periods[2018][4].Costs[1].Rate.Fixed.Currency.ShouldBe("EUR"); settlementResponse.Periods[2018][4].Costs[1].Rate.Percentage.ShouldBeNull(); settlementResponse.Periods[2018][4].Costs[1].AmountNet.Value.ShouldBe("0.5000"); settlementResponse.Periods[2018][4].Costs[1].AmountNet.Currency.ShouldBe("EUR"); settlementResponse.Periods[2018][4].Costs[1].AmountVat.Value.ShouldBe("0.1050"); settlementResponse.Periods[2018][4].Costs[1].AmountVat.Currency.ShouldBe("EUR"); settlementResponse.Periods[2018][4].Costs[1].AmountGross.Value.ShouldBe("0.6050"); settlementResponse.Periods[2018][4].Costs[1].AmountGross.Currency.ShouldBe("EUR"); settlementResponse.Links.ShouldNotBeNull(); settlementResponse.Links.Self.Href.ShouldBe("https://api.mollie.com/v2/settlements/open"); settlementResponse.Links.Self.Type.ShouldBe("application/hal+json"); settlementResponse.Links.Documentation.Href.ShouldBe("https://docs.mollie.com/reference/v2/settlements-api/get-open-settlement"); settlementResponse.Links.Documentation.Type.ShouldBe("text/html"); } [Fact] public async Task GetOpenSettlement_ResponseWithEmptyPeriods_ResponseIsParsed() { // Given: We request a list of captures string expectedUrl = $"{BaseMollieClient.DefaultBaseApiEndPoint}settlements/open"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Get, expectedUrl, emptyPeriodsSettlementResponse); HttpClient httpClient = mockHttp.ToHttpClient(); SettlementClient settlementClient = new SettlementClient("api-key", httpClient); // When: We make the request SettlementResponse settlementResponse = await settlementClient.GetOpenSettlement(); // Then: Response should be parsed mockHttp.VerifyNoOutstandingExpectation(); settlementResponse.ShouldNotBeNull(); settlementResponse.Periods.Count.ShouldBe(0); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetSettlementAsync_NoSettlementIdIsGiven_ArgumentExceptionIsThrown(string? settlementId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); SettlementClient settlementClient = new SettlementClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await settlementClient.GetSettlementAsync(settlementId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'settlementId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetSettlementPaymentsListAsync_NoSettlementIdIsGiven_ArgumentExceptionIsThrown(string? settlementId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); SettlementClient settlementClient = new SettlementClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await settlementClient.GetSettlementPaymentListAsync(settlementId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'settlementId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetSettlementRefundsListAsync_NoSettlementIdIsGiven_ArgumentExceptionIsThrown(string? settlementId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); SettlementClient settlementClient = new SettlementClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await settlementClient.GetSettlementRefundListAsync(settlementId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'settlementId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetSettlementChargebacksListAsync_NoSettlementIdIsGiven_ArgumentExceptionIsThrown(string? settlementId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); SettlementClient settlementClient = new SettlementClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await settlementClient.GetSettlementChargebackListAsync(settlementId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'settlementId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetSettlementCapturesListAsync_NoSettlementIdIsGiven_ArgumentExceptionIsThrown(string? settlementId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var settlementClient = new SettlementClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await settlementClient.GetSettlementCaptureListAsync(settlementId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'settlementId' is null or empty"); } private const string defaultSettlementId = "tr_Agfg241g"; private const string defaultPaymentId = "tr_WDqYK6vllg"; private const string defaultShipmentId = "shp_3wmsgCJN4U"; private const string defaultAmountValue = "1027.99"; private const string defaultAmountCurrency = "EUR"; private const string defaultInvoiceId = "inv_FrvewDA3Pr"; private string defaultCaptureListJsonResponse = $@"{{ ""_embedded"": {{ ""captures"": [ {{ ""resource"": ""capture"", ""id"": ""cpt_4qqhO89gsT"", ""mode"": ""live"", ""amount"": {{ ""value"": ""{defaultAmountValue}"", ""currency"": ""{defaultAmountCurrency}"" }}, ""settlementAmount"": {{ ""value"": ""1027.99"", ""currency"": ""EUR"" }}, ""paymentId"": ""{defaultPaymentId}"", ""shipmentId"": ""{defaultShipmentId}"", ""settlementId"": ""{defaultSettlementId}"", ""createdAt"": ""2018-08-02T09:29:56+00:00"", ""_links"": {{ ""self"": {{ ""href"": ""https://api.mollie.com/v2/payments/tr_WDqYK6vllg/captures/cpt_4qqhO89gsT"", ""type"": ""application/hal+json"" }}, ""payment"": {{ ""href"": ""https://api.mollie.com/v2/payments/tr_WDqYK6vllg"", ""type"": ""application/hal+json"" }}, ""shipment"": {{ ""href"": ""https://api.mollie.com/v2/orders/ord_8wmqcHMN4U/shipments/shp_3wmsgCJN4U"", ""type"": ""application/hal+json"" }}, ""settlement"": {{ ""href"": ""https://api.mollie.com/v2/settlements/stl_jDk30akdN"", ""type"": ""application/hal+json"" }}, ""documentation"": {{ ""href"": ""https://docs.mollie.com/reference/v2/captures-api/get-capture"", ""type"": ""text/html"" }} }} }} ] }}, ""count"": 1, ""_links"": {{ ""documentation"": {{ ""href"": ""https://docs.mollie.com/reference/v2/settlements-api/list-settlement-captures"", ""type"": ""text/html"" }}, ""self"": {{ ""href"": ""https://api.mollie.com/v2/settlements/stl_jDk30akdN/captures?limit=50"", ""type"": ""application/hal+json"" }}, ""previous"": null, ""next"": null }} }}"; private string defaultGetSettlementResponse = $@"{{ ""resource"": ""settlement"", ""id"": ""open"", ""status"": ""open"", ""amount"": {{ ""currency"": ""{defaultAmountCurrency}"", ""value"": ""{defaultAmountValue}"" }}, ""periods"": {{ ""2018"": {{ ""04"": {{ ""revenue"": [ {{ ""description"": ""iDEAL"", ""method"": ""ideal"", ""count"": 6, ""amountNet"": {{ ""value"": ""86.1000"", ""currency"": ""EUR"" }}, ""amountVat"": null, ""amountGross"": {{ ""value"": ""86.1000"", ""currency"": ""EUR"" }} }}, {{ ""description"": ""Refunds iDEAL"", ""method"": ""refund"", ""count"": 2, ""amountNet"": {{ ""value"": ""-43.2000"", ""currency"": ""EUR"" }}, ""amountVat"": null, ""amountGross"": {{ ""value"": ""43.2000"", ""currency"": ""EUR"" }} }} ], ""costs"": [ {{ ""description"": ""iDEAL"", ""method"": ""ideal"", ""count"": 6, ""rate"": {{ ""fixed"": {{ ""value"": ""0.3500"", ""currency"": ""EUR"" }}, ""percentage"": null }}, ""amountNet"": {{ ""value"": ""2.1000"", ""currency"": ""EUR"" }}, ""amountVat"": {{ ""value"": ""0.4410"", ""currency"": ""EUR"" }}, ""amountGross"": {{ ""value"": ""2.5410"", ""currency"": ""EUR"" }} }}, {{ ""description"": ""Refunds iDEAL"", ""method"": ""refund"", ""count"": 2, ""rate"": {{ ""fixed"": {{ ""value"": ""0.2500"", ""currency"": ""EUR"" }}, ""percentage"": null }}, ""amountNet"": {{ ""value"": ""0.5000"", ""currency"": ""EUR"" }}, ""amountVat"": {{ ""value"": ""0.1050"", ""currency"": ""EUR"" }}, ""amountGross"": {{ ""value"": ""0.6050"", ""currency"": ""EUR"" }} }} ], ""invoiceId"": ""{defaultInvoiceId}"" }} }} }}, ""_links"": {{ ""self"": {{ ""href"": ""https://api.mollie.com/v2/settlements/open"", ""type"": ""application/hal+json"" }}, ""documentation"": {{ ""href"": ""https://docs.mollie.com/reference/v2/settlements-api/get-open-settlement"", ""type"": ""text/html"" }} }} }}"; private readonly string emptyPeriodsSettlementResponse = @$"{{ ""resource"":""settlement"", ""id"":""open"", ""createdAt"":""2020-11-11T07:10:53+00:00"", ""status"":""open"", ""amount"":{{ ""value"":""0.00"", ""currency"":""EUR"" }}, ""periods"":[ ], ""_links"":{{ ""self"":{{ ""href"":""https://api.mollie.com/v2/settlements/open"", ""type"":""application/hal+json"" }}, ""documentation"":{{ ""href"":""https://docs.mollie.com/reference/v2/settlements-api/get-open-settlement"", ""type"":""text/html"" }} }} }}"; } } ================================================ FILE: tests/Mollie.Tests.Unit/Client/ShipmentClientTests.cs ================================================ using System; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Models; using Mollie.Api.Models.Shipment; using Mollie.Api.Models.Shipment.Request; using Mollie.Api.Models.Shipment.Response; using RichardSzalay.MockHttp; using Xunit; namespace Mollie.Tests.Unit.Client { public class ShipmentClientTests : BaseClientTests { [Fact] public async Task CreateShipmentAsync_ValidShipment_ResponseIsDeserializedInExpectedFormat() { // Given: We create a shipment var shipmentRequest = new ShipmentRequest { Tracking = new TrackingObject { Carrier = "tracking-carrier", Code = "tracking-code", Url = "tracking-url" }, Testmode = true, Lines = new List { new ShipmentLineRequest { Id = "shipment-line-id", Amount = new Amount(Currency.EUR, 50), Quantity = 1 } } }; const string orderId = "order-id"; const string expectedPartialRequest = @"{""tracking"":{""carrier"":""tracking-carrier"",""code"":""tracking-code"",""url"":""tracking-url""},""lines"":[{""id"":""shipment-line-id"",""quantity"":1,""amount"":{""currency"":""EUR"",""value"":""50.00""}}],""testmode"":true}"; var mockHttp = CreateMockHttpMessageHandler( HttpMethod.Post, $"{BaseMollieClient.DefaultBaseApiEndPoint}orders/{orderId}/shipments", DefaultShipmentJsonToReturn, expectedPartialRequest); HttpClient httpClient = mockHttp.ToHttpClient(); ShipmentClient shipmentClient = new ShipmentClient("abcde", httpClient); // When: We send the request ShipmentResponse shipmentResponse = await shipmentClient.CreateShipmentAsync(orderId, shipmentRequest); // Then mockHttp.VerifyNoOutstandingExpectation(); shipmentResponse.ShouldNotBeNull(); shipmentResponse.OrderId.ShouldBe(orderId); shipmentResponse.Tracking!.Carrier.ShouldBe(shipmentRequest.Tracking.Carrier); shipmentResponse.Tracking.Code.ShouldBe(shipmentRequest.Tracking.Code); shipmentResponse.Tracking.Url.ShouldBe(shipmentRequest.Tracking.Url); } [Theory] [InlineData("orders/order-id/shipments/shipment-id", false)] [InlineData("orders/order-id/shipments/shipment-id?testmode=true", true)] public async Task GetShipmentAsync_TestModeParameterCase_QueryStringOnlyContainsTestModeParameterIfTrue(string expectedUrl, bool testModeParameter) { // Given: We retrieve a shipment const string orderId = "order-id"; const string shipmentId = "shipment-id"; var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}{expectedUrl}") .Respond("application/json", DefaultShipmentJsonToReturn); HttpClient httpClient = mockHttp.ToHttpClient(); ShipmentClient shipmentClient = new ShipmentClient("abcde", httpClient); // When: We send the request ShipmentResponse shipmentResponse = await shipmentClient.GetShipmentAsync(orderId, shipmentId, testModeParameter); // Then mockHttp.VerifyNoOutstandingExpectation(); shipmentResponse.ShouldNotBeNull(); } [Theory] [InlineData("orders/order-id/shipments", false)] [InlineData("orders/order-id/shipments?testmode=true", true)] public async Task GetShipmentsListAsync_TestModeParameterCase_QueryStringOnlyContainsTestModeParameterIfTrue(string expectedUrl, bool testModeParameter) { // Given: We retrieve the list of shipments const string orderId = "order-id"; var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}{expectedUrl}") .Respond("application/json", DefaultShipmentJsonToReturn); HttpClient httpClient = mockHttp.ToHttpClient(); ShipmentClient shipmentClient = new ShipmentClient("abcde", httpClient); // When: We send the request var shipmentListResponse = await shipmentClient.GetShipmentListAsync(orderId, testModeParameter); // Then mockHttp.VerifyNoOutstandingExpectation(); shipmentListResponse.ShouldNotBeNull(); } [Fact] public async Task UpdateShipmentAsync_ValidUpdateShipmentRequest_ResponseIsDeserializedInExpectedFormat() { // Given: We create a shipment var updateShipmentRequest = new ShipmentUpdateRequest { Tracking = new TrackingObject { Carrier = "tracking-carrier", Code = "tracking-code", Url = "tracking-url" }, Testmode = true }; const string orderId = "order-id"; const string shipmentId = "shipment-id"; const string expectedPartialRequest = @"{""tracking"":{""carrier"":""tracking-carrier"",""code"":""tracking-code"",""url"":""tracking-url""},""testmode"":true}"; var mockHttp = CreateMockHttpMessageHandler( HttpMethod.Patch, $"{BaseMollieClient.DefaultBaseApiEndPoint}orders/{orderId}/shipments/{shipmentId}", DefaultShipmentJsonToReturn, expectedPartialRequest); HttpClient httpClient = mockHttp.ToHttpClient(); ShipmentClient shipmentClient = new ShipmentClient("abcde", httpClient); // When: We send the request ShipmentResponse shipmentResponse = await shipmentClient.UpdateShipmentAsync(orderId, shipmentId, updateShipmentRequest); // Then mockHttp.VerifyNoOutstandingExpectation(); shipmentResponse.ShouldNotBeNull(); shipmentResponse.OrderId.ShouldBe(orderId); shipmentResponse.Tracking!.Carrier.ShouldBe(updateShipmentRequest.Tracking.Carrier); shipmentResponse.Tracking.Code.ShouldBe(updateShipmentRequest.Tracking.Code); shipmentResponse.Tracking.Url.ShouldBe(updateShipmentRequest.Tracking.Url); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task CreateShipmentAsync_NoOrderIdIsGiven_ArgumentExceptionIsThrown(string? orderId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); ShipmentClient shipmentClient = new ShipmentClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await shipmentClient.CreateShipmentAsync(orderId, new ShipmentRequest())); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'orderId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetShipmentAsync_NoOrderIdIsGiven_ArgumentExceptionIsThrown(string? orderId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); ShipmentClient shipmentClient = new ShipmentClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await shipmentClient.GetShipmentAsync(orderId, "shipment-id")); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'orderId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetShipmentAsync_NoShipmentIdIsGiven_ArgumentExceptionIsThrown(string? shipmentId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); ShipmentClient shipmentClient = new ShipmentClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await shipmentClient.GetShipmentAsync("order-id", shipmentId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'shipmentId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetShipmentsListAsync_NoOrderIdIsGiven_ArgumentExceptionIsThrown(string? orderId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); ShipmentClient shipmentClient = new ShipmentClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await shipmentClient.GetShipmentListAsync(orderId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'orderId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task UpdateShipmentAsync_NoOrderIdIsGiven_ArgumentExceptionIsThrown(string? orderId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); ShipmentClient shipmentClient = new ShipmentClient("api-key", httpClient); var updateRequest = new ShipmentUpdateRequest { Tracking = new TrackingObject { Carrier = "carrier", Code = "code" } }; // When: We send the request var exception = await Assert.ThrowsAsync(async () => #pragma warning disable CS8604 // Possible null reference argument. await shipmentClient.UpdateShipmentAsync(orderId, "shipment-id", updateRequest)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'orderId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task UpdateShipmentAsync_NoShipmentIdIsGiven_ArgumentExceptionIsThrown(string? shipmentId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); ShipmentClient shipmentClient = new ShipmentClient("api-key", httpClient); var updateRequest = new ShipmentUpdateRequest { Tracking = new TrackingObject { Carrier = "carrier", Code = "code" } }; // When: We send the request var exception = await Assert.ThrowsAsync(async () => #pragma warning disable CS8604 // Possible null reference argument. await shipmentClient.UpdateShipmentAsync("order-id", shipmentId, updateRequest)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'shipmentId' is null or empty"); } private const string DefaultShipmentJsonToReturn = @"{ ""resource"": ""shipment"", ""id"": ""shipment-id"", ""orderId"": ""order-id"", ""createdAt"": ""2018-08-09T14:33:54+00:00"", ""tracking"": { ""carrier"": ""tracking-carrier"", ""code"": ""tracking-code"", ""url"": ""tracking-url"" }, ""lines"": [ { ""resource"": ""orderline"", ""id"": ""odl_dgtxyl"", ""orderId"": ""ord_pbjz8x"", ""name"": ""LEGO 42083 Bugatti Chiron"", ""sku"": ""5702016116977"", ""type"": ""physical"", ""status"": ""shipping"", ""metadata"": null, ""isCancelable"": true, ""quantity"": 1, ""amountShipped"": { ""value"": ""329.99"", ""currency"": ""EUR"" }, ""unitPrice"": { ""value"": ""399.00"", ""currency"": ""EUR"" }, ""vatRate"": ""21.00"", ""vatAmount"": { ""value"": ""51.89"", ""currency"": ""EUR"" }, ""discountAmount"": { ""value"": ""100.00"", ""currency"": ""EUR"" }, ""totalAmount"": { ""value"": ""299.00"", ""currency"": ""EUR"" }, ""createdAt"": ""2018-08-02T09:29:56+00:00"", ""_links"": { ""productUrl"": { ""href"": ""https://shop.lego.com/nl-NL/Bugatti-Chiron-42083"", ""type"": ""text/html"" }, ""imageUrl"": { ""href"": ""https://sh-s7-live-s.legocdn.com/is/image//LEGO/42083_alt1?$main$"", ""type"": ""text/html"" } } }, { ""resource"": ""orderline"", ""id"": ""odl_jp31jz"", ""orderId"": ""ord_pbjz8x"", ""name"": ""LEGO 42056 Porsche 911 GT3 RS"", ""sku"": ""5702015594028"", ""type"": ""physical"", ""status"": ""completed"", ""metadata"": null, ""isCancelable"": false, ""quantity"": 1, ""unitPrice"": { ""value"": ""329.99"", ""currency"": ""EUR"" }, ""vatRate"": ""21.00"", ""vatAmount"": { ""value"": ""57.27"", ""currency"": ""EUR"" }, ""totalAmount"": { ""value"": ""329.99"", ""currency"": ""EUR"" }, ""createdAt"": ""2018-08-02T09:29:56+00:00"", ""_links"": { ""productUrl"": { ""href"": ""https://shop.lego.com/nl-NL/Porsche-911-GT3-RS-42056"", ""type"": ""text/html"" }, ""imageUrl"": { ""href"": ""https://sh-s7-live-s.legocdn.com/is/image/LEGO/42056?$PDPDefault$"", ""type"": ""text/html"" } } } ], ""_links"": { ""self"": { ""href"": ""https://api.mollie.com/v2/order/ord_kEn1PlbGa/shipments/shp_3wmsgCJN4U"", ""type"": ""application/hal+json"" }, ""order"": { ""href"": ""https://api.mollie.com/v2/orders/ord_kEn1PlbGa"", ""type"": ""application/hal+json"" }, ""documentation"": { ""href"": ""https://docs.mollie.com/reference/v2/shipments-api/get-shipment"", ""type"": ""text/html"" } } }"; } } ================================================ FILE: tests/Mollie.Tests.Unit/Client/SubscriptionClientTests.cs ================================================ using System; using System.Net.Http; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Models; using Mollie.Api.Models.Subscription.Request; using RichardSzalay.MockHttp; using Xunit; namespace Mollie.Tests.Unit.Client { public class SubscriptionClientTests : BaseClientTests { [Theory] [InlineData(null, null, null,false, "")] [InlineData("from", null, null, false, "?from=from")] [InlineData("from", 50, null, false, "?from=from&limit=50")] [InlineData(null, null,null, true, "?testmode=true")] [InlineData(null, null,"profile-id", true, "?profileId=profile-id")] public async Task GetSubscriptionListAsync_TestModeParameterCase_QueryStringOnlyContainsTestModeParameterIfTrue(string? from, int? limit, string? profileId, bool testmode, string expectedQueryString) { // Given: We retrieve a list of subscriptions const string customerId = "customer-id"; var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}customers/customer-id/subscriptions{expectedQueryString}") .Respond("application/json", DefaultSubscriptionJsonToReturn); HttpClient httpClient = mockHttp.ToHttpClient(); var subscriptionClient = new SubscriptionClient("abcde", httpClient); // When: We send the request var result = await subscriptionClient.GetSubscriptionListAsync(customerId, from, limit, profileId, testmode); // Then mockHttp.VerifyNoOutstandingExpectation(); result.ShouldNotBeNull(); } [Theory] [InlineData(null, null, null,false, "")] [InlineData("from", null, null, false, "?from=from")] [InlineData("from", 50, null, false, "?from=from&limit=50")] [InlineData(null, null,null, true, "?testmode=true")] [InlineData(null, null,"profile-id", true, "?profileId=profile-id")] public async Task GetAllSubscriptionList_TestModeParameterCase_QueryStringOnlyContainsTestModeParameterIfTrue(string? from, int? limit, string? profileId, bool testmode, string expectedQueryString) { // Given: We retrieve a list of subscriptions var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}subscriptions{expectedQueryString}") .Respond("application/json", DefaultSubscriptionJsonToReturn); HttpClient httpClient = mockHttp.ToHttpClient(); var subscriptionClient = new SubscriptionClient("abcde", httpClient); // When: We send the request var result = await subscriptionClient.GetAllSubscriptionList(from, limit, profileId, testmode); // Then mockHttp.VerifyNoOutstandingExpectation(); result.ShouldNotBeNull(); } [Theory] [InlineData("customers/customer-id/subscriptions/subscription-id", false)] [InlineData("customers/customer-id/subscriptions/subscription-id?testmode=true", true)] public async Task GetSubscriptionAsync_TestModeParameterCase_QueryStringOnlyContainsTestModeParameterIfTrue(string expectedUrl, bool testModeParameter) { // Given: We retrieve a subscriptions const string customerId = "customer-id"; const string subscriptionId = "subscription-id"; var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}{expectedUrl}") .Respond("application/json", DefaultSubscriptionJsonToReturn); HttpClient httpClient = mockHttp.ToHttpClient(); var subscriptionClient = new SubscriptionClient("abcde", httpClient); // When: We send the request var result = await subscriptionClient.GetSubscriptionAsync(customerId, subscriptionId, testModeParameter); // Then mockHttp.VerifyNoOutstandingExpectation(); result.ShouldNotBeNull(); } [Fact] public async Task RevokeMandate_TestmodeIsTrue_RequestContainsTestmodeModel() { // Given: We make a request to retrieve a payment with embedded refunds const string customerId = "customer-id"; const string subscriptionId = "subscription-id"; string expectedContent = "\"testmode\":true"; var mockHttp = CreateMockHttpMessageHandler( HttpMethod.Delete, $"{BaseMollieClient.DefaultBaseApiEndPoint}customers/{customerId}/subscriptions/{subscriptionId}", DefaultSubscriptionJsonToReturn, expectedContent); HttpClient httpClient = mockHttp.ToHttpClient(); var subscriptionClient = new SubscriptionClient("abcde", httpClient); // When: We send the request await subscriptionClient.CancelSubscriptionAsync(customerId, subscriptionId, true); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Theory] [InlineData(null, null, false, "")] [InlineData("from", null, false, "?from=from")] [InlineData("from", 50, false, "?from=from&limit=50")] [InlineData(null, null,true, "?testmode=true")] public async Task GetSubscriptionPaymentListAsync_TestModeParameterCase_QueryStringOnlyContainsTestModeParameterIfTrue(string? from, int? limit, bool testmode, string expectedQueryString) { // Given: We retrieve a list of subscriptions const string customerId = "customer-id"; const string subscriptionId = "subscription-id"; var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}customers/{customerId}/subscriptions/{subscriptionId}/payments{expectedQueryString}") .Respond("application/json", DefaultSubscriptionJsonToReturn); HttpClient httpClient = mockHttp.ToHttpClient(); var subscriptionClient = new SubscriptionClient("abcde", httpClient); // When: We send the request var result = await subscriptionClient.GetSubscriptionPaymentListAsync(customerId, subscriptionId, from, limit, testmode); // Then mockHttp.VerifyNoOutstandingExpectation(); result.ShouldNotBeNull(); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetSubscriptionListAsync_NoCustomerIdIsGiven_ArgumentExceptionIsThrown(string? customerId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var subscriptionClient = new SubscriptionClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await subscriptionClient.GetSubscriptionListAsync(customerId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'customerId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetSubscriptionAsync_NoCustomerIdIsGiven_ArgumentExceptionIsThrown(string? customerId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var subscriptionClient = new SubscriptionClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await subscriptionClient.GetSubscriptionAsync(customerId, "subscription-id")); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'customerId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetSubscriptionAsync_NoSubscriptionIdIsGiven_ArgumentExceptionIsThrown(string? subscriptionId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var subscriptionClient = new SubscriptionClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await subscriptionClient.GetSubscriptionAsync("customer-Id", subscriptionId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'subscriptionId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task CreateSubscriptionAsync_NoCustomerIdIsGiven_ArgumentExceptionIsThrown(string? customerId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var subscriptionClient = new SubscriptionClient("api-key", httpClient); var subscriptionRequest = new SubscriptionRequest { Amount = new Amount(Currency.EUR, "100.00"), Times = 5, Interval = "1 month", Description = $"Subscription {Guid.NewGuid()}", // Subscriptions must have a unique name WebhookUrl = "http://www.google.nl", StartDate = DateTime.Now.AddDays(1), }; // When: We send the request var exception = await Assert.ThrowsAsync(async () => #pragma warning disable CS8604 // Possible null reference argument. await subscriptionClient.CreateSubscriptionAsync(customerId, subscriptionRequest)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'customerId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task CancelSubscriptionAsync_NoCustomerIdIsGiven_ArgumentExceptionIsThrown(string? customerId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var subscriptionClient = new SubscriptionClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await subscriptionClient.CancelSubscriptionAsync(customerId, "subscription-id")); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'customerId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task CancelSubscriptionAsync_NoSubscriptionIdIsGiven_ArgumentExceptionIsThrown(string? subscriptionId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var subscriptionClient = new SubscriptionClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await subscriptionClient.CancelSubscriptionAsync("customer-Id", subscriptionId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'subscriptionId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task UpdateSubscriptionAsync_NoCustomerIdIsGiven_ArgumentExceptionIsThrown(string? customerId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var subscriptionClient = new SubscriptionClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await subscriptionClient.UpdateSubscriptionAsync(customerId, "subscription-id", new SubscriptionUpdateRequest())); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'customerId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task UpdateSubscriptionAsync_NoSubscriptionIdIsGiven_ArgumentExceptionIsThrown(string? subscriptionId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var subscriptionClient = new SubscriptionClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await subscriptionClient.UpdateSubscriptionAsync("customer-Id", subscriptionId, new SubscriptionUpdateRequest())); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'subscriptionId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetSubscriptionPaymentListAsync_NoCustomerIdIsGiven_ArgumentExceptionIsThrown(string? customerId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var subscriptionClient = new SubscriptionClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await subscriptionClient.GetSubscriptionPaymentListAsync(customerId, "subscription-id")); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'customerId' is null or empty"); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetSubscriptionPaymentListAsync_NoSubscriptionIdIsGiven_ArgumentExceptionIsThrown(string? subscriptionId) { // Arrange var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var subscriptionClient = new SubscriptionClient("api-key", httpClient); // When: We send the request #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await subscriptionClient.GetSubscriptionPaymentListAsync("customer-Id", subscriptionId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'subscriptionId' is null or empty"); } private const string DefaultSubscriptionJsonToReturn = @"{ ""resource"": ""subscription"", ""id"": ""subscription-id"", ""mode"": ""live"", ""createdAt"": ""2016-06-01T12:23:34+00:00"", ""status"": ""active"", ""amount"": { ""value"": ""25.00"", ""currency"": ""EUR"" }, ""times"": 4, ""timesRemaining"": 4, ""interval"": ""3 months"", ""startDate"": ""2016-06-01"", ""nextPaymentDate"": ""2016-09-01"", ""description"": ""Quarterly payment"", ""method"": null, ""mandateId"": ""mdt_38HS4fsS"", ""webhookUrl"": ""https://webshop.example.org/payments/webhook"", ""metadata"": { ""plan"": ""small"" } }"; } } ================================================ FILE: tests/Mollie.Tests.Unit/Client/TerminalClientTests.cs ================================================ using System; using System.Net.Http; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Terminal.Response; using RichardSzalay.MockHttp; using Xunit; namespace Mollie.Tests.Unit.Client; public class TerminalClientTests : BaseClientTests { [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetTerminalAsync_NoTerminalIdIsGiven_ArgumentExceptionIsThrown(string? terminalId) { // Given var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var terminalClient = new TerminalClient("api-key", httpClient); // When #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await terminalClient.GetTerminalAsync(terminalId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'terminalId' is null or empty"); } [Fact] public async Task GetTerminalAsync_WithTerminalId_ResponseIsDeserializedInExpectedFormat() { // Given const string terminalId = "terminal-id"; const string description = "terminal-description"; const string serialNumber = "serial-number"; const string brand = "brand"; const string model = "model"; string jsonToReturnInMockResponse = CreateTerminalJsonResponse(terminalId, description, serialNumber, brand, model); var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}terminals/{terminalId}") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", jsonToReturnInMockResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var terminalClient = new TerminalClient("abcde", httpClient); // When TerminalResponse response = await terminalClient.GetTerminalAsync(terminalId); // Then mockHttp.VerifyNoOutstandingExpectation(); response.Id.ShouldBe(terminalId); response.Description.ShouldBe(description); response.SerialNumber.ShouldBe(serialNumber); response.Brand.ShouldBe(brand); response.Model.ShouldBe(model); response.Links.ShouldNotBeNull(); response.Links.Self.ShouldNotBeNull(); response.Links.Self.Href.ShouldBe($"https://api.mollie.com/v2/terminals/{terminalId}"); response.Links.Documentation.ShouldNotBeNull(); } [Theory] [InlineData(true, "?testmode=true")] [InlineData(false, "")] public async Task GetTerminalAsync_QueryParameterOptions_CorrectParametersAreAdded(bool testMode, string expectedQueryString) { // Given const string terminalId = "terminal-id"; const string description = "terminal-description"; const string serialNumber = "serial-number"; const string brand = "brand"; const string model = "model"; string jsonToReturnInMockResponse = CreateTerminalJsonResponse(terminalId, description, serialNumber, brand, model); var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}terminals/{terminalId}{expectedQueryString}") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", jsonToReturnInMockResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var terminalClient = new TerminalClient("abcde", httpClient); // When await terminalClient.GetTerminalAsync(terminalId, testMode); // Then mockHttp.VerifyNoOutstandingRequest(); } [Theory] [InlineData(null, null, null, false, "")] [InlineData("from", null, null, false, "?from=from")] [InlineData("from", 50, null, false, "?from=from&limit=50")] [InlineData(null, null, "profile-id", false, "?profileId=profile-id")] [InlineData(null, null, "profile-id", true, "?profileId=profile-id&testmode=true")] public async Task GetTerminalListAsync_QueryParameterOptions_CorrectParametersAreAdded(string? from, int? limit, string? profileId, bool testmode, string expectedQueryString) { // Given string jsonToReturnInMockResponse = CreateTerminalListJsonResponse(); var mockHttp = CreateMockHttpMessageHandler( HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}terminals{expectedQueryString}", jsonToReturnInMockResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var terminalClient = new TerminalClient("abcde", httpClient); // When await terminalClient.GetTerminalListAsync(from, limit, profileId, testmode); // Then mockHttp.VerifyNoOutstandingRequest(); } [Fact] public async Task GetTerminalListAsync_ResponseIsDeserializedInExpectedFormat() { // Given string jsonToReturnInMockResponse = CreateTerminalListJsonResponse(); var mockHttp = CreateMockHttpMessageHandler( HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}terminals", jsonToReturnInMockResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var terminalClient = new TerminalClient("abcde", httpClient); // When ListResponse response = await terminalClient.GetTerminalListAsync(); // Then response.Count.ShouldBe(1); response.Items.Count.ShouldBe(response.Count); response.Links.ShouldNotBeNull(); response.Links.Self.Href.ShouldNotBeNull(); } private string CreateTerminalListJsonResponse() { string terminalJson = CreateTerminalJsonResponse("terminal-id", "description", "serial", "brand", "model"); return @$"{{ ""count"": 1, ""_embedded"": {{ ""terminals"": [ {terminalJson} ] }}, ""_links"": {{ ""self"": {{ ""href"": ""https://api.mollie.com/v2/terminalss?limit=5"", ""type"": ""application/hal+json"" }}, ""previous"": null, ""next"": {{ ""href"": ""https://api.mollie.com/v2/terminals?from=term_7MgL4wea46qkRcoTZjWEH&limit=5"", ""type"": ""application/hal+json"" }}, ""documentation"": {{ ""href"": ""https://docs.mollie.com/reference/v2/terminals-api/list-terminals"", ""type"": ""text/html"" }} }} }}"; } private string CreateTerminalJsonResponse(string terminalId, string description, string serialNumber, string brand, string model) { return $@"{{ ""id"": ""{terminalId}"", ""profileId"": ""pfl_QkEhN94Ba"", ""status"": ""active"", ""brand"": ""{brand}"", ""model"": ""{model}"", ""serialNumber"": ""{serialNumber}"", ""currency"": ""EUR"", ""description"": ""{description}"", ""createdAt"": ""2022-02-12T11:58:35.0Z"", ""updatedAt"": ""2022-11-15T13:32:11+00:00"", ""deactivatedAt"": ""2022-02-12T12:13:35.0Z"", ""_links"": {{ ""self"": {{ ""href"": ""https://api.mollie.com/v2/terminals/{terminalId}"", ""type"": ""application/hal+json"" }}, ""documentation"": {{ ""href"": ""https://docs.mollie.com/reference/v2/terminals-api/get-terminal"", ""type"": ""text/html"" }} }} }}"; } } ================================================ FILE: tests/Mollie.Tests.Unit/Client/WalletClientTest.cs ================================================ using System; using System.Net.Http; using System.Threading.Tasks; using Shouldly; using Mollie.Api.Client; using Mollie.Api.Models.Wallet.Request; using Xunit; namespace Mollie.Tests.Unit.Client; public class WalletClientTest : BaseClientTests { private const string defaultApplePayPaymentSessionResponse = @"{ ""epochTimestamp"": 1555507053169, ""expiresAt"": 1555510653169, ""merchantSessionIdentifier"": ""SSH2EAF8AFAEAA94DEEA898162A5DAFD36E_916523AAED1343F5BC5815E12BEE9250AFFDC1A17C46B0DE5A943F0F94927C24"", ""nonce"": ""0206b8db"", ""merchantIdentifier"": ""BD62FEB196874511C22DB28A9E14A89E3534C93194F73EA417EC566368D391EB"", ""domainName"": ""pay.example.org"", ""displayName"": ""Chuck Norris's Store"", ""signature"": ""308006092a864886f7...8cc030ad3000000000000"" }"; [Fact] public async Task RequestApplePayPaymentSessionAsync_ResponseIsDeserializedInExpectedFormat() { // Arrange var request = new ApplePayPaymentSessionRequest() { Domain = "pay.mywebshop.com", ValidationUrl = "https://apple-pay-gateway-cert.apple.com/paymentservices/paymentSession" }; var mockHttp = CreateMockHttpMessageHandler( HttpMethod.Post, $"{BaseMollieClient.DefaultBaseApiEndPoint}wallets/applepay/sessions", defaultApplePayPaymentSessionResponse); using var walletClient = new WalletClient("abcde", mockHttp.ToHttpClient()); // Act var response = await walletClient.RequestApplePayPaymentSessionAsync(request); // Assert response.EpochTimestamp.ShouldBe(DateTimeOffset.FromUnixTimeMilliseconds(1555507053169).UtcDateTime); response.ExpiresAt.ShouldBe(DateTimeOffset.FromUnixTimeMilliseconds(1555510653169).UtcDateTime); response.MerchantSessionIdentifier.ShouldBe("SSH2EAF8AFAEAA94DEEA898162A5DAFD36E_916523AAED1343F5BC5815E12BEE9250AFFDC1A17C46B0DE5A943F0F94927C24"); response.Nonce.ShouldBe("0206b8db"); response.MerchantIdentifier.ShouldBe("BD62FEB196874511C22DB28A9E14A89E3534C93194F73EA417EC566368D391EB"); response.DomainName.ShouldBe("pay.example.org"); response.DisplayName.ShouldBe("Chuck Norris's Store"); response.Signature.ShouldBe("308006092a864886f7...8cc030ad3000000000000"); } } ================================================ FILE: tests/Mollie.Tests.Unit/Client/WebhookClientTests.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Mollie.Api.Client; using Mollie.Api.Models; using Mollie.Api.Models.List.Response; using Mollie.Api.Models.Webhook; using Mollie.Api.Models.Webhook.Request; using Mollie.Api.Models.Webhook.Response; using RichardSzalay.MockHttp; using Shouldly; using Xunit; namespace Mollie.Tests.Unit.Client; public class WebhookClientTests : BaseClientTests { [Fact] public async Task CreateWebhookAsync_WebhookWithRequiredParameters_ResponseIsDeserializedInExpectedFormat() { // Given: we create a webhook request with only the required parameters string webhookId = "webhook-id"; WebhookRequest request = new() { Name = "my-webhook", Url = "https://github.com/Viincenttt/MollieApi/-updated", EventTypes = [WebhookEventTypes.PaymentLinkPaid], Testmode = true }; string jsonToReturnInMockResponse = CreateWebhookJsonResponse(webhookId, request.Name, request.Url, [WebhookEventTypes.PaymentLinkPaid]); var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Post, $"{BaseMollieClient.DefaultBaseApiEndPoint}webhooks", jsonToReturnInMockResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var client = new WebhookClient("abcde", httpClient); // When: We send the request WebhookResponse response = await client.CreateWebhookAsync(request); // Then mockHttp.VerifyNoOutstandingExpectation(); response.ShouldNotBeNull(); response.Id.ShouldBe(webhookId); response.Resource.ShouldBe("webhook"); response.Name.ShouldBe(request.Name); response.Url.ShouldBe(request.Url); response.EventTypes.ShouldNotBeNull(); response.EventTypes.ShouldBe(new[] { WebhookEventTypes.PaymentLinkPaid }); } [Fact] public async Task UpdateWebhookAsync_WebhookWithRequiredParameters_ResponseIsDeserializedInExpectedFormat() { // Given: we update webhook with only the required parameters string webhookId = "webhook-id"; WebhookRequest request = new() { Name = "my-webhook", Url = "https://github.com/Viincenttt/MollieApi/-updated", EventTypes = [WebhookEventTypes.PaymentLinkPaid], Testmode = true }; string jsonToReturnInMockResponse = CreateWebhookJsonResponse(webhookId, request.Name, request.Url, [WebhookEventTypes.PaymentLinkPaid]); var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Patch, $"{BaseMollieClient.DefaultBaseApiEndPoint}webhooks/{webhookId}", jsonToReturnInMockResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var client = new WebhookClient("abcde", httpClient); // When: We send the request WebhookResponse response = await client.UpdateWebhookAsync(webhookId, request); // Then mockHttp.VerifyNoOutstandingExpectation(); response.ShouldNotBeNull(); response.Id.ShouldBe(webhookId); response.Resource.ShouldBe("webhook"); response.Name.ShouldBe(request.Name); response.Url.ShouldBe(request.Url); response.EventTypes.ShouldNotBeNull(); response.EventTypes.ShouldBe(new[] { WebhookEventTypes.PaymentLinkPaid }); } [Fact] public async Task DeleteWebhookAsync_WebhookWithRequiredParameters_ResponseIsDeserializedInExpectedFormat() { // Given string webhookId = "webhook-id"; var mockHttp = CreateMockHttpMessageHandler(HttpMethod.Delete, $"{BaseMollieClient.DefaultBaseApiEndPoint}webhooks/{webhookId}", ""); HttpClient httpClient = mockHttp.ToHttpClient(); var client = new WebhookClient("abcde", httpClient); // When: We send the request await client.DeleteWebhookAsync(webhookId); // Then mockHttp.VerifyNoOutstandingExpectation(); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetWebhookAsync_NoWebhookIdIsGiven_ArgumentExceptionIsThrown(string? webhookId) { // Given var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var client = new WebhookClient("api-key", httpClient); // When #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await client.GetWebhookAsync(webhookId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'webhookId' is null or empty"); } [Fact] public async Task GetWebhookAsync_WithWebhookId_ResponseIsDeserializedInExpectedFormat() { // Given const string webhookId = "webhook-id"; const string name = "webhook-name"; const string url = "https://mollie.com/webhook"; string[] eventTypes = [WebhookEventTypes.PaymentLinkPaid, WebhookEventTypes.SalesInvoiceCreated]; string jsonToReturnInMockResponse = CreateWebhookJsonResponse(webhookId, name, url, eventTypes); var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}webhooks/{webhookId}") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", jsonToReturnInMockResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var webhookClient = new WebhookClient("abcde", httpClient); // When WebhookResponse response = await webhookClient.GetWebhookAsync(webhookId); // Then mockHttp.VerifyNoOutstandingExpectation(); response.Id.ShouldBe(webhookId); response.Resource.ShouldBe("webhook"); response.Name.ShouldBe(name); response.Url.ShouldBe(url); response.EventTypes.ShouldNotBeNull(); response.EventTypes.ShouldBe(eventTypes); response.ProfileId.ShouldBe("pfl_8XcSdLtrNK"); response.CreatedAt.ShouldBe(DateTime.Parse("2024-12-06T10:09:56+00:00")); response.Status.ShouldBe("enabled"); response.Mode.ShouldBe(Mode.Test); } [Theory] [InlineData(true, "?testmode=true")] [InlineData(false, "")] public async Task GetWebhookAsync_QueryParameterOptions_CorrectParametersAreAdded(bool testMode, string expectedQueryString) { // Given const string webhookId = "webhook-id"; const string name = "webhook-name"; const string url = "https://mollie.com/webhook"; string[] eventTypes = [WebhookEventTypes.PaymentLinkPaid, WebhookEventTypes.SalesInvoiceCreated]; string jsonToReturnInMockResponse = CreateWebhookJsonResponse(webhookId, name, url, eventTypes); var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}webhooks/{webhookId}{expectedQueryString}") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", jsonToReturnInMockResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var webhookClient = new WebhookClient("abcde", httpClient); // When await webhookClient.GetWebhookAsync(webhookId, testMode); // Then mockHttp.VerifyNoOutstandingRequest(); } [Theory] [InlineData(null, null, false, "")] [InlineData("from", null, false, "?from=from")] [InlineData("from", 50, false, "?from=from&limit=50")] public async Task GetWebhookListAsync_QueryParameterOptions_CorrectParametersAreAdded(string? from, int? limit, bool testmode, string expectedQueryString) { // Given string jsonToReturnInMockResponse = CreateWebhookListJsonResponse(); var mockHttp = CreateMockHttpMessageHandler( HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}webhooks{expectedQueryString}", jsonToReturnInMockResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var webhookClient = new WebhookClient("abcde", httpClient); // When await webhookClient.GetWebhookListAsync(from, limit, testmode); // Then mockHttp.VerifyNoOutstandingRequest(); } [Fact] public async Task GetWebhookListAsync_ResponseIsDeserializedInExpectedFormat() { // Given string jsonToReturnInMockResponse = CreateWebhookListJsonResponse(); var mockHttp = CreateMockHttpMessageHandler( HttpMethod.Get, $"{BaseMollieClient.DefaultBaseApiEndPoint}webhooks", jsonToReturnInMockResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var webhookClient = new WebhookClient("abcde", httpClient); // When ListResponse response = await webhookClient.GetWebhookListAsync(); // Then response.Count.ShouldBe(1); response.Items.Count.ShouldBe(response.Count); response.Links.ShouldNotBeNull(); response.Links.Self.Href.ShouldNotBeNull(); } private string CreateWebhookListJsonResponse() { string webhookJson = CreateWebhookJsonResponse("hook_5foxphpBru4xNPCDJJPzH", "Webhook 1", "https://mollie.com/webhook1", [WebhookEventTypes.PaymentLinkPaid]); return $@"{{ ""_embedded"": {{ ""webhooks"": [ {webhookJson} ] }}, ""count"": 1, ""_links"": {{ ""documentation"": {{ ""href"": ""https://docs.mollie.com/reference/list-webhook"", ""type"": ""text/html"" }}, ""self"": {{ ""href"": ""https://api.mollie.localhost/v2/webhooks?from=hook_yjtBMWDCGw5YFSPQ3HPzH&limit=2"", ""type"": ""application/hal+json"" }}, ""previous"": {{ ""href"": ""https://api.mollie.localhost/v2/webhooks?from=hook_5foxphpBru4xNPCDJJPzH&limit=2"", ""type"": ""application/hal+json"" }}, ""next"": {{ ""href"": ""https://api.mollie.localhost/v2/webhooks?from=hook_fTqARmWsfs9oXvKbZEPzH&limit=2"", ""type"": ""application/hal+json"" }} }} }}"; } private string CreateWebhookJsonResponse(string webhookId, string name, string url, string[] eventTypes) { var jsonToReturnInMockResponse = string.Join(",", eventTypes.Select(x => $"\"{x}\"")); return $@"{{ ""resource"": ""webhook"", ""id"": ""{webhookId}"", ""url"": ""{url}"", ""profileId"": ""pfl_8XcSdLtrNK"", ""createdAt"": ""2024-12-06T10:09:56+00:00"", ""name"": ""{name}"", ""status"": ""enabled"", ""mode"": ""test"", ""eventTypes"": [{jsonToReturnInMockResponse}], ""_links"": {{ ""documentation"": {{ ""href"": ""https://docs.mollie.com/reference/get-webhook"", ""type"": ""text/html"" }} }} }}"; } } ================================================ FILE: tests/Mollie.Tests.Unit/Client/WebhookEventClientTests.cs ================================================ using System; using System.Net.Http; using System.Threading.Tasks; using Mollie.Api.Client; using Mollie.Api.Models; using Mollie.Api.Models.PaymentLink.Response; using RichardSzalay.MockHttp; using Shouldly; using Xunit; namespace Mollie.Tests.Unit.Client; public class WebhookEventClientTests : BaseClientTests { [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task GetWebhookEventAsync_NoWebhookIdIsGiven_ArgumentExceptionIsThrown(string? webhookEventId) { // Given var mockHttp = new MockHttpMessageHandler(); HttpClient httpClient = mockHttp.ToHttpClient(); var client = new WebhookEventClient("api-key", httpClient); // When #pragma warning disable CS8604 // Possible null reference argument. var exception = await Assert.ThrowsAsync(async () => await client.GetWebhookEventAsync(webhookEventId)); #pragma warning restore CS8604 // Possible null reference argument. // Then exception.Message.ShouldBe("Required URL argument 'webhookEventId' is null or empty"); } [Theory] [InlineData(true, "?testmode=true")] [InlineData(false, "")] public async Task GetWebhookEventAsync_QueryParameterOptions_CorrectParametersAreAdded(bool testMode, string expectedQueryString) { // Given const string webhookEventId = "webhook-event-id"; const string type = "payment-link.paid"; const string paymentLinkEntityId = "pl_qng5gbbv8NAZ5gpM5ZYgx"; string entityJson = CreatePaymentLinkJsonResponse(paymentLinkEntityId); string jsonToReturnInMockResponse = CreateWebhookEventJsonResponse(webhookEventId, type, paymentLinkEntityId, entityJson); var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}events/{webhookEventId}{expectedQueryString}") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", jsonToReturnInMockResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var webhookClient = new WebhookEventClient("abcde", httpClient); // When await webhookClient.GetWebhookEventAsync(webhookEventId, testMode); // Then mockHttp.VerifyNoOutstandingRequest(); } [Fact] public async Task GetWebhookEventAsync_With_Generic_Parameter_ResponseIsDeserializedInExpectedFormat() { // Given const string webhookEventId = "webhook-event-id"; const string type = "payment-link.paid"; const string paymentLinkEntityId = "pl_qng5gbbv8NAZ5gpM5ZYgx"; string entityJson = CreatePaymentLinkJsonResponse(paymentLinkEntityId); string jsonToReturnInMockResponse = CreateWebhookEventJsonResponse(webhookEventId, type, paymentLinkEntityId, entityJson); var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}events/{webhookEventId}") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", jsonToReturnInMockResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var webhookClient = new WebhookEventClient("abcde", httpClient); // When var response = await webhookClient.GetWebhookEventAsync(webhookEventId); // Then mockHttp.VerifyNoOutstandingRequest(); response.Id.ShouldBe(webhookEventId); response.Type.ShouldBe(type); response.CreatedAt.ShouldBe(new DateTime(2024, 12, 16, 15, 57, 04, DateTimeKind.Utc)); response.EntityId.ShouldBe(paymentLinkEntityId); response.Links.Documentation.Href.ShouldBe("https://docs.mollie.com/guides/webhooks"); response.Links.Entity.Href.ShouldBe($"/v2/payment-links/{paymentLinkEntityId}"); response.Links.Self.Href.ShouldBe($"https://api.mollie.com/v2/events/{webhookEventId}"); response.Entity.ShouldNotBeNull(); response.Entity.Id.ShouldBe(paymentLinkEntityId); response.Entity.Resource.ShouldBe("payment-link"); response.Entity.ProfileId.ShouldBe("pfl_D96wnsu869"); response.Entity.Mode.ShouldBe(Mode.Live); response.Entity.Description.ShouldBe("Bicycle tires"); response.Entity.Amount!.Currency.ShouldBe("EUR"); response.Entity.Amount!.Value.ShouldBe("24.95"); response.Entity.MinimumAmount.ShouldBeNull(); response.Entity.Archived.ShouldBeFalse(); } [Fact] public async Task GetWebhookEventAsync_ResponseIsDeserializedInExpectedFormat() { // Given const string webhookEventId = "webhook-event-id"; const string type = "payment-link.paid"; const string paymentLinkEntityId = "pl_qng5gbbv8NAZ5gpM5ZYgx"; string entityJson = CreatePaymentLinkJsonResponse(paymentLinkEntityId); string jsonToReturnInMockResponse = CreateWebhookEventJsonResponse(webhookEventId, type, paymentLinkEntityId, entityJson); var mockHttp = new MockHttpMessageHandler(); mockHttp.When($"{BaseMollieClient.DefaultBaseApiEndPoint}events/{webhookEventId}") .With(request => request.Headers.Contains("Idempotency-Key")) .Respond("application/json", jsonToReturnInMockResponse); HttpClient httpClient = mockHttp.ToHttpClient(); var webhookClient = new WebhookEventClient("abcde", httpClient); // When var response = await webhookClient.GetWebhookEventAsync(webhookEventId); // Then mockHttp.VerifyNoOutstandingRequest(); response.Id.ShouldBe(webhookEventId); response.Type.ShouldBe(type); response.CreatedAt.ShouldBe(new DateTime(2024, 12, 16, 15, 57, 04, DateTimeKind.Utc)); response.EntityId.ShouldBe(paymentLinkEntityId); response.Links.Documentation.Href.ShouldBe("https://docs.mollie.com/guides/webhooks"); response.Links.Entity.Href.ShouldBe($"/v2/payment-links/{paymentLinkEntityId}"); response.Links.Self.Href.ShouldBe($"https://api.mollie.com/v2/events/{webhookEventId}"); } private string CreateWebhookEventJsonResponse(string webhookEventId, string type, string entityId, string entityJson) { return $@"{{ ""resource"": ""event"", ""id"": ""{webhookEventId}"", ""type"": ""{type}"", ""entityId"": ""{entityId}"", ""createdAt"": ""2024-12-16T15:57:04.0Z"", ""_embedded"": {{ ""entity"": {entityJson} }}, ""_links"": {{ ""self"": {{ ""href"": ""https://api.mollie.com/v2/events/{webhookEventId}"", ""type"": ""application/hal+json"" }}, ""documentation"": {{ ""href"": ""https://docs.mollie.com/guides/webhooks"", ""type"": ""text/html"" }}, ""entity"": {{ ""href"": ""/v2/payment-links/{entityId}"", ""type"": ""application/hal+json"" }} }} }}"; } private string CreatePaymentLinkJsonResponse(string entityId) { return $@"{{ ""resource"": ""payment-link"", ""id"": ""{entityId}"", ""profileId"": ""pfl_D96wnsu869"", ""mode"": ""live"", ""description"": ""Bicycle tires"", ""amount"": {{ ""currency"": ""EUR"", ""value"": ""24.95"" }}, ""minimumAmount"": null, ""archived"": false, ""redirectUrl"": ""https://webshop.example.org/thanks"", ""webhookUrl"": null, ""reusable"": true, ""createdAt"": ""2021-03-20T09:29:56.0Z"", ""paidAt"": null, ""expiresAt"": ""2023-06-06T11:00:00.0Z"", ""allowedMethods"": null, ""applicationFee"": null, ""_links"": {{ ""self"": {{ ""href"": ""https://api.mollie.com/v2/payment-links/{entityId}"", ""type"": ""application/hal+json"" }}, ""paymentLink"": {{ ""href"": ""https://www.mollie.com/paymentscreen/example"", ""type"": ""text/html"" }}, ""documentation"": {{ ""href"": ""https://docs.mollie.com/reference/v2/payment-links-api/get-payment-link"", ""type"": ""text/html"" }} }} }}"; } } ================================================ FILE: tests/Mollie.Tests.Unit/DependencyInjectionTests.cs ================================================ using System.Linq; using System.Reflection; using Shouldly; using Microsoft.Extensions.DependencyInjection; using Mollie.Api; using Mollie.Api.Client.Abstract; using Mollie.Api.Framework; using Xunit; namespace Mollie.Tests.Unit; public class DependencyInjectionTests { [Fact] public void AddMollieClient_ShouldRegisterAllApiInterfaces() { // Arrange var serviceCollection = new ServiceCollection(); serviceCollection.AddMollieApi(options => { options.ApiKey = "access_api-key"; options.ClientId = "client-id"; options.ClientSecret = "client-secret"; options.RetryPolicy = MollieHttpRetryPolicies.TransientHttpErrorRetryPolicy(); }); // Act var serviceProvider = serviceCollection.BuildServiceProvider(); // Assert var assembly = Assembly.GetAssembly(typeof(IPaymentClient)); var apiClientInterfaces = assembly! .GetTypes() .Where(type => type.Namespace == typeof(IPaymentClient).Namespace); foreach (var apiClientInterface in apiClientInterfaces) { if (apiClientInterface != typeof(IBaseMollieClient)) { var apiClientImplementation = serviceProvider.GetService(apiClientInterface); apiClientImplementation.ShouldNotBeNull(); } } } } ================================================ FILE: tests/Mollie.Tests.Unit/Extensions/DictionaryExtensionsTests.cs ================================================ using System.Collections.Generic; using Shouldly; using Mollie.Api.Extensions; using Xunit; namespace Mollie.Tests.Unit.Extensions { public class DictionaryExtensionsTests { [Fact] public void ToQueryString_WhenMultipleKeyValuePairsAreAdded_MultipleParametersAreAddedToQueryString() { // Arrange var parameters = new Dictionary() { {"include", "issuers"}, {"testmode", "true"} }; var expected = "?include=issuers&testmode=true"; // Act var result = parameters.ToQueryString(); // Assert result.ShouldBe(expected); } [Fact] public void ToQueryString_WhenDictionaryIsEmpty_QueryStringIsEmpty() { // Arrange var parameters = new Dictionary(); string expected = string.Empty; // Act var result = parameters.ToQueryString(); // Assert result.ShouldBe(expected); } [Fact] public void AddValueIfNotNullOrEmpty_ValueIsNotNull_ValueIsAdded() { // Arrange var parameters = new Dictionary(); var parameterName = "include"; var parameterValue = "issuers"; // Act parameters.AddValueIfNotNullOrEmpty(parameterName, parameterValue); // Assert parameters.ShouldNotBeEmpty(); parameters[parameterName].ShouldBe(parameterValue); } [Fact] public void AddValueIfNotNullOrEmpty_ValueIsEmpty_ValueIsNotAdded() { // Arrange var parameters = new Dictionary(); // Act parameters.AddValueIfNotNullOrEmpty("include", ""); // Assert parameters.ShouldBeEmpty(); } [Fact] public void AddValueIfTrue_ValueIsTrue_ValueIsAdded() { // Arrange var parameters = new Dictionary(); var parameterName = "testmode"; // Act parameters.AddValueIfTrue(parameterName, true); // Assert parameters.ShouldNotBeEmpty(); parameters[parameterName].ShouldBe(bool.TrueString.ToLower()); } [Fact] public void AddValueIfTrue_ValueIsFalse_ValueIsNotAdded() { // Arrange var parameters = new Dictionary(); // Act parameters.AddValueIfTrue("testmode", false); // Assert parameters.ShouldBeEmpty(); } } } ================================================ FILE: tests/Mollie.Tests.Unit/Framework/AmountConversionTests.cs ================================================ using Mollie.Api.Models; using System; using Xunit; namespace Mollie.Tests.Unit.Framework { public class AmountConversionTests { [Fact] public void InvalidAmountValueWillThrowInvalidCastException() { // Initiate Amount with invalid decimal value var amount = new Amount(Currency.EUR, "NotAValidDecimal"); // When: We implicitly cast Amount to decimal // Then: An InvalidCastException will be thrown Assert.Throws(() => { // ReSharper disable once UnusedVariable decimal a = amount; }); } } } ================================================ FILE: tests/Mollie.Tests.Unit/Framework/Factories/BalanceReportResponseFactoryTests.cs ================================================ using System; using Shouldly; using Mollie.Api.Framework.Factories; using Mollie.Api.Models.Balance.Response.BalanceReport; using Mollie.Api.Models.Balance.Response.BalanceReport.Specific.StatusBalance; using Mollie.Api.Models.Balance.Response.BalanceReport.Specific.TransactionCategories; using Xunit; namespace Mollie.Tests.Unit.Framework.Factories { public class BalanceReportResponseFactoryTests { [Theory] [InlineData(ReportGrouping.StatusBalances, typeof(StatusBalanceReportResponse))] [InlineData(ReportGrouping.TransactionCategories, typeof(TransactionCategoriesReportResponse))] [InlineData("unknown", typeof(BalanceReportResponse))] public void Create_CreatesExpectedType(string grouping, Type expectedType) { // Given var factory = new BalanceReportResponseFactory(); // When var result = factory.Create(grouping); // Then result.ShouldBeOfType(expectedType); } } } ================================================ FILE: tests/Mollie.Tests.Unit/Framework/Factories/BalanceTransactionFactoryTests.cs ================================================ using System; using Shouldly; using Mollie.Api.Framework.Factories; using Mollie.Api.Models.Balance.Response.BalanceTransaction; using Mollie.Api.Models.Balance.Response.BalanceTransaction.Specific; using Xunit; namespace Mollie.Tests.Unit.Framework.Factories { public class BalanceTransactionFactoryTests { [Theory] [InlineData(BalanceTransactionContextType.Payment, typeof(PaymentBalanceTransactionResponse))] [InlineData(BalanceTransactionContextType.Capture, typeof(CaptureBalanceTransactionResponse))] [InlineData(BalanceTransactionContextType.UnauthorizedDirectDebit, typeof(PaymentBalanceTransactionResponse))] [InlineData(BalanceTransactionContextType.FailedPayment, typeof(PaymentBalanceTransactionResponse))] [InlineData(BalanceTransactionContextType.Refund, typeof(RefundBalanceTransactionResponse))] [InlineData(BalanceTransactionContextType.ReturnedRefund, typeof(RefundBalanceTransactionResponse))] [InlineData(BalanceTransactionContextType.Chargeback, typeof(ChargebackBalanceTransactionResponse))] [InlineData(BalanceTransactionContextType.ChargebackReversal, typeof(PaymentBalanceTransactionResponse))] [InlineData(BalanceTransactionContextType.OutgoingTransfer, typeof(SettlementBalanceTransactionResponse))] [InlineData(BalanceTransactionContextType.CanceledOutgoingTransfer, typeof(SettlementBalanceTransactionResponse))] [InlineData(BalanceTransactionContextType.ReturnedTransfer, typeof(SettlementBalanceTransactionResponse))] [InlineData(BalanceTransactionContextType.InvoiceCompensation, typeof(InvoiceBalanceTransactionResponse))] [InlineData(BalanceTransactionContextType.BalanceCorrection, typeof(BalanceTransactionResponse))] [InlineData(BalanceTransactionContextType.ApplicationFee, typeof(PaymentBalanceTransactionResponse))] [InlineData(BalanceTransactionContextType.SplitPayment, typeof(PaymentBalanceTransactionResponse))] [InlineData(BalanceTransactionContextType.PlatformPaymentRefund, typeof(RefundBalanceTransactionResponse))] [InlineData(BalanceTransactionContextType.PlatformPaymentChargeback, typeof(ChargebackBalanceTransactionResponse))] [InlineData("UnknownType", typeof(BalanceTransactionResponse))] public void Create_CreatesTypeBasedOnType(string type, Type expectedType) { // Given var sut = new BalanceTransactionFactory(); // When var result = sut.Create(type); // Then result.ShouldBeOfType(expectedType); } } } ================================================ FILE: tests/Mollie.Tests.Unit/Framework/Factories/PaymentResponseFactoryTests.cs ================================================ using System; using Shouldly; using Mollie.Api.Framework.Factories; using Mollie.Api.Models.Payment; using Mollie.Api.Models.Payment.Response; using Mollie.Api.Models.Payment.Response.PaymentSpecificParameters; using Xunit; namespace Mollie.Tests.Unit.Framework.Factories { public class PaymentResponseFactoryTests { [Theory] [InlineData(PaymentMethod.Bancontact, typeof(BancontactPaymentResponse))] [InlineData(PaymentMethod.BankTransfer, typeof(BankTransferPaymentResponse))] [InlineData(PaymentMethod.Belfius, typeof(BelfiusPaymentResponse))] [InlineData(PaymentMethod.CreditCard, typeof(CreditCardPaymentResponse))] [InlineData(PaymentMethod.DirectDebit, typeof(SepaDirectDebitResponse))] [InlineData(PaymentMethod.Eps, typeof(EpsPaymentResponse))] [InlineData(PaymentMethod.GiftCard, typeof(GiftcardPaymentResponse))] [InlineData(PaymentMethod.Giropay, typeof(GiropayPaymentResponse))] [InlineData(PaymentMethod.Ideal, typeof(IdealPaymentResponse))] [InlineData(PaymentMethod.IngHomePay, typeof(IngHomePayPaymentResponse))] [InlineData(PaymentMethod.Kbc, typeof(KbcPaymentResponse))] [InlineData(PaymentMethod.PayPal, typeof(PayPalPaymentResponse))] [InlineData(PaymentMethod.PaySafeCard, typeof(PaySafeCardPaymentResponse))] [InlineData(PaymentMethod.Sofort, typeof(SofortPaymentResponse))] [InlineData(PaymentMethod.Refund, typeof(PaymentResponse))] [InlineData(PaymentMethod.KlarnaPayLater, typeof(PaymentResponse))] [InlineData(PaymentMethod.KlarnaSliceIt, typeof(PaymentResponse))] [InlineData(PaymentMethod.KlarnaOne, typeof(PaymentResponse))] [InlineData(PaymentMethod.Przelewy24, typeof(PaymentResponse))] [InlineData(PaymentMethod.ApplePay, typeof(PaymentResponse))] [InlineData(PaymentMethod.MealVoucher, typeof(PaymentResponse))] [InlineData(PaymentMethod.In3, typeof(PaymentResponse))] [InlineData(PaymentMethod.PointOfSale, typeof(PointOfSalePaymentResponse))] [InlineData(PaymentMethod.Billie, typeof(PaymentResponse))] [InlineData(PaymentMethod.Trustly, typeof(PaymentResponse))] [InlineData(PaymentMethod.Twint, typeof(PaymentResponse))] [InlineData(PaymentMethod.Satispay, typeof(PaymentResponse))] [InlineData(PaymentMethod.Riverty, typeof(PaymentResponse))] [InlineData(PaymentMethod.Blik, typeof(PaymentResponse))] [InlineData(PaymentMethod.BancomatPay, typeof(PaymentResponse))] [InlineData(PaymentMethod.BacsDirectDebit, typeof(PaymentResponse))] [InlineData(PaymentMethod.Alma, typeof(PaymentResponse))] [InlineData(PaymentMethod.GooglePay, typeof(PaymentResponse))] [InlineData(PaymentMethod.Voucher, typeof(PaymentResponse))] [InlineData(PaymentMethod.MbWay, typeof(PaymentResponse))] [InlineData(PaymentMethod.Multibanco, typeof(PaymentResponse))] [InlineData(PaymentMethod.Swish, typeof(PaymentResponse))] [InlineData(PaymentMethod.KlarnaPayNow, typeof(PaymentResponse))] [InlineData("UnknownPaymentMethod", typeof(PaymentResponse))] public void Create_CreatesTypeBasedOnPaymentMethod(string paymentMethod, Type expectedType) { // Given var sut = new PaymentResponseFactory(); // When var result = sut.Create(paymentMethod); // Then result.ShouldBeOfType(expectedType); } } } ================================================ FILE: tests/Mollie.Tests.Unit/Framework/JsonConverterServiceTests.cs ================================================ using Shouldly; using Mollie.Api.Framework; using Mollie.Api.JsonConverters; using Mollie.Api.Models; using Mollie.Api.Models.Payment.Request; using Mollie.Api.Models.Payment.Response; using Xunit; namespace Mollie.Tests.Unit.Framework { public class JsonConverterServiceTests { [Fact] public void Serialize_JsonData_IsSerialized() { // Given: A JSON metadata value JsonConverterService jsonConverterService = new JsonConverterService(); PaymentRequest paymentRequest = new PaymentRequest() { Amount = new Amount(Currency.EUR, "100.00"), Description = "Description", RedirectUrl = "http://www.mollie.com", Metadata = "{\"firstName\":\"John\",\"lastName\":\"Doe\"}", }; string expectedJsonValue = "{\"amount\":{\"currency\":\"EUR\",\"value\":\"100.00\"},\"description\":\"Description\",\"redirectUrl\":\"http://www.mollie.com\",\"metadata\":{\"firstName\":\"John\",\"lastName\":\"Doe\"}}"; // When: We serialize the JSON string jsonValue = jsonConverterService.Serialize(paymentRequest); // Then: jsonValue.ShouldBe(expectedJsonValue); } [Fact] public void Deserialize_JsonData_IsDeserialized() { // Given: A JSON metadata value JsonConverterService jsonConverterService = new JsonConverterService(); string metadataJson = @"{ ""ReferenceNumber"": null, ""OrderID"": null, ""UserID"": ""534721"" }"; string paymentJson = @"{""metadata"":" + metadataJson + "}"; // When: We deserialize the JSON PaymentResponse payments = jsonConverterService.Deserialize(paymentJson)!; // Then: payments.Metadata.ShouldBe(metadataJson); } [Fact] public void Deserialize_StringData_IsDeserialized() { // Given: A JSON metadata value JsonConverterService jsonConverterService = new JsonConverterService(); string metadataJson = "This is my metadata"; string paymentJson = @"{""metadata"":""" + metadataJson + @"""}"; // When: We deserialize the JSON PaymentResponse payments = jsonConverterService.Deserialize(paymentJson)!; // Then: payments.Metadata.ShouldBe(metadataJson); } [Fact] public void Deserialize_JsonDataWithNullValues_IsDeserialized() { // Given: A JSON metadata value var jsonConverterService = new JsonConverterService(); string metadataJson = @"null"; string paymentJson = @"{""metadata"":" + metadataJson + "}"; // When: We deserialize the JSON PaymentResponse payments = jsonConverterService.Deserialize(paymentJson)!; // Then: payments.Metadata.ShouldBeNull(); } } } ================================================ FILE: tests/Mollie.Tests.Unit/Models/AmountTests.cs ================================================ using Shouldly; using Mollie.Api.Models; using Mollie.Api.Models.Payment.Response; using Xunit; namespace Mollie.Tests.Unit.Models { public class AmountTests { [Theory] [InlineData("EUR", 50.25, "50.25")] [InlineData("EUR", 50, "50.00")] [InlineData("JPY", 51, "51")] [InlineData("ISK", 52, "52")] [InlineData("ISK", 52.40, "52")] public void CreateAmount_DecimalIsConverted_ValueHasCorrectFormat(string currency, decimal value, string expectedResult) { // Arrange & act var amount = new Amount(currency, value); // Assert amount.Value.ShouldBe(expectedResult); } [Fact] public void Amount_ConvertedToDecimal_IsEqualToOriginalValue() { // Arrange decimal originalValue = 50.25m; var amount = new Amount(Currency.EUR, originalValue); // Act decimal convertedValue = amount; // Assert convertedValue.ShouldBe(originalValue); } [Fact] public void NullableAmount_ConvertedToNullableDecimal_IsEqualToOriginalValue() { // Arrange decimal originalValue = 50.25m; Amount? amount = new(Currency.EUR, originalValue); // Act decimal? convertedValue = amount; // Assert convertedValue.ShouldBe(originalValue); } [Fact] public void NullAmount_ConvertedToNullableDecimal_IsNull() { // Arrange Amount? amount = null; // Act decimal? convertedValue = amount; // Assert convertedValue.ShouldBeNull(); } } } ================================================ FILE: tests/Mollie.Tests.Unit/Models/Payment/Request/PaymentRequestTests.cs ================================================ using System; using Shouldly; using Mollie.Api.Models; using Mollie.Api.Models.Payment; using Mollie.Api.Models.Payment.Request; using Mollie.Api.Models.Payment.Request.PaymentSpecificParameters; using Xunit; namespace Mollie.Tests.Unit.Models.Payment.Request; public class PaymentRequestTests { [Theory] [InlineData(PaymentMethod.CreditCard, typeof(CreditCardPaymentRequest))] [InlineData(PaymentMethod.PayPal, typeof(PayPalPaymentRequest))] public void CreatePaymentRequest(string paymentMethod, Type expectedType) { var amount = new Amount(Currency.EUR, 50m); var description = "my-description"; var paymentRequest = new PaymentRequest() { Amount = amount, Description = description }; switch (paymentMethod) { case PaymentMethod.CreditCard: paymentRequest = new CreditCardPaymentRequest(paymentRequest) { CardToken = "card-token" }; break; case PaymentMethod.PayPal: paymentRequest = new PayPalPaymentRequest(paymentRequest) { DigitalGoods = true }; break; } paymentRequest.ShouldBeOfType(expectedType); paymentRequest.Amount.ShouldBe(amount); paymentRequest.Description.ShouldBe(description); } } ================================================ FILE: tests/Mollie.Tests.Unit/Mollie.Tests.Unit.csproj ================================================ net10.0 false enable all runtime; build; native; contentfiles; analyzers; buildtransitive